rn-iso 0.3.0 → 0.3.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rn-iso",
3
- "version": "0.3.0",
3
+ "version": "0.3.3",
4
4
  "description": "Isolated React Native dev environments per project/worktree",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,6 +7,7 @@ import { allocatePort, isMetroRunning } from '../ports.js';
7
7
  import {
8
8
  selectAndroidDevice,
9
9
  sortAndroidCandidates,
10
+ enumerateAndroidCandidates,
10
11
  bootAndroidEmulator,
11
12
  waitForBoot,
12
13
  adbReverse,
@@ -77,16 +78,16 @@ export default function androidCommand(program) {
77
78
  if (selection.kind === 'reuse') {
78
79
  ({ avdName, consolePort, isRunning } = selection);
79
80
  if (isRunning) {
80
- console.log(chalk.dim(`Reusing running emulator emulator-${consolePort} (${avdName})`));
81
+ console.log(chalk.dim(`Reusing running ${avdName} (emulator-${consolePort})`));
81
82
  } else {
82
- console.log(chalk.dim(`Booting assigned AVD ${avdName} on port ${consolePort}...`));
83
+ console.log(chalk.dim(`Booting assigned ${avdName} (emulator-${consolePort})...`));
83
84
  }
84
85
  } else if (selection.kind === 'allocate') {
85
86
  const picked = (selection.candidates.length === 1 || auto)
86
87
  ? { c: selection.candidates[0], prevClaim: null }
87
88
  : await pickAvd({
88
89
  candidates: selection.candidates,
89
- androidClaims: claimed.androidClaims,
90
+ androidClaimsByAvd: claimed.androidClaimsByAvd,
90
91
  });
91
92
  await releasePriorClaim(picked.prevClaim);
92
93
  ({ avdName, isRunning, consolePort } = picked.c);
@@ -94,8 +95,8 @@ export default function androidCommand(program) {
94
95
  consolePort = nextConsolePort(claimedPorts);
95
96
  }
96
97
  console.log(isRunning
97
- ? chalk.green(`Picked ${avdName} (running on emulator-${consolePort})`)
98
- : chalk.dim(`Booting ${avdName} on port ${consolePort}...`));
98
+ ? chalk.green(`Picked ${avdName} (emulator-${consolePort}, running)`)
99
+ : chalk.dim(`Booting ${avdName} (emulator-${consolePort})...`));
99
100
  } else if (selection.kind === 'allClaimed') {
100
101
  if (auto) {
101
102
  console.error(chalk.red('All Android AVDs are claimed by other rn-iso projects.'));
@@ -104,7 +105,7 @@ export default function androidCommand(program) {
104
105
  }
105
106
  const picked = await pickAvd({
106
107
  candidates: selection.candidates,
107
- androidClaims: claimed.androidClaims,
108
+ androidClaimsByAvd: claimed.androidClaimsByAvd,
108
109
  allClaimed: true,
109
110
  });
110
111
  await releasePriorClaim(picked.prevClaim);
@@ -115,8 +116,8 @@ export default function androidCommand(program) {
115
116
  consolePort = nextConsolePort(fresh);
116
117
  }
117
118
  console.log(isRunning
118
- ? chalk.green(`Took over ${avdName} (running on emulator-${consolePort})`)
119
- : chalk.dim(`Booting ${avdName} on port ${consolePort}...`));
119
+ ? chalk.green(`Took over ${avdName} (emulator-${consolePort}, running)`)
120
+ : chalk.dim(`Booting ${avdName} (emulator-${consolePort})...`));
120
121
  } else {
121
122
  console.error(chalk.red(
122
123
  'No AVDs available. Create one via Android Studio (Tools -> Device Manager).'
@@ -149,6 +150,7 @@ export default function androidCommand(program) {
149
150
  packageManager,
150
151
  scriptName,
151
152
  isExpo,
153
+ avdName,
152
154
  serial,
153
155
  port: proj.metroPort,
154
156
  useScript,
@@ -161,20 +163,34 @@ export default function androidCommand(program) {
161
163
  });
162
164
  }
163
165
 
164
- console.log(chalk.green(`\nAndroid ready on ${serial}, Metro port ${proj.metroPort}`));
166
+ console.log(chalk.green(`\nAndroid ready on ${avdName} (${serial}), Metro port ${proj.metroPort}`));
165
167
  });
166
168
  }
167
169
 
168
- async function pickAvd({ candidates, androidClaims = {}, allClaimed = false }) {
169
- const sorted = sortAndroidCandidates(candidates);
170
+ async function pickAvd({ candidates, androidClaimsByAvd = {}, allClaimed = false }) {
171
+ // Show every AVD on disk (parallel to the iOS picker), so the user can
172
+ // see what's claimed and optionally take it over. `candidates` is the
173
+ // unclaimed set passed in by selectAndroidDevice; AVDs outside it are
174
+ // claimed and will require a confirm prompt on selection.
175
+ const allAvds = enumerateAndroidCandidates();
176
+ const candidateAvds = new Set(candidates.map(c => c.avdName));
177
+ const sorted = sortAndroidCandidates(allAvds);
178
+
170
179
  const nameWidth = Math.max(...sorted.map(c => c.avdName.length), 18);
171
180
  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}]`) : '';
181
+ const claim = androidClaimsByAvd[c.avdName];
182
+ const isCandidate = candidateAvds.has(c.avdName);
183
+ const runTag = c.isRunning ? chalk.green(` [emulator-${c.consolePort}, running]`) : '';
184
+ if (claim || !isCandidate) {
185
+ const tag = claim ? chalk.yellow(` [claimed by ${claim.label}]`) : '';
186
+ return {
187
+ title: chalk.yellow(`${c.avdName.padEnd(nameWidth)}${tag}${runTag}`),
188
+ value: { c, claim: claim || null },
189
+ };
190
+ }
175
191
  return {
176
- title: `${c.avdName.padEnd(nameWidth)}${runTag}${claimTag}`,
177
- value: { c, claim: claim || null },
192
+ title: `${c.avdName.padEnd(nameWidth)}${runTag}`,
193
+ value: { c, claim: null },
178
194
  };
179
195
  });
180
196
  const message = allClaimed
@@ -4,7 +4,7 @@ import prompts from 'prompts';
4
4
  import { findProjectRoot, detectIsExpo, detectBundleId, detectAndroidPackage } from '../project.js';
5
5
  import { getProject, upsertProject, setMetro, setDevice, clearDevice, allClaimedDevices, recordSimUsage, getSimUsage } from '../config.js';
6
6
  import { allocatePort, isMetroRunning } from '../ports.js';
7
- import { selectIosDevice, bootIosSim, listIosRuntimes, createIosSim, parseRuntimeVersion, listAllIosSims, sortSims } from '../sim/ios.js';
7
+ import { selectIosDevice, bootIosSim, listIosRuntimes, createIosSim, parseRuntimeVersion, listAllIosSims, sortSims, formatIosLabel } from '../sim/ios.js';
8
8
  import { buildIosCommand, detectPackageManager } from '../runner.js';
9
9
  import { getExecutor } from '../exec.js';
10
10
  import { resolveLabel } from '../labels.js';
@@ -70,11 +70,12 @@ export default function iosCommand(program) {
70
70
  let udid;
71
71
  if (selection.kind === 'reuse') {
72
72
  udid = selection.udid;
73
+ const label = `${selection.name} (${udid})`;
73
74
  if (selection.state !== 'Booted') {
74
- console.log(chalk.dim(`Booting assigned sim ${udid}...`));
75
+ console.log(chalk.dim(`Booting assigned sim ${label}...`));
75
76
  bootIosSim(udid);
76
77
  } else {
77
- console.log(chalk.dim(`Reusing assigned sim ${udid} (already booted)`));
78
+ console.log(chalk.dim(`Reusing assigned sim ${label} (already booted)`));
78
79
  }
79
80
  } else if (selection.kind === 'allocate') {
80
81
  const picked = (selection.candidates.length === 1 || auto)
@@ -99,7 +100,7 @@ export default function iosCommand(program) {
99
100
  if (auto) {
100
101
  if (opts.deviceType) {
101
102
  udid = createNewSim({ deviceType: opts.deviceType, runtimeVersion: opts.runtime });
102
- console.log(chalk.green(`Created and booted new sim ${udid}`));
103
+ console.log(chalk.green(`Created and booted new sim ${formatIosLabel(udid)}`));
103
104
  } else {
104
105
  console.error(chalk.red('All iOS simulators are claimed by other rn-iso projects.'));
105
106
  console.error(chalk.dim('Re-run without --auto to confirm taking one over, or pass --device-type to create a new sim.'));
@@ -164,7 +165,7 @@ export default function iosCommand(program) {
164
165
 
165
166
  }
166
167
 
167
- console.log(chalk.green(`\nOK: iOS ready on sim ${udid}, Metro port ${proj.metroPort}`));
168
+ console.log(chalk.green(`\nOK: iOS ready on sim ${formatIosLabel(udid)}, Metro port ${proj.metroPort}`));
168
169
  });
169
170
  }
170
171
 
@@ -4,7 +4,7 @@ import prompts from 'prompts';
4
4
  import { resolveRegisteredProject } from '../project.js';
5
5
  import { getProject, clearDevice, findProjectByMetroPort } from '../config.js';
6
6
  import { findPidListeningOnPort } from '../metro.js';
7
- import { shutdownIosSim } from '../sim/ios.js';
7
+ import { shutdownIosSim, formatIosLabel } from '../sim/ios.js';
8
8
  import { shutdownAndroidEmulator } from '../sim/android.js';
9
9
 
10
10
  export default function releaseCommand(program) {
@@ -45,10 +45,10 @@ export default function releaseCommand(program) {
45
45
  if (opts.shutdown) {
46
46
  if (p === 'ios') {
47
47
  shutdownIosSim(entry.deviceUdid);
48
- console.log(chalk.green(`Shut down iOS sim ${entry.deviceUdid}`));
48
+ console.log(chalk.green(`Shut down iOS sim ${formatIosLabel(entry.deviceUdid)}`));
49
49
  } else {
50
50
  shutdownAndroidEmulator(`emulator-${entry.consolePort}`);
51
- console.log(chalk.green(`Shut down emulator-${entry.consolePort}`));
51
+ console.log(chalk.green(`Shut down ${entry.avdName} (emulator-${entry.consolePort})`));
52
52
  }
53
53
  }
54
54
  clearDevice(found, p);
package/src/config.js CHANGED
@@ -111,10 +111,12 @@ export function allClaimedDevices() {
111
111
  androidAvds: [],
112
112
  androidConsolePorts: [],
113
113
  // iosClaims: udid -> { label, path }. androidClaims: consolePort ->
114
- // { label, path, avdName }. `path` is the absolute project path so
115
- // take-over flows can call clearDevice on the owning project.
114
+ // { label, path, avdName }. androidClaimsByAvd: avdName -> { label,
115
+ // path, consolePort }. `path` is the absolute project path so take-
116
+ // over flows can call clearDevice on the owning project.
116
117
  iosClaims: {},
117
118
  androidClaims: {},
119
+ androidClaimsByAvd: {},
118
120
  };
119
121
  if (!cfg) return result;
120
122
  for (const [path, proj] of Object.entries(cfg.projects || {})) {
@@ -125,7 +127,14 @@ export function allClaimedDevices() {
125
127
  result.iosClaims[ios.deviceUdid] = { label, path };
126
128
  }
127
129
  const android = proj.platforms?.android;
128
- if (android?.avdName) result.androidAvds.push(android.avdName);
130
+ if (android?.avdName) {
131
+ result.androidAvds.push(android.avdName);
132
+ result.androidClaimsByAvd[android.avdName] = {
133
+ label,
134
+ path,
135
+ consolePort: android.consolePort,
136
+ };
137
+ }
129
138
  if (typeof android?.consolePort === 'number') {
130
139
  result.androidConsolePorts.push(android.consolePort);
131
140
  result.androidClaims[android.consolePort] = {
package/src/runner.js CHANGED
@@ -64,9 +64,11 @@ export function buildScriptCommand(packageManager, scriptName, extraArgs = []) {
64
64
  }
65
65
  }
66
66
 
67
- // Decide which CLI a script invokes. Affects flag names: Expo CLI takes
68
- // --device <UDID>, bare RN CLI takes --udid <UDID> for iOS (--device there
69
- // means physical device by name) and --deviceId for Android.
67
+ // Decide which CLI a script invokes. Affects flag names:
68
+ // iOS: Expo `--device <UDID>` | RN `--udid <UDID>`
69
+ // Android: Expo `--device <AVD-name>` | RN `--deviceId <serial>`
70
+ // Expo's run:android resolves --device by name (not by serial), so we pass
71
+ // the AVD name there even though we boot/track by serial.
70
72
  export function detectScriptCli(scriptBody) {
71
73
  if (typeof scriptBody !== 'string') return 'unknown';
72
74
  if (/\bexpo\s+(run:ios|run:android|start)\b/.test(scriptBody)) return 'expo';
@@ -96,13 +98,13 @@ export function buildIosCommand({ projectRoot, packageManager, scriptName, isExp
96
98
  return `npx react-native run-ios --udid ${udid} --port ${port}`;
97
99
  }
98
100
 
99
- export function buildAndroidCommand({ projectRoot, packageManager, scriptName, isExpo, serial, port, useScript = true }) {
101
+ export function buildAndroidCommand({ projectRoot, packageManager, scriptName, isExpo, avdName, serial, port, useScript = true }) {
100
102
  if (useScript && scriptName) {
101
103
  const script = getProjectScript(projectRoot, scriptName);
102
104
  if (script) {
103
105
  const cli = detectScriptCli(script);
104
- // Expo: --device <serial>; RN: --deviceId <serial>.
105
- const deviceFlag = cli === 'expo' ? `--device ${serial}` : `--deviceId ${serial}`;
106
+ // Expo: --device <AVD name>; RN: --deviceId <serial>.
107
+ const deviceFlag = cli === 'expo' ? `--device "${avdName}"` : `--deviceId ${serial}`;
106
108
  return buildScriptCommand(packageManager, scriptName, [
107
109
  deviceFlag,
108
110
  `--port ${port}`,
@@ -110,7 +112,7 @@ export function buildAndroidCommand({ projectRoot, packageManager, scriptName, i
110
112
  }
111
113
  }
112
114
  if (isExpo) {
113
- return `npx expo run:android --device ${serial} --port ${port}`;
115
+ return `npx expo run:android --device "${avdName}" --port ${port}`;
114
116
  }
115
117
  return `RCT_METRO_PORT=${port} npx react-native run-android --deviceId ${serial}`;
116
118
  }
@@ -35,37 +35,45 @@ export function nextConsolePort(claimedPorts) {
35
35
  return max + 2; // emulator console ports are even
36
36
  }
37
37
 
38
- export function selectAndroidDevice({ existingAvd, existingConsolePort, claimedAvds, claimedConsolePorts }) {
38
+ // Build the full candidate list: every AVD on disk, paired with whether it
39
+ // currently has a running emulator and (if so) on which console port.
40
+ // Emulators that fail to respond to `adb emu avd name` are dropped from
41
+ // the running map. Returns [] when no AVDs are installed.
42
+ export function enumerateAndroidCandidates() {
39
43
  const avds = listAvds();
40
- if (avds.length === 0) return { kind: 'noAvd' };
44
+ if (avds.length === 0) return [];
41
45
 
42
46
  const adbDevices = listAdbDevices();
43
- // Resolve which AVD each running emulator is. Emulators that don't respond
44
- // to `adb emu avd name` are dropped from the running map.
45
47
  const runningByAvd = {};
46
48
  for (const e of adbDevices.emulators) {
47
49
  const avdName = getAvdNameForSerial(e.serial);
48
50
  if (avdName) runningByAvd[avdName] = e.consolePort;
49
51
  }
50
52
 
51
- const buildCandidate = (avdName) => ({
53
+ return avds.map(avdName => ({
52
54
  avdName,
53
55
  isRunning: avdName in runningByAvd,
54
56
  consolePort: runningByAvd[avdName] ?? null,
55
- });
57
+ }));
58
+ }
56
59
 
57
- if (existingAvd && avds.includes(existingAvd)) {
58
- const runningPort = runningByAvd[existingAvd];
59
- return {
60
- kind: 'reuse',
61
- avdName: existingAvd,
62
- consolePort: runningPort ?? existingConsolePort ?? nextConsolePort(claimedConsolePorts),
63
- isRunning: runningPort != null,
64
- };
60
+ export function selectAndroidDevice({ existingAvd, existingConsolePort, claimedAvds, claimedConsolePorts }) {
61
+ const all = enumerateAndroidCandidates();
62
+ if (all.length === 0) return { kind: 'noAvd' };
63
+
64
+ if (existingAvd) {
65
+ const found = all.find(c => c.avdName === existingAvd);
66
+ if (found) {
67
+ return {
68
+ kind: 'reuse',
69
+ avdName: existingAvd,
70
+ consolePort: found.consolePort ?? existingConsolePort ?? nextConsolePort(claimedConsolePorts),
71
+ isRunning: found.isRunning,
72
+ };
73
+ }
65
74
  }
66
75
 
67
76
  const claimedAvdSet = new Set(claimedAvds);
68
- const all = avds.map(buildCandidate);
69
77
  const unclaimed = all.filter(c => !claimedAvdSet.has(c.avdName));
70
78
 
71
79
  if (unclaimed.length === 0) {
package/src/sim/ios.js CHANGED
@@ -31,6 +31,16 @@ export function listBootedIosSims() {
31
31
  return listAllIosSims().filter(s => s.state === 'Booted');
32
32
  }
33
33
 
34
+ // "iPhone 16 Pro (ABC-123-...)" if simctl knows about the UDID; the bare
35
+ // UDID otherwise (deleted sim, or simctl unavailable).
36
+ export function formatIosLabel(udid) {
37
+ try {
38
+ const sim = listAllIosSims().find(s => s.udid === udid);
39
+ if (sim) return `${sim.name} (${udid})`;
40
+ } catch { /* simctl not available */ }
41
+ return udid;
42
+ }
43
+
34
44
  export function deviceFamilyRank(name) {
35
45
  if (/^iPhone/i.test(name)) return 0;
36
46
  if (/^iPad/i.test(name)) return 1;
@@ -62,7 +72,7 @@ export function selectIosDevice({ existingUdid, claimedUdids, usage = {} }) {
62
72
  if (existingUdid) {
63
73
  const found = sims.find(s => s.udid === existingUdid);
64
74
  if (found) {
65
- return { kind: 'reuse', udid: found.udid, state: found.state };
75
+ return { kind: 'reuse', udid: found.udid, name: found.name, state: found.state };
66
76
  }
67
77
  }
68
78