diff --git a/plugins/expo/.claude-plugin/plugin.json b/plugins/expo/.claude-plugin/plugin.json index 7c5e5b6f..fb372968 100644 --- a/plugins/expo/.claude-plugin/plugin.json +++ b/plugins/expo/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "expo", - "version": "1.14.0", + "version": "1.14.1", "description": "Official Expo skills for building, deploying, upgrading, and debugging Expo apps", "author": { "name": "Expo Team", diff --git a/plugins/expo/plugin.json b/plugins/expo/plugin.json index 321e1110..f0396a4d 100644 --- a/plugins/expo/plugin.json +++ b/plugins/expo/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "expo", - "version": "1.14.0", + "version": "1.14.1", "description": "Official Expo skills for building, deploying, upgrading, and debugging Expo and React Native apps.", "author": { "name": "Expo Team", diff --git a/plugins/expo/skills/eas-simulator/SKILL.md b/plugins/expo/skills/eas-simulator/SKILL.md index b1a22611..a8ca0f5c 100644 --- a/plugins/expo/skills/eas-simulator/SKILL.md +++ b/plugins/expo/skills/eas-simulator/SKILL.md @@ -41,7 +41,8 @@ if the user set a limit you would go past, or if the work grows beyond what they - `--max-duration-minutes N` is the hard automatic-stop deadline. Any plan can set it, up to the plan limit (40 minutes on Free, 115 on paid plans). The timer starts when the session is ready. - `--max-idle-time-minutes N` stops a session after that many inactive minutes. Omitted means **no idle timeout**: the session runs until its maximum duration or an explicit stop. -- **Controller activity resets the idle timer** (`agent-device`, `argent`, and Appium commands, which are recorded as session events with producer `appium`). **Browser-preview activity does not reset it.** For a user-driven browser preview, rely on the maximum duration, not idle time, to bound the session. +- **Controller activity resets the idle timer** (`agent-device`, `argent`, and Appium commands). Current iOS automation-session workers also count preview input and host actions recorded as session events, such as app launches and screenshots. Settings, permissions, and app-file changes do not reset the timer. Watching video does not count; Android previews and older workers do not report preview activity. `web-preview-only` sessions do not enforce an idle timeout. Use the maximum duration as the hard limit, and omit the idle timeout when a preview must remain available without input. +- `npx --yes eas-cli@latest account:usage` shows the selected account's Simulator minutes. ## Check availability first @@ -90,10 +91,31 @@ To **watch** it live, hand the user the `webPreviewUrl` that `start` prints (in - **"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** → use `--max-duration-minutes N` (up to the plan limit). Browser-preview activity does not reset `--max-idle-time-minutes`, so idle timeout is not a reliable lifetime bound for this case. Tell the user when the session expires, using the CLI's reported duration or expiry. Keep it running for the requested preview; stop sessions created for one-shot tasks when the task finishes. +- **Keeping it alive for the user to drive** → use `--max-duration-minutes N` (up to the plan limit). Watching video does not reset the idle timer, and input reporting varies by platform and worker. Follow [Session lifetime](#session-lifetime) when choosing an idle timeout. Tell the user when the session expires, using the CLI's reported duration or expiry. Keep it running for the requested preview; stop sessions created for one-shot tasks when the task finishes. `start` also prints a job-run URL. +## Preview access and sharing + +With **eas-cli 25.0.0+**, new sessions default to an authenticated expo.dev preview +link. The viewer must sign in and have access to the project; a private session's +Share button is hidden. `simulator:get` follows the session's saved sharing choice, +including for older sessions. + +For a requested link that people can use without signing in, add `--public-link` +to `simulator:start`. Anyone holding that link can view and control the simulator, +including its app and logs. Share it only with the intended recipients. Keep the +default authenticated link for ordinary development and diagnostics. + +Private `get --json` output omits preview tokens and returns +`remoteConfig.previewApiUrl: null`. Private `start --json` does the same unless +the session grants embedding or cross-origin API access; those grants expose +the direct preview URL for the receiving page. Controller credentials remain available for +`simulator:exec`; keep the full JSON private. A missing preview API URL does not +mean that the session failed. Use controller logs or the authenticated browser +preview for diagnostics; do not recreate a private session with `--public-link` +just to read its logs. See [logs and crashes](./references/logs-and-crashes.md). + ## Always name the session Pass `--name ""` on every `simulator:start`. The name appears in `simulator:list`, `simulator:get`, and on the **Simulator sessions** page on expo.dev, where it replaces the generic title on each row. Unnamed, every row reads "Simulator session" over a random id — a wall of identical entries nobody can navigate. Write the name for a **human scanning that list days later**, not for yourself during this run. @@ -189,7 +211,7 @@ For the full verb set and the `argent` controller alternative, see [references/c ## When the app crashes: device logs and crash reports (iOS) -When the app crashes or closes on launch, read the iOS session's crash reports and device log before guessing from screenshots. Read them from the preview API URL in `simulator:get --json`. That URL carries the session token, so never print it. +When the app crashes or closes on launch, read the iOS session's crash reports and device log before guessing from screenshots. Use the authenticated browser preview or controller logs for a private session. When `simulator:get --json` supplies a preview API URL, it carries the session token, so never print it. **Hold the device log before you reproduce the crash.** Without the hold, the crash gets a report but an empty log tail. Commands, fields, and fallbacks are in [references/logs-and-crashes.md](./references/logs-and-crashes.md). @@ -203,9 +225,9 @@ The non-obvious mental model worth internalizing. Specific error→fix lookups ( - **Metro on its own port** — reuse only if you started it this session; else start one on a free port (`--port `, e.g. 8082), don't kill another server to reclaim `:8081` (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 your 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.) + If current code isn't rendering after your **first** connect, stop poking live state: **reset to baseline** (stop session → clear dotenv → kill your 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 tunnel error alone does not prove that the session ended. Check its original ID with `simulator:get --id --json` before replacing it; stop the original session if you create a replacement. 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). +3. **Act immediately; don't park an idle session.** Install and drive right after `start` so setup time does not consume the session limit. An idle timeout applies only when configured; follow [Session lifetime](#session-lifetime). 4. **Stop sessions you created on completion or failure and reset the dotenv.** `--non-interactive` does not stop a session when your task ends. For a requested live preview, follow the duration guidance above. Poll the existing session during a slow boot; starting another creates an extra session and overwrites the dotenv's session id. 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.) diff --git a/plugins/expo/skills/eas-simulator/references/controllers.md b/plugins/expo/skills/eas-simulator/references/controllers.md index f81a238d..39372ad9 100644 --- a/plugins/expo/skills/eas-simulator/references/controllers.md +++ b/plugins/expo/skills/eas-simulator/references/controllers.md @@ -7,7 +7,7 @@ - `argent` (Software Mansion) — a capable alternative controller; check its license for your use. - `web-preview-only` — browser preview with no programmatic control. -All four types include a web preview. Before setting `--max-idle-time-minutes`, follow [Session lifetime](../SKILL.md#session-lifetime); browser-preview activity does not reset the timer. +All four types include a web preview. Before setting `--max-idle-time-minutes`, follow [Session lifetime](../SKILL.md#session-lifetime); current iOS automation workers also count preview input, while watching video does not count; Android and older workers differ. ## Appium diff --git a/plugins/expo/skills/eas-simulator/references/logs-and-crashes.md b/plugins/expo/skills/eas-simulator/references/logs-and-crashes.md index ced2c309..ef77099e 100644 --- a/plugins/expo/skills/eas-simulator/references/logs-and-crashes.md +++ b/plugins/expo/skills/eas-simulator/references/logs-and-crashes.md @@ -2,11 +2,13 @@ When the user's app crashes, closes on launch, or misbehaves on an iOS session, read what the device recorded instead of guessing from screenshots. The session's preview server (serve-sim) keeps a shared buffer of the simulator's device log and collects the crash reports macOS writes for simulator apps, each with the app's own log lines from just before the crash. -This covers iOS sessions. Call `sim_api /crashes` first. If it returns `404 Not found`, the session's preview server predates these routes: fall back to `agent-device logs` (see [controllers.md](./controllers.md)) and tell the user crash reports aren't available on that session. Don't call `/logs` there, because older servers stream it without end. +This covers iOS sessions with a preview API URL. For private sessions on eas-cli 25.0.0+, `simulator:get --json` deliberately returns `remoteConfig.previewApiUrl: null`. Use `simulator:exec npx agent-device@latest logs` (check its `help logs` for options), or direct the user to Logs and Crashes in the authenticated browser preview. A null URL is not a boot failure; do not restart the session or enable public sharing to obtain it. + +When a preview API URL is available, call `sim_api /crashes` first. If it returns `404 Not found`, the session's preview server predates these routes: fall back to `agent-device logs` (see [controllers.md](./controllers.md)) and tell the user crash reports aren't available on that session. Don't call `/logs` there, because older servers stream it without end. ## Reach the preview API -`simulator:get --json` returns `remoteConfig.previewApiUrl`, the preview server's API URL with the session token already in its query. Keep it in a shell variable and don't print it: the token grants control of the session. As with `simulator:exec`, run this from the Expo project directory. Pass `--id` with the session you are driving, because another agent can replace the dotenv session between your commands. +For a session created with `--public-link`, `simulator:get --json` includes `remoteConfig.previewApiUrl` when the worker reports it: the preview server's API URL with the session token already in its query. Older sessions can also expose this URL. Keep it in a shell variable and don't print it: the token grants control of the session. As with `simulator:exec`, run this from the Expo project directory. Pass `--id` with the session you are driving, because another agent can replace the dotenv session between your commands. ```bash API=$(npx --yes eas-cli@latest simulator:get --json --id | node -e ' @@ -14,8 +16,12 @@ API=$(npx --yes eas-cli@latest simulator:get --json --id | node -e let j; try { j = JSON.parse(s); } catch { console.error("simulator:get did not print JSON"); process.exit(1); } const url = j.remoteConfig?.previewApiUrl; - if (j.status !== "IN_PROGRESS" || j.platform !== "IOS" || !url) { - console.error(`no preview API (status ${j.status}, platform ${j.platform})`); + if (j.status !== "IN_PROGRESS" || j.platform !== "IOS") { + console.error(`no iOS preview API (status ${j.status}, platform ${j.platform})`); + process.exit(1); + } + if (!url) { + console.error("No preview API URL in this output; private sessions omit it. Use controller logs or the authenticated browser preview."); process.exit(1); } process.stdout.write(url); diff --git a/plugins/expo/skills/eas-simulator/references/run-your-app.md b/plugins/expo/skills/eas-simulator/references/run-your-app.md index a3a36237..6ca34a83 100644 --- a/plugins/expo/skills/eas-simulator/references/run-your-app.md +++ b/plugins/expo/skills/eas-simulator/references/run-your-app.md @@ -67,6 +67,34 @@ npx --yes eas-cli@latest simulator:exec npx agent-device@latest open --- +## Preview embedding and API origins + +For a requested integration, CLI 25.1.1+ accepts repeatable start flags: +`--frame-ancestor https://app.example.com` permits that page to frame the +preview; `--cors-origin https://app.example.com` permits its JavaScript to call +the preview API. An iframe alone does not need the parent's CORS grant. +Use only the origins needed for that integration. The printed direct preview +URL carries a token and grants session control; keep it out of source control +and public pages. Embed that direct URL rather than the expo.dev wrapper. +Private `get --json` does not recover this URL from saved origin grants on CLI +25.3.0; keep the start output private or query the authenticated REST session. + +## Android system image + +With **eas-cli 25.3.0+**, `--system-image` selects the Android SDK system-image +package. `--device` selects the AVD hardware profile. Omit the image flag for the +runner's default; it does not select an iOS runtime. Quote the package string +because semicolons are shell separators: + +```bash +npx --yes eas-cli@latest simulator:start --platform android --device pixel_7 \ + --system-image 'system-images;android-35-ext15;google_apis_playstore;x86_64' \ + --non-interactive --name "Android compatibility check" +``` + +Use a package supported by the remote runner. Keep the existing device-selection +workflow for other session types. + ## 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. @@ -108,7 +136,7 @@ npx --yes eas-cli@latest build:list --platform ios --profile 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. +⚠️ **Order matters:** build FIRST, `start` the session LAST. Building can consume much of the session's maximum duration. Don't `start` until you have the artifact URL. ```bash # 1. Find or create a simulator build profile in eas.json. diff --git a/plugins/expo/skills/eas-simulator/references/troubleshooting.md b/plugins/expo/skills/eas-simulator/references/troubleshooting.md index 0f4f5d33..955f0423 100644 --- a/plugins/expo/skills/eas-simulator/references/troubleshooting.md +++ b/plugins/expo/skills/eas-simulator/references/troubleshooting.md @@ -7,15 +7,16 @@ Concrete errors seen while validating this flow, and the fix. | Approval review rejects the Mode C Metro tunnel or dev-client Connect action | Review may lack or misinterpret the transport details or existing authorization; a signed URL alone does not establish private access | Follow [Tunnel scope and approvals](./run-your-app.md#tunnel-scope-and-approvals). Include the original authorization source and verified transport in the request; use reconsideration only where the host permits it. Preserve the requested live workflow while resolving approval. | | Controller recording download fails or times out | The local transfer can fail even though EAS retains the recording | Fetch it from [EAS session artifacts](./controllers.md#recording-download-recovery) using the original EAS session id and the recording’s `downloadUrl`. | | `Command simulator:start not found` | `eas-cli` too old (commands are hidden but present from ≥ 20.3.0) | Run via `npx --yes eas-cli@latest …`, or upgrade `eas-cli`. | +| Preview stopped, but the controller still answers (or the reverse) | Video and automation run in separate processes with separate log streams | Check the matching controller and preview steps on the EAS session job page. An `IN_PROGRESS` session or responsive controller alone does not prove the video is live. | | `simulator:start` rejects `--name` (e.g. `Nonexistent flag: --name`) | `eas-cli` too old — `--name` was added after `simulator:start` itself | Run via `npx --yes eas-cli@latest …`, or upgrade `eas-cli`. If you can't upgrade, retry once **without** `--name`; the session starts unnamed. | | `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 | The value is above the plan limit (40 minutes on Free, 115 on paid plans); inspect the CLI error | Pass a value within the plan limit, or omit the flag to use the default. | -| Browser-preview session stops despite ongoing interaction | Browser-preview activity does not reset `--max-idle-time-minutes`; only controller activity (`agent-device`, `argent`, Appium) does | Use the maximum duration as the lifetime bound for user-driven previews. Set it with `--max-duration-minutes`, and omit `--max-idle-time-minutes` unless controller inactivity is the intended stop condition. | +| Browser-preview session stops while being watched | Viewing video is not input. Current iOS automation workers count preview input, but Android previews and older workers do not | Use `--max-duration-minutes` as the hard limit. Omit `--max-idle-time-minutes` when the preview must remain available without input. `web-preview-only` sessions do not enforce idle timeout. | | `simulator:start` fails with `not enabled for this account` / not-allowlisted | EAS Simulator is limited-access and isn't enabled for this account | Don't retry. Confirm with `simulator:availability`, then hand off gracefully — tell the user and fall back to a local sim / EAS Build (see SKILL.md *Check availability first*). | | `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. | +| `ERR_NGROK_3200` / endpoint offline; `Remote daemon is unavailable` | The endpoint is unavailable; the session may still be running | Check the original ID with `simulator:get --id --json`. If it is running, check its connection details and current device state before retrying. If you replace it, stop the original session explicitly. | | Two sessions running / orphaned session | A second `start` (e.g. to "retry" a slow boot) creates another session and overwrites the dotenv id, orphaning the first | Poll the existing session instead. Find orphans with `simulator:list --status in-progress` and stop those you created with `simulator:stop --id `. | | A device verb hangs (no return for a minute+) | Slow daemon; `press`/`screenshot` can block ~90s | Bound it with agent-device's own `--timeout ` (e.g. `--timeout 120000`) — **not** a shell `timeout` wrapper (macOS has no `timeout` binary, so `timeout 120 …` fails with `command not found` and skips the verb). 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). | @@ -44,7 +45,7 @@ Concrete errors seen while validating this flow, and the fix. | (Android) The emulator stopped, and `agent-device boot` times out (`Daemon request timed out`) while the device stays `booted=false` | Without `--headless`, agent-device starts the emulator with a window. The EAS Linux image cannot run the windowed emulator, so it exits at once | Boot headless. Set `AGENT_DEVICE_HEADLESS=1` for the whole run, or pass `--headless` on each `boot`: `AGENT_DEVICE_HEADLESS=1 npx --yes eas-cli@latest simulator:exec npx agent-device@latest boot --platform android --device `. Get the AVD name from `agent-device devices --platform android`. The variable is read by the local client, so set it where you run the command. | | (argent) Every `argent run`/`tools` call returns `401 Unauthorized` right after linking | `argent link` without `--yes` no-ops on an already-linked URL ("Already linked. No changes."), keeping a stale token from a previous session | Re-link with `--yes` so the new token is written — see the link command in [controllers.md](./controllers.md). | | `/crashes` returns `404 Not found` on the preview API | The session's preview server predates the crash and log routes | Use `agent-device logs` for the device log, and tell the user crash reports aren't available on that session. Don't call `/logs` there. See [logs-and-crashes.md](./logs-and-crashes.md). | -| `sim_api` stops with `API_BASE: no preview API` | The `API=` lookup failed: the session is not `IN_PROGRESS`, is not iOS, or has no preview API | Read the lookup's own error line above it. Check the `--id`, or start a new session. See [logs-and-crashes.md](./logs-and-crashes.md). | +| `sim_api` stops with `API_BASE: no preview API` | The lookup failed, or private JSON deliberately omitted `previewApiUrl` (eas-cli 25.0.0+) | Read the lookup error and check the explicit session id/status. For a live private session, use controller logs or the authenticated preview; do not restart or enable public sharing just to read logs. See [logs-and-crashes.md](./logs-and-crashes.md). | | `sim_api` fails with `401` | The session ended or its token changed, so the preview server rejects the old one | Run the `API=` lookup again. It prints the session's status when the session is no longer `IN_PROGRESS`. See [logs-and-crashes.md](./logs-and-crashes.md). | | A crash shows up but its `logTailSource` is `none`, `buffer-rolled-past`, or `no-app-lines` with no lines | Nothing held the device log when the crash happened, or a busy device rolled the buffer past it (common in the first minutes after boot) | Start the `/crashes?tail=1` hold (or call `sim_api /logs "snapshot=1&follow=1&limit=1"` more often than every 8 seconds) **before** reproducing. After a boot, reproduce again once logging settles, a minute or two later. |