@autifyhq/aximo-cli 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +79 -64
  2. package/dist/client.js +16 -1
  3. package/dist/commands/mobile/connect-core.d.ts +2 -1
  4. package/dist/commands/mobile/connect-core.js +104 -6
  5. package/dist/commands/mobile/connect.js +3 -1
  6. package/dist/commands/mobile/doctor.d.ts +3 -0
  7. package/dist/commands/mobile/doctor.js +44 -14
  8. package/dist/commands/mobile/doctor.test.d.ts +1 -0
  9. package/dist/commands/mobile/doctor.test.js +14 -0
  10. package/dist/commands/mobile/install.d.ts +9 -2
  11. package/dist/commands/mobile/install.js +43 -12
  12. package/dist/commands/mobile/install.test.js +44 -6
  13. package/dist/commands/mobile/list.d.ts +2 -0
  14. package/dist/commands/mobile/list.js +17 -11
  15. package/dist/commands/mobile/unregister.js +11 -6
  16. package/dist/mobile-control/app-install.d.ts +18 -5
  17. package/dist/mobile-control/app-install.js +33 -6
  18. package/dist/mobile-control/app-install.test.js +42 -7
  19. package/dist/mobile-control/appium-controller.d.ts +8 -0
  20. package/dist/mobile-control/appium-controller.js +19 -4
  21. package/dist/mobile-control/bundle-id-map.d.ts +9 -0
  22. package/dist/mobile-control/bundle-id-map.js +27 -0
  23. package/dist/mobile-control/devices.d.ts +6 -3
  24. package/dist/mobile-control/devices.js +26 -4
  25. package/dist/mobile-control/env-flags.d.ts +5 -0
  26. package/dist/mobile-control/env-flags.js +10 -0
  27. package/dist/mobile-control/env-flags.test.d.ts +1 -0
  28. package/dist/mobile-control/env-flags.test.js +21 -0
  29. package/dist/mobile-control/ios-auto-resign.d.ts +45 -0
  30. package/dist/mobile-control/ios-auto-resign.js +108 -0
  31. package/dist/mobile-control/ios-auto-resign.test.d.ts +1 -0
  32. package/dist/mobile-control/ios-auto-resign.test.js +150 -0
  33. package/dist/mobile-control/ios-devicectl.d.ts +39 -0
  34. package/dist/mobile-control/ios-devicectl.js +87 -0
  35. package/dist/mobile-control/ios-devicectl.test.d.ts +1 -0
  36. package/dist/mobile-control/ios-devicectl.test.js +110 -0
  37. package/dist/mobile-control/ios-identity.d.ts +27 -0
  38. package/dist/mobile-control/ios-identity.js +49 -0
  39. package/dist/mobile-control/ios-identity.test.d.ts +1 -0
  40. package/dist/mobile-control/ios-identity.test.js +102 -0
  41. package/dist/mobile-control/ios-mint.d.ts +39 -0
  42. package/dist/mobile-control/ios-mint.js +78 -0
  43. package/dist/mobile-control/ios-mint.test.d.ts +1 -0
  44. package/dist/mobile-control/ios-mint.test.js +94 -0
  45. package/dist/mobile-control/ios-profiles.d.ts +46 -0
  46. package/dist/mobile-control/ios-profiles.js +106 -0
  47. package/dist/mobile-control/ios-profiles.test.d.ts +1 -0
  48. package/dist/mobile-control/ios-profiles.test.js +154 -0
  49. package/dist/mobile-control/ios-resign.d.ts +37 -0
  50. package/dist/mobile-control/ios-resign.js +101 -0
  51. package/dist/mobile-control/ios-resign.test.d.ts +1 -0
  52. package/dist/mobile-control/ios-resign.test.js +84 -0
  53. package/dist/mobile-control/ios-session-error.d.ts +10 -0
  54. package/dist/mobile-control/ios-session-error.js +34 -0
  55. package/dist/mobile-control/ios-session-error.test.d.ts +1 -0
  56. package/dist/mobile-control/ios-session-error.test.js +30 -0
  57. package/dist/mobile-control/ios-signing.d.ts +33 -5
  58. package/dist/mobile-control/ios-signing.js +56 -0
  59. package/dist/mobile-control/ios-signing.test.js +87 -0
  60. package/dist/mobile-control/ios-tunnel.d.ts +54 -0
  61. package/dist/mobile-control/ios-tunnel.js +130 -0
  62. package/dist/mobile-control/ios-tunnel.test.d.ts +1 -0
  63. package/dist/mobile-control/ios-tunnel.test.js +115 -0
  64. package/dist/mobile-control/ios-wda-cache.d.ts +39 -0
  65. package/dist/mobile-control/ios-wda-cache.js +157 -0
  66. package/dist/mobile-control/ios-wda-cache.test.d.ts +1 -0
  67. package/dist/mobile-control/ios-wda-cache.test.js +128 -0
  68. package/dist/mobile-control/ios-wda-diagnostics.d.ts +14 -0
  69. package/dist/mobile-control/ios-wda-diagnostics.js +138 -0
  70. package/dist/mobile-control/ios-wda-diagnostics.test.d.ts +1 -0
  71. package/dist/mobile-control/ios-wda-diagnostics.test.js +77 -0
  72. package/dist/mobile-control/ios-wda-preinstall.d.ts +64 -0
  73. package/dist/mobile-control/ios-wda-preinstall.js +162 -0
  74. package/dist/mobile-control/ios-wda-preinstall.test.d.ts +1 -0
  75. package/dist/mobile-control/ios-wda-preinstall.test.js +107 -0
  76. package/dist/mobile-control/mobile-errors.d.ts +12 -0
  77. package/dist/mobile-control/mobile-errors.js +91 -0
  78. package/dist/mobile-control/mobile-errors.test.d.ts +1 -0
  79. package/dist/mobile-control/mobile-errors.test.js +48 -0
  80. package/dist/mobile-control/tunnel.js +45 -1
  81. package/package.json +2 -1
package/README.md CHANGED
@@ -64,47 +64,47 @@ Credentials and device registrations are **per-environment** — to switch betwe
64
64
 
65
65
  Run `aximo <command> --help` for detailed usage. `aximo schema [command.path]` returns machine-readable argument introspection (e.g. `aximo schema session.create`).
66
66
 
67
- | Command | Description |
68
- | ---------------------------- | ------------------------------------------------------- |
69
- | `aximo configure` | Save credentials to `$AXIMO_HOME/config.json` |
70
- | `aximo health check` | Check API reachability |
71
- | `aximo project list` | List organization projects |
72
- | `aximo session create` | Create and queue a test session |
73
- | `aximo session get` | Get session status |
74
- | `aximo session save-as-case` | Save a finished session as a reusable test case |
75
- | `aximo case create` | Create a test case |
76
- | `aximo case delete` | Delete a test case |
77
- | `aximo plan create` | Create a test plan |
78
- | `aximo plan add-case` | Add a case to a plan |
79
- | `aximo plan remove-case` | Remove a case from a plan |
80
- | `aximo trigger case run` | Trigger a dashboard-created test case |
81
- | `aximo trigger case get` | Get triggered case status |
82
- | `aximo trigger plan run` | Trigger a test plan |
83
- | `aximo trigger plan get` | Get plan run status |
84
- | `aximo upload create` | Create an upload slot for an APK/IPA |
85
- | `aximo upload list` | List previously uploaded apps |
86
- | `aximo upload get` | Poll upload processing status |
87
- | `aximo upload put` | Upload binary file to the slot |
88
- | `aximo upload submit` | Finalize upload and trigger processing |
89
- | `aximo device list` | List mobile devices from Device Farm |
90
- | `aximo device setup` | First-time BYOD onboarding (API + Python + register) |
91
- | `aximo device register` | Register this machine as a BYOD device |
92
- | `aximo device connect` | Start the BYOD agent and connect to the relay |
93
- | `aximo device status` | List your organization's BYOD devices |
94
- | `aximo device ping` | Check your BYOD device's server-side status |
95
- | `aximo device doctor` | Diagnose BYOD setup (env, Python, relay, status) |
96
- | `aximo device logs` | Show the BYOD agent log |
97
- | `aximo device unregister` | Remove a BYOD device server-side + clear local creds |
98
- | `aximo device reset` | Remove BYOD local state (Python, deps, credentials) |
99
- | `aximo mobile list` | List physically-connected phones (Android adb, iOS idb) |
100
- | `aximo mobile setup` | Install the Appium drivers used to drive phones |
101
- | `aximo mobile doctor` | Diagnose prerequisites (adb/idb, SDK/Xcode, driver) |
102
- | `aximo mobile register` | Register a connected Android phone or iPhone |
103
- | `aximo mobile install` | Install an app (.apk via adb, .ipa via idb) |
104
- | `aximo mobile connect` | Start managed Appium + open the tunnel for the phone |
105
- | `aximo mobile unregister` | Remove a local mobile device server-side + local creds |
106
- | `aximo schema` | Show CLI surface as JSON |
107
- | `aximo mcp` | Run a stdio MCP server for agents (see below) |
67
+ | Command | Description |
68
+ | ---------------------------- | -------------------------------------------------------------- |
69
+ | `aximo configure` | Save credentials to `$AXIMO_HOME/config.json` |
70
+ | `aximo health check` | Check API reachability |
71
+ | `aximo project list` | List organization projects |
72
+ | `aximo session create` | Create and queue a test session |
73
+ | `aximo session get` | Get session status |
74
+ | `aximo session save-as-case` | Save a finished session as a reusable test case |
75
+ | `aximo case create` | Create a test case |
76
+ | `aximo case delete` | Delete a test case |
77
+ | `aximo plan create` | Create a test plan |
78
+ | `aximo plan add-case` | Add a case to a plan |
79
+ | `aximo plan remove-case` | Remove a case from a plan |
80
+ | `aximo trigger case run` | Trigger a dashboard-created test case |
81
+ | `aximo trigger case get` | Get triggered case status |
82
+ | `aximo trigger plan run` | Trigger a test plan |
83
+ | `aximo trigger plan get` | Get plan run status |
84
+ | `aximo upload create` | Create an upload slot for an APK/IPA |
85
+ | `aximo upload list` | List previously uploaded apps |
86
+ | `aximo upload get` | Poll upload processing status |
87
+ | `aximo upload put` | Upload binary file to the slot |
88
+ | `aximo upload submit` | Finalize upload and trigger processing |
89
+ | `aximo device list` | List mobile devices from Device Farm |
90
+ | `aximo device setup` | First-time BYOD onboarding (API + Python + register) |
91
+ | `aximo device register` | Register this machine as a BYOD device |
92
+ | `aximo device connect` | Start the BYOD agent and connect to the relay |
93
+ | `aximo device status` | List your organization's BYOD devices |
94
+ | `aximo device ping` | Check your BYOD device's server-side status |
95
+ | `aximo device doctor` | Diagnose BYOD setup (env, Python, relay, status) |
96
+ | `aximo device logs` | Show the BYOD agent log |
97
+ | `aximo device unregister` | Remove a BYOD device server-side + clear local creds |
98
+ | `aximo device reset` | Remove BYOD local state (Python, deps, credentials) |
99
+ | `aximo mobile list` | List physically-connected phones (Android adb, iOS devicectl) |
100
+ | `aximo mobile setup` | Install the Appium drivers used to drive phones |
101
+ | `aximo mobile doctor` | Diagnose prerequisites (adb/Xcode/devicectl, signing, driver) |
102
+ | `aximo mobile register` | Register a connected Android phone or iPhone |
103
+ | `aximo mobile install` | Install an app (.apk via adb, .ipa auto-re-signed + devicectl) |
104
+ | `aximo mobile connect` | Start managed Appium + open the tunnel for the phone |
105
+ | `aximo mobile unregister` | Remove a local mobile device server-side + local creds |
106
+ | `aximo schema` | Show CLI surface as JSON |
107
+ | `aximo mcp` | Run a stdio MCP server for agents (see below) |
108
108
 
109
109
  Mutating commands support `--json '<raw body>'` for direct OpenAPI payloads and `--dry-run` to preview requests without executing.
110
110
 
@@ -405,7 +405,7 @@ Default `$AXIMO_HOME` is `~/.aximo` on macOS/Linux, `%USERPROFILE%\.aximo` on Wi
405
405
 
406
406
  The `mobile` commands let you register a **physically-connected Android phone or iPhone** as an Aximo device and drive it locally, over the same gateway tunnel as desktop BYOD. Aximo's agent controls the phone through a CLI-managed [Appium](https://appium.io) server — UiAutomator2 for Android, XCUITest for iOS — useful for running mobile tests against your own handset instead of a Device Farm device.
407
407
 
408
- > **iOS is macOS-only and needs Xcode + an Apple Developer account.** Driving a physical iPhone goes through Appium's XCUITest driver, which builds and code-signs a WebDriverAgent (WDA) runner onto the device. That requires macOS, Xcode, an Apple Developer **Team ID**, and a WDA bundle id backed by a valid provisioning profile — set via the `AXIMO_IOS_*` env vars below. Android has no such requirement, so iOS setup is heavier; everything after setup (`register` → `connect` → run) works the same on both platforms.
408
+ > **iOS is macOS-only and needs Xcode + an Apple ID.** Driving a physical iPhone requires a development-signed WebDriverAgent (WDA) runner on the device — an Apple platform rule, not an Aximo one. The CLI is **zero-config about it**: it discovers your signing identity and Team ID from the keychain, finds or mints provisioning profiles via Xcode automatic signing, and (on iOS 17+) preinstalls a prebuilt WDA with no Xcode build. A free/personal Apple ID works. Android has no signing requirement, so iOS setup is heavier; everything after setup (`register` → `connect` → run) works the same on both platforms.
409
409
 
410
410
  Terminology note: **`aximo mobile list`** shows phones plugged into _this_ machine; **`aximo device list`** (under [Device / BYOD](#device-byod--bring-your-own-device)) returns mobile devices from AWS Device Farm. Don't confuse the two.
411
411
 
@@ -418,7 +418,7 @@ Terminology note: **`aximo mobile list`** shows phones plugged into _this_ machi
418
418
  - **iOS** — **Developer Mode** enabled (Settings → Privacy & Security) and the computer trusted.
419
419
  - **On the machine that runs `aximo mobile connect`**:
420
420
  - **Android** — `adb` and the Android SDK, with `ANDROID_HOME` set — see [Set up your computer (Android)](#set-up-your-computer-android).
421
- - **iOS** — macOS with **Xcode** + command line tools, **`idb`**, and the `AXIMO_IOS_*` signing env vars — see [Set up your computer (iOS)](#set-up-your-computer-ios).
421
+ - **iOS** — macOS with **Xcode** and an Apple ID signed into it — see [Set up your computer (iOS)](#set-up-your-computer-ios).
422
422
 
423
423
  > The Appium server is bundled with the CLI; `aximo mobile setup` installs the UiAutomator2 (Android) and/or XCUITest (iOS) driver — you don't install or manage Appium yourself.
424
424
 
@@ -460,30 +460,43 @@ source ~/.bashrc
460
460
 
461
461
  ### Set up your computer (iOS)
462
462
 
463
- iOS local devices are **macOS-only** (Appium's XCUITest driver delegates to Xcode). You need Xcode, `idb`, and the signing env vars.
463
+ iOS local devices are **macOS-only** (device control delegates to Xcode's toolchain). Setup is two steps — no env vars:
464
464
 
465
465
  ```bash
466
- # 1. Xcode + command line tools (XCUITest builds WebDriverAgent with them):
467
- xcode-select --install # or install Xcode from the App Store, then: sudo xcodebuild -license accept
466
+ # 1. Xcode (full app, from the App Store) + accept the license:
467
+ sudo xcodebuild -license accept
468
468
 
469
- # 2. idb — discovers connected iPhones and installs .ipa builds:
470
- brew tap facebook/fb && brew install idb-companion
471
- pipx install fb-idb # or: pip install fb-idb
472
-
473
- # 3. Signing config for WebDriverAgent (required for a physical iPhone):
474
- export AXIMO_IOS_TEAM_ID="ABCDE12345" # your Apple Developer Team ID (developer.apple.com/account → Membership)
475
- # Optional overrides (sensible defaults shown):
476
- export AXIMO_IOS_SIGNING_ID="Apple Development"
477
- export AXIMO_IOS_WDA_BUNDLE_ID="com.facebook.WebDriverAgentRunner"
478
- # Optional: reuse a prebuilt WDA to skip per-session rebuilds:
479
- # export AXIMO_IOS_DERIVED_DATA_PATH="$HOME/Library/Developer/Xcode/DerivedData/WebDriverAgent-xxxx"
469
+ # 2. Sign into Xcode with your Apple ID and create a development certificate:
470
+ # Xcode → Settings → Accounts → + (your Apple ID)
471
+ # → Manage Certificates… → + → Apple Development
480
472
  ```
481
473
 
482
- `AXIMO_IOS_TEAM_ID` is the load-bearing setting: the XCUITest driver uses it (with the signing id and WDA bundle id) to build and sign WebDriverAgent so it can run on the device. The WDA bundle id must be associated with a valid provisioning profile under that team. Run `aximo mobile doctor --platform ios` to confirm Xcode, `idb`, and the signing config are all present.
474
+ Everything else is discovered automatically at `connect`/`install` time:
475
+
476
+ - **Signing identity & Team ID** — read from the keychain (`security find-identity`); no `AXIMO_IOS_TEAM_ID` needed. On a machine with several teams, set `AXIMO_IOS_TEAM_ID` (or MobileLink's `XCODE_TEAM_ID`) to pick one.
477
+ - **Provisioning profiles** — scanned from disk and matched against the device; when none fits, the CLI **mints one via Xcode automatic signing** (first run takes a minute or two; requires the Apple ID signed into Xcode).
478
+ - **WebDriverAgent** — on **iOS 17+**, a prebuilt WDA matching the bundled driver is downloaded, re-signed with your identity, and **preinstalled — no Xcode build, no iOS platform download**. Older devices fall back to the classic Xcode build (cached per device under `$AXIMO_HOME/ios-wda/<udid>/`). A WDA bundle id is derived per team (existing `*.WebDriverAgentRunner` profiles are reused); override with `AXIMO_IOS_WDA_BUNDLE_ID`.
479
+ - **App installs** — `.ipa` files that aren't signed for your device are **re-signed automatically** (see `aximo mobile install` below).
480
+ - **Device discovery** — via Xcode's `devicectl`; `idb` is only an optional fallback (`brew install idb-companion && pipx install fb-idb`).
483
481
 
484
- > Already set up Autify's MobileLink? The CLI also reads MobileLink's names as fallbacks — `XCODE_TEAM_ID`, `XCODE_SIGN_ID`, `WDA_BUNDLE_ID` — so an existing host needs no reconfiguration. `AXIMO_IOS_*` takes precedence when both are set.
482
+ Optional overrides / opt-outs:
485
483
 
486
- > First connect to a physical iPhone is slow: Xcode builds and installs WebDriverAgent (a minute or two). If the build fails on signing, open `WebDriverAgent.xcodeproj` in Xcode once, pick your team, and let it auto-manage signing — or set `AXIMO_IOS_DERIVED_DATA_PATH` to a prebuilt WDA.
484
+ ```bash
485
+ # export AXIMO_IOS_TEAM_ID="ABCDE12345" # pick a team on multi-team machines (else auto-detected)
486
+ # export AXIMO_IOS_SIGNING_ID="Apple Development"
487
+ # export AXIMO_IOS_WDA_BUNDLE_ID="com.<you>.WebDriverAgentRunner" # else derived/reused automatically
488
+ # export AXIMO_IOS_NO_RESIGN=1 # never re-sign the app under test
489
+ # export AXIMO_IOS_NO_PREINSTALLED_WDA=1 # force the classic Xcode build flow for WDA
490
+ # export AXIMO_IOS_DERIVED_DATA_PATH=... # pin a prebuilt WDA DerivedData (build flow)
491
+ # export AXIMO_IOS_NO_WDA_CACHE=1 # disable the WDA build cache (build flow)
492
+ # export AXIMO_IOS_SKIP_TUNNEL=1 # don't auto-start the RemoteXPC device tunnel (you manage it)
493
+ ```
494
+
495
+ Run `aximo mobile doctor --platform ios` to see what was discovered (identity, team, WDA id) and what's missing; failed commands include a `hint` naming the exact fix.
496
+
497
+ > **On-device steps you can't skip** (Apple requires them once per phone): enable **Developer Mode** (Settings → Privacy & Security → Developer Mode; the phone restarts), tap **Trust This Computer**, and — after the first install of an app signed by your team — **trust the developer certificate** (Settings → General → VPN & Device Management → your "Apple Development" profile → Trust; the phone needs internet). On **free/personal teams** profiles expire every ~7 days; the CLI re-mints them automatically, but a re-mint **resets the certificate trust**, so expect that trust tap to recur weekly. Free teams are also capped at 10 App IDs per week.
498
+ >
499
+ > On **iOS 17+**, `aximo mobile connect` also starts a small **device tunnel that needs admin rights** — it prompts once for your macOS password / Touch ID (reused on later runs, so at most once per boot). Devices below iOS 17 skip the tunnel entirely. If anything mentions "iOS X is not installed", run `xcodebuild -downloadPlatform iOS` (only the classic build flow needs it).
487
500
 
488
501
  ### Enable USB debugging on the phone
489
502
 
@@ -507,10 +520,10 @@ aximo mobile setup # installs the Appium driver(s) for
507
520
  aximo mobile doctor # checks tooling / drivers / bundled server
508
521
  # Limit to one platform if you only test on one:
509
522
  aximo mobile setup --platform ios # installs only the XCUITest driver
510
- aximo mobile doctor --platform ios # checks Xcode / idb / signing config
523
+ aximo mobile doctor --platform ios # checks Xcode / devicectl / discovered signing
511
524
  ```
512
525
 
513
- `setup` installs the Appium driver(s) — UiAutomator2 for Android, XCUITest for iOS — then prints what (if anything) is still missing. `doctor` is the first thing to run when anything's off: for Android it reports `adb` and the Android SDK; for iOS it reports `xcrun`/Xcode, `idb`, and your `AXIMO_IOS_TEAM_ID` signing config — alongside the bundled Appium server and driver — and exits non-zero if a required check fails.
526
+ `setup` installs the Appium driver(s) — UiAutomator2 for Android, XCUITest for iOS — then prints what (if anything) is still missing. `doctor` is the first thing to run when anything's off: for Android it reports `adb` and the Android SDK; for iOS it reports `xcrun`/Xcode, the discovered signing identity/team, and whether the iOS device tunnel is running — alongside the bundled Appium server and driver — and exits non-zero if a required check fails.
514
527
 
515
528
  ### Registering and connecting
516
529
 
@@ -526,7 +539,7 @@ aximo mobile connect # starts managed Ap
526
539
  # Ctrl+C to disconnect
527
540
  ```
528
541
 
529
- `register` writes credentials to `$AXIMO_HOME/mobile-devices/<deviceId>.json` (mode `0600`). It targets the single connected phone by default — pass `--serial` (Android) or `--udid` (iOS) to pick a specific one when several are attached. `connect` re-checks prerequisites, starts the managed Appium server, and opens the tunnel; it runs until Ctrl+C. If you have more than one registered phone, pass `--device-id` to choose which to connect.
542
+ `register` writes credentials to `$AXIMO_HOME/mobile-devices/<deviceId>.json` (mode `0600`). It targets the single connected phone by default — pass `--serial` (Android) or `--udid` (iOS) to pick a specific one when several are attached. `connect` re-checks prerequisites, then — on iOS 17+ — ensures the device tunnel is running (prompting once for admin the first time), starts the managed Appium server, and opens the gateway tunnel to Aximo; it runs until Ctrl+C. If you have more than one registered phone, pass `--device-id` to choose which to connect.
530
543
 
531
544
  Once connected, create a **mobile session in Aximo targeting this local device** — the agent routes its actions through the tunnel to the phone over Appium.
532
545
 
@@ -536,9 +549,11 @@ Unlike Device Farm, a local phone runs the apps already installed on it. To test
536
549
 
537
550
  ```bash
538
551
  aximo mobile install --app ./app-debug.apk # Android: adb install -r
539
- aximo mobile install --app ./MyApp.ipa # iOS: idb install (the .ipa must be signed for the device)
552
+ aximo mobile install --app ./MyApp.ipa # iOS: auto-re-signed for the device if needed, installed via devicectl
540
553
  ```
541
554
 
555
+ On iOS the CLI inspects the `.ipa` first: if its embedded profile already covers the device it installs as-is; otherwise it **re-signs it with your discovered identity** (the same thing AWS Device Farm does to every upload — so the exact `.ipa` you upload there works here). When the bundle id has to be remapped (free teams), launches by the original bundle id are translated automatically during `connect` sessions, and the JSON result reports both ids. App Store downloads are FairPlay-encrypted and can't be re-signed — use a development/ad-hoc build. Opt out with `AXIMO_IOS_NO_RESIGN=1`.
556
+
542
557
  Then create the session with the app's bundle id / package name as `--bundle-id` — the agent launches it on startup before the test begins:
543
558
 
544
559
  ```bash
package/dist/client.js CHANGED
@@ -94,7 +94,22 @@ export async function requestToFile(path, destPath, deps = {}) {
94
94
  const baseUrl = deps.baseUrl ?? getBaseUrl();
95
95
  const fetchImpl = deps.fetchImpl ?? fetch;
96
96
  const url = buildUrl(baseUrl, path);
97
- const res = await fetchImpl(url, { method: "GET", headers: { "x-api-key": apiKey } });
97
+ // Redirects (e.g. to a presigned S3 URL) are followed manually WITHOUT the
98
+ // x-api-key header — fetch only strips authorization/cookie on cross-origin
99
+ // redirects, so auto-follow would leak the org API key to the storage host.
100
+ let res = await fetchImpl(url, {
101
+ method: "GET",
102
+ headers: { "x-api-key": apiKey },
103
+ redirect: "manual",
104
+ });
105
+ // Only true redirects — other 3xx (e.g. 304 Not Modified) carry no Location
106
+ // and must fall through to normal handling instead of throwing here.
107
+ if ([301, 302, 303, 307, 308].includes(res.status)) {
108
+ const location = res.headers.get("location");
109
+ if (!location)
110
+ throw new Error(`Redirect from ${path} is missing a Location header`);
111
+ res = await fetchImpl(new URL(location, url).toString(), { method: "GET" });
112
+ }
98
113
  if (!res.ok) {
99
114
  const retryAfter = res.headers.get("retry-after") ?? undefined;
100
115
  throw httpError(res.status, await errorMessage(res), retryAfter);
@@ -1,5 +1,6 @@
1
1
  import type { ErrorEnvelope } from "../../types.js";
2
2
  import type { MobileController } from "../../mobile-control/controller.js";
3
+ import { type IosSigningConfig } from "../../mobile-control/ios-signing.js";
3
4
  import { type MobileDeviceConfig } from "./paths.js";
4
5
  export declare function getTunnelOrigin(): string;
5
6
  interface ConfigOk {
@@ -7,6 +8,6 @@ interface ConfigOk {
7
8
  config: MobileDeviceConfig;
8
9
  }
9
10
  export declare function resolveDeviceConfig(deviceId: string | undefined): ConfigOk | ErrorEnvelope;
10
- export declare function buildController(config: MobileDeviceConfig, endpoint: string): MobileController;
11
+ export declare function buildController(config: MobileDeviceConfig, endpoint: string, iosSigning?: IosSigningConfig, wdaLocalPort?: number | (() => Promise<number>)): MobileController;
11
12
  export declare function connectWithConfig(config: MobileDeviceConfig): Promise<void>;
12
13
  export {};
@@ -2,8 +2,13 @@ import { getBaseUrl } from "../../client.js";
2
2
  import { formatError } from "../../output.js";
3
3
  import { AppiumController } from "../../mobile-control/appium-controller.js";
4
4
  import { installUploadedApp } from "../../mobile-control/app-install.js";
5
- import { startAppiumServer } from "../../mobile-control/appium-server.js";
6
- import { resolveIosSigning } from "../../mobile-control/ios-signing.js";
5
+ import { pickFreePort, startAppiumServer } from "../../mobile-control/appium-server.js";
6
+ import { withBundleIdTranslation } from "../../mobile-control/bundle-id-map.js";
7
+ import { getIosDeviceInfo, iosMajorVersion } from "../../mobile-control/ios-devicectl.js";
8
+ import { ensureIosTunnel } from "../../mobile-control/ios-tunnel.js";
9
+ import { resolveIosSigning, resolveIosSigningAuto, } from "../../mobile-control/ios-signing.js";
10
+ import { resolveWdaCaching } from "../../mobile-control/ios-wda-cache.js";
11
+ import { resolvePreinstalledWdaStrategy } from "../../mobile-control/ios-wda-preinstall.js";
7
12
  import { runControlTunnel } from "../../mobile-control/tunnel.js";
8
13
  import { runMobileDoctor } from "./doctor.js";
9
14
  import { listMobileDeviceConfigs, readMobileDeviceConfig, } from "./paths.js";
@@ -33,12 +38,14 @@ export function resolveDeviceConfig(deviceId) {
33
38
  }
34
39
  return { ok: true, config: only };
35
40
  }
36
- export function buildController(config, endpoint) {
41
+ export function buildController(config, endpoint, iosSigning, wdaLocalPort) {
37
42
  return new AppiumController({
38
43
  endpoint,
39
44
  platform: config.platform,
40
45
  target: config.target,
41
- ...(config.platform === "ios" ? { iosSigning: resolveIosSigning() } : {}),
46
+ ...(config.platform === "ios"
47
+ ? { iosSigning: iosSigning ?? resolveIosSigning(), ...(wdaLocalPort ? { wdaLocalPort } : {}) }
48
+ : {}),
42
49
  });
43
50
  }
44
51
  // A blocking check is any hard failure, plus a missing automation driver
@@ -61,23 +68,114 @@ async function assertPrerequisites(platform) {
61
68
  throw new Error(`Missing ${platform} prerequisites:\n${lines.join("\n")}\n` +
62
69
  `Run \`${setup}\` and/or fix the above, then re-run. (\`${doctor}\` for details.)`);
63
70
  }
71
+ /**
72
+ * Preflight the physical device state via devicectl and fail with the exact
73
+ * on-device fix when an onboarding toggle is missing. Returns the device's iOS
74
+ * major version (null when devicectl can't see the device).
75
+ */
76
+ async function preflightIosDevice(udid) {
77
+ const info = await getIosDeviceInfo(udid);
78
+ if (!info)
79
+ return null;
80
+ if (info.developerModeEnabled === false) {
81
+ throw new Error("Developer Mode is disabled on the iPhone. Enable it on the device: Settings → " +
82
+ "Privacy & Security → Developer Mode → On (the phone restarts), then re-run.");
83
+ }
84
+ if (info.paired === false) {
85
+ throw new Error("The iPhone is not paired with this Mac. Unlock the phone, reconnect the USB cable, " +
86
+ "and tap 'Trust This Computer'.");
87
+ }
88
+ return iosMajorVersion(info.osVersion);
89
+ }
64
90
  export async function connectWithConfig(config) {
65
91
  await assertPrerequisites(config.platform);
92
+ let iosMajor = null;
93
+ if (config.platform === "ios") {
94
+ iosMajor = await preflightIosDevice(config.target);
95
+ // Physical iPhones on iOS 17+ need the RemoteXPC tunnel before XCUITest can
96
+ // reach WebDriverAgent; start it (prompting for admin) unless one is running.
97
+ // Older devices use the classic lockdown path — no tunnel, no admin prompt.
98
+ if (iosMajor !== null && iosMajor < 17) {
99
+ console.error(`iOS ${iosMajor} device — skipping the RemoteXPC tunnel (not needed).`);
100
+ }
101
+ else {
102
+ const tunnel = await ensureIosTunnel();
103
+ if (tunnel.reused)
104
+ console.error("Reusing the running iOS device tunnel.");
105
+ }
106
+ }
66
107
  const useDrivers = config.platform === "ios" ? ["xcuitest"] : ["uiautomator2"];
67
108
  const server = await startAppiumServer({ useDrivers });
68
- const controller = buildController(config, server.endpoint);
109
+ // WDA strategy ladder: (1) preinstalled prebuilt runner — no xcodebuild at all
110
+ // (iOS 17+, needs a discovered identity; profiles are minted on demand);
111
+ // (2) cached xcodebuild output (usePrebuiltWDA) when still valid; (3) full
112
+ // driver-managed xcodebuild. Everything is discovered from the keychain and
113
+ // installed profiles; env vars only override.
114
+ let iosSigning;
115
+ let wdaLocalPort;
116
+ if (config.platform === "ios") {
117
+ const signing = await resolveIosSigningAuto({ udid: config.target });
118
+ if (signing.teamId) {
119
+ console.error(`Signing as team ${signing.teamId} (WDA id ${signing.wdaBundleId}).`);
120
+ }
121
+ else {
122
+ console.error("No Apple signing identity found — physical-device WebDriverAgent signing will fail. " +
123
+ "Open Xcode → Settings → Accounts to add your Apple ID (see `aximo mobile doctor`).");
124
+ }
125
+ const preinstalled = await resolvePreinstalledWdaStrategy(signing, {
126
+ udid: config.target,
127
+ iosMajor,
128
+ log: (message) => console.error(message),
129
+ });
130
+ if (preinstalled) {
131
+ iosSigning = preinstalled;
132
+ console.error("Using the preinstalled WebDriverAgent (no Xcode build needed).");
133
+ }
134
+ else {
135
+ iosSigning = signing.teamId
136
+ ? await resolveWdaCaching(signing, { udid: config.target })
137
+ : signing;
138
+ if (iosSigning.usePrebuiltWDA)
139
+ console.error("Reusing the cached WebDriverAgent build.");
140
+ }
141
+ // Resolved lazily right before the Appium session is created — a port picked
142
+ // here would sit unbound (and stealable) until the first remote action.
143
+ wdaLocalPort = pickFreePort;
144
+ }
145
+ // Re-signing can remap bundle ids; launches from the backend use the original.
146
+ const installedBundleIds = new Map();
147
+ const rawController = buildController(config, server.endpoint, iosSigning, wdaLocalPort);
148
+ const controller = config.platform === "ios"
149
+ ? withBundleIdTranslation(rawController, installedBundleIds)
150
+ : rawController;
69
151
  try {
152
+ // Warm up the device session before connecting to Aximo so WDA build/signing/
153
+ // trust problems surface here — with a clear, actionable message — instead of
154
+ // failing opaquely on the first server-driven action.
155
+ if (config.platform === "ios") {
156
+ console.error("Preparing WebDriverAgent on the device…");
157
+ await controller.screenshot();
158
+ console.error("Device is ready.");
159
+ }
70
160
  await runControlTunnel({
71
161
  wsUrl: getTunnelOrigin(),
72
162
  deviceToken: config.deviceToken,
73
163
  deviceName: config.name,
74
164
  controller,
75
165
  installApp: async ({ uploadId }) => {
76
- await installUploadedApp({
166
+ const result = await installUploadedApp({
77
167
  uploadId,
78
168
  platform: config.platform,
79
169
  target: config.target,
170
+ log: (message) => console.error(message),
80
171
  });
172
+ if (result.originalBundleId &&
173
+ result.installedBundleId &&
174
+ result.originalBundleId !== result.installedBundleId) {
175
+ installedBundleIds.set(result.originalBundleId, result.installedBundleId);
176
+ console.error(`App re-signed for this device: ${result.originalBundleId} installed as ` +
177
+ `${result.installedBundleId}.`);
178
+ }
81
179
  },
82
180
  });
83
181
  }
@@ -1,4 +1,5 @@
1
1
  import { defineCommand } from "citty";
2
+ import { mobileErrorHint } from "../../mobile-control/mobile-errors.js";
2
3
  import { formatError, printResult } from "../../output.js";
3
4
  import { connectWithConfig, resolveDeviceConfig } from "./connect-core.js";
4
5
  const IOS_PHYSICAL_WARNING = "Driving a physical iPhone goes through Appium's XCUITest driver, which builds and " +
@@ -29,7 +30,8 @@ export default defineCommand({
29
30
  await connectWithConfig(resolved.config);
30
31
  }
31
32
  catch (error) {
32
- printResult(formatError("api_error", error instanceof Error ? error.message : String(error), true));
33
+ const message = error instanceof Error ? error.message : String(error);
34
+ printResult(formatError("api_error", message, true, mobileErrorHint(message, resolved.config.platform)));
33
35
  }
34
36
  },
35
37
  });
@@ -15,8 +15,11 @@ export interface MobileDoctorReport {
15
15
  info: number;
16
16
  };
17
17
  }
18
+ export declare function checkIosTunnel(isHealthy?: (port: number) => Promise<boolean>): Promise<Check>;
18
19
  export interface RunMobileDoctorOptions {
19
20
  platform?: "ios" | "android" | undefined;
21
+ /** Injectable tunnel probe for tests. */
22
+ isTunnelHealthy?: (port: number) => Promise<boolean>;
20
23
  }
21
24
  export declare function runMobileDoctor(options?: RunMobileDoctorOptions): Promise<Envelope<MobileDoctorReport>>;
22
25
  declare const _default: import("citty").CommandDef<{
@@ -6,7 +6,8 @@ import { join } from "node:path";
6
6
  import { formatSuccess, printResult } from "../../output.js";
7
7
  import { importAppium } from "../../mobile-control/appium-import.js";
8
8
  import { getAppiumHome, resolveAndroidSdk } from "../../mobile-control/appium-server.js";
9
- import { resolveIosSigning } from "../../mobile-control/ios-signing.js";
9
+ import { resolveIosSigningAuto } from "../../mobile-control/ios-signing.js";
10
+ import { DEFAULT_TUNNEL_REGISTRY_PORT, isTunnelHealthy } from "../../mobile-control/ios-tunnel.js";
10
11
  function which(cmd) {
11
12
  const probe = platform() === "win32" ? "where" : "which";
12
13
  const result = spawnSync(probe, [cmd], { encoding: "utf-8" });
@@ -78,7 +79,7 @@ function checkAndroid() {
78
79
  });
79
80
  return checks;
80
81
  }
81
- function checkIos() {
82
+ async function checkIos() {
82
83
  if (platform() !== "darwin") {
83
84
  return [
84
85
  { name: "ios", status: "info", message: "iOS device control is only available on macOS" },
@@ -104,37 +105,66 @@ function checkIos() {
104
105
  });
105
106
  const idb = which("idb");
106
107
  checks.push(idb
107
- ? { name: "idb", status: "pass", message: idb }
108
+ ? { name: "idb", status: "info", message: idb }
108
109
  : {
109
110
  name: "idb",
110
- status: "warn",
111
- message: "idb not found",
112
- hint: "Install idb (brew install idb-companion && pipx install fb-idb). It's used to discover connected iPhones and install .ipa builds.",
111
+ status: "info",
112
+ message: "idb not found (optional)",
113
+ hint: "Device discovery and installs use Xcode's devicectl; idb is only a fallback. Install it with `brew install idb-companion && pipx install fb-idb` if devicectl can't see your device.",
113
114
  });
114
- const teamId = resolveIosSigning().teamId;
115
- checks.push(teamId
116
- ? { name: "ios signing", status: "pass", message: `signing team id is set (${teamId})` }
115
+ // Zero-config signing: the identity + team come from the keychain; env vars
116
+ // (AXIMO_IOS_TEAM_ID etc.) are overrides, not requirements.
117
+ const signing = await resolveIosSigningAuto();
118
+ checks.push(signing.teamId
119
+ ? {
120
+ name: "ios signing",
121
+ status: "pass",
122
+ message: signing.identityHash
123
+ ? `signing identity found (team ${signing.teamId}, WDA id ${signing.wdaBundleId})`
124
+ : `signing team id set (${signing.teamId}) but no matching keychain identity`,
125
+ }
117
126
  : {
118
127
  name: "ios signing",
119
128
  status: "warn",
120
- message: "no signing team id is set",
121
- hint: "Driving a physical iPhone needs an Apple Developer Team ID to sign WebDriverAgent. Set AXIMO_IOS_TEAM_ID (or MobileLink's XCODE_TEAM_ID), and optionally AXIMO_IOS_SIGNING_ID / AXIMO_IOS_WDA_BUNDLE_ID.",
129
+ message: "no Apple Development signing identity found",
130
+ hint: "Open Xcode → Settings → Accounts, add your Apple ID, then Manage Certificates → + → Apple Development. A free Apple ID works (with weekly re-signing); AXIMO_IOS_TEAM_ID overrides team selection on multi-team machines.",
122
131
  });
123
132
  checks.push({
124
133
  name: "ios webdriveragent",
125
134
  status: "info",
126
- message: "Physical iPhones are driven via Appium's XCUITest driver, which builds and installs a WebDriverAgent runner on the device.",
127
- hint: "Enable Developer Mode (Settings → Privacy & Security), trust this computer, and ensure the WDA bundle id is backed by a valid provisioning profile.",
135
+ message: "On iOS 17+ a prebuilt WebDriverAgent is downloaded, re-signed and preinstalled (no Xcode build); older devices fall back to the Xcode build flow.",
136
+ hint: "One-time on the phone: enable Developer Mode (Settings → Privacy & Security), trust this computer, and trust the developer certificate (Settings → General → VPN & Device Management) after the first install.",
128
137
  });
129
138
  return checks;
130
139
  }
140
+ // The RemoteXPC tunnel is host-level (a registry port), so doctor can probe it
141
+ // without a connected device. `connect` starts it automatically, so a missing
142
+ // tunnel is informational, not a failure.
143
+ export async function checkIosTunnel(isHealthy = isTunnelHealthy) {
144
+ const running = await isHealthy(DEFAULT_TUNNEL_REGISTRY_PORT);
145
+ return running
146
+ ? {
147
+ name: "ios tunnel",
148
+ status: "pass",
149
+ message: `RemoteXPC tunnel running (registry port ${DEFAULT_TUNNEL_REGISTRY_PORT})`,
150
+ }
151
+ : {
152
+ name: "ios tunnel",
153
+ status: "info",
154
+ message: "RemoteXPC tunnel not running",
155
+ hint: "iOS 17+ needs it to reach WebDriverAgent; `aximo mobile connect` starts it automatically (prompts for admin).",
156
+ };
157
+ }
131
158
  export async function runMobileDoctor(options = {}) {
132
159
  const checks = [await checkAppium()];
133
160
  if (options.platform !== "ios") {
134
161
  checks.push(...checkAndroid());
135
162
  }
136
163
  if (options.platform !== "android") {
137
- checks.push(...checkIos());
164
+ checks.push(...(await checkIos()));
165
+ if (platform() === "darwin") {
166
+ checks.push(await checkIosTunnel(options.isTunnelHealthy));
167
+ }
138
168
  }
139
169
  const summary = { pass: 0, warn: 0, fail: 0, info: 0 };
140
170
  for (const c of checks)
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { checkIosTunnel } from "./doctor.js";
3
+ describe("checkIosTunnel", () => {
4
+ it("passes when the RemoteXPC tunnel is reachable", async () => {
5
+ const check = await checkIosTunnel(async () => true);
6
+ expect(check.status).toBe("pass");
7
+ expect(check.name).toBe("ios tunnel");
8
+ });
9
+ it("is informational (not a failure) when the tunnel is down", async () => {
10
+ const check = await checkIosTunnel(async () => false);
11
+ expect(check.status).toBe("info");
12
+ expect(check.hint).toContain("aximo mobile connect");
13
+ });
14
+ });
@@ -1,4 +1,6 @@
1
1
  import { type ExecFn } from "../../mobile-control/exec.js";
2
+ import { ensureInstallableIpa } from "../../mobile-control/ios-auto-resign.js";
3
+ import { installIosApp } from "../../mobile-control/ios-devicectl.js";
2
4
  import type { Envelope } from "../../types.js";
3
5
  import type { MobilePlatform } from "./paths.js";
4
6
  export interface MobileInstallResult {
@@ -9,6 +11,10 @@ export interface MobileInstallResult {
9
11
  udid?: string;
10
12
  app: string;
11
13
  output: string;
14
+ /** iOS only: set when the app was auto-re-signed for this device. */
15
+ resigned?: boolean;
16
+ originalBundleId?: string;
17
+ installedBundleId?: string;
12
18
  }
13
19
  export interface MobileInstallOptions {
14
20
  app?: string | undefined;
@@ -18,9 +24,10 @@ export interface MobileInstallOptions {
18
24
  }
19
25
  /**
20
26
  * Install an app on the connected phone: Android `.apk` via `adb install -r`,
21
- * iOS `.ipa` via `idb install`. The iOS build must be signed for the device.
27
+ * iOS `.ipa` via devicectl — auto-re-signed for the device first when its
28
+ * signature doesn't already cover it (disable with AXIMO_IOS_NO_RESIGN).
22
29
  */
23
- export declare function runMobileInstall(options: MobileInstallOptions, exec?: ExecFn): Promise<Envelope<MobileInstallResult>>;
30
+ export declare function runMobileInstall(options: MobileInstallOptions, exec?: ExecFn, ensureInstallable?: typeof ensureInstallableIpa, installIos?: typeof installIosApp): Promise<Envelope<MobileInstallResult>>;
24
31
  declare const _default: import("citty").CommandDef<{
25
32
  readonly app: {
26
33
  readonly type: "string";