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.
- package/.claude/settings.local.json +7 -0
- package/CLAUDE.md +178 -0
- package/README.md +90 -0
- package/bin/cli.js +35 -0
- package/docs/plans/2026-04-25-rn-iso-implementation.md +2653 -0
- package/docs/specs/2026-04-25-rn-iso-design.md +282 -0
- package/package.json +20 -0
- package/skill/SKILL.md +112 -0
- package/src/commands/android.js +112 -0
- package/src/commands/device.js +43 -0
- package/src/commands/ios.js +210 -0
- package/src/commands/logs.js +28 -0
- package/src/commands/prune.js +57 -0
- package/src/commands/release.js +51 -0
- package/src/commands/reserve.js +176 -0
- package/src/commands/shutdown.js +41 -0
- package/src/commands/start.js +43 -0
- package/src/commands/status.js +60 -0
- package/src/commands/stop.js +51 -0
- package/src/commands/unreserve.js +57 -0
- package/src/config.js +221 -0
- package/src/exec.js +31 -0
- package/src/metro.js +73 -0
- package/src/ports.js +50 -0
- package/src/project.js +186 -0
- package/src/runner.js +136 -0
- package/src/sim/android.js +103 -0
- package/src/sim/ios.js +128 -0
- package/test/config.test.js +208 -0
- package/test/exec.test.js +26 -0
- package/test/fixtures/sample-bare-project/android/app/build.gradle +6 -0
- package/test/fixtures/sample-bare-project/ios/SampleApp.xcodeproj/project.pbxproj +10 -0
- package/test/fixtures/sample-bare-project/package.json +4 -0
- package/test/fixtures/sample-expo-project/app.json +6 -0
- package/test/fixtures/sample-expo-project/package.json +4 -0
- package/test/fixtures/sample-expo-project/src/.keep +0 -0
- package/test/metro.test.js +34 -0
- package/test/ports.test.js +76 -0
- package/test/project.test.js +109 -0
- package/test/runner.test.js +209 -0
- package/test/sim-android.test.js +140 -0
- package/test/sim-ios.test.js +168 -0
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();
|