rn-iso 0.5.0 → 0.6.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/README.md CHANGED
@@ -51,6 +51,7 @@ All commands below take the same `npx rn-iso` prefix.
51
51
  | `start` | Start Metro detached, no platform action |
52
52
  | `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. |
53
53
  | `logs [<port>\|<shortcut>\|<path>] [-n <lines>] [--follow]` | Print the managed Metro log (bundle progress, resolution errors, client logs). |
54
+ | `prune` | Remove entries for deleted project directories, freeing their devices and ports. |
54
55
  | `device [--platform ios\|android] [--json]` | Print the assigned device target |
55
56
  | `status` | Show all projects' state |
56
57
  | `reserve [ios\|android]` | Lock a manually-started sim/emulator to the current project (no build) |
package/bin/cli.js CHANGED
@@ -7,6 +7,7 @@ import androidCommand from '../src/commands/android.js';
7
7
  import startCommand from '../src/commands/start.js';
8
8
  import stopCommand from '../src/commands/stop.js';
9
9
  import logsCommand from '../src/commands/logs.js';
10
+ import pruneCommand from '../src/commands/prune.js';
10
11
  import statusCommand from '../src/commands/status.js';
11
12
  import releaseCommand from '../src/commands/release.js';
12
13
  import reserveCommand from '../src/commands/reserve.js';
@@ -28,6 +29,7 @@ androidCommand(program);
28
29
  startCommand(program);
29
30
  stopCommand(program);
30
31
  logsCommand(program);
32
+ pruneCommand(program);
31
33
  statusCommand(program);
32
34
  releaseCommand(program);
33
35
  reserveCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rn-iso",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Isolated React Native dev environments per project/worktree",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -77,7 +77,7 @@ Reserve binds the sim to the current project the same way `ios` does, but skips
77
77
  ## When things go wrong
78
78
 
79
79
  - **"No rn-iso assignment for project"** — run `npx rn-iso ios` (or android) first.
80
- - **"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.
80
+ - **"All iOS simulators are claimed by other rn-iso projects"** (under `--auto`) — every existing sim is held by another project. Claims from deleted worktrees don't count (they're auto-reclaimed), so these are all live projects. Options: `npx rn-iso prune` if you suspect stale state, 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.
81
81
  - **"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
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
83
  - **Blank screen / app installed but nothing renders** — check `npx rn-iso status`. Metro `stopped` almost always means the build ran WITHOUT `--managed-metro`, so Metro died with the shell that ran it: recover with `npx rn-iso start`, then relaunch the app (`xcrun simctl launch <UDID> <bundleId>`), and pass the flag next time. If Metro IS running, read `npx rn-iso logs -n 50` for bundle/resolution errors (a stale `node_modules` after a branch switch is a classic — reinstall deps, then `npx rn-iso stop` + `start`).
@@ -89,6 +89,7 @@ Reserve binds the sim to the current project the same way `ios` does, but skips
89
89
 
90
90
  - `npx rn-iso status` — show all projects, their assignments, and Metro state.
91
91
  - `npx rn-iso logs [<port>|<shortcut>|<path>] [-n <lines>] [--follow]` — print the managed Metro log (default: last 50 lines of the current project's). This is where bundle progress, module-resolution errors, and client console logs land. **Check this first on a blank screen or red box** — it's faster than screenshots.
92
+ - `npx rn-iso prune` — remove entries for projects whose directory no longer exists (deleted worktrees), freeing their sims/emulators and ports, and killing any orphaned Metro. Live projects are never touched. Claims from deleted worktrees are also ignored automatically during device selection, so prune is housekeeping, not a prerequisite.
92
93
  - `npx rn-iso start [--reset-cache] [-- <extras...>]` — start Metro detached on the project's assigned port WITHOUT building/installing. `--reset-cache` clears Metro's transform cache. Other extras after `--` are forwarded to `expo start` / `react-native start`.
93
94
  - `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.
94
95
  - `npx rn-iso release [<port>|<shortcut>|<path>] [--platform <p>] [--shutdown]` — free a project's sim assignment. Defaults to the current project. Target can also be a Metro port (`8083`) or a shortcut (label / unique basename). `--shutdown` also stops the sim/emulator.
@@ -0,0 +1,44 @@
1
+ // src/commands/prune.js
2
+ import chalk from 'chalk';
3
+ import { pruneDeadProjects } from '../config.js';
4
+ import { findPidListeningOnPort } from '../metro.js';
5
+
6
+ export default function pruneCommand(program) {
7
+ program
8
+ .command('prune')
9
+ .description('Remove entries for projects whose directory no longer exists (deleted worktrees), freeing their sims/emulators and Metro ports. Live projects are never touched.')
10
+ .action(() => {
11
+ const removed = pruneDeadProjects();
12
+ if (removed.length === 0) {
13
+ console.log(chalk.dim('Nothing to prune: every registered project path still exists.'));
14
+ return;
15
+ }
16
+
17
+ for (const { path, project } of removed) {
18
+ const freed = [];
19
+ const ios = project.platforms?.ios;
20
+ if (ios?.deviceUdid) freed.push(`ios sim ${ios.deviceUdid}`);
21
+ const android = project.platforms?.android;
22
+ if (android?.avdName) freed.push(`android avd ${android.avdName}`);
23
+ else if (android?.serial) freed.push(`android device ${android.serial}`);
24
+
25
+ console.log(chalk.green(`Pruned ${path}`));
26
+ if (freed.length) console.log(chalk.dim(` freed: ${freed.join(', ')}`));
27
+
28
+ // A Metro started from the deleted directory can outlive it and squat
29
+ // on the port; kill it so the port is genuinely free.
30
+ if (typeof project.metroPort === 'number') {
31
+ const pid = findPidListeningOnPort(project.metroPort);
32
+ if (pid) {
33
+ try {
34
+ process.kill(pid, 'SIGTERM');
35
+ console.log(chalk.dim(` killed orphaned Metro pid ${pid} on port ${project.metroPort}`));
36
+ } catch {
37
+ console.log(chalk.dim(` could not kill pid ${pid} on port ${project.metroPort}`));
38
+ }
39
+ }
40
+ }
41
+ }
42
+ console.log(chalk.dim(`\n${removed.length} project entr${removed.length === 1 ? 'y' : 'ies'} removed.`));
43
+ });
44
+ }
package/src/config.js CHANGED
@@ -187,6 +187,10 @@ export function allClaimedDevices() {
187
187
  };
188
188
  if (!cfg) return result;
189
189
  for (const [path, proj] of Object.entries(cfg.projects || {})) {
190
+ // Claims from project paths that no longer exist on disk are orphaned --
191
+ // nothing can ever run from a deleted worktree again -- so pickers treat
192
+ // those devices as free. `prune` removes the dead entries themselves.
193
+ if (!existsSync(path)) continue;
190
194
  const label = path.split('/').pop() || path;
191
195
  const ios = proj.platforms?.ios;
192
196
  if (ios?.deviceUdid) {
@@ -218,6 +222,22 @@ export function allClaimedDevices() {
218
222
  return result;
219
223
  }
220
224
 
225
+ // Remove project entries whose path no longer exists on disk (deleted
226
+ // worktrees). Returns the removed entries so callers can report what was
227
+ // freed and clean up any process still bound to their Metro ports.
228
+ export function pruneDeadProjects() {
229
+ const cfg = loadConfig();
230
+ if (!cfg?.projects) return [];
231
+ const removed = [];
232
+ for (const [path, proj] of Object.entries(cfg.projects)) {
233
+ if (existsSync(path)) continue;
234
+ removed.push({ path, project: proj });
235
+ delete cfg.projects[path];
236
+ }
237
+ if (removed.length) saveConfig(cfg);
238
+ return removed;
239
+ }
240
+
221
241
  export function recordSimUsage(platform, identifier) {
222
242
  if (platform !== 'ios' && platform !== 'android') return;
223
243
  const cfg = ensureConfig();
package/src/ports.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { request } from 'http';
2
+ import { existsSync } from 'fs';
2
3
  import { loadConfig, allMetroPorts, removeProject } from './config.js';
3
4
 
4
5
  export function isMetroRunning(port) {
@@ -29,6 +30,10 @@ export async function findReclaimablePort(excludeProjectPath, probe = isMetroRun
29
30
  const candidates = [];
30
31
  for (const [path, proj] of Object.entries(cfg.projects)) {
31
32
  if (path === excludeProjectPath) continue;
33
+ // Only projects whose path no longer exists are reclaimable: reclaiming
34
+ // removes the whole entry, and doing that to a live project would also
35
+ // drop its device claim out from under it.
36
+ if (existsSync(path)) continue;
32
37
  if (typeof proj.metroPort === 'number') {
33
38
  candidates.push({ port: proj.metroPort, ownerPath: path });
34
39
  }