From 17703ca1502f773df1198ad6d1c62518d1f7250f Mon Sep 17 00:00:00 2001 From: Gabe Debes Date: Sat, 17 Oct 2026 19:29:00 -0700 Subject: [PATCH 1/4] [eas-cli] Preserve literal simulator control arguments --- .changeset/simulator-control-literals.md | 5 ++ packages/eas-cli/README.md | 8 ++-- .../simulator/__tests__/control.test.ts | 47 +++++++++++++++++++ .../eas-cli/src/commands/simulator/control.ts | 23 ++++++++- packages/eas-cli/src/simulator/hostActions.ts | 7 +-- 5 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 .changeset/simulator-control-literals.md diff --git a/.changeset/simulator-control-literals.md b/.changeset/simulator-control-literals.md new file mode 100644 index 0000000000..e4bd0ac666 --- /dev/null +++ b/.changeset/simulator-control-literals.md @@ -0,0 +1,5 @@ +--- +'eas-cli': patch +--- + +Preserve simulator control values after `--` as literal positional arguments. diff --git a/packages/eas-cli/README.md b/packages/eas-cli/README.md index bf56a22453..57b1c37fbf 100644 --- a/packages/eas-cli/README.md +++ b/packages/eas-cli/README.md @@ -2788,8 +2788,8 @@ USAGE $ eas sim:control [ACTION...] [-h] [--id ] [--list] [--output ] [--json] [--non-interactive] ARGUMENTS - [ACTION...] Action to run, followed by its values in order or as name=value. Omit to list the actions. Put -- before - values that start with --. + [ACTION...] Action to run, followed by its values in order or as name=value. Omit to list the actions. Values after -- are + positional literals, including text that starts with -- or name=. FLAGS -h, --help Show command help, or usage for the selected action from the session. @@ -3263,8 +3263,8 @@ USAGE $ eas simulator:control [ACTION...] [-h] [--id ] [--list] [--output ] [--json] [--non-interactive] ARGUMENTS - [ACTION...] Action to run, followed by its values in order or as name=value. Omit to list the actions. Put -- before - values that start with --. + [ACTION...] Action to run, followed by its values in order or as name=value. Omit to list the actions. Values after -- are + positional literals, including text that starts with -- or name=. FLAGS -h, --help Show command help, or usage for the selected action from the session. diff --git a/packages/eas-cli/src/commands/simulator/__tests__/control.test.ts b/packages/eas-cli/src/commands/simulator/__tests__/control.test.ts index 53af67182c..8c20995360 100644 --- a/packages/eas-cli/src/commands/simulator/__tests__/control.test.ts +++ b/packages/eas-cli/src/commands/simulator/__tests__/control.test.ts @@ -179,6 +179,53 @@ describe(SimulatorControl, () => { } }); + it.each(['text=hello', '--', '--help'])( + 'preserves the literal value after the separator: %s', + async value => { + served = { + ...catalog, + 'clipboard.write': { + type: 'object', + properties: { udid: { type: 'string' }, text: { type: 'string' } }, + required: ['udid', 'text'], + }, + }; + await createCommand(['clipboard.write', '--id', 'session-123', '--', value]).runAsync(); + expect(actionRequests()).toEqual([['clipboard.write', { text: value }]]); + } + ); + + it('does not treat a flag value of -- as the literal separator', async () => { + served = { + ...catalog, + 'clipboard.write': { + type: 'object', + properties: { udid: { type: 'string' }, text: { type: 'string' } }, + required: ['udid', 'text'], + }, + }; + await createCommand(['clipboard.write', 'text=hello', '--id', '--']).runAsync(); + expect(actionRequests()).toEqual([['clipboard.write', { text: 'hello' }]]); + }); + + it('keeps named arguments before the separator and positional values after it', async () => { + await createCommand([ + 'permissions.set', + 'action=grant', + '--id', + 'session-123', + '--', + 'service=camera', + 'bundleId=literal', + ]).runAsync(); + expect(actionRequests()).toEqual([ + [ + 'permissions.set', + { action: 'grant', service: 'service=camera', bundleId: 'bundleId=literal' }, + ], + ]); + }); + it('runs an action from the catalog with its values and prints the output', async () => { await createCommand(['hardwareKeyboard.set', 'off', '--id', 'session-123']).runAsync(); diff --git a/packages/eas-cli/src/commands/simulator/control.ts b/packages/eas-cli/src/commands/simulator/control.ts index 00eb4dd2df..f272b6b92b 100644 --- a/packages/eas-cli/src/commands/simulator/control.ts +++ b/packages/eas-cli/src/commands/simulator/control.ts @@ -53,7 +53,7 @@ export default class SimulatorControl extends EasCommand { static override args = { action: Args.string({ description: - 'Action to run, followed by its values in order or as name=value. Omit to list the actions. Put -- before values that start with --.', + 'Action to run, followed by its values in order or as name=value. Omit to list the actions. Values after -- are positional literals, including text that starts with -- or name=.', }), }; @@ -82,6 +82,21 @@ export default class SimulatorControl extends EasCommand { async runAsync(): Promise { const { args, argv, flags } = await this.parse(SimulatorControl); + let literalFrom = argv.length - 1; + for (let index = 0; index < this.argv.length; index++) { + const value = this.argv[index]; + if (value === '--') { + literalFrom = argv.length - 1 - (this.argv.length - index - 1); + break; + } + // A standalone -- consumed as a flag value is not an argument separator. + const flag = value.startsWith('--') + ? SimulatorControl.flags[value.slice(2) as keyof typeof SimulatorControl.flags] + : undefined; + if (flag?.type === 'option') { + index++; + } + } const { json: jsonFlag, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags); if (jsonFlag) { @@ -138,6 +153,7 @@ export default class SimulatorControl extends EasCommand { sessionId, action, values: argv.slice(1).map(String), + literalFrom, outputPath: flags.output, jsonFlag, }); @@ -178,6 +194,7 @@ export default class SimulatorControl extends EasCommand { Log.newLine(); Log.log(` ${chalk.bold('USAGE')}`); Log.log(` eas simulator:control ${formatHostActionUsage(action, entry)}`); + Log.log(' Values after -- are positional literals.'); const outputFlag = entry.output === 'base64' ? '--output ' : '[--output ]'; Log.log(` ${entry.output ? `${outputFlag} ` : ''}[--id ] [--json]`); Log.newLine(); @@ -208,17 +225,19 @@ export default class SimulatorControl extends EasCommand { sessionId, action, values, + literalFrom, outputPath, jsonFlag, }: { sessionId: string; action: string; values: string[]; + literalFrom: number; outputPath: string | undefined; jsonFlag: boolean; } ): Promise { - const params = buildHostActionParams(action, entry, values); + const params = buildHostActionParams(action, entry, values, literalFrom); await assertHostActionOutputAsync(action, entry, outputPath); const localFile = await findLocalUploadAsync(entry, params); diff --git a/packages/eas-cli/src/simulator/hostActions.ts b/packages/eas-cli/src/simulator/hostActions.ts index f5bcc4d60c..660907637f 100644 --- a/packages/eas-cli/src/simulator/hostActions.ts +++ b/packages/eas-cli/src/simulator/hostActions.ts @@ -392,13 +392,14 @@ export function formatHostActionUsage(name: string, entry: HostActionCatalogEntr export function buildHostActionParams( name: string, entry: HostActionCatalogEntry, - values: string[] + values: string[], + literalFrom = values.length ): Record { const properties = hostActionProperties(entry); const params: Record = {}; const positional: string[] = []; - for (const value of values) { - const named = NAMED_VALUE.exec(value); + for (const [index, value] of values.entries()) { + const named = index < literalFrom ? NAMED_VALUE.exec(value) : null; if (!named || !properties.has(named[1])) { positional.push(value); } else if (Object.hasOwn(params, named[1])) { From 1b0d8b05eba91181505abd32b1113928566ae8a4 Mon Sep 17 00:00:00 2001 From: Gabe Debes Date: Sat, 17 Oct 2026 21:34:47 -0700 Subject: [PATCH 2/4] [eas-cli] Clarify quoted literal simulator control examples --- packages/eas-cli/README.md | 14 ++++++++++---- packages/eas-cli/src/commands/simulator/control.ts | 3 ++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/eas-cli/README.md b/packages/eas-cli/README.md index 57b1c37fbf..81c0db1f7e 100644 --- a/packages/eas-cli/README.md +++ b/packages/eas-cli/README.md @@ -2788,8 +2788,9 @@ USAGE $ eas sim:control [ACTION...] [-h] [--id ] [--list] [--output ] [--json] [--non-interactive] ARGUMENTS - [ACTION...] Action to run, followed by its values in order or as name=value. Omit to list the actions. Values after -- are - positional literals, including text that starts with -- or name=. + [ACTION...] Action to run, followed by its values in order or as name=value. Omit to list the actions. Values after + -- are positional literals, including text that starts with -- or name=. Shell quotes alone do not + disable name=value parsing. FLAGS -h, --help Show command help, or usage for the selected action from the session. @@ -2817,6 +2818,8 @@ EXAMPLES $ eas simulator:control clipboard.write -- --some-text + $ eas simulator:control clipboard.write -- 'text=hello' + $ eas simulator:control hardwareKeyboard.set off $ eas simulator:control permissions.set grant camera com.example.app @@ -3263,8 +3266,9 @@ USAGE $ eas simulator:control [ACTION...] [-h] [--id ] [--list] [--output ] [--json] [--non-interactive] ARGUMENTS - [ACTION...] Action to run, followed by its values in order or as name=value. Omit to list the actions. Values after -- are - positional literals, including text that starts with -- or name=. + [ACTION...] Action to run, followed by its values in order or as name=value. Omit to list the actions. Values after + -- are positional literals, including text that starts with -- or name=. Shell quotes alone do not + disable name=value parsing. FLAGS -h, --help Show command help, or usage for the selected action from the session. @@ -3292,6 +3296,8 @@ EXAMPLES $ eas simulator:control clipboard.write -- --some-text + $ eas simulator:control clipboard.write -- 'text=hello' + $ eas simulator:control hardwareKeyboard.set off $ eas simulator:control permissions.set grant camera com.example.app diff --git a/packages/eas-cli/src/commands/simulator/control.ts b/packages/eas-cli/src/commands/simulator/control.ts index f272b6b92b..6ef9130dab 100644 --- a/packages/eas-cli/src/commands/simulator/control.ts +++ b/packages/eas-cli/src/commands/simulator/control.ts @@ -43,6 +43,7 @@ export default class SimulatorControl extends EasCommand { '$ eas simulator:control --id ', '$ eas simulator:control screenshot.capture --help', '$ eas simulator:control clipboard.write -- --some-text', + "$ eas simulator:control clipboard.write -- 'text=hello'", '$ eas simulator:control hardwareKeyboard.set off', '$ eas simulator:control permissions.set grant camera com.example.app', '$ eas simulator:control app.install ./MyApp.ipa', @@ -53,7 +54,7 @@ export default class SimulatorControl extends EasCommand { static override args = { action: Args.string({ description: - 'Action to run, followed by its values in order or as name=value. Omit to list the actions. Values after -- are positional literals, including text that starts with -- or name=.', + 'Action to run, followed by its values in order or as name=value. Omit to list the actions. Values after -- are positional literals, including text that starts with -- or name=. Shell quotes alone do not disable name=value parsing.', }), }; From 58cf39930b8c43e0f3edc9536067371dc49c085f Mon Sep 17 00:00:00 2001 From: Gabe Debes Date: Sat, 17 Oct 2026 21:37:18 -0700 Subject: [PATCH 3/4] [eas-cli] Label clipboard literal examples in oclif help --- packages/eas-cli/README.md | 16 ++++++++++++++-- .../eas-cli/src/commands/simulator/control.ts | 10 +++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/eas-cli/README.md b/packages/eas-cli/README.md index 81c0db1f7e..54ee8ebc08 100644 --- a/packages/eas-cli/README.md +++ b/packages/eas-cli/README.md @@ -2818,7 +2818,13 @@ EXAMPLES $ eas simulator:control clipboard.write -- --some-text - $ eas simulator:control clipboard.write -- 'text=hello' + Write hello using the named text parameter (shell quotes do not change parsing): + + $ eas simulator:control clipboard.write 'text=hello' + + Write the literal text=hello using --: + + $ eas simulator:control clipboard.write -- 'text=hello' $ eas simulator:control hardwareKeyboard.set off @@ -3296,7 +3302,13 @@ EXAMPLES $ eas simulator:control clipboard.write -- --some-text - $ eas simulator:control clipboard.write -- 'text=hello' + Write hello using the named text parameter (shell quotes do not change parsing): + + $ eas simulator:control clipboard.write 'text=hello' + + Write the literal text=hello using --: + + $ eas simulator:control clipboard.write -- 'text=hello' $ eas simulator:control hardwareKeyboard.set off diff --git a/packages/eas-cli/src/commands/simulator/control.ts b/packages/eas-cli/src/commands/simulator/control.ts index 6ef9130dab..11607c69aa 100644 --- a/packages/eas-cli/src/commands/simulator/control.ts +++ b/packages/eas-cli/src/commands/simulator/control.ts @@ -43,7 +43,15 @@ export default class SimulatorControl extends EasCommand { '$ eas simulator:control --id ', '$ eas simulator:control screenshot.capture --help', '$ eas simulator:control clipboard.write -- --some-text', - "$ eas simulator:control clipboard.write -- 'text=hello'", + { + description: + 'Write hello using the named text parameter (shell quotes do not change parsing):', + command: "$ eas simulator:control clipboard.write 'text=hello'", + }, + { + description: 'Write the literal text=hello using --:', + command: "$ eas simulator:control clipboard.write -- 'text=hello'", + }, '$ eas simulator:control hardwareKeyboard.set off', '$ eas simulator:control permissions.set grant camera com.example.app', '$ eas simulator:control app.install ./MyApp.ipa', From e849ca5c36cb16049c3a31ec008ecf5f8385faad Mon Sep 17 00:00:00 2001 From: Gabe Debes Date: Thu, 22 Oct 2026 22:43:19 -0700 Subject: [PATCH 4/4] [eas-cli] Preserve simulator control input order Read action values from ordered raw oclif tokens, keep the complete usage block together, and explain named arguments before the literal separator. Add repeated-token and help-layout regressions. Co-authored-by: Codex --- .changeset/simulator-control-literals.md | 2 +- .../simulator/__tests__/control.test.ts | 46 ++++++++++++++++++- .../eas-cli/src/commands/simulator/control.ts | 17 +++++-- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/.changeset/simulator-control-literals.md b/.changeset/simulator-control-literals.md index e4bd0ac666..681a81d242 100644 --- a/.changeset/simulator-control-literals.md +++ b/.changeset/simulator-control-literals.md @@ -2,4 +2,4 @@ 'eas-cli': patch --- -Preserve simulator control values after `--` as literal positional arguments. +Preserve the input order of repeated simulator control arguments. Values after `--`, including `name=value`, are now positional literals; place named arguments before `--`. diff --git a/packages/eas-cli/src/commands/simulator/__tests__/control.test.ts b/packages/eas-cli/src/commands/simulator/__tests__/control.test.ts index 8c20995360..e28939fd28 100644 --- a/packages/eas-cli/src/commands/simulator/__tests__/control.test.ts +++ b/packages/eas-cli/src/commands/simulator/__tests__/control.test.ts @@ -179,7 +179,7 @@ describe(SimulatorControl, () => { } }); - it.each(['text=hello', '--', '--help'])( + it.each(['text=hello', '--', '--help', ''])( 'preserves the literal value after the separator: %s', async value => { served = { @@ -195,6 +195,44 @@ describe(SimulatorControl, () => { } ); + it.each([ + { + argv: ['permissions.set', '--id', 's', 'action=grant', '--', 's', 'camera'], + params: { action: 'grant', service: 's', bundleId: 'camera' }, + }, + { + argv: ['permissions.set', '--id', 's', '--', 'grant', '--', 'com.app'], + params: { action: 'grant', service: '--', bundleId: 'com.app' }, + }, + { + argv: ['permissions.set', '--id', 's', '--', 'grant', 'permissions.set', 'com.app'], + params: { action: 'grant', service: 'permissions.set', bundleId: 'com.app' }, + }, + { + argv: ['permissions.set', '--id', 'camera', 'grant', 'microphone', 'camera'], + params: { action: 'grant', service: 'microphone', bundleId: 'camera' }, + }, + ])( + 'preserves input order when positional tokens repeat earlier input: $argv', + async ({ argv, params }) => { + await createCommand(argv).runAsync(); + expect(actionRequests()).toEqual([['permissions.set', params]]); + } + ); + + it('treats values as literal when the separator precedes the action', async () => { + served = { + ...catalog, + 'clipboard.write': { + type: 'object', + properties: { udid: { type: 'string' }, text: { type: 'string' } }, + required: ['udid', 'text'], + }, + }; + await createCommand(['--id', 'session-123', '--', 'clipboard.write', 'text=hello']).runAsync(); + expect(actionRequests()).toEqual([['clipboard.write', { text: 'text=hello' }]]); + }); + it('does not treat a flag value of -- as the literal separator', async () => { served = { ...catalog, @@ -259,6 +297,12 @@ describe(SimulatorControl, () => { expect(jest.mocked(Log.log)).toHaveBeenCalledWith( ' eas simulator:control permissions.set ' ); + const lines = jest.mocked(Log.log).mock.calls.map(([line]) => String(line)); + const usage = lines.indexOf( + ' eas simulator:control permissions.set ' + ); + expect(lines[usage + 1]).toBe(' [--id ] [--json]'); + expect(lines.indexOf(' Values after -- are positional literals.')).toBeGreaterThan(usage + 1); expect(actionRequests()).toEqual([]); expect(close).toHaveBeenCalledTimes(1); }); diff --git a/packages/eas-cli/src/commands/simulator/control.ts b/packages/eas-cli/src/commands/simulator/control.ts index 11607c69aa..31722c9eed 100644 --- a/packages/eas-cli/src/commands/simulator/control.ts +++ b/packages/eas-cli/src/commands/simulator/control.ts @@ -90,12 +90,18 @@ export default class SimulatorControl extends EasCommand { }; async runAsync(): Promise { - const { args, argv, flags } = await this.parse(SimulatorControl); - let literalFrom = argv.length - 1; + const { args, raw, flags } = await this.parse(SimulatorControl); + // oclif's argv sorts repeated values by their first occurrence in the input. + // Raw arg tokens preserve their actual order, including literals after --. + const values = raw + .filter(token => token.type === 'arg') + .map(token => token.input) + .slice(1); + let literalFrom = values.length; for (let index = 0; index < this.argv.length; index++) { const value = this.argv[index]; if (value === '--') { - literalFrom = argv.length - 1 - (this.argv.length - index - 1); + literalFrom = Math.max(0, values.length - (this.argv.length - index - 1)); break; } // A standalone -- consumed as a flag value is not an argument separator. @@ -161,7 +167,7 @@ export default class SimulatorControl extends EasCommand { await this.runActionAsync(connection, entry, { sessionId, action, - values: argv.slice(1).map(String), + values, literalFrom, outputPath: flags.output, jsonFlag, @@ -203,10 +209,11 @@ export default class SimulatorControl extends EasCommand { Log.newLine(); Log.log(` ${chalk.bold('USAGE')}`); Log.log(` eas simulator:control ${formatHostActionUsage(action, entry)}`); - Log.log(' Values after -- are positional literals.'); const outputFlag = entry.output === 'base64' ? '--output ' : '[--output ]'; Log.log(` ${entry.output ? `${outputFlag} ` : ''}[--id ] [--json]`); Log.newLine(); + Log.log(' Values after -- are positional literals.'); + Log.newLine(); Log.log(` ${chalk.bold('OPTIONS')}`); Log.log(` --id Defaults to ${SIMULATOR_DOTENV_FILE_NAME}.`); Log.log(' --json Print the action schema as JSON. Implies --non-interactive.');