Metadata-Version: 2.5
Name: autosana
Version: 0.14.0
Summary: Local testing infrastructure for Autosana — mobile and web
Project-URL: Homepage, https://autosana.ai
License: Proprietary
Requires-Python: >=3.9
Requires-Dist: filelock>=3.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: typer>=0.9.0
Provides-Extra: local
Requires-Dist: pillow>=10.0.0; extra == 'local'
Description-Content-Type: text/markdown

# Autosana CLI

Connect local simulators, emulators, physical devices, and dev servers to [Autosana](https://autosana.ai) to test locally.

## Install

```
pipx install autosana
```

Or with uv:

```
uv tool install autosana
```

## Quick Start

```bash
# Check prerequisites
autosana doctor

# Auto-install Node.js + Appium + cloudflared
autosana doctor --fix

# Start local session (iOS)
autosana up --platform ios

# Start local session on a physical iPhone by UDID
autosana up --platform ios --device 00008140-00064D5930E3C01C --detach --json

# Diagnose a physical iPhone setup before running flows
autosana doctor --platform ios --device 00008140-00064D5930E3C01C --json

# Start local session (web — point to your dev server, http or https)
# The scheme (http vs https) is auto-detected. HTTPS dev servers like
# `next dev --experimental-https` and `vite --https` work out of the box.
autosana up --platform web --port 3000

# Multi-device — start sessions for all booted simulators
autosana up --platform ios --all-devices --detach --json

# Or start specific devices by name/UDID
autosana up --platform ios --device "iPhone 16 Pro" --detach --json
autosana up --platform ios --device "iPhone 17 Pro" --detach --json

# Check running sessions (includes device and screenshot readiness checks)
autosana status

# Recover a degraded session without a full restart when possible
autosana recover --session-id <SESSION_ID> --json

# Stop all sessions
autosana down --all
```

## Working with flows

Store your end-to-end tests as YAML under a `.autosana/` folder ([code-managed flows](https://docs.autosana.ai/code-managed-flows)), then validate, export, and run them from the terminal.

```bash
# Validate your .autosana/ flow & suite files offline (same rules the sync applies)
autosana flows validate

# Export existing dashboard flows/suites to .autosana/ YAML for review + commit
autosana flows export --all

# Run your branch's flows in the cloud (attributed to the current git ref), no PR needed
autosana flows run --flow login --bundle-id com.acme.app --platform ios

# In a GitLab CI job: sync .autosana/ on the default branch, check it in merge requests
autosana flows sync
```

### Run a flow on your local simulator

Run the flow you're *currently editing* — your uncommitted `.autosana/` YAML — on the simulator you booted with `autosana up`. No commit, no push, no cloud device.

```bash
# 1. Boot a local session (once)
autosana up --platform ios

# 2. Run your working-copy flow on it
autosana run login --local

# ...or a whole suite (its setup_flow runs on the device first)
autosana run --suite checkout --local

# Web: connect your dev server, then target its Autosana app ID
autosana up --platform web --port 3000
autosana run login --local --app-id <APP_ID>
```

`autosana run` is shorthand for `autosana flows run`. Targeting uses sensible defaults, so the everyday command is just `autosana run <flow> --local`:

- **App** — web runs use `--app-id`; mobile runs use `--bundle-id`, the `AUTOSANA_BUNDLE_ID` env var, or your org's single app for the platform.
- **Platform** — web is inferred from `--app-id`; mobile is taken from your `up` session. Pass `--platform` / `--session-id` only to disambiguate mobile sessions.
- **Environment** — `--environment <name>` selects the variable set that `${env:…}` references (and your flows) resolve against; required when the app spans multiple environments.

Notes:

- Runs your **instructions**; hooks are not executed locally (they manipulate the environment, not the device) — a suite's `setup_flow` gives you on-device setup instead.

## API keys

```bash
# Create a key and copy it to the clipboard (a one-click browser approval if your last was 2+ hours ago)
autosana keys create
autosana keys create --name "GitHub Actions"   # what it is for, shown in Settings → API Keys
KEY="$(autosana keys create --print)"           # print it instead; also what happens with no clipboard
```

Use it as `AUTOSANA_API_KEY`, which the CLI prefers over a stored login. The key is not stored on this machine and is not shown again. A key made without a new approval is revoked along with the login that made it.

## Upload a build

Send a build you just made to Autosana so cloud runs test it. It becomes the app's active build for the whole workspace: flow and suite runs that don't pin a build use it, and the app's CI/CD automations start, as for any build upload.

```bash
# iOS Simulator build: pass the .app folder (zipped for you); the bundle id comes from Info.plist
autosana builds upload ~/Library/Developer/Xcode/DerivedData/MyApp-*/Build/Products/Debug-iphonesimulator/MyApp.app --json

# Real-iPhone build (.ipa), or Android: an .apk names its own package, an .aab needs --bundle-id
autosana builds upload MyApp.ipa
autosana builds upload app-debug.apk
autosana builds upload app-release.aab --bundle-id com.acme.app

# Upload to a specific existing app (its `id` from apps_list), in its own environment
autosana builds upload app-debug.apk --app-id <APP_ID>
```

A device build (an `.app` from an Xcode archive, platform `iphoneos`) can't run on the iOS Simulator, so it is refused; build for a Simulator destination or export an `.ipa`. `--json` prints `app_id` and `app_build_id`.

No login? An agent connected to the Autosana MCP (OAuth) calls the `builds_upload` tool for a signed link, runs `autosana builds upload <path> --link "<url>"` (checks and uploads the build, no login or API key), then calls `builds_upload` again with the printed `upload_id` to make it the active build.

## Sandboxes

Drive a cloud simulator or emulator directly, one command at a time. Built for coding agents that want to check their work on a real device while they build, without writing a flow first. Sandboxes have to be enabled for your company.

```bash
# Lease a device, install your app's active build, launch it, save the first screenshot
autosana sandbox start --platform ios --bundle-id com.example.app

# Or install a build you just made, for this sandbox only
autosana sandbox start --upload android/app/build/outputs/apk/debug/app-debug.apk

# Tap at X,Y in pixels of the last screenshot; the settled screen is saved as the next one
autosana sandbox tap 603,1311

# Double-tap
autosana sandbox tap 603,1311 --count 2

# Press and hold
autosana sandbox long-press 603,1311 --duration 1.5s

# Set a field's text (replaces what it holds); --submit presses return
autosana sandbox type "jane@example.com" --into 603,880
autosana sandbox type "hunter2" --into 603,1020 --submit

# Scroll (down shows what is further down), or swipe between two points
autosana sandbox scroll down
autosana sandbox swipe 600,1200 100,1200

# A swipe starts exactly where you say, so in from the left edge goes back on Android.
# --flick lifts without pausing first, so what you swiped keeps moving
autosana sandbox swipe 0,700 400,700
autosana sandbox swipe 320,1200 320,400 --flick

# Open a link, go home, then bring the app back: as it was, as a new process, or as a new install
autosana sandbox open myapp://orders/42
autosana sandbox press home
autosana sandbox launch
autosana sandbox launch --cold
autosana sandbox launch --reset

# Put a new build on, e.g. after a native change: over the app (its data stays), or clean with --reset
autosana sandbox upload android/app/build/outputs/apk/debug/app-debug.apk

# Send your app a push notification (iOS simulators), from flags or a JSON file
autosana sandbox push --title "Order shipped" --body "Your order is on its way."
autosana sandbox push payload.json

# Change the device, or just wait for the app
autosana sandbox set orientation landscape
autosana sandbox set location 37.77,-122.41
autosana sandbox wait 2s

# Look again without acting
autosana sandbox look

# Reach your own machine from the device (Android only for now): Metro, so the app
# hot-reloads from your code, and your API. The app is opened from Metro.
autosana sandbox forward 8081 3000

# List live sandboxes (* marks this worktree's current one), then release the device
autosana sandbox list
autosana sandbox end
```

- **Builds:** `start --upload <path>` uploads a build you made and installs it. It is for that sandbox only: it does not become the app's active build and starts no automations. `upload <path>` does the same in a running sandbox: the build goes on over the app, which keeps its data as after an update, or clean with `--reset` (needed for a build signed differently or older than the one installed). Otherwise `start` installs a build already in Autosana (`--app-build-id`, default: the app's active build) and prints which one. iOS needs a simulator build (`.app` or `.zip`), not an `.ipa`. The app is named by `--bundle-id` (with `--environment` when it is in several), by `--app-id`, or by the build itself. Apps that ship with the simulator (Safari, Reminders, Photos) need no build: `--bundle-id com.apple.mobilesafari`. The app can be left out when `.autosana/config.yaml` names a default.
- **Device:** `--device "iPhone 17 Pro" --os 26.0` picks a model and OS; the default is the latest.
- **Lifetime:** a sandbox holds its device until `end`, until `--ttl` runs out (default 2h, at most 12h), or after 15 minutes without a command (`status` counts as one). End sandboxes you are done with.
- **Screenshots:** saved to `.autosana/.sandboxes/<sandbox-id>/step-<n>.png` in your repository (the folder ignores itself in git), or under `~/.autosana/sandboxes/` outside a repository. They stay after `end`, which prints the folder; the 1,000 most recently used ended sandboxes keep theirs.
- **Coordinates:** tap in pixels of the most recent screenshot, exactly as you see it. Screenshots are scaled so their longest side is 1568 px (an iPhone 17 Pro comes back as 721x1568), which image readers show without resizing, so there is nothing to convert. `--max-size <px>` on `start`, `look` and `tap` changes that, for example `--max-size 2622` to read fine print at full resolution; the next tap then uses that screenshot's pixels.
- **Actions:** `tap`, `long-press`, `type`, `scroll`, `swipe` (in from the left edge is Android's back), `press home`, `open`, `launch`, `wait`, `push` and `set` (`location`, `orientation`, `clipboard`, `locale`, `network`). Each one answers like a tap: what it did, whether the screen changed, and the next screenshot. Positions are always pixels of the most recent screenshot.
- **Push notifications:** `push` delivers a notification to your app the way your server would, so you can check the banner, what tapping it opens, and what your app does with the payload. Pass `--title` and `--body`, or a JSON file with an `aps` object for anything more (badges, custom keys). iOS simulators only, and your app must have been allowed to show notifications. The banner stays up for about five seconds: tap it, or `wait 5s` before tapping elsewhere, because a tap elsewhere while it is up may not reach your app.
- **Launching:** `launch` brings your app back to the front exactly as it was. `launch --cold` closes it first, so it starts as a new process with its saved data. `launch --reset` reinstalls it, so it starts as it does after a first install.
- **Typing:** `type` sets the field's whole text, it does not append. `--into X,Y` picks the field; without it the text goes to whichever field has the keyboard. Afterwards the keyboard goes away, as in a flow, through its own Done key (in some forms that submits or moves to the next field); `--keep-keyboard` leaves it up, and `--submit` presses return. `type "" --into X,Y` empties a field.
- **Speed:** every action waits for the screen to settle and returns it. `--no-settle` captures immediately; `--no-observe` skips the screenshot.
- **What a tap tells you:** whether the screen changed (`screen changed` or `no change on screen`, `changed` in `--json`) and how long the tap and the settle each took, so you can often skip opening the screenshot. Without settling it makes no claim about change.
- **Forwarding (Android only for now):** `forward 8081 3000` makes those ports on the device lead to the same ports on your machine, through a private tunnel that nothing else can reach. The app reaches them at `localhost:<port>` or `10.0.2.2:<port>` (the address an Android emulator uses for the machine it runs on), so the development config your app already has for a local emulator works as is. When a port leads to Metro, `forward` opens your app from it: an Expo development build through its development-client link, a plain React Native debug build by restarting it (it looks for `10.0.2.2:8081` by itself). `3000:8000` sends the device's 3000 to your 8000. Run `forward` again to add ports, or with none to reconnect after the tunnel drops; later commands tell you when it has. It works through an HTTPS proxy, as cloud agents' machines have. The forwards last until `end`.
- **Watching:** `start` prints a `watch` link to the sandbox's page in the Autosana dashboard, where a signed-in teammate sees each step and its screenshot as it happens. The same page is the replay once the sandbox ends; `end` prints it again (`watch_url` in `--json`).
- **Name:** `--name "checkout confirm button"` says what the sandbox is for and is the title of its page. Without it the sandbox is named after the git branch you are on.
- **Several sandboxes:** each git worktree has its own current sandbox. Address another with `--sandbox <id>`, using an id from `autosana sandbox list`.
- **If the app stops running:** when your app crashes or is closed, the sandbox keeps its device. The next `tap` relaunches the app and fails with code `app_relaunched` without tapping, because the screen you aimed at is gone; look at the screenshot it saved, then tap again. `look` shows whatever is on the device, which may be the home screen.
- **Scripting:** every command takes `--json`. Exit codes: `0` ok, `1` the command failed, `2` bad usage, `3` not logged in, not enabled, or at the sandbox limit, `4` the sandbox is gone.

`look` and `tap` go straight to the sandbox's own address, authorized by a short-lived sandbox token that `start` receives. Only `start`, `list` and `end` call the Autosana API.

## Multi-Device Parallel Testing

Test across multiple simulators simultaneously:

1. Boot multiple iOS Simulators or Android Emulators
2. Run `autosana up --platform ios --all-devices --detach --json` to start sessions for all devices
3. Your coding agent dispatches flows to different devices in parallel via `device_tunnel_map`
4. All flows execute simultaneously — 3 devices = 3x faster

Works with both iOS and Android. Each device gets its own Appium server and Cloudflare tunnel.

## Device Health Checks

`autosana up` and `autosana status` verify that devices are actually responsive — not just listed by ADB/simctl:

- **Android**: runs `adb shell echo ok` with a 5s timeout to detect zombie emulators
- **iOS Simulators**: runs `xcrun simctl getenv` with a 5s timeout to detect frozen simulators
- **Physical iOS devices**: uses Xcode CoreDevice (`xcrun devicectl`) to verify the device is paired, connected, awake, and available to Xcode. If the device is locked, unplugged, offline, or missing a Trust This Computer prompt, `autosana up` exits before starting Appium.
- **Web**: probes `http://127.0.0.1:<port>` and `https://127.0.0.1:<port>` in parallel (3 attempts with backoff, to cover dev servers still booting). The responding scheme is used for the Cloudflare tunnel origin, preferring https on tie. HTTPS origins skip TLS verification on loopback — self-signed certs from mkcert, `next dev --experimental-https`, and `vite --https` just work. Verification is intentionally disabled here because the cloudflared origin is `127.0.0.1` (no MITM surface); to lock cloudflared back into strict verification with a trusted CA, you'd run cloudflared yourself instead of via `autosana up`.

If a mobile device is unresponsive, `autosana up` fast-fails before wasting time on Appium/tunnel setup. `autosana status --json` reports `device_responsive: false` with an actionable issue message.

`autosana status --json` also includes `components`, `next_action`, and each session's `session_id`. Agents should read these before every local run. If a Cloudflare tunnel or local proxy process died, run `autosana recover --session-id <SESSION_ID> --json` using the matching `status.sessions[].session_id` first. Recovery restarts the smallest failed scope when possible: tunnel first, local proxy next, and full session restart only when Appium/WebDriverAgent needs it.

The detected scheme is reported in `autosana up --json` as `scheme: "http"` or `scheme: "https"`.

## Physical iPhone Readiness

Physical iPhone local runs use Appium/WebDriverAgent (WDA) for both control and screenshots. WDA is installed on the device by `autosana up` and validated with a real screenshot capture before the session reports ready — no extra brew installs are required. For physical iOS sessions, `autosana up --platform ios --device <UDID> --detach --json` starts a local Autosana proxy in front of Appium and tunnels that proxy. The proxy exposes:

- `/_autosana/health`
- `/_autosana/screenshot`
- `/_autosana/diagnostics`

`autosana up` fails fast if no real screenshot provider is ready. `autosana status --json` includes physical-readiness fields:

- `screenshot_ready`
- `screenshot_provider`
- `screen_non_black`
- `wda_ready`
- `screenshot_diagnostics`
- `local_proxy_token` (physical iPhone only, internal token for Autosana proxy screenshots)
- `components`
- `next_action`

Treat `local_proxy_token` as a secret. Read it from `autosana status --json`, pass it directly to Autosana MCP tools for physical iPhone runs, and do not paste real token values into bug reports, chat logs, or public docs. Re-read status after recovery or restart because the token can change. If recovery rotates the token during an in-flight run, start a fresh `flows_run` with the new session values.

Run `autosana doctor --platform ios --device <UDID> --json` when physical setup fails. It checks CoreDevice connectivity, pairing/trust, Developer Mode, DDI services, WebDriverAgent signing, and the optional iPhone Mirroring host capture fallback. WDA screenshots are validated by `autosana up` itself at session start, so doctor warnings on the optional fallback do not block `up` — the doctor only surfaces the concrete blocker when the WDA path itself fails.

Keep the iPhone awake, unlocked, trusted, in Developer Mode, and plugged into power during physical local runs. If the display turns off, WDA may still return an accessibility tree, but Autosana treats black screenshots as a hard error and will not reconstruct a fake screenshot from accessibility data.

## Requirements

- **iOS Simulator**: macOS with one or more booted iOS Simulators
- **Physical iOS**: macOS with Xcode, a paired/unlocked iPhone in Developer Mode, and an Apple Development signing identity (with private key) available to Xcode. After WebDriverAgent first installs on the device, you must trust the developer certificate: **Settings → General → VPN & Device Management → tap your "Apple Development" entry → Trust**. For team-specific WebDriverAgent signing, set `AUTOSANA_IOS_XCODE_ORG_ID` and optionally `AUTOSANA_IOS_UPDATED_WDA_BUNDLE_ID`.
- **Android**: macOS, Linux, or Windows with one or more booted Android Emulators
- **Web**: a local dev server running on any OS
- An [Autosana](https://autosana.ai) account
