@autifyhq/aximo-cli 0.5.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 (98) hide show
  1. package/README.md +196 -64
  2. package/dist/cli.js +4 -0
  3. package/dist/client.d.ts +2 -0
  4. package/dist/client.js +33 -0
  5. package/dist/commands/mobile/connect-core.d.ts +2 -1
  6. package/dist/commands/mobile/connect-core.js +127 -15
  7. package/dist/commands/mobile/connect.js +3 -1
  8. package/dist/commands/mobile/doctor.d.ts +3 -0
  9. package/dist/commands/mobile/doctor.js +55 -6
  10. package/dist/commands/mobile/doctor.test.d.ts +1 -0
  11. package/dist/commands/mobile/doctor.test.js +14 -0
  12. package/dist/commands/mobile/index.js +1 -0
  13. package/dist/commands/mobile/install.d.ts +50 -0
  14. package/dist/commands/mobile/install.js +107 -0
  15. package/dist/commands/mobile/install.test.d.ts +1 -0
  16. package/dist/commands/mobile/install.test.js +142 -0
  17. package/dist/commands/mobile/list.d.ts +2 -0
  18. package/dist/commands/mobile/list.js +17 -11
  19. package/dist/commands/mobile/register.d.ts +6 -1
  20. package/dist/commands/mobile/register.js +12 -6
  21. package/dist/commands/mobile/register.test.js +34 -3
  22. package/dist/commands/mobile/unregister.js +11 -6
  23. package/dist/commands/schema.js +15 -8
  24. package/dist/mobile-control/app-install.d.ts +34 -0
  25. package/dist/mobile-control/app-install.js +55 -0
  26. package/dist/mobile-control/app-install.test.d.ts +1 -0
  27. package/dist/mobile-control/app-install.test.js +113 -0
  28. package/dist/mobile-control/appium-controller.d.ts +11 -0
  29. package/dist/mobile-control/appium-controller.js +24 -4
  30. package/dist/mobile-control/bundle-id-map.d.ts +9 -0
  31. package/dist/mobile-control/bundle-id-map.js +27 -0
  32. package/dist/mobile-control/devices.d.ts +8 -3
  33. package/dist/mobile-control/devices.js +57 -5
  34. package/dist/mobile-control/devices.test.js +41 -1
  35. package/dist/mobile-control/env-flags.d.ts +5 -0
  36. package/dist/mobile-control/env-flags.js +10 -0
  37. package/dist/mobile-control/env-flags.test.d.ts +1 -0
  38. package/dist/mobile-control/env-flags.test.js +21 -0
  39. package/dist/mobile-control/ios-auto-resign.d.ts +45 -0
  40. package/dist/mobile-control/ios-auto-resign.js +108 -0
  41. package/dist/mobile-control/ios-auto-resign.test.d.ts +1 -0
  42. package/dist/mobile-control/ios-auto-resign.test.js +150 -0
  43. package/dist/mobile-control/ios-devicectl.d.ts +39 -0
  44. package/dist/mobile-control/ios-devicectl.js +87 -0
  45. package/dist/mobile-control/ios-devicectl.test.d.ts +1 -0
  46. package/dist/mobile-control/ios-devicectl.test.js +110 -0
  47. package/dist/mobile-control/ios-identity.d.ts +27 -0
  48. package/dist/mobile-control/ios-identity.js +49 -0
  49. package/dist/mobile-control/ios-identity.test.d.ts +1 -0
  50. package/dist/mobile-control/ios-identity.test.js +102 -0
  51. package/dist/mobile-control/ios-mint.d.ts +39 -0
  52. package/dist/mobile-control/ios-mint.js +78 -0
  53. package/dist/mobile-control/ios-mint.test.d.ts +1 -0
  54. package/dist/mobile-control/ios-mint.test.js +94 -0
  55. package/dist/mobile-control/ios-profiles.d.ts +46 -0
  56. package/dist/mobile-control/ios-profiles.js +106 -0
  57. package/dist/mobile-control/ios-profiles.test.d.ts +1 -0
  58. package/dist/mobile-control/ios-profiles.test.js +154 -0
  59. package/dist/mobile-control/ios-resign.d.ts +37 -0
  60. package/dist/mobile-control/ios-resign.js +101 -0
  61. package/dist/mobile-control/ios-resign.test.d.ts +1 -0
  62. package/dist/mobile-control/ios-resign.test.js +84 -0
  63. package/dist/mobile-control/ios-session-error.d.ts +10 -0
  64. package/dist/mobile-control/ios-session-error.js +34 -0
  65. package/dist/mobile-control/ios-session-error.test.d.ts +1 -0
  66. package/dist/mobile-control/ios-session-error.test.js +30 -0
  67. package/dist/mobile-control/ios-signing.d.ts +56 -0
  68. package/dist/mobile-control/ios-signing.js +94 -0
  69. package/dist/mobile-control/ios-signing.test.d.ts +1 -0
  70. package/dist/mobile-control/ios-signing.test.js +160 -0
  71. package/dist/mobile-control/ios-tunnel.d.ts +54 -0
  72. package/dist/mobile-control/ios-tunnel.js +130 -0
  73. package/dist/mobile-control/ios-tunnel.test.d.ts +1 -0
  74. package/dist/mobile-control/ios-tunnel.test.js +115 -0
  75. package/dist/mobile-control/ios-wda-cache.d.ts +39 -0
  76. package/dist/mobile-control/ios-wda-cache.js +157 -0
  77. package/dist/mobile-control/ios-wda-cache.test.d.ts +1 -0
  78. package/dist/mobile-control/ios-wda-cache.test.js +128 -0
  79. package/dist/mobile-control/ios-wda-diagnostics.d.ts +14 -0
  80. package/dist/mobile-control/ios-wda-diagnostics.js +138 -0
  81. package/dist/mobile-control/ios-wda-diagnostics.test.d.ts +1 -0
  82. package/dist/mobile-control/ios-wda-diagnostics.test.js +77 -0
  83. package/dist/mobile-control/ios-wda-preinstall.d.ts +64 -0
  84. package/dist/mobile-control/ios-wda-preinstall.js +162 -0
  85. package/dist/mobile-control/ios-wda-preinstall.test.d.ts +1 -0
  86. package/dist/mobile-control/ios-wda-preinstall.test.js +107 -0
  87. package/dist/mobile-control/mobile-errors.d.ts +12 -0
  88. package/dist/mobile-control/mobile-errors.js +91 -0
  89. package/dist/mobile-control/mobile-errors.test.d.ts +1 -0
  90. package/dist/mobile-control/mobile-errors.test.js +48 -0
  91. package/dist/mobile-control/tunnel.d.ts +5 -1
  92. package/dist/mobile-control/tunnel.js +60 -3
  93. package/dist/mobile-control/tunnel.test.js +1 -1
  94. package/dist/proxy.d.ts +11 -0
  95. package/dist/proxy.js +85 -0
  96. package/dist/proxy.test.d.ts +1 -0
  97. package/dist/proxy.test.js +72 -0
  98. package/package.json +4 -2
package/README.md CHANGED
@@ -64,46 +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 via adb) |
100
- | `aximo mobile setup` | Install the Appium drivers used to drive phones |
101
- | `aximo mobile doctor` | Diagnose Appium prerequisites (adb, SDK, driver) |
102
- | `aximo mobile register` | Register a connected Android phone as an Aximo device |
103
- | `aximo mobile connect` | Start managed Appium + open the tunnel for the phone |
104
- | `aximo mobile unregister` | Remove a local mobile device server-side + local creds |
105
- | `aximo schema` | Show CLI surface as JSON |
106
- | `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) |
107
108
 
108
109
  Mutating commands support `--json '<raw body>'` for direct OpenAPI payloads and `--dry-run` to preview requests without executing.
109
110
 
@@ -355,16 +356,22 @@ Normal for brief network blips — the agent retries with exponential backoff (1
355
356
  - Confirm outbound WebSocket on 443 isn't blocked: `aximo device doctor` runs a handshake probe.
356
357
 
357
358
  **Python download or pip install fails (corporate proxy).**
358
- The embedded Python is fetched from `github.com/astral-sh/python-build-standalone/releases`; packages come from PyPI. Behind a proxy:
359
+ The embedded Python is fetched from `github.com/astral-sh/python-build-standalone/releases` and packages come from PyPI, so `aximo device setup` needs outbound access to both. On a network that only reaches the internet through a proxy, set the standard proxy variables — the CLI, pip, and the desktop agent all honor them:
359
360
 
360
361
  ```bash
361
362
  # macOS / Linux
362
- ~/.aximo/python/bin/python3 -m pip config set global.proxy http://your-proxy:port
363
+ export HTTPS_PROXY=http://your-proxy:port
364
+ export HTTP_PROXY=http://your-proxy:port
363
365
 
364
- # Windows
365
- %USERPROFILE%\.aximo\python\python.exe -m pip config set global.proxy http://your-proxy:port
366
+ # Windows (PowerShell)
367
+ $env:HTTPS_PROXY = "http://your-proxy:port"
368
+ $env:HTTP_PROXY = "http://your-proxy:port"
366
369
  ```
367
370
 
371
+ Loopback and a private-range `AXIMO_BASE_URL` (e.g. an on-prem `172.30.x.x` server) are always reached directly, so you don't have to list them in `NO_PROXY`. If your proxy performs TLS interception, also point `NODE_EXTRA_CA_CERTS` at the proxy's CA so the download's certificate verifies.
372
+
373
+ If the proxy cannot reach GitHub at all, ask your network team to allow-list both `github.com` and its release-asset CDN `release-assets.githubusercontent.com` — the download redirects from the former to the latter, so allow-listing only `github.com` is not enough.
374
+
368
375
  To retry from a clean slate: `aximo device reset --python && aximo device setup`.
369
376
 
370
377
  **Device shows online, but tests fail on screenshot / click.**
@@ -394,11 +401,11 @@ Default `$AXIMO_HOME` is `~/.aximo` on macOS/Linux, `%USERPROFILE%\.aximo` on Wi
394
401
 
395
402
  > Devices are registered per-environment (see [Configuration](#configuration)) — a device registered against sandbox can't connect to production and vice versa.
396
403
 
397
- ## Mobile (local Android phone)
404
+ ## Mobile (local Android phone or iPhone)
398
405
 
399
- The `mobile` commands let you register a **physically-connected Android phone** 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) (UiAutomator2) server — useful for running mobile tests against your own handset instead of a Device Farm device.
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.
400
407
 
401
- > **Android only today.** iOS local devices are not yet supported — `aximo mobile register --platform ios` is blocked, and registration accepts only `--platform android`. (The Appium XCUITest plumbing is partially present but disabled; iOS local devices are planned for a future release.)
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.
402
409
 
403
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.
404
411
 
@@ -406,34 +413,158 @@ Terminology note: **`aximo mobile list`** shows phones plugged into _this_ machi
406
413
 
407
414
  - **Node.js 20+**, plus `aximo-cli` installed globally (see [Install](#install)).
408
415
  - **An Aximo API key**, configured via `aximo configure` or env vars.
409
- - **A USB-connected Android phone** with **USB debugging** enabled in Developer Options, plugged in, and authorized (accept the "Allow USB debugging?" prompt on the phone).
410
- - **adb** (Android platform-tools) on your `PATH` — Appium delegates to it. Install via `brew install --cask android-platform-tools` (macOS), your distro's `adb` package (Linux), or the Android Studio SDK Manager (Windows).
411
- - **The Android SDK**, with `ANDROID_HOME` (or `ANDROID_SDK_ROOT`) set — e.g. installed via Android Studio. Appium's UiAutomator2 driver requires it even though adb is already on PATH.
416
+ - **A USB-connected phone**:
417
+ - **Android** — **USB debugging** enabled and authorized — see [Enable USB debugging on the phone](#enable-usb-debugging-on-the-phone).
418
+ - **iOS** — **Developer Mode** enabled (Settings → Privacy & Security) and the computer trusted.
419
+ - **On the machine that runs `aximo mobile connect`**:
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** and an Apple ID signed into it — see [Set up your computer (iOS)](#set-up-your-computer-ios).
422
+
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
+
425
+ ### Set up your computer (Android)
426
+
427
+ You only need **`adb`** and the **Android SDK** on the machine running `aximo mobile connect`. Appium delegates to `adb`, and its UiAutomator2 driver needs the SDK (even though `adb` is already on `PATH`).
428
+
429
+ **macOS**
430
+
431
+ ```bash
432
+ # Option A — standalone platform-tools (just adb):
433
+ brew install --cask android-platform-tools
434
+ # Option B — full Android Studio (also installs the SDK):
435
+ brew install --cask android-studio
436
+
437
+ # Point ANDROID_HOME at the SDK (Android Studio's default location shown):
438
+ echo 'export ANDROID_HOME="$HOME/Library/Android/sdk"' >> ~/.zshrc
439
+ echo 'export PATH="$PATH:$ANDROID_HOME/platform-tools"' >> ~/.zshrc
440
+ source ~/.zshrc
441
+ ```
442
+
443
+ **Linux**
444
+
445
+ ```bash
446
+ # Debian/Ubuntu — adb from the distro (or download platform-tools from Google):
447
+ sudo apt-get install -y android-tools-adb
448
+ # Install the SDK via Android Studio or sdkmanager, then export its path:
449
+ echo 'export ANDROID_HOME="$HOME/Android/Sdk"' >> ~/.bashrc
450
+ echo 'export PATH="$PATH:$ANDROID_HOME/platform-tools"' >> ~/.bashrc
451
+ source ~/.bashrc
452
+ ```
453
+
454
+ **Windows**
455
+
456
+ - Install **Android Studio** (bundles the SDK + platform-tools), or download the standalone **SDK Platform-Tools** zip from Google.
457
+ - Set `ANDROID_HOME` to the SDK path (e.g. `%LOCALAPPDATA%\Android\Sdk`) and add `%ANDROID_HOME%\platform-tools` to `PATH` via _System Properties → Environment Variables_.
458
+
459
+ > `aximo mobile doctor` auto-detects the SDK at the standard locations above, so `ANDROID_HOME` is optional when you use the defaults — but setting it explicitly is the most reliable. Confirm `adb` works with `adb version`.
460
+
461
+ ### Set up your computer (iOS)
462
+
463
+ iOS local devices are **macOS-only** (device control delegates to Xcode's toolchain). Setup is two steps — no env vars:
464
+
465
+ ```bash
466
+ # 1. Xcode (full app, from the App Store) + accept the license:
467
+ sudo xcodebuild -license accept
468
+
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
472
+ ```
412
473
 
413
- > The Appium server is bundled with the CLI, and the UiAutomator2 driver is installed by `aximo mobile setup` — you don't manage Appium yourself.
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`).
481
+
482
+ Optional overrides / opt-outs:
483
+
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).
500
+
501
+ ### Enable USB debugging on the phone
502
+
503
+ Exact labels vary by manufacturer and Android version, but the flow is:
504
+
505
+ 1. **Unlock Developer options** — open **Settings → About phone** and tap **Build number** seven times until you see "You are now a developer".
506
+ 2. **Turn on USB debugging** — open **Settings → System → Developer options** and enable **USB debugging**.
507
+ 3. **Connect and authorize** — plug the phone into your computer with a **data-capable** USB cable, then tap **Allow** on the "Allow USB debugging?" prompt on the phone (tick "Always allow from this computer" to skip it next time).
508
+ 4. **Verify the computer sees it**:
509
+ ```bash
510
+ adb devices # or: aximo mobile list
511
+ ```
512
+ The phone should appear in state **`device`**. **`unauthorized`** → re-accept the prompt on the phone; **`offline`** → re-plug the cable and unlock the screen. Emulators are not supported — connect a physical phone.
513
+
514
+ > Keep the phone **unlocked** the first time you run a session: the initial command installs Appium's UiAutomator2 helper and creates the automation session on the device.
414
515
 
415
516
  ### First-time setup
416
517
 
417
518
  ```bash
418
- aximo mobile setup # installs the UiAutomator2 Appium driver
419
- aximo mobile doctor # checks adb / Android SDK / driver / bundled server
519
+ aximo mobile setup # installs the Appium driver(s) for your platform(s)
520
+ aximo mobile doctor # checks tooling / drivers / bundled server
521
+ # Limit to one platform if you only test on one:
522
+ aximo mobile setup --platform ios # installs only the XCUITest driver
523
+ aximo mobile doctor --platform ios # checks Xcode / devicectl / discovered signing
420
524
  ```
421
525
 
422
- `setup` installs the Appium driver, then prints what (if anything) is still missing. `doctor` is the first thing to run when anything's off — it reports the bundled Appium server, the UiAutomator2 driver, `adb`, and the Android SDK in one shot, 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.
423
527
 
424
528
  ### Registering and connecting
425
529
 
426
530
  ```bash
427
- aximo mobile list # see connected phones + their adb serials
531
+ aximo mobile list # see connected phones (adb serials / iOS udids)
532
+
533
+ # Android:
428
534
  aximo mobile register --name "My Pixel" --platform android # --platform defaults to android
535
+ # iOS:
536
+ aximo mobile register --name "My iPhone" --platform ios # targets the single connected iPhone
537
+
429
538
  aximo mobile connect # starts managed Appium + opens the tunnel
430
539
  # Ctrl+C to disconnect
431
540
  ```
432
541
 
433
- `register` writes credentials to `$AXIMO_HOME/mobile-devices/<deviceId>.json` (mode `0600`). It targets the single connected phone by default — pass `--serial` 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.
434
543
 
435
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.
436
545
 
546
+ ### Testing your own app
547
+
548
+ Unlike Device Farm, a local phone runs the apps already installed on it. To test your own build, install it first:
549
+
550
+ ```bash
551
+ aximo mobile install --app ./app-debug.apk # Android: adb install -r
552
+ aximo mobile install --app ./MyApp.ipa # iOS: auto-re-signed for the device if needed, installed via devicectl
553
+ ```
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
+
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:
558
+
559
+ ```bash
560
+ # Android:
561
+ aximo session create --platform ANDROID --user-device-id DEVICE_ID --bundle-id com.example.app \
562
+ --project-id PROJECT_ID --title "Smoke" --prompt "Sign in and open settings"
563
+ # iOS:
564
+ aximo session create --platform IOS --user-device-id DEVICE_ID --bundle-id com.example.MyApp \
565
+ --project-id PROJECT_ID --title "Smoke" --prompt "Sign in and open settings"
566
+ ```
567
+
437
568
  ### Managing devices
438
569
 
439
570
  ```bash
@@ -443,14 +574,15 @@ aximo mobile unregister --device-id DEVICE_ID # target a specific device when se
443
574
 
444
575
  ### Flags reference
445
576
 
446
- | Command | Flags |
447
- | ------------------------- | ----------------------------------------------------------------------------------- |
448
- | `aximo mobile list` | _(none)_ |
449
- | `aximo mobile setup` | _(none)_ |
450
- | `aximo mobile doctor` | _(none)_ |
451
- | `aximo mobile register` | `--name <name>` (required), `--platform android` (default), `--serial <adb-serial>` |
452
- | `aximo mobile connect` | `--device-id <id>` (required only if multiple devices are registered) |
453
- | `aximo mobile unregister` | `--device-id <id>` (defaults to the locally registered device) |
577
+ | Command | Flags |
578
+ | ------------------------- | ---------------------------------------------------------------------------------------------------- |
579
+ | `aximo mobile list` | _(none)_ |
580
+ | `aximo mobile setup` | `--platform android \| ios` (optional; installs both drivers by default) |
581
+ | `aximo mobile doctor` | `--platform android \| ios` (optional; checks both by default) |
582
+ | `aximo mobile register` | `--name <name>` (required), `--platform android \| ios` (default android), `--serial`/`--udid` |
583
+ | `aximo mobile install` | `--app <path-to-.apk\|.ipa>` (required), `--platform android \| ios` (inferred), `--serial`/`--udid` |
584
+ | `aximo mobile connect` | `--device-id <id>` (required only if multiple devices are registered) |
585
+ | `aximo mobile unregister` | `--device-id <id>` (defaults to the locally registered device) |
454
586
 
455
587
  ## MCP server
456
588
 
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { defineCommand, runMain } from "citty";
3
3
  import pkg from "../package.json" with { type: "json" };
4
+ import { installProxySupport } from "./proxy.js";
4
5
  // citty's --help prints to stdout; route to stderr for `mcp` since its stdout
5
6
  // is JSON-RPC.
6
7
  const args = process.argv.slice(2);
@@ -34,4 +35,7 @@ const main = defineCommand({
34
35
  mcp: () => import("./commands/mcp/index.js").then((m) => m.default),
35
36
  },
36
37
  });
38
+ // Honor HTTP_PROXY / HTTPS_PROXY / NO_PROXY for every fetch the CLI makes (the
39
+ // embedded-Python download, API calls). No-op unless a proxy is configured.
40
+ await installProxySupport();
37
41
  runMain(main);
package/dist/client.d.ts CHANGED
@@ -16,3 +16,5 @@ export interface ClientDeps {
16
16
  }
17
17
  export declare function request<T>(path: string, options?: RequestOptions, deps?: ClientDeps): Promise<T>;
18
18
  export declare function requestBuffer(path: string, deps?: ClientDeps): Promise<Buffer>;
19
+ /** Authenticated GET that streams the response body to a file (no full-buffering). */
20
+ export declare function requestToFile(path: string, destPath: string, deps?: ClientDeps): Promise<void>;
package/dist/client.js CHANGED
@@ -88,3 +88,36 @@ export async function requestBuffer(path, deps = {}) {
88
88
  }
89
89
  return Buffer.from(await res.arrayBuffer());
90
90
  }
91
+ /** Authenticated GET that streams the response body to a file (no full-buffering). */
92
+ export async function requestToFile(path, destPath, deps = {}) {
93
+ const apiKey = deps.apiKey ?? getApiKey();
94
+ const baseUrl = deps.baseUrl ?? getBaseUrl();
95
+ const fetchImpl = deps.fetchImpl ?? fetch;
96
+ const url = buildUrl(baseUrl, path);
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
+ }
113
+ if (!res.ok) {
114
+ const retryAfter = res.headers.get("retry-after") ?? undefined;
115
+ throw httpError(res.status, await errorMessage(res), retryAfter);
116
+ }
117
+ if (!res.body)
118
+ throw new Error(`Empty response body from ${path}`);
119
+ const { Readable } = await import("node:stream");
120
+ const { pipeline } = await import("node:stream/promises");
121
+ const { createWriteStream } = await import("node:fs");
122
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(destPath));
123
+ }
@@ -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 {};
@@ -1,7 +1,14 @@
1
1
  import { getBaseUrl } from "../../client.js";
2
2
  import { formatError } from "../../output.js";
3
3
  import { AppiumController } from "../../mobile-control/appium-controller.js";
4
- import { startAppiumServer } from "../../mobile-control/appium-server.js";
4
+ import { installUploadedApp } from "../../mobile-control/app-install.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";
5
12
  import { runControlTunnel } from "../../mobile-control/tunnel.js";
6
13
  import { runMobileDoctor } from "./doctor.js";
7
14
  import { listMobileDeviceConfigs, readMobileDeviceConfig, } from "./paths.js";
@@ -31,40 +38,145 @@ export function resolveDeviceConfig(deviceId) {
31
38
  }
32
39
  return { ok: true, config: only };
33
40
  }
34
- export function buildController(config, endpoint) {
35
- return new AppiumController({ endpoint, platform: config.platform, target: config.target });
41
+ export function buildController(config, endpoint, iosSigning, wdaLocalPort) {
42
+ return new AppiumController({
43
+ endpoint,
44
+ platform: config.platform,
45
+ target: config.target,
46
+ ...(config.platform === "ios"
47
+ ? { iosSigning: iosSigning ?? resolveIosSigning(), ...(wdaLocalPort ? { wdaLocalPort } : {}) }
48
+ : {}),
49
+ });
36
50
  }
37
- // A blocking check is any hard failure, plus a missing UiAutomator2 driver
51
+ // A blocking check is any hard failure, plus a missing automation driver
38
52
  // (only a warning for `doctor`, but a hard prerequisite for connecting).
39
- function isBlockingCheck(check) {
53
+ function isBlockingCheck(check, driverName) {
40
54
  if (check.status === "fail")
41
55
  return true;
42
- return check.name === "uiautomator2 driver" && check.status === "warn";
56
+ return check.name === driverName && check.status === "warn";
43
57
  }
44
- async function assertAndroidPrerequisites() {
45
- const report = await runMobileDoctor({ platform: "android" });
58
+ async function assertPrerequisites(platform) {
59
+ const driverName = platform === "ios" ? "xcuitest driver" : "uiautomator2 driver";
60
+ const report = await runMobileDoctor({ platform });
46
61
  const checks = report.ok ? report.data.checks : [];
47
- const blockers = checks.filter(isBlockingCheck);
62
+ const blockers = checks.filter((c) => isBlockingCheck(c, driverName));
48
63
  if (blockers.length === 0)
49
64
  return;
50
65
  const lines = blockers.map((c) => ` - ${c.message}${c.hint ? `\n ${c.hint}` : ""}`);
51
- throw new Error(`Missing Appium prerequisites:\n${lines.join("\n")}\n` +
52
- "Run `aximo mobile setup` and/or fix the above, then re-run. " +
53
- "(`aximo mobile doctor` for details.)");
66
+ const setup = platform === "ios" ? "aximo mobile setup --platform ios" : "aximo mobile setup";
67
+ const doctor = platform === "ios" ? "aximo mobile doctor --platform ios" : "aximo mobile doctor";
68
+ throw new Error(`Missing ${platform} prerequisites:\n${lines.join("\n")}\n` +
69
+ `Run \`${setup}\` and/or fix the above, then re-run. (\`${doctor}\` for details.)`);
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);
54
89
  }
55
90
  export async function connectWithConfig(config) {
56
- if (config.platform === "android") {
57
- await assertAndroidPrerequisites();
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
+ }
58
106
  }
59
107
  const useDrivers = config.platform === "ios" ? ["xcuitest"] : ["uiautomator2"];
60
108
  const server = await startAppiumServer({ useDrivers });
61
- 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;
62
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
+ }
63
160
  await runControlTunnel({
64
161
  wsUrl: getTunnelOrigin(),
65
162
  deviceToken: config.deviceToken,
66
163
  deviceName: config.name,
67
164
  controller,
165
+ installApp: async ({ uploadId }) => {
166
+ const result = await installUploadedApp({
167
+ uploadId,
168
+ platform: config.platform,
169
+ target: config.target,
170
+ log: (message) => console.error(message),
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
+ }
179
+ },
68
180
  });
69
181
  }
70
182
  finally {
@@ -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<{