feat: add public --launch-arg CLI flag for iOS open#598
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds support for passing repeatable --launch-args through the open flow so callers can append platform-specific launch arguments when launching apps on iOS/Android.
Changes:
- Introduces
--launch-args <arg>(repeatable) to the CLI flag schema and propagates it through client/daemon/context/dispatch to interactors. - Appends launch arguments to iOS simulator/device launch commands and Android
adb shell am startinvocations. - Adds iOS and Android test coverage validating argument ordering and simulator URL-open rejection behavior.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/command-schema.ts | Adds launchArgs to CLI flags and flag definitions (--launch-args). |
| src/platforms/ios/apps.ts | Threads launchArgs into iOS launch paths; rejects simulator URL opens with launchArgs. |
| src/platforms/ios/tests/index.test.ts | Adds tests for iOS simulator/device argument passing and simulator URL+args rejection. |
| src/platforms/android/app-lifecycle.ts | Updates openAndroidApp signature; appends launchArgs to am start command construction. |
| src/platforms/android/tests/index.test.ts | Updates existing call sites and adds tests verifying launchArgs appended for multiple launch modes. |
| src/daemon/handlers/session-open.ts | Removes launchArgs from runtime URL launch context payload. |
| src/daemon/context.ts | Adds launchArgs to daemon context construction; introduces a complexity-ignore comment. |
| src/daemon-client.ts | Adds launchArgs to openApp options and request flags mapping. |
| src/core/launch-console.ts | Adds user-facing error message for unsupported simulator URL-open + --launch-args. |
| src/core/interactors/apple.ts | Passes launchArgs into openIosApp. |
| src/core/interactors/android.ts | Passes activity + launchArgs object into updated openAndroidApp API. |
| src/core/interactor-types.ts | Extends Interactor.open options type with launchArgs. |
| src/core/dispatch.ts | Adds launchArgs to interactor.open calls for both app and URL opens. |
| src/core/dispatch-context.ts | Adds launchArgs to dispatch context type. |
| src/commands/session-lifecycle/definition.ts | Allows launchArgs flag for the open command. |
| src/client-types.ts | Extends client request option types with launchArgs. |
| src/client-normalizers.ts | Adds launchArgs to normalized CommandFlags. |
| src/cli/commands/open.ts | Passes CLI launchArgs through to the client open call. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| export type DaemonCommandContext = DispatchContext & ScreenshotRuntimeFlags; | ||
|
|
||
| // fallow-ignore-next-line complexity |
There was a problem hiding this comment.
this ignore comment is used in other parts of this repo for this use case
| const args = ['device', 'process', 'launch', '--device', device.id, bundleId]; | ||
| if (options?.payloadUrl) { | ||
| args.push('--payload-url', options.payloadUrl); | ||
| } | ||
| if (options?.launchArgs && options.launchArgs.length > 0) { | ||
| args.push(...options.launchArgs); | ||
| } |
| export async function openAndroidApp( | ||
| device: DeviceInfo, | ||
| app: string, | ||
| activity?: string, | ||
| options?: { activity?: string; launchArgs?: string[] }, | ||
| ): Promise<void> { |
There was a problem hiding this comment.
public api is agent device CLI itself, so this is internal and OK to make change
|
Thanks for contributing this! The feature direction makes sense, especially exposing launch args for iOS app launches and handling the iOS simulator URL limitation explicitly. A couple of things need sorting before this can merge:
So I think the feature is promising, but it needs a rebase plus a tightened platform contract before review can continue. |
4fcc27a to
4a61990
Compare
Thanks for the guidance @thymikee! Will take out of draft when the above has been completed 😃 |
4a61990 to
d68f6f7
Compare
Stacked on top of the iOS-only --launch-args PR (callstackincubator#598). Removes the Android UNSUPPORTED_OPERATION guard added with the Maestro work and threads launchArgs through all five Android open paths. Per-path threading: - openAndroidPackage (-p package launch + activity-fallback) - openAndroidPackageActivity (-n component override) - openAndroidIntent (named intent action) - openAndroidDeepLink (-a VIEW -d <url>, with optional -p) - openAndroidAppBoundDeepLink (-a VIEW -d <url> -p <resolved>) `adb shell` joins its argv with spaces and feeds the result to a device shell, which re-tokenises. The other am-start arguments are well-known and never contain shell-significant characters, so they round-trip untouched. Launch arguments are user-supplied and may contain JSON, spaces, `#`, etc.; each is single-quoted unless it consists entirely of safe shell characters (the same approach long used in adb-driven tooling for the same reason). Help text on --launch-args is updated to describe the Android shape (`adb shell am start args, e.g. --es key value` for typed Intent extras) and macOS remains the only rejected platform. Tests: - src/platforms/android/__tests__/index.test.ts: five new tests cover package, activity-override, deep-link URL, app-bound URL, and JSON-with-metacharacters shell-quoting paths. - src/core/__tests__/dispatch-open.test.ts: the previous "rejects Android launch arguments" test is inverted into a forwarding test that asserts openAndroidApp receives the args. Validated end-to-end against an internal Android debug build: a JSON TEST_ACCOUNT_ARGS extra containing email/password/header (with `#`, `/`, `:`) was delivered intact to AutomationSyncActivity, which read the extra and persisted it to AppPreferences.
Stacked on top of the iOS-only --launch-args PR (callstackincubator#598). Removes the Android UNSUPPORTED_OPERATION guard added with the Maestro work and threads launchArgs through all five Android open paths. Per-path threading: - openAndroidPackage (-p package launch + activity-fallback) - openAndroidPackageActivity (-n component override) - openAndroidIntent (named intent action) - openAndroidDeepLink (-a VIEW -d <url>, with optional -p) - openAndroidAppBoundDeepLink (-a VIEW -d <url> -p <resolved>) `adb shell` joins its argv with spaces and feeds the result to a device shell, which re-tokenises. The other am-start arguments are well-known and never contain shell-significant characters, so they round-trip untouched. Launch arguments are user-supplied and may contain JSON, spaces, `#`, etc.; each is single-quoted unless it consists entirely of safe shell characters (the same approach long used in adb-driven tooling for the same reason). Help text on --launch-args is updated to describe the Android shape (`adb shell am start args, e.g. --es key value` for typed Intent extras) and macOS remains the only rejected platform. Tests: - src/platforms/android/__tests__/index.test.ts: five new tests cover package, activity-override, deep-link URL, app-bound URL, and JSON-with-shell-metacharacters quoting paths. - src/core/__tests__/dispatch-open.test.ts: the previous "rejects Android launch arguments" test is inverted into a forwarding test that asserts openAndroidApp receives the args. Validated end-to-end on a Pixel emulator running a debug build whose launcher activity reads an Intent extra to bootstrap test configuration: a JSON value containing `#`, `/`, `:` survived single-quoted transit through `adb shell` and arrived at the activity unchanged.
d68f6f7 to
4c77aca
Compare
Stacked on top of the iOS-only --launch-args PR (callstackincubator#598). Removes the Android UNSUPPORTED_OPERATION guard added with the Maestro work and threads launchArgs through all five Android open paths. Per-path threading: - openAndroidPackage (-p package launch + activity-fallback) - openAndroidPackageActivity (-n component override) - openAndroidIntent (named intent action) - openAndroidDeepLink (-a VIEW -d <url>, with optional -p) - openAndroidAppBoundDeepLink (-a VIEW -d <url> -p <resolved>) `adb shell` joins its argv with spaces and feeds the result to a device shell, which re-tokenises. The other am-start arguments are well-known and never contain shell-significant characters, so they round-trip untouched. Launch arguments are user-supplied and may contain JSON, spaces, `#`, etc.; each is single-quoted unless it consists entirely of safe shell characters (the same approach long used in adb-driven tooling for the same reason). Help text on --launch-args is updated to describe the Android shape (`adb shell am start args, e.g. --es key value` for typed Intent extras) and macOS remains the only rejected platform. Tests: - src/platforms/android/__tests__/index.test.ts: five new tests cover package, activity-override, deep-link URL, app-bound URL, and JSON-with-shell-metacharacters quoting paths. - src/core/__tests__/dispatch-open.test.ts: the previous "rejects Android launch arguments" test is inverted into a forwarding test that asserts openAndroidApp receives the args. Validated end-to-end on a Pixel emulator running a debug build whose launcher activity reads an Intent extra to bootstrap test configuration: a JSON value containing `#`, `/`, `:` survived single-quoted transit through `adb shell` and arrived at the activity unchanged.
4c77aca to
51178ab
Compare
Stacked on top of the iOS-only --launch-args PR (callstackincubator#598). Removes the Android UNSUPPORTED_OPERATION guard added with the Maestro work and threads launchArgs through all five Android open paths. Per-path threading: - openAndroidPackage (-p package launch + activity-fallback) - openAndroidPackageActivity (-n component override) - openAndroidIntent (named intent action) - openAndroidDeepLink (-a VIEW -d <url>, with optional -p) - openAndroidAppBoundDeepLink (-a VIEW -d <url> -p <resolved>) `adb shell` joins its argv with spaces and feeds the result to a device shell, which re-tokenises. The other am-start arguments are well-known and never contain shell-significant characters, so they round-trip untouched. Launch arguments are user-supplied and may contain JSON, spaces, `#`, etc.; each is single-quoted unless it consists entirely of safe shell characters (the same approach long used in adb-driven tooling for the same reason). Help text on --launch-args is updated to describe the Android shape (`adb shell am start args, e.g. --es key value` for typed Intent extras) and macOS remains the only rejected platform. Tests: - src/platforms/android/__tests__/index.test.ts: five new tests cover package, activity-override, deep-link URL, app-bound URL, and JSON-with-shell-metacharacters quoting paths. - src/core/__tests__/dispatch-open.test.ts: the previous "rejects Android launch arguments" test is inverted into a forwarding test that asserts openAndroidApp receives the args. Validated end-to-end on a Pixel emulator running a debug build whose launcher activity reads an Intent extra to bootstrap test configuration: a JSON value containing `#`, `/`, `:` survived single-quoted transit through `adb shell` and arrived at the activity unchanged.
|
@thymikee good to go. As mentioned in the updated PR description, this PR is now scoped to iOS only and Android support will now be a follow-up. |
51178ab to
3a02cdc
Compare
| surface: enumField(SURFACE_VALUES), | ||
| activity: stringField('Android activity name.'), | ||
| launchConsole: stringField('Launch console mode.'), | ||
| launchArgs: stringArrayField('Launch arguments forwarded to the app.'), |
| const previousPath = process.env.PATH; | ||
| const previousArgsFile = process.env.AGENT_DEVICE_TEST_ARGS_FILE; | ||
| process.env.PATH = `${tmpDir}${path.delimiter}${previousPath ?? ''}`; | ||
| process.env.AGENT_DEVICE_TEST_ARGS_FILE = argsLogPath; |
ea66eac to
fbea566
Compare
fbea566 to
7431517
Compare
7431517 to
09a90af
Compare
09a90af to
4ffcfe8
Compare
4ffcfe8 to
8a2ed84
Compare
| const args = ['device', 'process', 'launch', '--device', device.id]; | ||
| if (options?.launchArgs && options.launchArgs.length > 0) { | ||
| if (options?.payloadUrl) { | ||
| args.push('--payload-url', options.payloadUrl); | ||
| } | ||
| // `devicectl` uses Swift ArgumentParser; without `--` a leading-dash app | ||
| // arg can be re-interpreted as a devicectl option. The marker must come | ||
| // before the positional bundle id so it is consumed by devicectl instead | ||
| // of being forwarded to the app as argv[1]. | ||
| args.push('--', bundleId, ...options.launchArgs); | ||
| } else { | ||
| args.push(bundleId); | ||
| if (options?.payloadUrl) { | ||
| args.push('--payload-url', options.payloadUrl); | ||
| } | ||
| } |
There was a problem hiding this comment.
will make devicectl use one canonical order: command options first (--device, optional --payload-url), then the positional bundle/app args boundary.
8a2ed84 to
9391dc0
Compare
9391dc0 to
a976fb6
Compare
| 'launch', | ||
| '--device', | ||
| 'ios-device-1', | ||
| 'com.apple.mobilesafari', |
There was a problem hiding this comment.
This changed to keep the devicectl invocation in one canonical order: options first, then positionals. So --payload-url is always emitted before the bundle id, regardless of whether launch args are present.
Without launch args:
devicectl device process launch --device <udid> --payload-url <url> <bundle>
With launch args:
devicectl device process launch --device <udid> --payload-url <url> -- <bundle> <args...>
This keeps the command structure predictable. All devicectl options, including --payload-url, come before the bundle id. Then the bundle id and app launch arguments come at the end.
|
@thymikee I had to revise the PR again due to additional changes on main. Please see this new version. I have also included videos showing expected launch arg handling on both simulator and device. |
a976fb6 to
ec0c004
Compare
Summary
Adds public
--launch-arg <arg>support toagent-device openfor iOS launches (Android to come as follow-up). The flag is repeatable and is forwarded through the CLI, client options, daemon request flags, and iOS launch implementation.Before: launch arguments existed only as internal iOS plumbing and were not available from the public
opencommand. After: callers can pass iOS launch arguments from CLI or client APIs, including dash-prefixed values such as-FeatureFlag. EmptylaunchArgs: []is normalized as “not provided” for programmatic callers.Platform behavior:
<device> <bundle id>insimctl launch, matchingsimctl's documented argv position.--before the positional bundle id (devicectl ... [options] -- <bundle> <args...>) so dash-prefixed app args are not parsed asdevicectloptions and the separator is not forwarded to the app. This also works with--payload-url.launchArgsbecausesimctl openurlignores launch args; callers should launch the app with args first, then open the URL separately.launchArgs; Android remains rejected by the existing platform guard and is out of scope.Validation
End-to-End:
Simulator
sim.mov
Device
device.mov
Automated validation passed:
pnpm formatpnpm typecheckpnpm exec vitest run src/platforms/ios/__tests__/index.test.ts src/utils/__tests__/args.test.tsBehavior covered by tests:
--launch-arg, including single-dash and double-dash values.devicectl-- <bundle>boundary.--payload-urlfollowed by-- <bundle> <args...>.launchArgsis non-empty.launchArgs: []behaves like no launch args for simulator URL opens and macOS.