rn-iso 0.1.0 → 0.2.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/LICENSE +21 -0
- package/README.md +41 -36
- package/bin/cli.js +0 -6
- package/package.json +28 -2
- package/skill/SKILL.md +41 -43
- package/src/commands/android.js +120 -14
- package/src/commands/ios.js +95 -33
- package/src/commands/release.js +19 -25
- package/src/commands/reserve.js +141 -144
- package/src/commands/status.js +1 -15
- package/src/commands/stop.js +62 -30
- package/src/commands/unreserve.js +23 -43
- package/src/config.js +14 -91
- package/src/labels.js +25 -0
- package/src/project.js +25 -7
- package/src/sim/android.js +31 -18
- package/src/sim/ios.js +7 -1
- package/.claude/settings.local.json +0 -7
- package/CLAUDE.md +0 -178
- package/docs/plans/2026-04-25-rn-iso-implementation.md +0 -2653
- package/docs/specs/2026-04-25-rn-iso-design.md +0 -282
- package/src/commands/logs.js +0 -28
- package/src/commands/prune.js +0 -57
- package/src/commands/shutdown.js +0 -41
- package/test/config.test.js +0 -208
- package/test/exec.test.js +0 -26
- package/test/fixtures/sample-bare-project/android/app/build.gradle +0 -6
- package/test/fixtures/sample-bare-project/ios/SampleApp.xcodeproj/project.pbxproj +0 -10
- package/test/fixtures/sample-bare-project/package.json +0 -4
- package/test/fixtures/sample-expo-project/app.json +0 -6
- package/test/fixtures/sample-expo-project/package.json +0 -4
- package/test/fixtures/sample-expo-project/src/.keep +0 -0
- package/test/metro.test.js +0 -34
- package/test/ports.test.js +0 -76
- package/test/project.test.js +0 -109
- package/test/runner.test.js +0 -209
- package/test/sim-android.test.js +0 -140
- package/test/sim-ios.test.js +0 -168
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Janic Duplessis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -6,77 +6,82 @@ Per-project Metro server and dedicated simulator/emulator for React Native / Exp
|
|
|
6
6
|
|
|
7
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
8
|
|
|
9
|
-
##
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
npm install -g rn-iso
|
|
13
|
-
```
|
|
9
|
+
## Quick start
|
|
14
10
|
|
|
15
|
-
|
|
11
|
+
Run via `npx` from any RN/Expo project directory — no install needed:
|
|
16
12
|
|
|
17
13
|
```bash
|
|
18
|
-
npx
|
|
14
|
+
npx rn-iso ios # ensure sim, allocate port, build/install
|
|
15
|
+
npx rn-iso device # print the assigned UDID
|
|
19
16
|
```
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
In any RN/Expo project directory:
|
|
18
|
+
In a different worktree of the same app:
|
|
24
19
|
|
|
25
20
|
```bash
|
|
26
|
-
rn-iso ios #
|
|
27
|
-
rn-iso device # print the assigned UDID
|
|
21
|
+
npx rn-iso ios # gets a different sim and Metro port
|
|
28
22
|
```
|
|
29
23
|
|
|
30
|
-
|
|
24
|
+
Both run side by side. For non-interactive / agent use, pass `--auto` to skip the picker (this is also implied automatically when stdin isn't a TTY):
|
|
31
25
|
|
|
32
26
|
```bash
|
|
33
|
-
rn-iso ios
|
|
27
|
+
npx rn-iso ios --auto
|
|
34
28
|
```
|
|
35
29
|
|
|
36
|
-
|
|
30
|
+
For AI coding agents, install the skill so the agent knows how to drive the CLI:
|
|
37
31
|
|
|
38
32
|
```bash
|
|
39
|
-
rn-iso
|
|
33
|
+
npx skills add janicduplessis/rn-iso
|
|
40
34
|
```
|
|
41
35
|
|
|
42
36
|
## Commands
|
|
43
37
|
|
|
38
|
+
All commands below take the same `npx rn-iso` prefix.
|
|
39
|
+
|
|
44
40
|
| Command | Purpose |
|
|
45
41
|
|---|---|
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
54
|
-
| `
|
|
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 |
|
|
42
|
+
| `ios [--auto] [--device-type <name>] [--runtime <ver>] [--script <name>] [--pm <name>] [--no-script] [--no-install]` | Ensure iOS sim + Metro + build/install |
|
|
43
|
+
| `android [--auto] [--script <name>] [--pm <name>] [--no-script] [--no-install]` | Same for Android |
|
|
44
|
+
| `start` | Start Metro detached, no platform action |
|
|
45
|
+
| `stop [<port>\|<shortcut>\|<path>]` | Kill Metro. No arg = current project; pass a port (e.g. 8083), a project shortcut (label or unique basename), or an absolute path. |
|
|
46
|
+
| `device [--platform ios\|android] [--json]` | Print the assigned device target |
|
|
47
|
+
| `status` | Show all projects' state |
|
|
48
|
+
| `reserve [ios\|android]` | Lock a manually-started sim/emulator to the current project (no build) |
|
|
49
|
+
| `unreserve [ios\|android]` | Drop the current project's lock without shutting the sim down |
|
|
50
|
+
| `release [<shortcut>\|<path>] [--platform <p>] [--shutdown]` | Free a project's assignment; `--shutdown` also stops the sim |
|
|
58
51
|
|
|
59
52
|
## How it works
|
|
60
53
|
|
|
61
54
|
- **Config** at `~/.rn-iso/config.json`, keyed by absolute project path. Symlinked worktrees collapse via `realpath`.
|
|
62
55
|
- **Port allocation:** assigns 8082, 8083, 8084 etc., reclaiming dead ports on the way.
|
|
63
|
-
- **Simulator pool:** prefers the project's existing assignment; otherwise picks
|
|
56
|
+
- **Simulator / AVD pool:** prefers the project's existing assignment; otherwise picks an unclaimed device — running ones first, shutdown ones next (booting them). On iOS, does not auto-create new sims — pass `--device-type "iPhone 17 Pro" [--runtime 26.2]` to opt in. The interactive picker (iOS or Android) also lets you take over a device claimed by another project after a confirm prompt.
|
|
64
57
|
- **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.
|
|
58
|
+
- **Metro is started by the build CLI** on the assigned port, not by rn-iso. `npx rn-iso start` is the standalone "I just want Metro" path. `npx rn-iso stop` finds Metro by port via `lsof`, so it works regardless of who started it.
|
|
66
59
|
|
|
67
60
|
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
61
|
|
|
69
|
-
##
|
|
62
|
+
## Reserving a manually-started sim
|
|
63
|
+
|
|
64
|
+
If you booted a simulator yourself (Xcode, Simulator.app, `xcrun simctl boot`, or a manual `expo run:ios`) and want rn-iso to know that sim belongs to the current project — so other rn-iso projects skip it:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npx rn-iso reserve --label agent-1 # picks from booted iOS sims
|
|
68
|
+
npx rn-iso reserve android # picks from running emulators
|
|
69
|
+
npx rn-iso unreserve # drop the lock without shutting the sim down
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Reserve binds the sim to the current project the same way `ios` / `android` would, but without running a build. If the sim is already held by another project, the picker prompts you to take it over.
|
|
73
|
+
|
|
74
|
+
## Project shortcuts (--label)
|
|
70
75
|
|
|
71
|
-
|
|
76
|
+
Each registered project has a "shortcut" you can pass to `stop` / `release` instead of the full path. The first time you run `ios`, `android`, or `reserve` interactively you'll be prompted for one (the directory basename is the default — hit enter to accept). Override any time with `--label <name>`:
|
|
72
77
|
|
|
73
78
|
```bash
|
|
74
|
-
rn-iso
|
|
75
|
-
rn-iso
|
|
76
|
-
rn-iso
|
|
79
|
+
npx rn-iso ios --label agent-1
|
|
80
|
+
npx rn-iso stop agent-1 # later, from anywhere
|
|
81
|
+
npx rn-iso release agent-1 --shutdown
|
|
77
82
|
```
|
|
78
83
|
|
|
79
|
-
|
|
84
|
+
Under `--auto` (or any non-TTY invocation) the prompt is skipped — the project's basename serves as its shortcut by default. Shortcut collisions (two projects sharing the same basename, or two labels colliding) error out and list the candidates so you can disambiguate with the absolute path.
|
|
80
85
|
|
|
81
86
|
## Requirements
|
|
82
87
|
|
package/bin/cli.js
CHANGED
|
@@ -5,11 +5,8 @@ import iosCommand from '../src/commands/ios.js';
|
|
|
5
5
|
import androidCommand from '../src/commands/android.js';
|
|
6
6
|
import startCommand from '../src/commands/start.js';
|
|
7
7
|
import stopCommand from '../src/commands/stop.js';
|
|
8
|
-
import logsCommand from '../src/commands/logs.js';
|
|
9
8
|
import statusCommand from '../src/commands/status.js';
|
|
10
9
|
import releaseCommand from '../src/commands/release.js';
|
|
11
|
-
import shutdownCommand from '../src/commands/shutdown.js';
|
|
12
|
-
import pruneCommand from '../src/commands/prune.js';
|
|
13
10
|
import reserveCommand from '../src/commands/reserve.js';
|
|
14
11
|
import unreserveCommand from '../src/commands/unreserve.js';
|
|
15
12
|
|
|
@@ -24,11 +21,8 @@ iosCommand(program);
|
|
|
24
21
|
androidCommand(program);
|
|
25
22
|
startCommand(program);
|
|
26
23
|
stopCommand(program);
|
|
27
|
-
logsCommand(program);
|
|
28
24
|
statusCommand(program);
|
|
29
25
|
releaseCommand(program);
|
|
30
|
-
shutdownCommand(program);
|
|
31
|
-
pruneCommand(program);
|
|
32
26
|
reserveCommand(program);
|
|
33
27
|
unreserveCommand(program);
|
|
34
28
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rn-iso",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Isolated React Native dev environments per project/worktree",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"rn-iso": "bin/cli.js"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"skill",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
9
16
|
"scripts": {
|
|
10
17
|
"test": "node --test test/*.test.js"
|
|
11
18
|
},
|
|
@@ -16,5 +23,24 @@
|
|
|
16
23
|
},
|
|
17
24
|
"engines": {
|
|
18
25
|
"node": ">=20"
|
|
19
|
-
}
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/janicduplessis/rn-iso.git"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/janicduplessis/rn-iso#readme",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/janicduplessis/rn-iso/issues"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"react-native",
|
|
37
|
+
"expo",
|
|
38
|
+
"metro",
|
|
39
|
+
"simulator",
|
|
40
|
+
"emulator",
|
|
41
|
+
"worktree",
|
|
42
|
+
"agent"
|
|
43
|
+
],
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"author": "Janic Duplessis"
|
|
20
46
|
}
|
package/skill/SKILL.md
CHANGED
|
@@ -8,16 +8,18 @@ user_invocable: true
|
|
|
8
8
|
|
|
9
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
10
|
|
|
11
|
+
Invoke the CLI via `npx`: `npx rn-iso <command>`. Don't `npm install -g`; `npx` resolves the latest published version.
|
|
12
|
+
|
|
11
13
|
## Core workflow
|
|
12
14
|
|
|
13
15
|
From the project root (or any subdirectory):
|
|
14
16
|
|
|
15
|
-
1. **Ensure the platform is ready** — `rn-iso ios --auto` (or `rn-iso android`). This:
|
|
17
|
+
1. **Ensure the platform is ready** — `npx rn-iso ios --auto` (or `npx rn-iso android`). This:
|
|
16
18
|
- Allocates a Metro port for the project (or reuses the assigned one)
|
|
17
19
|
- Picks a dedicated unclaimed sim (booting it if shutdown). With `--auto`, picks the first candidate without prompting.
|
|
18
20
|
- 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
21
|
|
|
20
|
-
2. **Get the device target** — `rn-iso device --platform ios --json`:
|
|
22
|
+
2. **Get the device target** — `npx rn-iso device --platform ios --json`:
|
|
21
23
|
```json
|
|
22
24
|
{"platform":"ios","udid":"ABC-...","metroPort":8083}
|
|
23
25
|
```
|
|
@@ -27,85 +29,81 @@ From the project root (or any subdirectory):
|
|
|
27
29
|
|
|
28
30
|
## CRITICAL rules
|
|
29
31
|
|
|
30
|
-
- **
|
|
31
|
-
-
|
|
32
|
+
- **Pass `--auto` for non-interactive use** of `ios` or `android`. Without it, the command will prompt with an arrow-key picker if multiple unclaimed sims/AVDs exist. `--auto` is also implied automatically when stdin isn't a TTY (e.g., when an agent pipes the command), so under most agent harnesses you don't have to remember the flag — but passing it explicitly is harmless and clearer.
|
|
33
|
+
- **`--auto` will NOT take over a claimed sim/AVD.** If every device is claimed by other rn-iso projects, `--auto` errors. To take one over, run the command interactively (no `--auto`, with a real TTY) and confirm at the prompt — only do this if the user explicitly asks.
|
|
34
|
+
- **Always use `npx rn-iso device` to discover your target.** Never assume `booted` is your sim — another project's simulator might be booted too.
|
|
32
35
|
- **Always pass the UDID/serial explicitly** to `xcrun simctl` and `adb -s`. Examples:
|
|
33
36
|
- `xcrun simctl io <UDID> screenshot out.png`
|
|
34
37
|
- `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.
|
|
38
|
+
- **Don't call `release` or `release --shutdown`** unless the user explicitly asks. Other agents may be using neighboring sims; keep yours up so the user can come back to it.
|
|
39
|
+
- **Don't manually start Metro on a different port.** `npx rn-iso start` (or `npx rn-iso ios/android`) already handles port assignment.
|
|
37
40
|
- **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
41
|
|
|
39
42
|
## Typical agent workflow
|
|
40
43
|
|
|
41
44
|
```bash
|
|
42
45
|
# Once per session -- ensure the project's sim and Metro are up.
|
|
43
|
-
rn-iso ios --auto
|
|
46
|
+
npx rn-iso ios --auto
|
|
44
47
|
|
|
45
48
|
# Get the target.
|
|
46
|
-
UDID=$(rn-iso device --platform ios)
|
|
49
|
+
UDID=$(npx rn-iso device --platform ios)
|
|
47
50
|
|
|
48
51
|
# Use the target for UI interactions (delegate to agent-device or your tool).
|
|
49
52
|
xcrun simctl io "$UDID" screenshot /tmp/screen.png
|
|
50
53
|
|
|
51
54
|
# 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.
|
|
55
|
+
# Only re-run `npx rn-iso ios` after native code changes or new native modules.
|
|
53
56
|
```
|
|
54
57
|
|
|
55
|
-
##
|
|
58
|
+
## Locking a manually-started sim
|
|
56
59
|
|
|
57
|
-
If
|
|
60
|
+
If the user has already booted a sim and started the app themselves (Xcode, Simulator.app, `xcrun simctl boot`, manual `expo run:ios`), and asks you to "lock" or "claim" that sim for the current project, use `reserve`:
|
|
58
61
|
|
|
59
62
|
```bash
|
|
60
|
-
#
|
|
61
|
-
rn-iso reserve
|
|
62
|
-
rn-iso
|
|
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
|
|
63
|
+
npx rn-iso reserve # picks from booted iOS sims (current project)
|
|
64
|
+
npx rn-iso reserve android # picks from running emulators
|
|
65
|
+
npx rn-iso unreserve # drop the project's lock (without shutting down)
|
|
74
66
|
```
|
|
75
67
|
|
|
76
|
-
|
|
68
|
+
Reserve binds the sim to the current project the same way `ios` does, but skips the build/install step. After reserving, `npx rn-iso device` will return that sim's UDID. Other rn-iso projects will see it as claimed.
|
|
77
69
|
|
|
78
70
|
## When things go wrong
|
|
79
71
|
|
|
80
|
-
- **"No rn-iso assignment for project"** — run `rn-iso ios` (or android) first.
|
|
81
|
-
- **"
|
|
72
|
+
- **"No rn-iso assignment for project"** — run `npx rn-iso ios` (or android) first.
|
|
73
|
+
- **"All iOS simulators are claimed by other rn-iso projects"** (under `--auto`) — every existing sim is held by another project. Options: free another project (`npx rn-iso release` from there), pass `--device-type "iPhone 17 Pro"` to create a new sim, or re-run without `--auto` (in a real TTY) and ask the user before confirming the take-over prompt.
|
|
74
|
+
- **"All Android AVDs are claimed by other rn-iso projects"** — same situation on Android. Free another project or re-run interactively to take one over.
|
|
82
75
|
- **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.
|
|
76
|
+
- **Metro port collision** — `npx rn-iso ios` reclaims dead ports automatically. If you see "port busy by non-Metro process," another tool is using that port; close it.
|
|
77
|
+
- **Sim was deleted** — `npx rn-iso ios` detects the stale assignment and re-allocates.
|
|
85
78
|
- **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
79
|
|
|
87
80
|
## Other useful commands
|
|
88
81
|
|
|
89
|
-
- `rn-iso status` — show all projects, their assignments, and Metro state.
|
|
90
|
-
- `rn-iso start` — start Metro detached on the project's assigned port WITHOUT building/installing. Useful
|
|
91
|
-
- `rn-iso
|
|
92
|
-
- `rn-iso
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
82
|
+
- `npx rn-iso status` — show all projects, their assignments, and Metro state.
|
|
83
|
+
- `npx rn-iso start` — start Metro detached on the project's assigned port WITHOUT building/installing. Useful to keep Metro alive across builds.
|
|
84
|
+
- `npx rn-iso stop [<port>|<shortcut>|<path>]` — kill Metro. No arg = current project. Passing a port (e.g. `8083`) kills whatever is on it; a project shortcut (label or unique basename) or absolute path targets that project. Finds the process by port, so it works whether Metro was started by `npx rn-iso start` or by the build CLI.
|
|
85
|
+
- `npx rn-iso release [<shortcut>|<path>] [--platform <p>] [--shutdown]` — free a project's sim assignment. Defaults to the current project. `--shutdown` also stops the sim/emulator.
|
|
86
|
+
|
|
87
|
+
### Project shortcuts (--label)
|
|
88
|
+
|
|
89
|
+
Every project has a "shortcut" you can pass to `stop` / `release` instead of the full path. The first interactive run of `ios` / `android` / `reserve` prompts for one (default: directory basename); under `--auto` / non-TTY the prompt is skipped and the basename is used implicitly. To set or override explicitly, pass `--label <name>`:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npx rn-iso ios --auto --label agent-1
|
|
93
|
+
npx rn-iso stop agent-1
|
|
94
|
+
```
|
|
99
95
|
|
|
100
96
|
## Sort order in the picker
|
|
101
97
|
|
|
102
|
-
When the
|
|
103
|
-
1. Family (iPhone before iPad before others
|
|
98
|
+
When the iOS picker fires, sims are sorted by:
|
|
99
|
+
1. Family (iPhone before iPad before others)
|
|
104
100
|
2. State (booted before shutdown within family)
|
|
105
101
|
3. Usage count (most-used floats up; tracked per UDID across all projects)
|
|
106
102
|
4. Name (alphabetical, stable tiebreak)
|
|
107
103
|
|
|
108
|
-
|
|
104
|
+
When the Android picker fires, AVDs are sorted by running state (running emulators first), then alphabetically.
|
|
105
|
+
|
|
106
|
+
Sims/AVDs claimed by other rn-iso projects show in yellow with a `[claimed by ...]` tag. They're selectable but require a confirm prompt before being taken over.
|
|
109
107
|
|
|
110
108
|
## Differences from `react-native-worktree`
|
|
111
109
|
|
package/src/commands/android.js
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
// src/commands/android.js
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import prompts from 'prompts';
|
|
3
4
|
import { findProjectRoot, detectIsExpo, detectBundleId, detectAndroidPackage } from '../project.js';
|
|
4
|
-
import { getProject, upsertProject, setMetro, setDevice, allClaimedDevices } from '../config.js';
|
|
5
|
+
import { getProject, upsertProject, setMetro, setDevice, clearDevice, allClaimedDevices } from '../config.js';
|
|
5
6
|
import { allocatePort, isMetroRunning } from '../ports.js';
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
selectAndroidDevice,
|
|
9
|
+
sortAndroidCandidates,
|
|
10
|
+
bootAndroidEmulator,
|
|
11
|
+
waitForBoot,
|
|
12
|
+
adbReverse,
|
|
13
|
+
nextConsolePort,
|
|
14
|
+
} from '../sim/android.js';
|
|
7
15
|
import { buildAndroidCommand, detectPackageManager } from '../runner.js';
|
|
8
16
|
import { getExecutor } from '../exec.js';
|
|
17
|
+
import { resolveLabel } from '../labels.js';
|
|
9
18
|
|
|
10
19
|
export default function androidCommand(program) {
|
|
11
20
|
program
|
|
12
21
|
.command('android')
|
|
13
22
|
.description('Ensure a dedicated Android emulator + Metro for the current project; build/install if needed')
|
|
23
|
+
.option('--auto', 'Non-interactive: pick the first unclaimed AVD without prompting (also implied when stdin is not a TTY)')
|
|
24
|
+
.option('--label <name>', 'Optional shortcut name; refer to the project as <name> in stop / release / etc.')
|
|
14
25
|
.option('--script <name>', 'package.json script to invoke for build/install (default: android)', 'android')
|
|
15
26
|
.option('--no-script', 'Skip the package.json script lookup; run expo/react-native CLI directly')
|
|
16
27
|
.option('--pm <name>', 'Package manager: npm, yarn, pnpm, bun (default: detected from lockfile)')
|
|
@@ -26,7 +37,14 @@ export default function androidCommand(program) {
|
|
|
26
37
|
const androidPackage = detectAndroidPackage(root);
|
|
27
38
|
const isExpo = detectIsExpo(root);
|
|
28
39
|
|
|
29
|
-
|
|
40
|
+
const existing = getProject(root);
|
|
41
|
+
const label = await resolveLabel({ root, existingProject: existing, optsLabel: opts.label });
|
|
42
|
+
upsertProject(root, {
|
|
43
|
+
bundleId,
|
|
44
|
+
androidPackage,
|
|
45
|
+
isExpo,
|
|
46
|
+
...(label ? { label } : {}),
|
|
47
|
+
});
|
|
30
48
|
let proj = getProject(root);
|
|
31
49
|
|
|
32
50
|
if (!proj.metroPort) {
|
|
@@ -41,6 +59,7 @@ export default function androidCommand(program) {
|
|
|
41
59
|
(metroAlreadyUp ? ' (already running)' : ' (will be started by build CLI)')
|
|
42
60
|
));
|
|
43
61
|
|
|
62
|
+
const auto = isAuto(opts);
|
|
44
63
|
const claimed = allClaimedDevices();
|
|
45
64
|
const myAvd = proj.platforms?.android?.avdName || null;
|
|
46
65
|
const myPort = proj.platforms?.android?.consolePort || null;
|
|
@@ -54,19 +73,59 @@ export default function androidCommand(program) {
|
|
|
54
73
|
claimedConsolePorts: claimedPorts,
|
|
55
74
|
});
|
|
56
75
|
|
|
57
|
-
|
|
76
|
+
let avdName, consolePort, isRunning;
|
|
77
|
+
if (selection.kind === 'reuse') {
|
|
78
|
+
({ avdName, consolePort, isRunning } = selection);
|
|
79
|
+
if (isRunning) {
|
|
80
|
+
console.log(chalk.dim(`Reusing running emulator emulator-${consolePort} (${avdName})`));
|
|
81
|
+
} else {
|
|
82
|
+
console.log(chalk.dim(`Booting assigned AVD ${avdName} on port ${consolePort}...`));
|
|
83
|
+
}
|
|
84
|
+
} else if (selection.kind === 'allocate') {
|
|
85
|
+
const picked = (selection.candidates.length === 1 || auto)
|
|
86
|
+
? { c: selection.candidates[0], prevClaim: null }
|
|
87
|
+
: await pickAvd({
|
|
88
|
+
candidates: selection.candidates,
|
|
89
|
+
androidClaims: claimed.androidClaims,
|
|
90
|
+
});
|
|
91
|
+
await releasePriorClaim(picked.prevClaim);
|
|
92
|
+
({ avdName, isRunning, consolePort } = picked.c);
|
|
93
|
+
if (!isRunning) {
|
|
94
|
+
consolePort = nextConsolePort(claimedPorts);
|
|
95
|
+
}
|
|
96
|
+
console.log(isRunning
|
|
97
|
+
? chalk.green(`Picked ${avdName} (running on emulator-${consolePort})`)
|
|
98
|
+
: chalk.dim(`Booting ${avdName} on port ${consolePort}...`));
|
|
99
|
+
} else if (selection.kind === 'allClaimed') {
|
|
100
|
+
if (auto) {
|
|
101
|
+
console.error(chalk.red('All Android AVDs are claimed by other rn-iso projects.'));
|
|
102
|
+
console.error(chalk.dim('Re-run without --auto to confirm taking one over, or create a new AVD via Android Studio.'));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
const picked = await pickAvd({
|
|
106
|
+
candidates: selection.candidates,
|
|
107
|
+
androidClaims: claimed.androidClaims,
|
|
108
|
+
allClaimed: true,
|
|
109
|
+
});
|
|
110
|
+
await releasePriorClaim(picked.prevClaim);
|
|
111
|
+
({ avdName, isRunning, consolePort } = picked.c);
|
|
112
|
+
if (!isRunning) {
|
|
113
|
+
// Prior owner's port is freed by releasePriorClaim, but compute fresh.
|
|
114
|
+
const fresh = allClaimedDevices().androidConsolePorts.filter(p => p !== myPort);
|
|
115
|
+
consolePort = nextConsolePort(fresh);
|
|
116
|
+
}
|
|
117
|
+
console.log(isRunning
|
|
118
|
+
? chalk.green(`Took over ${avdName} (running on emulator-${consolePort})`)
|
|
119
|
+
: chalk.dim(`Booting ${avdName} on port ${consolePort}...`));
|
|
120
|
+
} else {
|
|
58
121
|
console.error(chalk.red(
|
|
59
|
-
'No AVDs available
|
|
60
|
-
'Create one via Android Studio (Tools -> Device Manager).'
|
|
122
|
+
'No AVDs available. Create one via Android Studio (Tools -> Device Manager).'
|
|
61
123
|
));
|
|
62
124
|
process.exit(1);
|
|
63
125
|
}
|
|
64
126
|
|
|
65
|
-
const { avdName, consolePort, isRunning } = selection;
|
|
66
127
|
const serial = `emulator-${consolePort}`;
|
|
67
|
-
|
|
68
128
|
if (!isRunning) {
|
|
69
|
-
console.log(chalk.dim(`Booting emulator ${avdName} on port ${consolePort}...`));
|
|
70
129
|
bootAndroidEmulator(avdName, consolePort);
|
|
71
130
|
console.log(chalk.dim('Waiting for boot to complete (this can take 10-30s)...'));
|
|
72
131
|
const ok = await waitForBoot(serial, 120000);
|
|
@@ -74,15 +133,10 @@ export default function androidCommand(program) {
|
|
|
74
133
|
console.error(chalk.red(`Emulator ${serial} did not finish booting within 2 minutes.`));
|
|
75
134
|
process.exit(1);
|
|
76
135
|
}
|
|
77
|
-
} else {
|
|
78
|
-
console.log(chalk.dim(`Reusing running emulator ${serial}`));
|
|
79
136
|
}
|
|
80
137
|
|
|
81
138
|
setDevice(root, 'android', { avdName, consolePort });
|
|
82
139
|
|
|
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
140
|
adbReverse(serial, proj.metroPort);
|
|
87
141
|
console.log(chalk.dim(`adb reverse tcp:${proj.metroPort} configured for ${serial}`));
|
|
88
142
|
|
|
@@ -110,3 +164,55 @@ export default function androidCommand(program) {
|
|
|
110
164
|
console.log(chalk.green(`\nAndroid ready on ${serial}, Metro port ${proj.metroPort}`));
|
|
111
165
|
});
|
|
112
166
|
}
|
|
167
|
+
|
|
168
|
+
async function pickAvd({ candidates, androidClaims = {}, allClaimed = false }) {
|
|
169
|
+
const sorted = sortAndroidCandidates(candidates);
|
|
170
|
+
const nameWidth = Math.max(...sorted.map(c => c.avdName.length), 18);
|
|
171
|
+
const choices = sorted.map(c => {
|
|
172
|
+
const claim = androidClaims[c.consolePort];
|
|
173
|
+
const claimTag = claim ? chalk.yellow(` [claimed by ${claim.label}]`) : '';
|
|
174
|
+
const runTag = c.isRunning ? chalk.green(` [running on emulator-${c.consolePort}]`) : '';
|
|
175
|
+
return {
|
|
176
|
+
title: `${c.avdName.padEnd(nameWidth)}${runTag}${claimTag}`,
|
|
177
|
+
value: { c, claim: claim || null },
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
const message = allClaimed
|
|
181
|
+
? 'All AVDs are claimed. Pick one to take over:'
|
|
182
|
+
: 'Pick an AVD (claimed AVDs will prompt to confirm):';
|
|
183
|
+
const answer = await prompts({
|
|
184
|
+
type: 'select',
|
|
185
|
+
name: 'pick',
|
|
186
|
+
message,
|
|
187
|
+
choices,
|
|
188
|
+
});
|
|
189
|
+
if (!answer.pick) {
|
|
190
|
+
console.error(chalk.red('Cancelled.'));
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
const { c, claim } = answer.pick;
|
|
194
|
+
if (claim) {
|
|
195
|
+
const ok = await prompts({
|
|
196
|
+
type: 'confirm',
|
|
197
|
+
name: 'ok',
|
|
198
|
+
message: `${c.avdName} is currently held by project "${claim.label}". Take it over?`,
|
|
199
|
+
initial: false,
|
|
200
|
+
});
|
|
201
|
+
if (!ok.ok) {
|
|
202
|
+
console.error(chalk.red('Cancelled.'));
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
return { c, prevClaim: claim };
|
|
206
|
+
}
|
|
207
|
+
return { c, prevClaim: null };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function releasePriorClaim(prevClaim) {
|
|
211
|
+
if (!prevClaim?.path) return;
|
|
212
|
+
clearDevice(prevClaim.path, 'android');
|
|
213
|
+
console.log(chalk.dim(`Released prior assignment from "${prevClaim.label}"`));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function isAuto(opts) {
|
|
217
|
+
return opts.auto || !process.stdin.isTTY;
|
|
218
|
+
}
|