diff --git a/README.md b/README.md index 1b64148..bfcb79d 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ Agents choose the right skill from the task context and each skill's description | `expo-cicd-workflows` | EAS Workflow YAML files and CI/CD automation. | | `expo-observe` | EAS Observe setup and launch, route, event, and version metrics. | | `eas-update-insights` | EAS Update health, crash rates, launch counts, payload size, and rollout gates. | +| `eas-simulator` | Run and drive your app on a remote iOS simulator or Android emulator on EAS cloud — from the CLI or an agent, with a live browser preview (iOS only). | ### Maintenance diff --git a/plugins/expo/.claude-plugin/plugin.json b/plugins/expo/.claude-plugin/plugin.json index 062e035..82849f7 100644 --- a/plugins/expo/.claude-plugin/plugin.json +++ b/plugins/expo/.claude-plugin/plugin.json @@ -1,10 +1,6 @@ { "name": "expo", - - - - "version": "1.4.2", - + "version": "1.5.0", "description": "Official Expo skills for building, deploying, upgrading, and debugging Expo apps", "author": { "name": "Expo Team", diff --git a/plugins/expo/.codex-plugin/plugin.json b/plugins/expo/.codex-plugin/plugin.json index b30251e..8c63ac0 100644 --- a/plugins/expo/.codex-plugin/plugin.json +++ b/plugins/expo/.codex-plugin/plugin.json @@ -1,10 +1,6 @@ { "name": "expo", - - - - "version": "1.4.2", - + "version": "1.5.0", "description": "Official Expo skills for building, deploying, upgrading, and debugging Expo and React Native apps.", "author": { "name": "Expo Team", diff --git a/plugins/expo/.cursor-plugin/plugin.json b/plugins/expo/.cursor-plugin/plugin.json index 7ea57a1..2e15e93 100644 --- a/plugins/expo/.cursor-plugin/plugin.json +++ b/plugins/expo/.cursor-plugin/plugin.json @@ -1,11 +1,7 @@ { "name": "expo", "displayName": "Expo", - - - - "version": "1.4.2", - + "version": "1.5.0", "description": "Official Expo skills for building, deploying, upgrading, and debugging Expo and React Native apps.", "author": { "name": "Expo Team", diff --git a/plugins/expo/README.md b/plugins/expo/README.md index 8e071b4..9093104 100644 --- a/plugins/expo/README.md +++ b/plugins/expo/README.md @@ -77,6 +77,7 @@ Official AI agent skills from the Expo team for building, deploying, upgrading, ### Deployment - **eas-update-insights** — Check EAS Update health, crash rates, adoption, and payload size +- **eas-simulator** — Run and drive your app on a remote iOS/Android simulator on EAS cloud, from the CLI or an AI agent - **expo-deployment** — Deploy to iOS App Store, Android Play Store, and web hosting - **expo-cicd-workflows** — EAS workflow YAML files for CI/CD pipelines diff --git a/plugins/expo/skills/eas-simulator/SKILL.md b/plugins/expo/skills/eas-simulator/SKILL.md new file mode 100644 index 0000000..e57a7fd --- /dev/null +++ b/plugins/expo/skills/eas-simulator/SKILL.md @@ -0,0 +1,154 @@ +--- +name: eas-simulator +description: "Run and control a user's app on a remote iOS/Android simulator hosted on EAS cloud. Always read before executing any `eas simulator:*` commands — it has the current syntax for this experimental API. Use whenever the user needs a simulator they can't run locally — 'run my app on a cloud simulator', 'use eas simulator to run/install/screenshot my app', 'I'm on Linux/Cursor and need an iOS device', 'no sim on this box / headless CI', 'let an agent click through my app and screenshot it', 'test my dev build on a remote sim with live reload', 'stream a sim's screen to my browser' — even when they don't say 'EAS Simulator' or 'cloud'. On a host WITHOUT a local simulator (Linux, CI, cloud sandbox) it's the default — just use it; on macOS, do NOT auto-trigger for a plain 'run on the simulator' — use it only for a cloud/remote/shareable sim, an iOS version they lack, or an agent-driven session. NOT for local sims (expo run:ios, Xcode, Android Studio), EAS Build/Update, web preview, or physical devices." +version: 1.0.0 +license: MIT +allowed-tools: "Bash(npx *eas-cli@*), Bash(npx *agent-device@*), Bash(npx expo *), Bash(eas *), Bash(expo *), Bash(xcodebuild*), Bash(pod*)" +--- + +# EAS Simulator + +EAS Simulator runs a remote iOS simulator or Android emulator on EAS infrastructure that you drive from your machine — from the CLI, from an AI agent (via `agent-device`), and from a browser preview. It's the unlock for **environments that can't run a simulator locally** (Linux boxes, cloud/background agents like Cursor Cloud), and for letting an agent *verify* a change on a real device instead of only reasoning about code. + +The `simulator:*` commands are **experimental and hidden**, and need a recent eas-cli (≥ 20.3.0 as of writing) — which is why this skill runs everything via `npx --yes eas-cli@latest`. Flags and verbs may change; if a command fails, **` --help` is authoritative.** + +## When to use + +The frontmatter `description` carries the trigger phrases. In short: use this to get a user's app onto a **cloud** simulator and interact with it — especially from a Mac-less or cloud/sandbox agent. **Not** for local sims (`expo run:ios`, Xcode, Android Studio), store builds/signing (that's EAS Build), or physical devices. For the macOS case, see *Cloud vs local* next. + +## Cloud vs local: decide this first + +- **Non-macOS** (Linux / CI / cloud sandbox like Cursor Cloud, detect via `uname -s` ≠ `Darwin`): the only way to get a sim — **just proceed.** +- **macOS:** local sims exist and a cloud session costs money + latency, so **ask first** ("a remote cloud sim — to share a live preview, offload, or test an iOS version you lack — or just run locally?") unless the user explicitly said cloud/remote/shareable. +- Always honor an explicit choice; for "run it locally" hand off to `expo run:ios` / Xcode. + +```bash +# Programmatic detection — run this to decide before doing anything else: +if [ "$(uname -s)" != "Darwin" ] || ! xcrun --find simctl &>/dev/null 2>&1; then + echo "no local sim — proceed with EAS Simulator" +else + echo "local sim available — ask the user (cloud or local?)" +fi +``` + +## Prerequisites + +- **Run every `eas` command via `npx --yes eas-cli@latest …`** — guarantees a CLI new enough to have `simulator:*` (a global `eas` is often too old), and `--yes` skips npx's prompt. (Bare `eas` is fine if `eas --version` is current.) +- **Authenticated.** Interactive machine → `npx --yes eas-cli@latest login`. **Cloud sandbox / CI / headless agent has no browser login — set `EXPO_TOKEN`** (expo.dev → Account → Access Tokens) in the env instead. Verify either way with `npx --yes eas-cli@latest whoami`. +- Run from an Expo **project directory.** A fresh app needs one-time setup: `npx --yes eas-cli@latest init` to create/link the project (when there's no `projectId`), and **set `ios.bundleIdentifier`** in app config if it's missing — a fresh `create-expo-app` often has none, and `prebuild`/`eas build` need it (they prompt or fail without it; e.g. `dev..`). Read current config with `npx expo config --json` (it may live in `app.config.js`). The first Mode-C run is slow (native build); later runs reuse it. +- A controller to drive the device. This skill uses **agent-device** (open source, MIT), run on demand via `npx agent-device@latest` — nothing globally installed. **argent** is an alternative (`--type argent` in `simulator:start`); see [references/controllers.md](./references/controllers.md). +- **`.env.eas-simulator`** is written/managed by eas-cli (not this skill): it holds the session id (`EAS_SIMULATOR_SESSION_ID`) + the daemon URL/**token**, so `get`/`stop`/`exec` default to that session (usually **omit `--id`**; pass `--id ` to target another). It carries a **token → keep it gitignored** (eas-cli marks it "do not commit" but may not add the ignore rule, and a fresh app's `.gitignore` won't cover it — add `.env.eas-simulator` if missing). +- `--max-duration-minutes` is paid-plan only; otherwise a default applies. + +## The core loop (always the same) + +A session is: **start → (install your app) → drive → stop.** `eas-cli` owns the *session*; the device *verbs* (open/tap/screenshot) come from the controller, which `npx --yes eas-cli@latest simulator:exec` runs for you with the session's connection env loaded. + +```bash +# 1. Start a session (boots the remote sim + agent-device daemon; writes .env.eas-simulator). +printf '# managed by eas-cli\n' > .env.eas-simulator # clear any stale session first +npx --yes eas-cli@latest simulator:start --platform ios --type agent-device --non-interactive +# Then confirm it's live: simulator:get --json → status IN_PROGRESS (bounded poll in run-your-app.md). + +# 2. Drive it through `exec` (loads the session env, then runs the command you give it). +# agent-device runs on demand via npx — nothing installed globally. +npx --yes eas-cli@latest simulator:exec npx agent-device@latest open --platform ios +npx --yes eas-cli@latest simulator:exec npx agent-device@latest snapshot -i # interactive UI tree → @e1, @e2 refs +npx --yes eas-cli@latest simulator:exec npx agent-device@latest press @e2 # tap a ref (NOTE: 'press', not 'tap') +npx --yes eas-cli@latest simulator:exec npx agent-device@latest screenshot ./shot.png + +# 3. Stop (ends billing; tears down the VM) and reset the dotenv. Omit --id to target the dotenv session. +npx --yes eas-cli@latest simulator:stop +printf '# managed by eas-cli\n' > .env.eas-simulator +``` + +To **watch** it live, hand the user the `webPreviewUrl` that `start` prints (an `--type agent-device` iOS session runs serve-sim alongside the daemon, so it emits one — agent control *and* a browser preview in one session; Android has no preview, and `--type serve-sim` is preview-only). **This URL is for the *user's* browser — you cannot open it for them, and it must never touch the sim:** +- **"Open it here" (Cursor/VS Code)** → print the URL on its own line and tell the user to open Simple Browser (`Cmd/Ctrl+Shift+P` → "Simple Browser: Show") and paste it. Then **stop**: do not shell out to a system browser or a Cursor/VS Code URL handler, and do not ask "did a tab appear?" — you can't confirm it, the handoff is done. +- **Never `open` the `webPreviewUrl` on the sim.** It's a browser preview, not a deep link and not an `agent-device open` argument; routing it to the device renders a browser-in-a-browser (a real past failure). +- **Headless agent** (no display) → just return the URL as the deliverable. +- **Keeping it alive for the user to drive** → bound it: start with `--max-duration-minutes N` so it auto-stops; tell them it bills until stopped and when it auto-stops; offer to reopen/extend when it ends. (This is the one case where "stop right away" doesn't apply; one-shot `screenshot`/`get` runs still stop immediately.) + +`start` also prints a job-run URL. + +## Commands at a glance + +| Command | Purpose | +|---|---| +| `npx --yes eas-cli@latest simulator:start --platform ios\|android [--type agent-device\|argent\|serve-sim] [--package-version X] [--max-duration-minutes N] [--non-interactive] [--json]` | Create a session; boot the sim + controller; write `.env.eas-simulator`; print `webPreviewUrl` + job-run URL | +| `npx --yes eas-cli@latest simulator:exec [args…]` | Load `.env.eas-simulator`, then run `` with that env. The bridge to the controller. | +| `npx --yes eas-cli@latest simulator:get [--id] [--json]` | Session status + connection details. **Use this to confirm readiness** (see *Operating principles*). | +| `npx --yes eas-cli@latest simulator:list [--status …] [--type …] [--platform …]` | List an app's sessions | +| `npx --yes eas-cli@latest simulator:stop [--id]` | Stop a session (idempotent) | + +## Running the user's app — pick a mode + +The remote sim boots **blank — no Expo Go, no apps.** Install a build, then drive it — but **match the build *type* to the goal first** (the box below); that's where live-session runs derail. Full sequences: [references/run-your-app.md](./references/run-your-app.md) — read before running a mode. + +> **Match the build to the goal before installing anything — this is where live-session runs derail.** Two traps, same root (grabbing a build that doesn't fit the request): +> 1. **Wrong type.** Live edits (Mode C) **require a dev build.** A *static* build — a local Release (A), the default EAS sim build (B), or **any build left on the sim from an earlier screenshot run** — freezes its JS at build time and **can never hot-reload.** For a live request, **ignore existing builds entirely** and install a **dev** build (local Debug, or an EAS build with `developmentClient: true`). Never reconnect Metro to a static build hoping it'll reload — it won't. +> 2. **Stale.** A static look must match current source — reuse only a fingerprint-matched build, else build fresh; reuse is explicit-only. +> +> So a leftover EAS/release build is **not** a shortcut for "iterate live" — it's the wrong binary. The fact that a build *exists* never makes it the right one. + +| Mode | What it is | Choose when | Live edits? | +|---|---|---|---| +| **A — Local release build** | Build a Release `.app` locally, `agent-device install` it (uploads) | User has a Mac toolchain and wants a quick "run my current code on a cloud device" | No (rebuild to see changes) | +| **B — EAS build** (rare, explicit-only) | `eas build` a simulator build, `agent-device install-from-source ` (the VM downloads it) | **Only when explicitly asked** — the user names an existing/EAS build, or wants a static EAS artifact for CI/sharing. Not for "show me"/"iterate" (use C). Sim builds need no credentials. | No | +| **C — Local dev build + tunnel** | Dev (Debug) build + `EXPO_UNSTABLE_TUNNEL_V2=1 expo start --tunnel` + connect the dev client to Metro | **The agentic edit-and-see loop** — change code and see it live (Fast Refresh) | **Yes** | + +Quick decision — **default to C; A and B are explicit-only:** +- **C (almost everything):** iterate, interact, poke the app, live edits — *and* most "show me my app" (current code needs a build anyway, so live+current wins). Mac → dev client builds locally; no Mac → build it on EAS (`developmentClient: true`). **Unsure → C.** +- **A:** only an explicit one-shot **static** screenshot on a Mac. +- **B:** only when the user names an existing/EAS build or wants a static EAS artifact (CI/sharing) — see the box above for why a static build is the wrong tool for "iterate." + +## Driving the device (agent-device) + +`agent-device` is the controller. Common verbs (run each as `npx --yes eas-cli@latest simulator:exec npx agent-device@latest `): + +| Verb | Does | +|---|---| +| `apps --platform ios` | List installed apps (the blank sim shows none) | +| `install --platform ios` | Install a local `.app` (uploads it) | +| `install-from-source --platform ios` | Install from a URL — the VM downloads it (use for EAS artifacts) | +| `open --platform ios` | Launch an app (bundle id) or follow an app **deep link** (`exp+slug://…`). **Not** for the `webPreviewUrl` — that's a browser preview for the user, never the device. | +| `snapshot -i` | Interactive accessibility tree → `@e1`-style refs | +| `press ` | Tap (e.g. `press @e2` or `press 'label="Open"'`) — **the tap verb is `press`, not `tap`** | +| `fill "text"` | Type into a field | +| `screenshot ` | Capture the screen to a local PNG (downloaded from the daemon) — requires an app to be open (`open` first) | +| `metro prepare` / `metro reload` | Point a dev client at Metro / reload (Mode C) | + +For the full verb set and the `argent` controller alternative, see [references/controllers.md](./references/controllers.md). + +## Operating principles + +The non-obvious mental model worth internalizing. Specific error→fix lookups (hung verbs, `tap`→`press`, `--platform`, `--json`, `pod install` locale, orphaned sessions, boot variability) live in [references/troubleshooting.md](./references/troubleshooting.md). + +1. **Establish ground truth, then reset — don't patch-loop.** Never assume an existing session or Metro is yours or healthy. Before driving, confirm: + - **cwd** — you're in the intended Expo project dir (a misdirected `start`/`exec` sessions the *wrong app* + drops a stray `.env.eas-simulator`; `pwd` / check `app.json`). + - **session live** — `IN_PROGRESS` via `simulator:get --json` (a stopped session keeps its id + `remoteConfig`, so the dotenv alone isn't proof). + - **one Metro on `:8081`** — reuse if it's yours, else free the port before starting (run-your-app.md). + - **build fits intent** — a **release build can't live-reload**; if live edits are wanted and a release build is installed, **install the dev build, don't reconnect**. + + If current code isn't rendering after your **first** connect, stop poking live state: **reset to baseline** (stop session → clear dotenv → kill Metro) and redo the mode **once**; a second failure → stop and report. Never restart Metro in place, reconnect more than once, rebuild the native client to fix a JS/connection problem, or surface a preview URL while state is unknown. (A daemon drop — `ERR_NGROK_3200` / `Remote daemon is unavailable` — is the same: reset, don't retry.) +2. **`exec` is a wrapper, not a driver.** `simulator:exec` loads `.env.eas-simulator` and spawns the command you pass; the device verbs come from the controller (`npx agent-device@latest`). There is no `simulator:tap`. +3. **Act immediately; don't park an idle session.** Sessions are short-lived — install and drive right after `start`. Leaving one idle drops the tunnel/daemon (→ reset, per #1). +4. **Stop on every exit path (billing) and reset the dotenv.** `--non-interactive` doesn't auto-stop, and a forgotten session bills until stopped. Don't `start` again to "retry" a slow boot — that orphans a second billed session. +5. **Screenshot only the correct, fresh build.** Mode C only after the dev client connects to Metro; A/B only from a build matching current source — reusing a pre-existing build is the #1 "my edits don't show" cause (see the build caveat above). (`9:41` in the status bar is the sim default, not staleness.) + +## Stop and clean up + +Stop the session (ends billing) **and reset the dotenv** so a later run doesn't try to reuse the dead session: + +```bash +npx --yes eas-cli@latest simulator:stop # omit --id → stops the dotenv session (or pass --id ) +printf '# managed by eas-cli\n' > .env.eas-simulator # clear the stale session id so it isn't reused +# if you started Metro for Mode C, stop it too (Ctrl+C in its terminal, or kill the expo process) +``` + +## References + +- [references/run-your-app.md](./references/run-your-app.md) — full tested command sequences for modes A, B, and C (read before running a mode). +- [references/controllers.md](./references/controllers.md) — agent-device verb reference and the `argent` alternative. +- [references/troubleshooting.md](./references/troubleshooting.md) — concrete errors and fixes. + +Source of truth: Expo docs and the `eas` / `agent-device` CLIs (`npx --yes eas-cli@latest simulator:* --help`, `agent-device --help`). This skill teaches how to apply them; it doesn't replace them. diff --git a/plugins/expo/skills/eas-simulator/references/controllers.md b/plugins/expo/skills/eas-simulator/references/controllers.md new file mode 100644 index 0000000..387ea68 --- /dev/null +++ b/plugins/expo/skills/eas-simulator/references/controllers.md @@ -0,0 +1,67 @@ +# Controllers: agent-device and argent + +`eas-cli` has no device verbs — it manages the *session*. The verbs (open/tap/type/screenshot/inspect) come from a **controller** that `npx --yes eas-cli@latest simulator:exec` runs locally and that talks to the controller daemon on the remote VM. Two controllers are supported by `npx --yes eas-cli@latest simulator:start --type`: + +- `agent-device` (Callstack, MIT) — used throughout this skill; runs on demand via `npx agent-device@latest`, nothing installed globally. +- `argent` (Software Mansion) — a capable alternative controller; check its license for your use. +- `serve-sim` — not a controller; a streaming/preview-only type (iOS), no programmatic control. + +## agent-device verbs (run via `npx --yes eas-cli@latest simulator:exec npx agent-device@latest `) + +agent-device is a thin **client** talking to a **daemon** (the daemon runs on the VM in a session). `npx --yes eas-cli@latest simulator:exec` sets `AGENT_DEVICE_DAEMON_BASE_URL` + `AGENT_DEVICE_DAEMON_AUTH_TOKEN` from `.env.eas-simulator`, which switches the client into remote mode. Selectors and `@e`-refs come from the latest `snapshot`. + +The CLI help is written for agents and is the source of truth — run these for the full verb set and agentic loop guidance: + +```bash +npx --yes eas-cli@latest simulator:exec npx agent-device@latest --help +npx --yes eas-cli@latest simulator:exec npx agent-device@latest help workflow +``` + +EAS-specific notes: + +- **`press`, not `tap`.** The tap verb is `press` — `tap` is not a verb. +- **`snapshot -i` is slow on iOS** — tens of seconds is normal; wait for it. +- **`install` uploads** a local binary to the daemon; **`install-from-source`** has the VM download from a URL (use for EAS artifacts — avoids a large upload). +- **Proven in this skill's flows:** `apps`, `install`, `install-from-source`, `open`, `snapshot -i`, `press`, `fill`, `screenshot`. Others (`gesture`/`scroll`/`logs`/`record`/`network`/`perf`/`metro`) are real in the CLI but not exercised here — confirm via ` --help` before relying on them. + +## argent (alternative) + +`npx --yes eas-cli@latest simulator:start --type argent` provisions an argent remote session. The connection config it returns is different (`ARGENT_TOOLS_URL` / `ARGENT_AUTH_TOKEN`). + +**argent sessions cannot install apps today.** `--type argent` provisions only an argent daemon on the VM — there is no agent-device daemon, so agent-device install commands don't apply. Use argent to drive an app that is already on the sim (e.g. start with `--type agent-device` to install, then switch; or use an EAS build with `install-from-source` via an agent-device session first). + +**Connecting via MCP (Cursor, Claude Code, Codex, and others).** Install the CLI globally first — the package is `@swmansion/argent`, not `argent`: + +```bash +npm install -g @swmansion/argent +``` + +Then run `argent init --yes` to register the Argent MCP server. Link the session credentials with `argent link` — the recommended path: + +```bash +argent link '' --token '' --yes +``` + +Reload the agent after linking so its `argent mcp` process picks up the remote session. + +**Sandboxed shells** (Claude Code, some CI environments) can't write to `~/.argent/` so `argent link` won't work there. Use env vars in the MCP config file instead — this is argent's highest-precedence resolution and overrides any link: + +```json +{ + "mcpServers": { + "argent": { + "command": "argent", + "args": ["mcp"], + "env": { + "ARGENT_TOOLS_URL": "", + "ARGENT_AUTH_TOKEN": "" + } + } + } +} +``` + +MCP config file location: `.cursor/mcp.json` (Cursor), `.claude/mcp.json` (Claude Code), `mcp.json` in the Codex project root. It carries a session token — **add it to `.gitignore`**. + +**Known issues:** +- `argent init --help` launches an interactive wizard regardless of the flag — use `--yes` to skip it, or read the package source for non-interactive flags. diff --git a/plugins/expo/skills/eas-simulator/references/run-your-app.md b/plugins/expo/skills/eas-simulator/references/run-your-app.md new file mode 100644 index 0000000..9348560 --- /dev/null +++ b/plugins/expo/skills/eas-simulator/references/run-your-app.md @@ -0,0 +1,185 @@ +# Running your app on the remote sim — tested sequences + +The remote sim boots blank. You install a **simulator-targeted** build onto the session, then open it. Pick a mode from `SKILL.md`. (Sequences validated against eas-cli 20.3.x + agent-device 0.17.x in mid-2026; the commands are experimental — if one fails, re-check ` --help`.) + +In all modes, the session is started the same way and driven through `npx --yes eas-cli@latest simulator:exec`. Replace `dev.example.app` with the app's iOS `bundleIdentifier` (from `app.json` → `ios.bundleIdentifier`), and run from the project directory. + +> These sequences are **iOS**. For **Android**: build via `npx --yes eas-cli@latest build --platform android` (or local Gradle), `install` the `.apk` instead of an `.app`, skip `pod install`, and note there's **no `webPreviewUrl`** (Android is agent-driven / screenshot-only). + +## Starting a session (shared by all modes) + +```bash +# Reset the dotenv first so the new session id isn't masked by an "Overwriting previous session" warning. +printf '# managed by eas-cli\n' > .env.eas-simulator + +# Start (no --json, so it writes .env.eas-simulator). It boots the sim + agent-device daemon. +npx --yes eas-cli@latest simulator:start --platform ios --type agent-device --non-interactive +``` + +`start`'s own poll is unreliable, so confirm liveness with a bounded loop (boot is ~90s–15min). `get`/`exec`/`stop` default to the session in `.env.eas-simulator`, so you can omit `--id`: + +```bash +# Poll up to ~16 min; IN_PROGRESS + remoteConfig = live; a terminal status = failed boot (stop + restart). +for i in $(seq 1 64); do + S=$(npx --yes eas-cli@latest simulator:get --json --non-interactive 2>/dev/null) + echo "$S" | grep -q '"status": *"IN_PROGRESS"' && echo "$S" | grep -q remoteConfig && { echo "live"; break; } + echo "$S" | grep -qE '"status": *"(STOPPED|ERRORED)"' && { echo "boot failed — stop + restart"; break; } + sleep 15 +done +``` + +If you need the id explicitly, it's `EAS_SIMULATOR_SESSION_ID` in `.env.eas-simulator`. `start` also prints a `webPreviewUrl` (iOS-only browser preview — surface it per the SKILL.md "watch it live" rules) and a job-run URL. Once live, the session env is in `.env.eas-simulator`, so `simulator:exec` works. + +--- + +## Mode A — Local release build (embedded JS, no Metro) + +A Release build bundles the JS into the binary, so it renders without Metro. Good for a quick "run my current code on a cloud device" when a Mac toolchain is available. + +```bash +# 1. Generate native project + build a Release simulator .app +npx expo prebuild --platform ios # set ios.bundleIdentifier in app.json first to avoid prompts +# pod install can fail on Ruby 4 + CocoaPods with a Unicode/ASCII-8BIT error — fix with a UTF-8 locale: +( cd ios && LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 pod install ) +LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 xcodebuild \ + -workspace ios/.xcworkspace -scheme \ + -configuration Release -sdk iphonesimulator -derivedDataPath ios/build build +# → ios/build/Build/Products/Release-iphonesimulator/.app + +# 2. Start a session (see "Starting a session" above), then install + open + drive +APP=ios/build/Build/Products/Release-iphonesimulator/.app +npx --yes eas-cli@latest simulator:exec npx agent-device@latest install dev.example.app "$APP" --platform ios +npx --yes eas-cli@latest simulator:exec npx agent-device@latest open dev.example.app --platform ios +npx --yes eas-cli@latest simulator:exec npx agent-device@latest screenshot ./shot.png + +# 3. Stop +npx --yes eas-cli@latest simulator:stop # omit --id → stops the dotenv session +``` + +The `install` here **uploads** the (~90MB) `.app` to the remote daemon over the tunnel, which installs it on the sim with `simctl`. + +--- + +## Mode B — EAS build (the VM downloads it; no credentials) + +**Explicit-only** (see the SKILL.md mode picker): a *static* EAS artifact for CI/sharing, or when the user names an existing EAS build. For no-Mac **live** iteration use Mode C with an EAS dev-client build (see Mode C below), not this. **Simulator builds are unsigned, so EAS asks for no credentials.** + +⚠️ **Check for an existing build first.** Before triggering a new build, check if a fingerprint-matched one already exists — it saves ~15-20 min: + +```bash +npx --yes eas-cli@latest build:list --platform ios --profile --status finished --json | \ + head -20 # look for a sim build whose fingerprint matches current source +``` + +If one matches, skip straight to step 3 with its artifact URL. + +⚠️ **Order matters:** build FIRST, `start` the session LAST. The build takes ~15-20 min and a session left idle that long times out (`ERR_NGROK_3200`) — don't `start` until you have the artifact URL. + +```bash +# 1. Find or create a simulator build profile in eas.json. +# Read eas.json if it exists and look for a build profile with ios.simulator: true. +# If one exists, note its name and skip to step 2. +# If not, add one named "sim" — use node, python3, jq, or a direct JSON edit, whichever +# is available. Preserve all other profiles. Minimum: { "ios": { "simulator": true } } + +# 2. Build (no credentials prompt for a simulator build). Prints an artifact URL when done (~15-20 min). +npx --yes eas-cli@latest build --platform ios --profile sim --non-interactive +# → https://expo.dev/artifacts/eas/.tar.gz + +# 3. Start a session, then install-from-source so the VM downloads the artifact (no local upload) +ART="https://expo.dev/artifacts/eas/.tar.gz" +npx --yes eas-cli@latest simulator:exec npx agent-device@latest install-from-source "$ART" --platform ios +npx --yes eas-cli@latest simulator:exec npx agent-device@latest open dev.example.app --platform ios +npx --yes eas-cli@latest simulator:exec npx agent-device@latest screenshot ./shot.png + +# 4. Stop +npx --yes eas-cli@latest simulator:stop # omit --id → stops the dotenv session +``` + +**Build freshness:** reuse only a build whose **fingerprint matches current source** (`npx --yes eas-cli@latest build:list --platform ios --json`, or `get-build` by fingerprint per Callstack's public `eas-agent-device` workflow); otherwise **rebuild** or use Mode C. Tell the user which build you used. (Why this matters → SKILL.md "Reusing an existing build" caveat.) + +--- + +## Mode C — Local dev build + tunnel (live edits via Fast Refresh) + +This is the agentic edit-and-see loop: a **dev (Debug) build** loads JS from your local **Metro** over **tunnel v2**, so code edits appear on the remote sim via Fast Refresh. It has the most steps — each is necessary. + +⚠️ **Don't install a release build as a "quick interim" and screenshot it** — that interim shows stale, build-time code (the "outdated screenshot" trap). Go straight to the dev build + Metro; screenshot only after the dev client is connected to Metro. + +**No local Mac toolchain?** (the common cloud/Linux case) Build the dev client on **EAS** instead of step 1 below. ⚠️ Same order-matters rule as Mode B: build first, start the session after you have the artifact URL. + +```bash +# ── Non-Mac path: replace step 1 with these ────────────────────────────────── + +# Find or create a dev-client simulator build profile in eas.json. +# Read eas.json if it exists and look for a build profile with developmentClient: true + ios.simulator: true. +# If one exists, note its name and skip to the build step. +# If not, add one named "dev-sim" — use node, python3, jq, or a direct JSON edit, whichever +# is available. Preserve all other profiles. Minimum: { "developmentClient": true, "ios": { "simulator": true } } + +# Build (~15-20 min). Prints an artifact URL when done. +npx --yes eas-cli@latest build --platform ios --profile dev-sim --non-interactive +# → https://expo.dev/artifacts/eas/.tar.gz + +# Start a session AFTER the build finishes (don't start early — idle sessions time out). +# Then in step 3 below, use install-from-source (VM downloads the artifact) instead of local install: +ART="https://expo.dev/artifacts/eas/.tar.gz" +npx --yes eas-cli@latest simulator:exec npx agent-device@latest install-from-source "$ART" --platform ios +# Continue from step 3a (open the dev client, enter Metro URL) onward — identical to the Mac path. +``` + +```bash +# 1. Add expo-dev-client and build a Debug (dev-client) simulator .app +npx expo install expo-dev-client +npx expo prebuild --platform ios --clean # set ios.bundleIdentifier first (as in Mode A) to avoid prompts +( cd ios && LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 pod install ) +LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 xcodebuild \ + -workspace ios/.xcworkspace -scheme \ + -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build-debug build +DEVAPP=ios/build-debug/Build/Products/Debug-iphonesimulator/.app + +# 2. Start Metro with tunnel v2 — exactly ONE instance. First check :8081: +# `curl -sf localhost:8081/status` answers AND it's your Metro → reuse it, skip this step. +# Port taken but NOT yours → `PIDS=$(lsof -ti:8081); [ -n "$PIDS" ] && kill $PIDS` BEFORE starting +# (relaunching onto an occupied 8081 is the "port already in use" clash). A bare `&` won't survive across agent +# shell calls — use a long-lived/background run or a separate terminal. tunnel v2 (durable-object, +# not ngrok) works from robot/cloud agents where plain --tunnel is blocked. +EXPO_UNSTABLE_TUNNEL_V2=1 npx expo start --tunnel --port 8081 +# → note the deep link exp+://.on.expo.app; the manifest URL is https://.on.expo.app + +# 3. Start a session, install the dev build, then connect it to Metro. +# RELIABLE path = "open the dev client, then Enter URL manually". The deep-link + system "Open in +# ''?" dialog is flaky: the dialog may not appear, and `press 'label="Open"'` can hang ~90s +# against a slow daemon. Don't make the loop depend on it. +# The button labels below ("Enter URL manually"/"Connect"/"Reload"/"Go back", and the system "Open") +# are expo-dev-client / iOS / expo-router UI — the same across ANY Expo app (not app-specific), but +# UI text that can shift across versions. Treat them as illustrative: if a label doesn't match, +# `snapshot -i` and press the current ref. The flow matters, not the exact strings. +npx --yes eas-cli@latest simulator:exec npx agent-device@latest install dev.example.app "$DEVAPP" --platform ios + +# a) launch the dev client (it opens its launcher, which only auto-discovers LAN Metro — ours is remote): +npx --yes eas-cli@latest simulator:exec npx agent-device@latest open dev.example.app --platform ios + +# b) point it at your remote Metro via "Enter URL manually": +npx --yes eas-cli@latest simulator:exec npx agent-device@latest press 'label="Enter URL manually"' +npx --yes eas-cli@latest simulator:exec npx agent-device@latest snapshot -i # get the text-field ref +npx --yes eas-cli@latest simulator:exec npx agent-device@latest fill @ "https://.on.expo.app" +npx --yes eas-cli@latest simulator:exec npx agent-device@latest press 'label="Connect"' + +# c) first-run dev menu → Reload to fetch the bundle (first build+transfer over the tunnel ~40-60s): +npx --yes eas-cli@latest simulator:exec npx agent-device@latest press 'label="Reload"' + +# d) expo-router may show "Unmatched Route" (the connect URL was parsed as a path) → go to home: +npx --yes eas-cli@latest simulator:exec npx agent-device@latest press 'label="Go back"' + +# 4. Edit a source file locally → Fast Refresh pushes it to the remote sim with NO reload. Screenshot to confirm. +npx --yes eas-cli@latest simulator:exec npx agent-device@latest screenshot ./live.png + +# 5. Stop the session AND Metro +npx --yes eas-cli@latest simulator:stop # omit --id → stops the dotenv session +# kill the `expo start --tunnel` process +``` + +Notes: +- The launcher's auto-discovery only scans the LAN, so a remote Metro must be entered via "Enter URL manually" — that's why this is the connect step. +- **This "Enter URL manually" + public tunnel URL flow is the ONLY connect path.** If it fails, don't switch mechanisms or reconnect in a loop — reset to baseline and redo Mode C once (SKILL.md principle 1). (`agent-device`'s `metro prepare --proxy-base-url` bridge exists but is not part of this loop.) diff --git a/plugins/expo/skills/eas-simulator/references/troubleshooting.md b/plugins/expo/skills/eas-simulator/references/troubleshooting.md new file mode 100644 index 0000000..f953bbe --- /dev/null +++ b/plugins/expo/skills/eas-simulator/references/troubleshooting.md @@ -0,0 +1,38 @@ +# Troubleshooting + +Concrete errors seen while validating this flow, and the fix. + +| Symptom | Cause | Fix | +|---|---|---| +| `Command simulator:start not found` | `eas-cli` too old (commands are hidden but present from ≥ 20.3.0) | Run via `npx eas-cli@latest …`, or upgrade `eas-cli`. | +| `An Expo user account is required` / `whoami` shows logged-out | No browser login on a cloud/CI/headless box, or `EXPO_TOKEN` unset/invalid | Set **`EXPO_TOKEN`** (expo.dev → Account → Access Tokens) in the env; verify `npx --yes eas-cli@latest whoami`. (Interactive machines can `eas login`.) | +| `simulator:start`/`build`: no linked project / missing `projectId` | A fresh `create-expo-app` isn't linked to EAS | `npx --yes eas-cli@latest init` to create/link it (writes `extra.eas.projectId`). | +| `prebuild`/`eas build` prompts for or fails on a missing **iOS bundle identifier** | A fresh app often has no `ios.bundleIdentifier` | Set it in app config (e.g. `dev..`); confirm via `npx expo config --json` (may live in `app.config.js`). | +| `--max-duration-minutes` rejected, or `simulator:start` denied / not-allowlisted / quota error | The flag is **paid-plan only**, or the experimental feature isn't enabled for the account | Drop `--max-duration-minutes` for the default; check the plan / that EAS Simulator is enabled for the account. | +| `start` keeps "Waiting for … session to be ready" but it never returns | `start`'s readiness poll can miss a session that's actually live | Don't rely on it — poll `npx --yes eas-cli@latest simulator:get --id --json` for `status: IN_PROGRESS` + a populated `remoteConfig`. | +| `ERR_NGROK_3200` / endpoint offline; `Remote daemon is unavailable` | The session's tunnel/daemon dropped — left idle and timed out, or the VM was torn down | A drop invalidates the **whole** session (installed app, `@e` refs, Metro). **Don't retry the failed verb** — start a fresh session, reset the dotenv, and re-run install→open→drive from the top, acting immediately. | +| Two sessions running / orphaned session / surprise double billing | A second `start` (e.g. to "retry" a slow boot) creates a second billed session and overwrites the dotenv id, orphaning the first | Never `start` again to retry — poll the existing session instead. Find orphans with `simulator:list --status IN_PROGRESS` and stop each with `simulator:stop --id `. | +| A device verb hangs (no return for a minute+) | Slow daemon; `press`/`screenshot` can block ~90s | Wrap verbs in `timeout 120 …`; on timeout `snapshot -i` to see if the action landed before retrying (taps can double-fire). Don't blind-retry. | +| `install requires an active session or an explicit device selector` | `install` can't infer the device | Pass `--platform ios` (or `open` something first to establish a session). | +| `Unknown command: tap` | The tap verb is `press` | Use `press ` (e.g. `press @e2` or `press 'label="Open"'`). | +| `SESSION_NOT_FOUND: No active session. Run open first.` | A verb (e.g. `screenshot`) ran before any app/session was opened | `open ` first (or pass `--platform ios`). | +| `simulator:exec` / `build` / `simulator:stop`: "Run this command inside a project directory." | Run from the wrong cwd | Run from the Expo project directory (where `app.json`/`eas.json` live). | +| New session's id shows as the *previous* one; "Overwriting previous simulator session (id: …)" | The stale `.env.eas-simulator` had an old `EAS_SIMULATOR_SESSION_ID`; the warning line masks the new id | Reset the dotenv before `start`: `printf '# managed by eas-cli\n' > .env.eas-simulator`. | +| No `.env.eas-simulator` written after `start` | `--json` suppresses the dotenv | Run `start` *without* `--json` for the `exec` flow; with `--json` you must read `remoteConfig` from stdout and set the env yourself. | +| `pod install` fails: `Unicode Normalization not appropriate for ASCII-8BIT` | Ruby 4 + CocoaPods with a non-UTF-8 locale | Re-run with `LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 pod install`. | +| (Mode C) Deep-link `open` lands on the dev-client launcher, not the app | Opening the deep link triggers a system "Open in ''?" dialog; and the launcher only auto-discovers Metro on the LAN | `press 'label="Open"'` to dismiss the dialog, then "Enter URL manually" → `fill` the `https://.on.expo.app` manifest URL → "Connect". | +| (Mode C) App shows expo-router "Unmatched Route" | The connect URL was parsed as a route path | `press 'label="Go back"'` (or navigate to `/`). | +| (Mode C) Dev client shows a `?` placeholder / blank after connect | Bundle not fetched yet | `press 'label="Reload"'` and wait ~40-60s for the first build+transfer over the tunnel. | +| (Mode C) `expo start` fails: "port 8081 already in use" | A Metro is already bound to 8081 (often one from earlier in the session) | Don't relaunch onto it. Reuse if it's yours (`curl -sf localhost:8081/status`), else free it (`PIDS=$(lsof -ti:8081); [ -n "$PIDS" ] && kill $PIDS`) then start one. There is only ever one Metro. | +| (Mode C) `expo start` / `node` killed with **exit 137** | 137 = SIGKILL — almost always the **OOM killer** (memory pressure, common in constrained cloud sandboxes, esp. a native build + Metro at once). **Not** a port clash. | Reduce memory pressure: don't run a native build and Metro concurrently; give the sandbox more memory; retry. | +| (Mode C) Edits won't live-reload no matter how often you reconnect | A **release** build is installed — its JS is baked in, so it ignores Metro | Stop reconnecting: **install the dev (Debug) build**, connect it to Metro, reload. Reconnecting a release build to Metro is a no-op. | +| `expo start --tunnel` errors for a robot/`EXPO_TOKEN` user | The ngrok robot-user guard | Use tunnel v2: `EXPO_UNSTABLE_TUNNEL_V2=1 expo start --tunnel`. | +| Unexpected charges / a session you forgot | `start --non-interactive` does NOT auto-stop | Always `npx --yes eas-cli@latest simulator:stop --id `. List leftovers with `npx --yes eas-cli@latest simulator:list`. | +| Screenshot shows **old content** / my recent edits don't appear | Running a **release build (Mode A/B)** whose JS was baked in *before* your edits — typically a reused/stale build | A/B reflect code at build time, not now. **Rebuild** (ensure the build's fingerprint matches current source), or use **Mode C** (dev + Metro) so live edits show via Fast Refresh. The screenshot itself is fresh — it's the build that's stale. (`9:41` in the status bar is the sim default, not staleness.) | + +## Performance expectations + +Set the user's expectations honestly — this is experimental: +- **Boot is variable**: ~90s warm to ~15 min cold. Poll patiently. +- **`snapshot` can be slow** on iOS (tens of seconds). +- **First bundle load** over the tunnel (Mode C) is the slow part; subsequent Fast Refreshes are fast. diff --git a/skills.sh.json b/skills.sh.json index af4e787..34a916a 100644 --- a/skills.sh.json +++ b/skills.sh.json @@ -32,7 +32,8 @@ "expo-deployment", "expo-cicd-workflows", "expo-observe", - "eas-update-insights" + "eas-update-insights", + "eas-simulator" ] }, {