rn-iso 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/CLAUDE.md +178 -0
  3. package/README.md +90 -0
  4. package/bin/cli.js +35 -0
  5. package/docs/plans/2026-04-25-rn-iso-implementation.md +2653 -0
  6. package/docs/specs/2026-04-25-rn-iso-design.md +282 -0
  7. package/package.json +20 -0
  8. package/skill/SKILL.md +112 -0
  9. package/src/commands/android.js +112 -0
  10. package/src/commands/device.js +43 -0
  11. package/src/commands/ios.js +210 -0
  12. package/src/commands/logs.js +28 -0
  13. package/src/commands/prune.js +57 -0
  14. package/src/commands/release.js +51 -0
  15. package/src/commands/reserve.js +176 -0
  16. package/src/commands/shutdown.js +41 -0
  17. package/src/commands/start.js +43 -0
  18. package/src/commands/status.js +60 -0
  19. package/src/commands/stop.js +51 -0
  20. package/src/commands/unreserve.js +57 -0
  21. package/src/config.js +221 -0
  22. package/src/exec.js +31 -0
  23. package/src/metro.js +73 -0
  24. package/src/ports.js +50 -0
  25. package/src/project.js +186 -0
  26. package/src/runner.js +136 -0
  27. package/src/sim/android.js +103 -0
  28. package/src/sim/ios.js +128 -0
  29. package/test/config.test.js +208 -0
  30. package/test/exec.test.js +26 -0
  31. package/test/fixtures/sample-bare-project/android/app/build.gradle +6 -0
  32. package/test/fixtures/sample-bare-project/ios/SampleApp.xcodeproj/project.pbxproj +10 -0
  33. package/test/fixtures/sample-bare-project/package.json +4 -0
  34. package/test/fixtures/sample-expo-project/app.json +6 -0
  35. package/test/fixtures/sample-expo-project/package.json +4 -0
  36. package/test/fixtures/sample-expo-project/src/.keep +0 -0
  37. package/test/metro.test.js +34 -0
  38. package/test/ports.test.js +76 -0
  39. package/test/project.test.js +109 -0
  40. package/test/runner.test.js +209 -0
  41. package/test/sim-android.test.js +140 -0
  42. package/test/sim-ios.test.js +168 -0
@@ -0,0 +1,282 @@
1
+ # rn-iso — design
2
+
3
+ Date: 2026-04-25
4
+ Status: draft
5
+
6
+ ## Purpose
7
+
8
+ A CLI that lets you run multiple React Native / Expo projects (or worktrees of the same project) concurrently on one machine, each with its own Metro server and its own simulator/emulator, without manually juggling ports or device targets.
9
+
10
+ Primary motivator: AI coding agents working in parallel across multiple worktrees. Each agent — and any UI-driving skill it uses — should be able to "just work" against the right device without the user wiring it up by hand.
11
+
12
+ Secondary motivator: a human jumping between several projects locally, without colliding ports and without mental overhead about which sim is which.
13
+
14
+ ## Non-goals (v1)
15
+
16
+ - **No shared-device mode and no mutex/lock.** With dedicated sims per project, contention disappears. If a user is on tight hardware and wants one shared sim, they can use `react-native-worktree` instead.
17
+ - **No build-cache shenanigans.** Trust `expo run:ios` / `react-native run-ios` and the underlying toolchain.
18
+ - **No automatic shutdown of simulators.** Manual via `prune --shutdown` or `shutdown`. Users frequently bounce between agents and don't want their sim killed out from under them.
19
+ - **No Expo Go support.** Custom dev clients only — Expo Go can't have its Metro target rewritten cleanly.
20
+ - **No concurrency limits / build-slot semaphore.** Add later if it proves to be a problem.
21
+
22
+ ## User scenarios
23
+
24
+ ### Scenario A — multi-agent, multiple worktrees
25
+
26
+ Janic spawns three Claude Code sessions in three worktrees of the same app. Each runs `rn-iso ios` once. Each gets:
27
+ - A unique Metro port (8082, 8083, 8084).
28
+ - A dedicated booted iOS simulator (different UDIDs).
29
+ - An installed copy of the app pointing to that worktree's port.
30
+
31
+ Each agent calls `rn-iso device` to learn its UDID and uses it for `agent-device` calls. No locking required; no agent disturbs another.
32
+
33
+ ### Scenario B — solo multi-project switching
34
+
35
+ Janic works on app A in the morning and app B in the afternoon. Each project's first `rn-iso ios` invocation assigned it a sim. Going back to app A's directory and running `rn-iso ios` boots its sim if shut down and reuses everything else.
36
+
37
+ ### Scenario C — agent working alongside human
38
+
39
+ Agent owns sim X on port 8083. Human runs the same app from a different worktree on sim Y, port 8084. They don't interact. No protocol required.
40
+
41
+ ## Architecture
42
+
43
+ ### Global config
44
+
45
+ Stored at `~/.rn-iso/config.json`. Keyed by absolute project path (resolved via `realpath` to handle symlinks consistently).
46
+
47
+ ```json
48
+ {
49
+ "version": 1,
50
+ "projects": {
51
+ "/Users/janic/Developer/myapp": {
52
+ "bundleId": "com.myapp",
53
+ "androidPackage": "com.myapp",
54
+ "isExpo": true,
55
+ "metroPort": 8082,
56
+ "metroPid": 12345,
57
+ "platforms": {
58
+ "ios": {
59
+ "deviceUdid": "A1B2C3D4-..."
60
+ },
61
+ "android": {
62
+ "avdName": "Pixel_6_API_34",
63
+ "consolePort": 5554
64
+ }
65
+ }
66
+ },
67
+ "/Users/janic/Developer/myapp-feat-auth": {
68
+ "...": "..."
69
+ }
70
+ }
71
+ }
72
+ ```
73
+
74
+ Notes:
75
+ - Single Metro per project, shared across iOS and Android (same as `react-native run-*` defaults).
76
+ - `metroPort` and `metroPid` live at the project level — one Metro serves both platforms.
77
+ - `metroPid` is recorded when Metro is started detached, used by `rn-iso stop` and `rn-iso logs`.
78
+ - The project path is the key — git worktrees produce different absolute paths, so they're naturally separate entries.
79
+ - Schema is versioned to allow forward migrations.
80
+
81
+ ### Project detection
82
+
83
+ `rn-iso` resolves the "current project" by walking up from CWD until it finds a directory containing `package.json`. That directory is the canonical project root used as the config key.
84
+
85
+ Inside that root, detection determines:
86
+ - **Bare vs Expo:** presence of `expo` in `package.json` dependencies AND presence of `app.json` / `app.config.{js,ts}`. If both, treat as Expo.
87
+ - **Bundle ID / package name:** lifted from the existing `react-native-worktree` logic — checks `app.json` (`expo.ios.bundleIdentifier`, `expo.android.package`) and `app.config.{js,ts}` via regex.
88
+
89
+ ### Port allocation
90
+
91
+ Same logic as `react-native-worktree`:
92
+ - On first assignment for a project, scan all ports in config across all projects, plus probe `localhost:<port>/status` to detect dead Metros.
93
+ - If a dead Metro port is found, reclaim it and remove the dead entry.
94
+ - Else assign `max(allPorts, 8081) + 1`.
95
+
96
+ Both platforms in the same project share one Metro port.
97
+
98
+ ### Device assignment
99
+
100
+ Sticky and explicit. First `rn-iso ios` for a project picks a sim and writes it to config. Subsequent invocations reuse it.
101
+
102
+ **Selection algorithm (iOS):**
103
+ 1. List booted sims via `xcrun simctl list devices booted -j`.
104
+ 2. Compute "claimed" UDIDs from config (across all projects).
105
+ 3. **Prefer reuse:** if config has a UDID for this project and it's bootable (exists in `simctl list devices`), use it. Boot it if not booted.
106
+ 4. **Else allocate from booted-and-unclaimed:** pick the first booted sim that's not claimed by any project.
107
+ 5. **Else prompt to boot a new one:**
108
+ - Interactive: `xcrun simctl list devicetypes` → arrow-key picker; default to the most recently used iPhone runtime.
109
+ - Non-interactive (`--auto` / `--device-type "iPhone 15 Pro"`): boot a fresh sim of that type, no prompt.
110
+ 6. Boot it via `xcrun simctl boot <UDID>` and `open -a Simulator`.
111
+
112
+ **Selection algorithm (Android):**
113
+ 1. List existing AVDs via `emulator -list-avds`.
114
+ 2. List currently-running emulator console ports via `adb devices` (entries like `emulator-5554`).
115
+ 3. Compute claimed AVDs and console ports from config.
116
+ 4. Prefer reuse: if config has an AVD for this project, start it on its assigned console port if not running.
117
+ 5. Else pick an unclaimed AVD; assign next free even console port starting at 5554.
118
+ 6. Else prompt to create a new AVD (or fail with a helpful message — AVD creation is gnarly enough that v1 may just instruct the user to create one in Android Studio).
119
+
120
+ When booting Android: `emulator -avd <name> -port <consolePort>` (detached).
121
+
122
+ ### App install
123
+
124
+ iOS:
125
+ - `expo run:ios --device <UDID> --port <PORT>` (Expo) or `react-native run-ios --simulator <name> --port <PORT>` (bare; note: bare RN takes a name, not UDID — translate via `simctl list`).
126
+ - These build, install, and launch.
127
+ - On reruns of `rn-iso ios` against the same project + sim, this is fast because the build is incremental.
128
+
129
+ Android:
130
+ - `expo run:android --device <serial> --port <PORT>` (Expo) or `react-native run-android --deviceId <serial>` (bare).
131
+ - After install, set up `adb -s <serial> reverse tcp:<PORT> tcp:<PORT>` so the emulator can reach Metro on the host.
132
+
133
+ ### Metro management
134
+
135
+ `rn-iso ios` / `rn-iso android` ensure Metro is running on the project's assigned port:
136
+ - If a process is already responding to `/status` on that port, leave it alone.
137
+ - Else spawn detached: `npx expo start --port <PORT>` or `npx react-native start --port <PORT>`. Record PID in config. Pipe stdout/stderr to `~/.rn-iso/logs/<project-hash>-metro.log`.
138
+
139
+ `rn-iso start` is the standalone form: ensure Metro, do nothing else.
140
+ `rn-iso logs` tails the log file.
141
+ `rn-iso stop` kills Metro by PID.
142
+ `rn-iso status` shows per-project state.
143
+
144
+ ### Cleanup
145
+
146
+ - `rn-iso release [--platform <p>]` — clear device assignment for the current project. Does not shut down the sim.
147
+ - `rn-iso shutdown [--platform <p>]` — `simctl shutdown <UDID>` / kill emulator process for the current project. Releases assignment.
148
+ - `rn-iso prune [--shutdown]` — scan config:
149
+ - Drop projects whose path no longer exists.
150
+ - Drop platform assignments whose UDID/AVD no longer exists.
151
+ - With `--shutdown`: also shut down any sims/emulators referenced only by dropped entries.
152
+
153
+ ## Command surface
154
+
155
+ ```
156
+ rn-iso ios [--device-type <name>] [--detach] [--auto]
157
+ rn-iso android [--device-type <name>] [--detach] [--auto]
158
+ rn-iso start [--detach] # metro only
159
+ rn-iso device [--platform ios|android] # print UDID/serial; exit 0 if assigned, 1 if not
160
+ rn-iso status # all projects, current project highlighted
161
+ rn-iso release [--platform ios|android] # unbind device(s) for current project
162
+ rn-iso shutdown [--platform ios|android] # release + shut down sim(s)
163
+ rn-iso prune [--shutdown] # GC dead entries machine-wide
164
+ rn-iso logs [--platform ios|android] # tail metro logs
165
+ rn-iso stop # kill metro for current project
166
+ ```
167
+
168
+ The "current project" is always the package.json root walking up from CWD. No flags to override.
169
+
170
+ ## Platform specifics
171
+
172
+ ### iOS
173
+
174
+ - Boot: `xcrun simctl boot <UDID>` (idempotent — errors if already booted; ignore "Booted" error).
175
+ - Install: `expo run:ios --device <UDID>` or equivalent.
176
+ - Launch: handled by run-ios. To relaunch later: `xcrun simctl terminate <UDID> <bundleId>` + `xcrun simctl launch <UDID> <bundleId>`.
177
+ - Port-on-existing-app trick (carried over from `react-native-worktree`):
178
+ ```
179
+ xcrun simctl spawn <UDID> defaults write <bundleId> RCT_jsLocation "localhost:<port>"
180
+ ```
181
+ Used when the user wants to repoint without rebuilding (rare in dedicated-sim mode but useful for "I changed Metro port" or "I moved this worktree's port to reclaim").
182
+
183
+ ### Android (more involved than iOS)
184
+
185
+ - Each emulator instance occupies a console port (`-port <consolePort>`); ADB serial is `emulator-<consolePort>`.
186
+ - Reverse port mapping is per-device: `adb -s emulator-5554 reverse tcp:<MetroPort> tcp:<MetroPort>`.
187
+ - Boot is slow (10-30s); we should poll `adb -s <serial> shell getprop sys.boot_completed` until "1" before trying to install.
188
+ - Multiple instances of the same AVD are technically supported (`-port` differs), but each instance keeps a private system image overlay; clean disk usage matters. For v1, prefer one AVD per running instance.
189
+ - `debug_http_host` SharedPref trick (from `react-native-worktree`) is the equivalent of iOS `RCT_jsLocation` for repointing without rebuilding.
190
+
191
+ ### Bare vs Expo dispatch
192
+
193
+ | Action | Expo | Bare |
194
+ |---|---|---|
195
+ | Run iOS | `npx expo run:ios --device <UDID> --port <P>` | `npx react-native run-ios --simulator "<name>" --port <P>` |
196
+ | Run Android | `npx expo run:android --device <serial> --port <P>` | `npx react-native run-android --deviceId <serial>` (bare RN takes port via `RCT_METRO_PORT` env) |
197
+ | Start Metro | `npx expo start --port <P>` | `npx react-native start --port <P>` |
198
+
199
+ Bare RN's `run-ios` takes a device *name*, not a UDID — we'll resolve UDID → name via `simctl list devices -j`. If multiple sims share a name (common), error out with "ambiguous; please rename one in the Simulator app."
200
+
201
+ ## Agent integration
202
+
203
+ A skill (provisional name `rn-iso`) shipped with the CLI that teaches agents how to use it. Skill content covers:
204
+ - Run `rn-iso ios` (or android) from the project root before any device interaction.
205
+ - Use `rn-iso device --platform ios` to get the UDID, pass to `agent-device`'s `--device <UDID>` form (or whatever the platform-specific flag is).
206
+ - Pass `--auto --device-type "iPhone 15 Pro"` (or similar) for non-interactive runs.
207
+ - Always reuse — never call `release` or `shutdown` unless the user asks.
208
+
209
+ The skill installs the same way as `react-native-worktree`'s skill (`curl ... -o ~/.claude/skills/rn-iso/SKILL.md`).
210
+
211
+ `rn-iso device` is the single point of integration. Output format:
212
+ - Success: `<UDID>` (or `<emulator-serial>`) on stdout, exit 0.
213
+ - Not assigned: empty stdout, error message on stderr, exit 1.
214
+ - `--platform <p>` filters; default = ios.
215
+ - `--json` form returns `{ "platform": "ios", "udid": "...", "metroPort": 8083 }` for tooling.
216
+
217
+ ## Failure modes & error handling
218
+
219
+ | Situation | Behavior |
220
+ |---|---|
221
+ | No `package.json` found walking up from CWD | Error: "Not in a React Native project (no package.json)" |
222
+ | Bundle ID undetectable | Error: "Could not detect bundle ID; provide via `--bundle-id <id>`" (later — v1 requires app.json) |
223
+ | Assigned sim no longer exists | Warn, drop assignment, run normal allocation |
224
+ | Assigned sim exists but won't boot | Error with the simctl message; don't auto-fall-through |
225
+ | Metro port collides with non-rn-iso process | Detect via probe; if `/status` doesn't return `packager-status:running`, error: "port X busy by non-Metro process" |
226
+ | Two instances of `rn-iso ios` for the same project run concurrently | File-based lock on the project's config entry during mutation. Race-window only during allocation. |
227
+ | Expo run hangs / fails | Surface stderr; don't silently retry |
228
+
229
+ ## Testing strategy
230
+
231
+ - **Unit tests** for: config schema parsing/migration, port allocation, project root detection, bundle ID detection, device-pool selection algorithm (mocked simctl/adb output).
232
+ - **Integration tests** are hard for sim/emulator interactions and out of scope for v1; rely on manual verification on the developer's machine.
233
+ - Use `node --test` (same as `react-native-worktree` — no test framework deps).
234
+
235
+ ## Implementation notes
236
+
237
+ - Language: JavaScript (ESM), no TS toolchain. Match `react-native-worktree`'s style for legibility / cross-pollination.
238
+ - Dependencies: `commander` (CLI), `chalk` (color), `prompts` or built-in readline (interactive picker). Prefer minimal deps.
239
+ - File layout:
240
+ ```
241
+ bin/cli.js
242
+ src/
243
+ commands/
244
+ ios.js
245
+ android.js
246
+ start.js
247
+ device.js
248
+ status.js
249
+ release.js
250
+ shutdown.js
251
+ prune.js
252
+ logs.js
253
+ stop.js
254
+ config.js # global config CRUD, project root detection, bundle ID detection
255
+ ports.js # port allocation + Metro probing
256
+ sim/
257
+ ios.js # simctl wrappers + iOS device pool
258
+ android.js # adb/emulator wrappers + Android device pool
259
+ metro.js # detached spawn, log piping, PID lifecycle
260
+ runner.js # bare vs Expo dispatch
261
+ test/
262
+ *.test.js
263
+ skill/
264
+ SKILL.md
265
+ ```
266
+
267
+ ## Open questions
268
+
269
+ 1. **Skill name.** `rn-iso` is fine for the CLI. The skill name visible to agents could be the same, or something more discoverable like `react-native-isolated-dev`. I'll default to `rn-iso` and we can rename later.
270
+ 2. **Coexistence with `react-native-worktree`.** A user might have both installed. They don't conflict (different config dirs), but the skill descriptions might both trigger and confuse agents. The skill description should be specific enough that agents only invoke `rn-iso` when the user has set it up.
271
+ 3. **Multi-app projects (one repo, multiple Expo apps via `expo prebuild --variant`).** Out of scope for v1. Treat each project path as a single app.
272
+ 4. **Windows / Linux support.** macOS-first. Android-on-Linux is plausible later. iOS is mac-only by definition.
273
+ 5. **Should `device --json` include the Metro port?** Yes for v1 — agents may want to verify Metro is up before kicking off a UI test.
274
+ 6. **AVD creation in `rn-iso android` first-run.** Defer to user; instruct them to use Android Studio. Revisit if it's a common pain point.
275
+
276
+ ## Future / later
277
+
278
+ - Build-slot semaphore (limit concurrent native builds).
279
+ - Auto-shutdown sim after N hours of inactivity (opt-in).
280
+ - Hot-rebind without rebuild via `RCT_jsLocation` / `debug_http_host` (the `react-native-worktree` trick) — exposed as `rn-iso rebind` for power users.
281
+ - TUI dashboard.
282
+ - Replace `commander` with a smaller arg parser if package size becomes a concern.
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "rn-iso",
3
+ "version": "0.1.0",
4
+ "description": "Isolated React Native dev environments per project/worktree",
5
+ "type": "module",
6
+ "bin": {
7
+ "rn-iso": "bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node --test test/*.test.js"
11
+ },
12
+ "dependencies": {
13
+ "chalk": "^5.4.1",
14
+ "commander": "^13.1.0",
15
+ "prompts": "^2.4.2"
16
+ },
17
+ "engines": {
18
+ "node": ">=20"
19
+ }
20
+ }
package/skill/SKILL.md ADDED
@@ -0,0 +1,112 @@
1
+ ---
2
+ name: rn-iso
3
+ description: Manage isolated React Native / Expo dev environments. Each project (or worktree) gets its own Metro server and dedicated simulator/emulator. Use to ensure the right simulator is booted with the right port, and to discover which device to target for UI interactions.
4
+ user_invocable: true
5
+ ---
6
+
7
+ # rn-iso — Isolated RN Dev Environments
8
+
9
+ You are an AI agent working on a React Native / Expo project, possibly alongside other agents working on different projects or worktrees. Each project owns its own dedicated simulator and Metro server. There is no locking — your sim is yours.
10
+
11
+ ## Core workflow
12
+
13
+ From the project root (or any subdirectory):
14
+
15
+ 1. **Ensure the platform is ready** — `rn-iso ios --auto` (or `rn-iso android`). This:
16
+ - Allocates a Metro port for the project (or reuses the assigned one)
17
+ - Picks a dedicated unclaimed sim (booting it if shutdown). With `--auto`, picks the first candidate without prompting.
18
+ - Builds and installs the app via the project's `ios` / `android` script if present, else `expo run:ios` / `react-native run-ios`. The build CLI starts Metro itself on the assigned port; rn-iso doesn't spawn a separate Metro. Detects the package manager from the lockfile (walks up for monorepos).
19
+
20
+ 2. **Get the device target** — `rn-iso device --platform ios --json`:
21
+ ```json
22
+ {"platform":"ios","udid":"ABC-...","metroPort":8083}
23
+ ```
24
+ Use the UDID for `agent-device` / `xcrun simctl` / `idb`. For Android, the `serial` field gives you `emulator-<port>` to use with `adb -s`.
25
+
26
+ 3. **Interact with the device** — pass the UDID/serial to your UI tools. Never call `simctl <verb>` without `<UDID>` — `booted` could be the wrong sim.
27
+
28
+ ## CRITICAL rules
29
+
30
+ - **Always pass `--auto` for non-interactive use.** Without it, `rn-iso ios` will prompt with an arrow-key picker if multiple unclaimed sims exist. Agents must use `--auto` to skip.
31
+ - **Always use `rn-iso device` to discover your target.** Never assume `booted` is your sim — another project's simulator might be booted too.
32
+ - **Always pass the UDID/serial explicitly** to `xcrun simctl` and `adb -s`. Examples:
33
+ - `xcrun simctl io <UDID> screenshot out.png`
34
+ - `adb -s emulator-5556 shell input tap 100 200`
35
+ - **Don't call `release` or `shutdown`** unless the user explicitly asks. Other agents may be using neighboring sims; keep yours up so the user can come back to it.
36
+ - **Don't manually start Metro on a different port.** `rn-iso start` (or `rn-iso ios/android`) already handles port assignment.
37
+ - **rn-iso never auto-creates simulators.** It reuses existing unclaimed sims (booted or shutdown). If none are available, it errors. To create a new one explicitly, pass `--device-type "iPhone 17 Pro" [--runtime 26.2]`.
38
+
39
+ ## Typical agent workflow
40
+
41
+ ```bash
42
+ # Once per session -- ensure the project's sim and Metro are up.
43
+ rn-iso ios --auto
44
+
45
+ # Get the target.
46
+ UDID=$(rn-iso device --platform ios)
47
+
48
+ # Use the target for UI interactions (delegate to agent-device or your tool).
49
+ xcrun simctl io "$UDID" screenshot /tmp/screen.png
50
+
51
+ # When you change app code, Metro hot-reloads automatically. No restart needed.
52
+ # Only re-run `rn-iso ios` after native code changes or new native modules.
53
+ ```
54
+
55
+ ## Reserving sims used by external processes
56
+
57
+ If you boot a sim outside of rn-iso (Xcode, manual `simctl boot`, another agent that doesn't use rn-iso), tell rn-iso to skip it during allocation:
58
+
59
+ ```bash
60
+ # Direct form -- if you know the UDID:
61
+ rn-iso reserve ios <UDID> --label "agent-1"
62
+ rn-iso reserve android emulator-5554 --label "agent-2"
63
+
64
+ # Interactive form -- pick from currently booted sims / running emulators:
65
+ rn-iso reserve # both platforms, multi-select picker
66
+ rn-iso reserve ios # iOS only
67
+ rn-iso reserve --list # show current reservations
68
+
69
+ # Release when done -- by UDID/serial OR by the label:
70
+ rn-iso unreserve <UDID>
71
+ rn-iso unreserve agent-1 # by label
72
+ rn-iso release agent-1 # `release` accepts the label too
73
+ rn-iso unreserve --all
74
+ ```
75
+
76
+ Reserved sims show grayed out as `[reserved]` in `rn-iso ios` pickers and won't be picked by allocation.
77
+
78
+ ## When things go wrong
79
+
80
+ - **"No rn-iso assignment for project"** — run `rn-iso ios` (or android) first.
81
+ - **"No unclaimed iOS simulator available"** — every existing sim is claimed by another project or reserved. Options: open a sim in Simulator.app, run `rn-iso unreserve --all` if you have stale reservations, free another project (`rn-iso release`), or pass `--device-type "iPhone 17 Pro"` to create a new one.
82
+ - **Wrong sim got the app** — older `@expo/cli` (< 54.0.24) had a bug where the launch ignored `--device`. Bump expo to 54.0.34+ if on SDK 54.
83
+ - **Metro port collision** — `rn-iso ios` reclaims dead ports automatically. If you see "port busy by non-Metro process," another tool is using that port; close it.
84
+ - **Sim was deleted** — `rn-iso ios` detects the stale assignment and re-allocates. If not, run `rn-iso prune` then `rn-iso ios`.
85
+ - **Detection picked the wrong CLI** (e.g. project has `expo` in deps but uses `react-native run-ios`) — rn-iso prefers your `ios` / `android` script and detects the CLI from its body. Override with `--script <name>` or skip with `--no-script` to force the direct CLI fallback. Override package manager with `--pm <npm|yarn|pnpm|bun>`.
86
+
87
+ ## Other useful commands
88
+
89
+ - `rn-iso status` — show all projects, their assignments, and Metro state. Reservations appear in their own section.
90
+ - `rn-iso start` — start Metro detached on the project's assigned port WITHOUT building/installing. Useful if you want logs (`rn-iso logs`) or to keep Metro alive across builds.
91
+ - `rn-iso logs` — tail the Metro log file (only available if Metro was started via `rn-iso start`; the build CLI's Metro doesn't write to our log).
92
+ - `rn-iso stop` — kill the project's Metro. Finds the process by port, so it works whether Metro was started by `rn-iso start` or by the build CLI.
93
+ - `rn-iso prune` — GC dead entries machine-wide; safe to run periodically.
94
+ - `rn-iso release [target]` — free a project assignment OR a reservation. `[target]` is an absolute project path, the `--label` you set when reserving, or a UDID/serial. Defaults to the current project. Examples:
95
+ - `rn-iso release` -- current project
96
+ - `rn-iso release /Users/x/Developer/myapp` -- specific project by path
97
+ - `rn-iso release agent-1` -- reservation by label (works across iOS/Android)
98
+ - `rn-iso unreserve <id|label>` — same as `rn-iso release` for reservations specifically. Accepts UDID, emulator serial, OR the `--label` from when you reserved. Pass `--platform ios|android` to restrict.
99
+
100
+ ## Sort order in the picker
101
+
102
+ When the `ios` picker fires, sims are sorted by:
103
+ 1. Family (iPhone before iPad before others — set by user preference if you usually use iPhones)
104
+ 2. State (booted before shutdown within family)
105
+ 3. Usage count (most-used floats up; tracked per UDID across all projects)
106
+ 4. Name (alphabetical, stable tiebreak)
107
+
108
+ Claimed and reserved sims are listed but greyed out and skipped by the cursor.
109
+
110
+ ## Differences from `react-native-worktree`
111
+
112
+ `react-native-worktree` shares one simulator across worktrees with a mutex. `rn-iso` gives each project its own dedicated simulator — no locking, no contention. If both are installed, prefer `rn-iso` unless the user explicitly asks for the shared-sim model.
@@ -0,0 +1,112 @@
1
+ // src/commands/android.js
2
+ import chalk from 'chalk';
3
+ import { findProjectRoot, detectIsExpo, detectBundleId, detectAndroidPackage } from '../project.js';
4
+ import { getProject, upsertProject, setMetro, setDevice, allClaimedDevices } from '../config.js';
5
+ import { allocatePort, isMetroRunning } from '../ports.js';
6
+ import { selectAndroidDevice, bootAndroidEmulator, waitForBoot, adbReverse, listAdbDevices } from '../sim/android.js';
7
+ import { buildAndroidCommand, detectPackageManager } from '../runner.js';
8
+ import { getExecutor } from '../exec.js';
9
+
10
+ export default function androidCommand(program) {
11
+ program
12
+ .command('android')
13
+ .description('Ensure a dedicated Android emulator + Metro for the current project; build/install if needed')
14
+ .option('--script <name>', 'package.json script to invoke for build/install (default: android)', 'android')
15
+ .option('--no-script', 'Skip the package.json script lookup; run expo/react-native CLI directly')
16
+ .option('--pm <name>', 'Package manager: npm, yarn, pnpm, bun (default: detected from lockfile)')
17
+ .option('--no-install', 'Skip the build/install step')
18
+ .action(async (opts) => {
19
+ const root = findProjectRoot(process.cwd());
20
+ if (!root) {
21
+ console.error(chalk.red('Not in a React Native project (no package.json found).'));
22
+ process.exit(1);
23
+ }
24
+
25
+ const bundleId = detectBundleId(root);
26
+ const androidPackage = detectAndroidPackage(root);
27
+ const isExpo = detectIsExpo(root);
28
+
29
+ upsertProject(root, { bundleId, androidPackage, isExpo });
30
+ let proj = getProject(root);
31
+
32
+ if (!proj.metroPort) {
33
+ const port = await allocatePort(root);
34
+ setMetro(root, port, null);
35
+ proj = getProject(root);
36
+ console.log(chalk.dim(`Allocated Metro port: ${port}`));
37
+ }
38
+ const metroAlreadyUp = await isMetroRunning(proj.metroPort);
39
+ console.log(chalk.dim(
40
+ `Metro port: ${proj.metroPort}` +
41
+ (metroAlreadyUp ? ' (already running)' : ' (will be started by build CLI)')
42
+ ));
43
+
44
+ const claimed = allClaimedDevices();
45
+ const myAvd = proj.platforms?.android?.avdName || null;
46
+ const myPort = proj.platforms?.android?.consolePort || null;
47
+ const claimedAvds = claimed.androidAvds.filter(a => a !== myAvd);
48
+ const claimedPorts = claimed.androidConsolePorts.filter(p => p !== myPort);
49
+
50
+ const selection = selectAndroidDevice({
51
+ existingAvd: myAvd,
52
+ existingConsolePort: myPort,
53
+ claimedAvds,
54
+ claimedConsolePorts: claimedPorts,
55
+ });
56
+
57
+ if (selection.kind === 'noAvd') {
58
+ console.error(chalk.red(
59
+ 'No AVDs available (or all are claimed by other projects). ' +
60
+ 'Create one via Android Studio (Tools -> Device Manager).'
61
+ ));
62
+ process.exit(1);
63
+ }
64
+
65
+ const { avdName, consolePort, isRunning } = selection;
66
+ const serial = `emulator-${consolePort}`;
67
+
68
+ if (!isRunning) {
69
+ console.log(chalk.dim(`Booting emulator ${avdName} on port ${consolePort}...`));
70
+ bootAndroidEmulator(avdName, consolePort);
71
+ console.log(chalk.dim('Waiting for boot to complete (this can take 10-30s)...'));
72
+ const ok = await waitForBoot(serial, 120000);
73
+ if (!ok) {
74
+ console.error(chalk.red(`Emulator ${serial} did not finish booting within 2 minutes.`));
75
+ process.exit(1);
76
+ }
77
+ } else {
78
+ console.log(chalk.dim(`Reusing running emulator ${serial}`));
79
+ }
80
+
81
+ setDevice(root, 'android', { avdName, consolePort });
82
+
83
+ // Metro is started by the build CLI; we don't spawn our own. For
84
+ // Metro-only (no build/install), use `rn-iso start`.
85
+
86
+ adbReverse(serial, proj.metroPort);
87
+ console.log(chalk.dim(`adb reverse tcp:${proj.metroPort} configured for ${serial}`));
88
+
89
+ if (opts.install !== false) {
90
+ const packageManager = opts.pm || detectPackageManager(root);
91
+ const useScript = opts.script !== false;
92
+ const scriptName = useScript ? (typeof opts.script === 'string' ? opts.script : 'android') : null;
93
+ const cmd = buildAndroidCommand({
94
+ projectRoot: root,
95
+ packageManager,
96
+ scriptName,
97
+ isExpo,
98
+ serial,
99
+ port: proj.metroPort,
100
+ useScript,
101
+ });
102
+ console.log(chalk.dim(`> ${cmd}`));
103
+ const exec = getExecutor();
104
+ const child = exec.spawn('sh', ['-c', cmd], { cwd: root, stdio: 'inherit' });
105
+ await new Promise((resolve, reject) => {
106
+ child.on('exit', (code) => code === 0 ? resolve() : reject(new Error(`Build failed (exit ${code})`)));
107
+ });
108
+ }
109
+
110
+ console.log(chalk.green(`\nAndroid ready on ${serial}, Metro port ${proj.metroPort}`));
111
+ });
112
+ }
@@ -0,0 +1,43 @@
1
+ // src/commands/device.js
2
+ import chalk from 'chalk';
3
+ import { findProjectRoot } from '../project.js';
4
+ import { getProject } from '../config.js';
5
+
6
+ export default function deviceCommand(program) {
7
+ program
8
+ .command('device')
9
+ .description('Print the assigned device UDID/serial for the current project')
10
+ .option('--platform <platform>', 'ios or android', 'ios')
11
+ .option('--json', 'Emit JSON with full assignment info')
12
+ .action((opts) => {
13
+ const root = findProjectRoot(process.cwd());
14
+ if (!root) {
15
+ console.error(chalk.red('Not in a React Native project (no package.json found).'));
16
+ process.exit(1);
17
+ }
18
+ const proj = getProject(root);
19
+ if (!proj) {
20
+ console.error(chalk.red(`No rn-iso assignment for project ${root}. Run \`rn-iso ${opts.platform}\` first.`));
21
+ process.exit(1);
22
+ }
23
+ const platformEntry = proj.platforms?.[opts.platform];
24
+ if (!platformEntry) {
25
+ console.error(chalk.red(`No ${opts.platform} device assigned. Run \`rn-iso ${opts.platform}\` first.`));
26
+ process.exit(1);
27
+ }
28
+
29
+ if (opts.json) {
30
+ const payload = opts.platform === 'ios'
31
+ ? { platform: 'ios', udid: platformEntry.deviceUdid, metroPort: proj.metroPort }
32
+ : { platform: 'android', serial: `emulator-${platformEntry.consolePort}`, avdName: platformEntry.avdName, consolePort: platformEntry.consolePort, metroPort: proj.metroPort };
33
+ console.log(JSON.stringify(payload));
34
+ return;
35
+ }
36
+
37
+ if (opts.platform === 'ios') {
38
+ console.log(platformEntry.deviceUdid);
39
+ } else {
40
+ console.log(`emulator-${platformEntry.consolePort}`);
41
+ }
42
+ });
43
+ }