Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/expo/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion plugins/expo/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
32 changes: 27 additions & 5 deletions plugins/expo/skills/eas-simulator/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 "<description>"` 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.
Expand Down Expand Up @@ -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).

Expand All @@ -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 <N>`, 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 <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.)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 10 additions & 4 deletions plugins/expo/skills/eas-simulator/references/logs-and-crashes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,26 @@

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 <session-id> | node -e '
let s = ""; process.stdin.on("data", (d) => (s += d)).on("end", () => {
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);
Expand Down
30 changes: 29 additions & 1 deletion plugins/expo/skills/eas-simulator/references/run-your-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,34 @@ npx --yes eas-cli@latest simulator:exec npx agent-device@latest open <bundleId>

---

## 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.
Expand Down Expand Up @@ -108,7 +136,7 @@ npx --yes eas-cli@latest build:list --platform ios --profile <your-sim-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.
Expand Down
Loading
Loading