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,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(git -C /Users/janicduplessis/Developer/rn-iso remote -v)"
5
+ ]
6
+ }
7
+ }
package/CLAUDE.md ADDED
@@ -0,0 +1,178 @@
1
+ # rn-iso — agent guide
2
+
3
+ Quick orientation for AI assistants working in this repo.
4
+
5
+ ## What this is
6
+
7
+ A Node.js CLI that gives each React Native / Expo project (or git worktree)
8
+ its own Metro server and dedicated simulator/emulator, so multiple agents can
9
+ work on different projects in parallel without device or port collisions.
10
+
11
+ State lives in `~/.rn-iso/config.json`, keyed by absolute project path. The
12
+ `RN_ISO_HOME` env var redirects this for tests.
13
+
14
+ ## Architecture conventions
15
+
16
+ - **ESM only.** `"type": "module"`, no transpiler, Node 20+ directly. No
17
+ CommonJS, no `require()`.
18
+ - **Single exec wrapper.** All `child_process` calls go through
19
+ `src/exec.js` (`getExecutor()`). Tests inject a mock via `setExecutor()`.
20
+ Anywhere outside `exec.js` that imports `child_process` directly is a bug.
21
+ - **Pure parsing separate from invocation.** Functions like `parseSimctlList`,
22
+ `parseAdbDevices`, `selectIosDevice`, `sortSims` are pure and unit-tested;
23
+ the I/O wrappers around them are thin.
24
+ - **ASCII in source files.** No em dashes, smart quotes, or check marks in
25
+ `src/`, `bin/`, `test/`. Markdown files (README, SKILL, this file) may use
26
+ them. The hooks have flagged this before.
27
+
28
+ ## File layout
29
+
30
+ ```
31
+ bin/cli.js # commander entry, registers each command module
32
+ src/
33
+ exec.js # mockable child_process wrapper
34
+ config.js # config CRUD, reservations, sim-usage tracking
35
+ project.js # project root walk, bundle-id detection (incl. native fallbacks)
36
+ ports.js # Metro port allocation + reclamation
37
+ runner.js # script-vs-CLI dispatch, package-manager detection (walks up for monorepos)
38
+ metro.js # detached Metro spawn, PID + log lifecycle
39
+ sim/
40
+ ios.js # simctl wrappers, sim selection, sortSims, parseRuntimeVersion
41
+ android.js # adb/emulator wrappers, AVD selection
42
+ commands/
43
+ ios.js android.js # the main user-facing commands
44
+ start.js stop.js logs.js
45
+ status.js
46
+ device.js # `rn-iso device --json` -> agent-device target
47
+ release.js shutdown.js prune.js
48
+ reserve.js unreserve.js
49
+ test/
50
+ *.test.js # `node --test` (no framework)
51
+ skill/SKILL.md # the agent-facing skill
52
+ ```
53
+
54
+ ## Particularities to remember
55
+
56
+ ### 1. Update `skill/SKILL.md` whenever user-facing behavior changes
57
+
58
+ The skill is what installed AI agents read to learn how to use the CLI. When
59
+ you add a command, change a flag, change picker UX, or alter defaults — open
60
+ `skill/SKILL.md` and update the relevant section in the same change. Quick
61
+ checklist:
62
+
63
+ - New command? Add it under "Other useful commands" or its own section if
64
+ meaty (like `reserve`).
65
+ - New / changed flag on `ios` or `android`? Update "Core workflow" and
66
+ "Critical rules" if the flag matters for non-interactive agent use.
67
+ - Behavior change (e.g., picker now does X)? Update both the
68
+ description and the "When things go wrong" section.
69
+
70
+ The skill is shipped to users via the curl line in the README; staleness
71
+ breaks agent guidance.
72
+
73
+ ### 2. Don't auto-create simulators
74
+
75
+ `selectIosDevice` returns `needsBoot` only when no unclaimed sim exists at
76
+ all. `commands/ios.js` then errors unless `--device-type` is passed. We do
77
+ NOT prompt and create on the user's behalf — that was the original UX and
78
+ was removed because it accumulated junk sims. The picker only chooses among
79
+ EXISTING sims (booted or shutdown). When you change device-selection logic,
80
+ preserve this invariant.
81
+
82
+ ### 3. The post-install verification step is intentionally absent
83
+
84
+ Earlier versions ran `xcrun simctl install/launch` after the build CLI to
85
+ work around a wrong-sim bug in `@expo/cli` (since fixed in 54.0.24). That
86
+ step caused double-launches and was removed. If you find yourself wanting
87
+ to add it back, the upstream bug is the right place to fix things —
88
+ `patch-package` for stuck users, not workaround code in `commands/ios.js`.
89
+
90
+ ### 3b. `rn-iso ios` / `android` do NOT spawn Metro
91
+
92
+ The build CLI (`expo run:ios` / `react-native run-ios`) starts Metro
93
+ itself on the `--port` we pass. We used to also pre-spawn a detached
94
+ Metro before the build, which led to two Metros on the same port.
95
+ Removed.
96
+
97
+ `rn-iso start` is still around for the explicit "I just want Metro" case
98
+ — it spawns Metro detached and tracks the PID + log file. The build
99
+ commands rely on the build CLI's Metro; `rn-iso stop` looks up the PID
100
+ by port (via `lsof`) so it works regardless of who started Metro.
101
+
102
+ ### 4. Reservations are first-class claims
103
+
104
+ `allClaimedDevices()` returns BOTH project-claimed AND reservation-claimed
105
+ devices. If you add a new claim source (e.g., a new section in config.json),
106
+ extend `allClaimedDevices` AND `iosClaims` so the picker greys it out with a
107
+ useful label. Don't filter at any one call site — keep the policy in
108
+ `config.js`.
109
+
110
+ ### 5. Package-manager / script detection
111
+
112
+ `runner.js` prefers the project's `ios` / `android` script over a direct
113
+ `expo run:ios` / `react-native run-ios` invocation. Reasons: respects user
114
+ flags, picks the right CLI, works with non-standard setups (rainbow has
115
+ `expo` in deps but uses `react-native run-ios`).
116
+
117
+ `detectScriptCli` regex-matches the script body to decide flag names
118
+ (`--device <UDID>` for Expo, `--udid <UDID>` for RN). If you ever need a
119
+ new flag, update both the script-path branch and the direct fallback.
120
+
121
+ `detectPackageManager` walks up from the project root looking for a
122
+ lockfile (monorepo support). Don't single-directory-check.
123
+
124
+ ### 6. `RN_ISO_HOME` is the test redirect
125
+
126
+ All config + log paths derive from `getConfigDir()`, which respects
127
+ `RN_ISO_HOME`. Every config-touching test does:
128
+
129
+ ```js
130
+ beforeEach(() => {
131
+ tmpHome = mkdtempSync(join(tmpdir(), 'rn-iso-test-'));
132
+ process.env.RN_ISO_HOME = tmpHome;
133
+ });
134
+ afterEach(() => {
135
+ rmSync(tmpHome, { recursive: true, force: true });
136
+ delete process.env.RN_ISO_HOME;
137
+ });
138
+ ```
139
+
140
+ If you add new state-touching code, follow this pattern.
141
+
142
+ ### 7. `findProjectRoot` uses `realpath`
143
+
144
+ So symlinked worktrees collapse to the same canonical key as the
145
+ non-symlinked path. Don't add code that compares paths without
146
+ canonicalizing first.
147
+
148
+ ## Local development
149
+
150
+ ```bash
151
+ npm install # one-time
152
+ npm test # node --test test/*.test.js
153
+ npm link # symlink rn-iso onto your PATH for live testing
154
+ ```
155
+
156
+ After `npm link`, edits to `src/` are picked up immediately by the linked
157
+ `rn-iso` command.
158
+
159
+ ## Commit conventions
160
+
161
+ - GPG signing is enabled globally — commits sign automatically. Don't pass
162
+ `--no-gpg-sign`. If you need to re-sign an existing commit (e.g.,
163
+ someone forgot signing), `git commit --amend --no-edit -S` works.
164
+ - Conventional-style prefixes are used (`feat:`, `fix:`, `docs:`,
165
+ `chore:`, `revert:`). Keep titles under ~70 chars; details in the body.
166
+ - One commit per logical change. The post-install removal and the
167
+ script-based runner came in as separate commits even though they shipped
168
+ in the same session.
169
+
170
+ ## Things explicitly out of scope (for now)
171
+
172
+ - Locking / mutex around device usage. The whole premise is dedicated sims.
173
+ - Auto-shutdown of sims after N hours of inactivity.
174
+ - Cross-platform support beyond macOS (iOS) + macOS/Linux (Android).
175
+ - Multi-app projects (one repo, multiple Expo apps via `--variant`).
176
+ - A daemon or TUI dashboard.
177
+
178
+ If a request edges into these, raise it instead of building it.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # rn-iso
2
+
3
+ Per-project Metro server and dedicated simulator/emulator for React Native / Expo, so multiple worktrees (or coding agents) can build the same app in parallel without port or device collisions.
4
+
5
+ > **Experimental.** APIs, flags, and on-disk state may change. File issues if anything breaks.
6
+
7
+ State lives in `~/.rn-iso/config.json`, keyed by absolute project path. Worktrees count as separate projects. There is no shared mutex — each project is pinned to its own port and its own sim.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install -g rn-iso
13
+ ```
14
+
15
+ To install the agent skill (so AI coding agents know how to drive the CLI):
16
+
17
+ ```bash
18
+ npx skills add janicduplessis/rn-iso
19
+ ```
20
+
21
+ ## Quick start
22
+
23
+ In any RN/Expo project directory:
24
+
25
+ ```bash
26
+ rn-iso ios # ensure sim, allocate port, build/install
27
+ rn-iso device # print the assigned UDID
28
+ ```
29
+
30
+ In a different worktree of the same app:
31
+
32
+ ```bash
33
+ rn-iso ios # gets a different sim and Metro port
34
+ ```
35
+
36
+ Both run side by side. For non-interactive / agent use, pass `--auto` to skip the picker:
37
+
38
+ ```bash
39
+ rn-iso ios --auto
40
+ ```
41
+
42
+ ## Commands
43
+
44
+ | Command | Purpose |
45
+ |---|---|
46
+ | `rn-iso ios [--auto] [--device-type <name>] [--runtime <ver>] [--script <name>] [--pm <name>] [--no-script] [--no-install]` | Ensure iOS sim + Metro + build/install |
47
+ | `rn-iso android [--auto] [--script <name>] [--pm <name>] [--no-script] [--no-install]` | Same for Android |
48
+ | `rn-iso start` | Start Metro detached, no platform action |
49
+ | `rn-iso device [--platform ios\|android] [--json]` | Print the assigned device target |
50
+ | `rn-iso status` | Show all projects' state and reservations |
51
+ | `rn-iso reserve [<platform> <id>] [--label <name>] [--list]` | Mark an external sim/emulator as in-use so rn-iso skips it |
52
+ | `rn-iso unreserve [<id\|label>] [--all]` | Release a reservation |
53
+ | `rn-iso release [<project\|label\|udid>] [--platform <p>]` | Unbind a project assignment or reservation |
54
+ | `rn-iso shutdown [--platform <p>]` | Release and shut down sims for current project |
55
+ | `rn-iso prune [--shutdown]` | GC dead entries machine-wide |
56
+ | `rn-iso logs` | Tail Metro log for current project |
57
+ | `rn-iso stop` | Kill Metro for current project |
58
+
59
+ ## How it works
60
+
61
+ - **Config** at `~/.rn-iso/config.json`, keyed by absolute project path. Symlinked worktrees collapse via `realpath`.
62
+ - **Port allocation:** assigns 8082, 8083, 8084 etc., reclaiming dead ports on the way.
63
+ - **Simulator pool:** prefers the project's existing assignment; otherwise picks any unclaimed booted sim, then any unclaimed shutdown sim (booting it). Does not auto-create new sims — pass `--device-type "iPhone 17 Pro" [--runtime 26.2]` to opt in.
64
+ - **Build via your project's `ios` / `android` script** when present. Falls back to `npx expo run:ios` / `npx react-native run-ios --udid <UDID>` when no script exists. Override with `--script <name>` or skip with `--no-script`. Package manager is detected from your lockfile (walks up for monorepos); override with `--pm <npm|yarn|pnpm|bun>`.
65
+ - **Metro is started by the build CLI** on the assigned port, not by rn-iso. `rn-iso start` is the standalone "I just want Metro" path. `rn-iso stop` finds Metro by port via `lsof`, so it works regardless of who started it.
66
+
67
+ If you need a single shared sim with a mutex instead of one-per-project, see [`react-native-worktree`](https://github.com/aleqsio/react-native-worktree).
68
+
69
+ ## Reservations
70
+
71
+ If you boot a sim outside rn-iso (Xcode, manual `simctl boot`, another tool), reserve it so rn-iso's allocator skips it:
72
+
73
+ ```bash
74
+ rn-iso reserve ios <UDID> --label agent-1
75
+ rn-iso reserve # interactive multi-select picker
76
+ rn-iso unreserve agent-1 # release by label, UDID, or serial
77
+ ```
78
+
79
+ Reserved sims appear greyed-out as `[reserved]` in the picker.
80
+
81
+ ## Requirements
82
+
83
+ - macOS (iOS); macOS or Linux (Android)
84
+ - Node 20+
85
+ - Xcode (iOS), Android SDK + at least one AVD (Android)
86
+ - `expo` or `react-native` in the project's `package.json`
87
+
88
+ ## License
89
+
90
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import deviceCommand from '../src/commands/device.js';
4
+ import iosCommand from '../src/commands/ios.js';
5
+ import androidCommand from '../src/commands/android.js';
6
+ import startCommand from '../src/commands/start.js';
7
+ import stopCommand from '../src/commands/stop.js';
8
+ import logsCommand from '../src/commands/logs.js';
9
+ import statusCommand from '../src/commands/status.js';
10
+ import releaseCommand from '../src/commands/release.js';
11
+ import shutdownCommand from '../src/commands/shutdown.js';
12
+ import pruneCommand from '../src/commands/prune.js';
13
+ import reserveCommand from '../src/commands/reserve.js';
14
+ import unreserveCommand from '../src/commands/unreserve.js';
15
+
16
+ const program = new Command();
17
+ program
18
+ .name('rn-iso')
19
+ .description('Isolated React Native dev environments per project/worktree')
20
+ .version('0.1.0');
21
+
22
+ deviceCommand(program);
23
+ iosCommand(program);
24
+ androidCommand(program);
25
+ startCommand(program);
26
+ stopCommand(program);
27
+ logsCommand(program);
28
+ statusCommand(program);
29
+ releaseCommand(program);
30
+ shutdownCommand(program);
31
+ pruneCommand(program);
32
+ reserveCommand(program);
33
+ unreserveCommand(program);
34
+
35
+ program.parse();