Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
712df42
[agent-cli] Verify the project root of a scanned dev server (llp/0004…
vonovak Sep 30, 2026
4169ba2
[agent-cli] Discovery rejects a server whose project root it cannot r…
vonovak Sep 30, 2026
743203e
[agent-cli] the dev-server lock never publishes a port that serves an…
vonovak Oct 6, 2026
375f261
[agent-cli] the dev --detach building test runs on its own free --por…
vonovak Oct 7, 2026
5ad026f
[agent-cli] discovery's project-root rules live in llp/0004 (§Discove…
vonovak Oct 8, 2026
260bd7d
[agent-cli] changelog for #132
vonovak Oct 8, 2026
4c152bc
[agent-cli] the /status read gets its own 3 s budget, a named port is…
vonovak Oct 8, 2026
b3e6c8b
[agent-cli] the status-read budget constant sits above discoverDevSer…
vonovak Oct 8, 2026
ff59297
[agent-cli] the /status read is retried after a socket error only, an…
vonovak Oct 8, 2026
f94f7c1
test(agent-cli): wait for telemetry worker exit on Windows
vonovak Oct 9, 2026
7a1db8a
fix(agent-cli): detached readiness waits for this project's Metro
vonovak Oct 9, 2026
1caa5cc
[agent-cli] prep for device binding: simulators.ts, adb.ts, eas.ts, R…
vonovak Oct 8, 2026
f7985b2
[agent-cli] the stub simctl's bootstatus -b marks the simulator Boote…
vonovak Oct 8, 2026
4e51f5d
[agent-cli] the device registry: one iOS simulator per worktree, boun…
vonovak Oct 8, 2026
d9b0409
[agent-cli] dev binds the worktree's iOS simulator, and every verb re…
vonovak Oct 8, 2026
881bee8
[agent-cli] e2e seeds the binding where a verb meets a booted simulat…
vonovak Oct 8, 2026
80c28f9
[agent-cli] changelog link for #134
vonovak Oct 8, 2026
3bacb86
[agent-cli] the review of #134: absent survives a missing adb, the Tr…
vonovak Oct 8, 2026
591c236
[agent-cli] a created simulator is a mainstream iPhone on the newest …
vonovak Oct 8, 2026
d333338
[agent-cli] dev --ios binds nothing off macOS, and the module's tests…
vonovak Oct 8, 2026
4ec8e63
[agent-cli] a bound simulator whose runtime is gone counts as gone, s…
vonovak Oct 8, 2026
019b827
test(agent-cli): model the iOS host in device binding cases
vonovak Oct 9, 2026
9c83c32
[agent-cli] the registry spawns a read-only emulator instance per wor…
vonovak Oct 8, 2026
e8ae773
[agent-cli] every verb drives the bound emulator instance: dev --andr…
vonovak Oct 8, 2026
5be6148
[agent-cli] the Android binding's changelog, the llp sentences it cha…
vonovak Oct 8, 2026
0db5f1c
fix(agent-cli): reject emulator boot after child exit
vonovak Oct 8, 2026
c591768
test(agent-cli): isolate local device assumptions in CI
vonovak Oct 9, 2026
d440865
feat(agent-cli): manage worktree device lifecycle
vonovak Oct 8, 2026
c650900
fix(agent-cli): keep smoke cleanup scoped to its device
vonovak Oct 8, 2026
583a2a1
fix(agent-cli): make device lifecycle fixtures Windows-safe
vonovak Oct 9, 2026
4be2d71
test(agent-cli): seed worktree bindings for status follow-ups
vonovak Oct 9, 2026
f43a989
feat(agent-cli): bind EAS sessions to worktrees
vonovak Oct 8, 2026
800b583
fix(agent-cli): recover EAS sessions after stale bindings
vonovak Oct 8, 2026
aa35175
test(agent-cli): use native paths in cloud binding fixture
vonovak Oct 9, 2026
15d364d
fix(agent-cli): keep the cloud session guard on one command line
vonovak Oct 9, 2026
c805ff1
fix(agent-cli): preserve cloud controller argv through Windows shims
vonovak Oct 9, 2026
4d37342
test(agent-cli): decode the base64 guard target in cloud reload and s…
vonovak Oct 9, 2026
dd96a07
fix(agent-cli): reconnect reused EAS development clients
vonovak Oct 11, 2026
204a3fb
fix(agent-cli): reconnect local device reloads to Metro
vonovak Oct 12, 2026
99b403e
fix(agent-cli): reserve offline emulator ports
vonovak Oct 12, 2026
a21c94e
fix(agent-cli): preserve Expo Go target detection during reload
vonovak Oct 12, 2026
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
8 changes: 8 additions & 0 deletions packages/@expo/agent-cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

### 🎉 New features

- `dev --android` binds an Android emulator instance of its own to the git worktree it runs in, and every verb of that worktree uses it. The instance is a read-only emulator of the first AVD, started by this CLI with `-avd <avd> -ports <port>,<port+1> -no-snapshot-save -read-only` on the first free even console port from 5554 to 5584, so two worktrees run two instances of one AVD side by side and neither writes the AVD's image; the binding records its serial and pid, the boot is verified with `sys.boot_completed`, and the build and install steps carry `--device <serial>` (`expo run:android` matches a serial since SDK 58). `navigate`, `runtime:reload`, `runtime:stop` and `status --explain` read the binding through `adb get-state` and never pick the first `adb` device: with no binding they refuse with `NO_BOUND_DEVICE` (exit 20) and name `dev --android --detach --wait-ready`. `smoke --android` gets its instance from the binding, installs the app onto it, and leaves it bound; nothing runs `adb emu kill` any more. When every console port is taken, `dev` refuses `DEVICE_UNAVAILABLE` (`no-free-port`) naming each serial and the worktree it is bound to. On a host with no Android SDK, `dev --android` binds nothing and serves for a device elsewhere, as before. ([#TBD](https://github.com/expo/expo-agent-cli/pull/TBD) by [@vonovak](https://github.com/vonovak))
- `dev --ios` binds an iOS simulator of its own to the git worktree it runs in, and every verb of that worktree uses it. The simulator is one this CLI created, named `agent-cli <digest>`, or the one the worktree bound before; it is booted with `simctl bootstatus -b`, the build and install steps carry `--device <udid>`, and the open deep-links to it. `navigate`, `runtime:reload`, `runtime:stop` and `status --explain` read the binding, extend its 60-minute lease, and never pick the first booted simulator: with no binding they refuse with `NO_BOUND_DEVICE` (exit 20) and name `dev --ios --detach --wait-ready`. `dev --detach` on a running server boots the worktree's own simulator and refuses `not-reusable` when it has none; a device refusal in the detached child reaches the caller with its own code, exit and `Try:` line. `smoke --ios` gets its simulator from the binding too and leaves it bound. The bindings live under `<expo home>/agent-cli/bindings`, written under one machine-wide lock. ([#134](https://github.com/expo/expo-agent-cli/pull/134) by [@vonovak](https://github.com/vonovak))
- `status --explain` reports what the app installed on a device was built from. It reads the fingerprint a debug build embeds (`expo-constants`, SDK 58+) out of the app on an Android device or a booted iOS simulator, compares it with the project's fingerprint for that platform, and says `up to date`, `rebuild required` or `unknown`, with the commands that fix a stale one. On an older SDK it says the app cannot be checked and reads no device. Right about a build somebody else made, which the freshness line cannot be. Android reads a few hundred KB of the APK through `adb` instead of pulling it; `--device <name>` narrows the read to one device. ([#59](https://github.com/expo/expo-agent-cli/pull/59) by [@vonovak](https://github.com/vonovak))
- `status --explain --device <phone>` reads the app on a physical iPhone. `devicectl` exposes no app container, so the app is launched with a one-time URL and posts its embedded fingerprint back over the LAN; it needs a debug build whose `expo-dev-client` answers the check. Naming the phone is the consent, because the probe launches the app and `status` otherwise starts nothing. `--device-timeout <secs>` bounds how long the phone gets to answer. A phone that does not answer in time reads as unknown. ([#90](https://github.com/expo/expo-agent-cli/pull/90) by [@vonovak](https://github.com/vonovak))
- `smoke --eas` brings its own EAS Simulator session the way `smoke` boots a simulator: its dev server starts through `dev --eas` (tunnelled; a build the plan needs is the simulator profile on EAS), the session in progress on the platform is reused, and otherwise one is started with Expo Go or with the finished EAS build of this fingerprint — and stopped again when the run ends. A development build with no finished build on EAS is a failed `start-session` phase naming `dev --eas`; the gate never compiles. `dev:stop --eas` also ends this project's session, by id, and reports it under `session`. ([#37](https://github.com/expo/expo-agent-cli/pull/37) by [@kudo](https://github.com/kudo))
Expand All @@ -21,6 +23,12 @@

### 🐛 Bug fixes

- Local device reloads keep Expo Go as the target when a disconnected project declares an iOS bundle identifier or Android package name. Only an explicit `--app-id` overrides target detection; stopping and reopening use the selected target's app ID.
- Android emulator allocation reserves the ports of offline emulators, including those started outside the CLI, while device commands still require a ready device.
- Local `runtime:reload --method device` reconnects a development build to the selected Metro before opening an optional route, including Android port forwarding. A stale debugger target no longer suppresses the connection link, and an expired reload budget no longer stops the app.

- `dev --detach --wait-ready` rejects another project's Metro and follows its own child's port changes within the original timeout. The same readiness check applies when reusing a running server.
- Discovery skips a dev server whose `/status` names another project root, on the scanned ports and on the port `start.log` names, and reports the foreign servers in `status` when nothing else answered. A `/status` that does not answer within 3 s is not accepted. The dev-server lock never publishes a port that serves another project, named on the command line or not. ([#132](https://github.com/expo/expo-agent-cli/pull/132) by [@vonovak](https://github.com/vonovak))
- `status --explain`, the `--eas` device commands, and a failed `eas` step of `dev` no longer quote the first line of the EAS CLI's "project not configured" message with the `eas init` forms cut off. They say the project is not linked to an EAS project and name `npx --yes eas-cli@latest init --account <account> --non-interactive`, with the account filled in when EAS listed one — the answer `deploy` already gave. An unlinked project is its own needs-human scenario, `eas-project-unlinked` (`EAS_PROJECT_NOT_LINKED`), where the generic `eas-prompt` row used to answer for it and tell the caller to answer a question in a terminal. ([#38](https://github.com/expo/expo-agent-cli/pull/38) by [@kudo](https://github.com/kudo))
- `dev --eas` no longer builds the `development` profile, which is a device build no simulator can install — not the EAS Simulator, and not the one on this Mac, so the `eas build:run` the old plan suggested could not have installed it either. ([#36](https://github.com/expo/expo-agent-cli/pull/36) by [@kudo](https://github.com/kudo))
- `inspect:build-log` now locates a `bun install` dependency 404. EAS builds a bun-lockfile project with bun, whose error shape is nothing like npm's, so those failures were unlocatable before. ([#15](https://github.com/expo/expo-agent-cli/pull/15) by [@kudo](https://github.com/kudo))
Expand Down
20 changes: 20 additions & 0 deletions packages/@expo/agent-cli/e2e-live/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Device bindings and two worktrees

Each worktree has its own simulator or emulator binding. Local `dev --ios` runs use
`--device "$AGENT_CLI_LIVE_UDID"` when that variable is set; otherwise `dev` creates a
simulator for the worktree. Explicit devices are never shut down or deleted by cleanup.
The live runner uses `dev:stop --release` when cleaning up its servers.

To exercise isolation manually, open two checkouts and run the following in each, using
8082 for the second checkout:

```sh
npx expo-agent-cli dev --ios --detach --wait-ready --port 8081
npx expo-agent-cli status --json
```

The `binding` arrays should name different devices. In the first checkout, run
`npx expo-agent-cli dev:stop`, then start it again: it should reuse its simulator. Run
`npx expo-agent-cli dev:stop --release --json` in each checkout to park the simulators
(or end spawned Android instances). For explicit selection, assign a different simulator
UDID to each worktree; naming the other worktree's device must refuse.
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,10 @@ describeLive('live-cloud', gate)('live-cloud: an EAS Simulator session, on expo-
];
const dev = await runLiveEasAsync(run, projectRoot, devArgs, {
label: 'dev-public',
env: suiteEnv(),
// This machine only hosts Metro; the device lives in the EAS Simulator session below.
// `--no-open` still binds a local device on hosts with the Android SDK, so disable device
// acquisition for this one child invocation.
env: { ...suiteEnv(), AGENT_CLI_NO_DEVICE: '1' },
});
expectExit(dev, 0);
expect(parseJson(dev).port).toBe(port);
Expand Down
11 changes: 11 additions & 0 deletions packages/@expo/agent-cli/e2e-live/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,17 @@ export async function runLiveAsync(
argv: string[],
{ env, label }: LiveOptions = {}
): Promise<LiveResult> {
const udid = env?.AGENT_CLI_LIVE_UDID ?? process.env.AGENT_CLI_LIVE_UDID;
if (
argv[0] === 'dev' &&
argv.includes('--ios') &&
!argv.includes('--eas') &&
udid &&
!argv.some((arg) => arg === '--device' || arg.startsWith('--device='))
) {
argv = [...argv, '--device', udid];
}
if (argv[0] === 'dev:stop' && !argv.includes('--release')) argv = [...argv, '--release'];
run.spend.commands += 1;
const startedAt = Date.now();

Expand Down
11 changes: 8 additions & 3 deletions packages/@expo/agent-cli/e2e/__tests__/auth-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,16 @@ function isolatedEnv(dir: string): Record<string, string> {
};
}

/** {@link isolatedEnv} on the real Expo home, for a test that pins the session file under `os.homedir()`. */
function realHomeEnv(dir: string): Record<string, string | undefined> {
return { ...isolatedEnv(dir), __UNSAFE_EXPO_HOME_DIRECTORY: undefined };
}

describe('auth commands outside an Expo project', () => {
it(`should answer whoami with the EAS CLI through the package runner when there is no expo`, async () => {
const dir = await setupBareDirAsync([]);

const result = await executeAgentCliAsync(dir, ['whoami'], { env: isolatedEnv(dir) });
const result = await executeAgentCliAsync(dir, ['whoami'], { env: realHomeEnv(dir) });

// `--yes` and the spec ahead of the verb. `@latest` because a bare directory declares nothing.
expect(readInvocations(dir)).toEqual([
Expand All @@ -157,7 +162,7 @@ describe('auth commands outside an Expo project', () => {
const dir = await setupBareDirAsync(['eas']);

const result = await executeAgentCliAsync(dir, ['whoami'], {
env: { ...isolatedEnv(dir), EXPO_STAGING: '1' },
env: { ...realHomeEnv(dir), EXPO_STAGING: '1' },
});

expect(result.stderr).toContain(path.join(os.homedir(), '.expo-staging', 'state.json'));
Expand All @@ -171,7 +176,7 @@ describe('auth commands outside an Expo project', () => {
const dir = await setupBareDirAsync(['eas']);

const result = await executeAgentCliAsync(dir, ['whoami', '--json'], {
env: isolatedEnv(dir),
env: realHomeEnv(dir),
});

expect(JSON.parse(result.stdout)).toEqual({
Expand Down
Loading
Loading