rn-iso 0.2.1 → 0.3.2
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 +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +1 -1
- package/src/commands/android.js +31 -16
- package/src/commands/ios.js +6 -5
- package/src/commands/release.js +53 -10
- package/src/commands/status.js +20 -5
- package/src/commands/stop.js +6 -16
- package/src/config.js +20 -3
- package/src/metro.js +7 -0
- package/src/sim/android.js +23 -15
- package/src/sim/ios.js +11 -1
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ All commands below take the same `npx rn-iso` prefix.
|
|
|
47
47
|
| `status` | Show all projects' state |
|
|
48
48
|
| `reserve [ios\|android]` | Lock a manually-started sim/emulator to the current project (no build) |
|
|
49
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
|
|
50
|
+
| `release [<port>\|<shortcut>\|<path>] [--platform <p>] [--shutdown]` | Free a project's assignment. Target can be a Metro port (`8083`), a shortcut (label or unique basename), or an absolute path. `--shutdown` also stops the sim. |
|
|
51
51
|
|
|
52
52
|
## How it works
|
|
53
53
|
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -82,7 +82,7 @@ Reserve binds the sim to the current project the same way `ios` does, but skips
|
|
|
82
82
|
- `npx rn-iso status` — show all projects, their assignments, and Metro state.
|
|
83
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
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.
|
|
85
|
+
- `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.
|
|
86
86
|
|
|
87
87
|
### Project shortcuts (--label)
|
|
88
88
|
|
package/src/commands/android.js
CHANGED
|
@@ -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
|
|
81
|
+
console.log(chalk.dim(`Reusing running ${avdName} (emulator-${consolePort})`));
|
|
81
82
|
} else {
|
|
82
|
-
console.log(chalk.dim(`Booting assigned
|
|
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
|
-
|
|
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} (
|
|
98
|
-
: chalk.dim(`Booting ${avdName}
|
|
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
|
-
|
|
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} (
|
|
119
|
-
: chalk.dim(`Booting ${avdName}
|
|
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).'
|
|
@@ -161,20 +162,34 @@ export default function androidCommand(program) {
|
|
|
161
162
|
});
|
|
162
163
|
}
|
|
163
164
|
|
|
164
|
-
console.log(chalk.green(`\nAndroid ready on ${serial}, Metro port ${proj.metroPort}`));
|
|
165
|
+
console.log(chalk.green(`\nAndroid ready on ${avdName} (${serial}), Metro port ${proj.metroPort}`));
|
|
165
166
|
});
|
|
166
167
|
}
|
|
167
168
|
|
|
168
|
-
async function pickAvd({ candidates,
|
|
169
|
-
|
|
169
|
+
async function pickAvd({ candidates, androidClaimsByAvd = {}, allClaimed = false }) {
|
|
170
|
+
// Show every AVD on disk (parallel to the iOS picker), so the user can
|
|
171
|
+
// see what's claimed and optionally take it over. `candidates` is the
|
|
172
|
+
// unclaimed set passed in by selectAndroidDevice; AVDs outside it are
|
|
173
|
+
// claimed and will require a confirm prompt on selection.
|
|
174
|
+
const allAvds = enumerateAndroidCandidates();
|
|
175
|
+
const candidateAvds = new Set(candidates.map(c => c.avdName));
|
|
176
|
+
const sorted = sortAndroidCandidates(allAvds);
|
|
177
|
+
|
|
170
178
|
const nameWidth = Math.max(...sorted.map(c => c.avdName.length), 18);
|
|
171
179
|
const choices = sorted.map(c => {
|
|
172
|
-
const claim =
|
|
173
|
-
const
|
|
174
|
-
const runTag = c.isRunning ? chalk.green(`
|
|
180
|
+
const claim = androidClaimsByAvd[c.avdName];
|
|
181
|
+
const isCandidate = candidateAvds.has(c.avdName);
|
|
182
|
+
const runTag = c.isRunning ? chalk.green(` [emulator-${c.consolePort}, running]`) : '';
|
|
183
|
+
if (claim || !isCandidate) {
|
|
184
|
+
const tag = claim ? chalk.yellow(` [claimed by ${claim.label}]`) : '';
|
|
185
|
+
return {
|
|
186
|
+
title: chalk.yellow(`${c.avdName.padEnd(nameWidth)}${tag}${runTag}`),
|
|
187
|
+
value: { c, claim: claim || null },
|
|
188
|
+
};
|
|
189
|
+
}
|
|
175
190
|
return {
|
|
176
|
-
title: `${c.avdName.padEnd(nameWidth)}${runTag}
|
|
177
|
-
value: { c, claim:
|
|
191
|
+
title: `${c.avdName.padEnd(nameWidth)}${runTag}`,
|
|
192
|
+
value: { c, claim: null },
|
|
178
193
|
};
|
|
179
194
|
});
|
|
180
195
|
const message = allClaimed
|
package/src/commands/ios.js
CHANGED
|
@@ -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 ${
|
|
75
|
+
console.log(chalk.dim(`Booting assigned sim ${label}...`));
|
|
75
76
|
bootIosSim(udid);
|
|
76
77
|
} else {
|
|
77
|
-
console.log(chalk.dim(`Reusing assigned sim ${
|
|
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
|
|
package/src/commands/release.js
CHANGED
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
// src/commands/release.js
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import prompts from 'prompts';
|
|
3
4
|
import { resolveRegisteredProject } from '../project.js';
|
|
4
|
-
import { getProject, clearDevice } from '../config.js';
|
|
5
|
-
import {
|
|
5
|
+
import { getProject, clearDevice, findProjectByMetroPort } from '../config.js';
|
|
6
|
+
import { findPidListeningOnPort } from '../metro.js';
|
|
7
|
+
import { shutdownIosSim, formatIosLabel } from '../sim/ios.js';
|
|
6
8
|
import { shutdownAndroidEmulator } from '../sim/android.js';
|
|
7
9
|
|
|
8
10
|
export default function releaseCommand(program) {
|
|
9
11
|
program
|
|
10
12
|
.command('release [target]')
|
|
11
|
-
.description('Free a project assignment. [target] is an absolute
|
|
13
|
+
.description('Free a project assignment. [target] is a Metro port (e.g. 8083), a project shortcut (label or unique basename), or an absolute path. Defaults to the current project.')
|
|
12
14
|
.option('--platform <platform>', 'ios or android (default: both)')
|
|
13
15
|
.option('--shutdown', 'Also shut down the simulator/emulator after releasing')
|
|
14
|
-
.action((target, opts) => {
|
|
15
|
-
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
.action(async (target, opts) => {
|
|
17
|
+
let found;
|
|
18
|
+
if (target && /^\d+$/.test(target)) {
|
|
19
|
+
const port = parseInt(target, 10);
|
|
20
|
+
found = findProjectByMetroPort(port);
|
|
21
|
+
if (!found) {
|
|
22
|
+
await handleUnmatchedPort(port);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
const result = resolveRegisteredProject(target);
|
|
27
|
+
if (!result.found) {
|
|
28
|
+
console.error(chalk.red(result.error));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
found = result.found;
|
|
19
32
|
}
|
|
20
33
|
const proj = getProject(found);
|
|
21
34
|
if (!proj) {
|
|
@@ -32,10 +45,10 @@ export default function releaseCommand(program) {
|
|
|
32
45
|
if (opts.shutdown) {
|
|
33
46
|
if (p === 'ios') {
|
|
34
47
|
shutdownIosSim(entry.deviceUdid);
|
|
35
|
-
console.log(chalk.green(`Shut down iOS sim ${entry.deviceUdid}`));
|
|
48
|
+
console.log(chalk.green(`Shut down iOS sim ${formatIosLabel(entry.deviceUdid)}`));
|
|
36
49
|
} else {
|
|
37
50
|
shutdownAndroidEmulator(`emulator-${entry.consolePort}`);
|
|
38
|
-
console.log(chalk.green(`Shut down emulator-${entry.consolePort}`));
|
|
51
|
+
console.log(chalk.green(`Shut down ${entry.avdName} (emulator-${entry.consolePort})`));
|
|
39
52
|
}
|
|
40
53
|
}
|
|
41
54
|
clearDevice(found, p);
|
|
@@ -43,3 +56,33 @@ export default function releaseCommand(program) {
|
|
|
43
56
|
}
|
|
44
57
|
});
|
|
45
58
|
}
|
|
59
|
+
|
|
60
|
+
async function handleUnmatchedPort(port) {
|
|
61
|
+
const pid = findPidListeningOnPort(port);
|
|
62
|
+
if (!pid) {
|
|
63
|
+
console.error(chalk.red(`No registered project has Metro port ${port}, and nothing is listening on that port.`));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
console.log(chalk.dim(`No registered project has Metro port ${port}, but pid ${pid} is listening.`));
|
|
67
|
+
if (!process.stdin.isTTY) {
|
|
68
|
+
console.error(chalk.red('Refusing to kill an unrecognized process under non-TTY (no way to confirm).'));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
const ok = await prompts({
|
|
72
|
+
type: 'confirm',
|
|
73
|
+
name: 'ok',
|
|
74
|
+
message: `Kill pid ${pid} on port ${port}?`,
|
|
75
|
+
initial: false,
|
|
76
|
+
});
|
|
77
|
+
if (!ok.ok) {
|
|
78
|
+
console.error(chalk.red('Cancelled.'));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
process.kill(pid, 'SIGTERM');
|
|
83
|
+
console.log(chalk.green(`Killed pid ${pid} on port ${port}.`));
|
|
84
|
+
} catch (e) {
|
|
85
|
+
console.error(chalk.red(`Could not kill pid ${pid}: ${e.message}`));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/commands/status.js
CHANGED
|
@@ -3,7 +3,8 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { loadConfig } from '../config.js';
|
|
4
4
|
import { isMetroRunning } from '../ports.js';
|
|
5
5
|
import { isPidAlive } from '../metro.js';
|
|
6
|
-
import { findProjectRoot } from '../project.js';
|
|
6
|
+
import { findProjectRoot, projectShortcut } from '../project.js';
|
|
7
|
+
import { listAllIosSims } from '../sim/ios.js';
|
|
7
8
|
|
|
8
9
|
export default function statusCommand(program) {
|
|
9
10
|
program
|
|
@@ -19,11 +20,20 @@ export default function statusCommand(program) {
|
|
|
19
20
|
|
|
20
21
|
const cwdRoot = findProjectRoot(process.cwd());
|
|
21
22
|
|
|
23
|
+
// Build a UDID -> sim name map so we can show "iPhone 16 Pro (UDID)"
|
|
24
|
+
// rather than a raw UDID. Tolerate missing simctl (Linux dev box, etc.).
|
|
25
|
+
let iosNameByUdid = {};
|
|
26
|
+
try {
|
|
27
|
+
for (const sim of listAllIosSims()) iosNameByUdid[sim.udid] = sim.name;
|
|
28
|
+
} catch { /* no simctl available; we'll just print the udid */ }
|
|
29
|
+
|
|
22
30
|
for (const [path, proj] of Object.entries(cfg?.projects || {})) {
|
|
23
31
|
const isCurrent = path === cwdRoot;
|
|
24
|
-
const
|
|
32
|
+
const shortcut = projectShortcut(path, proj);
|
|
33
|
+
const headerText = `${shortcut} ${chalk.dim(`(${path})`)}`;
|
|
34
|
+
const header = isCurrent ? chalk.bold.cyan(`* ${shortcut}`) + ' ' + chalk.dim(`(${path})`) : headerText;
|
|
25
35
|
console.log('\n' + header);
|
|
26
|
-
console.log(chalk.dim(` app: ${proj.bundleId} (${proj.isExpo ? 'expo' : 'bare'})`));
|
|
36
|
+
console.log(chalk.dim(` app: ${proj.bundleId ?? '?'} (${proj.isExpo ? 'expo' : 'bare'})`));
|
|
27
37
|
|
|
28
38
|
if (proj.metroPort) {
|
|
29
39
|
const running = await isMetroRunning(proj.metroPort);
|
|
@@ -37,9 +47,14 @@ export default function statusCommand(program) {
|
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
const ios = proj.platforms?.ios;
|
|
40
|
-
if (ios)
|
|
50
|
+
if (ios) {
|
|
51
|
+
const name = iosNameByUdid[ios.deviceUdid] || chalk.dim('(unknown sim)');
|
|
52
|
+
console.log(` ios: ${chalk.cyan(name)} ${chalk.dim(`(${ios.deviceUdid})`)}`);
|
|
53
|
+
}
|
|
41
54
|
const android = proj.platforms?.android;
|
|
42
|
-
if (android)
|
|
55
|
+
if (android) {
|
|
56
|
+
console.log(` android: ${chalk.cyan(android.avdName)} ${chalk.dim(`(emulator-${android.consolePort})`)}`);
|
|
57
|
+
}
|
|
43
58
|
}
|
|
44
59
|
console.log('');
|
|
45
60
|
});
|
package/src/commands/stop.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// src/commands/stop.js
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { resolveRegisteredProject } from '../project.js';
|
|
4
|
-
import {
|
|
5
|
-
import { killMetroByPid } from '../metro.js';
|
|
6
|
-
import { getExecutor } from '../exec.js';
|
|
4
|
+
import { getProject, setMetro, findProjectByMetroPort } from '../config.js';
|
|
5
|
+
import { killMetroByPid, findPidListeningOnPort } from '../metro.js';
|
|
7
6
|
|
|
8
7
|
export default function stopCommand(program) {
|
|
9
8
|
program
|
|
@@ -65,19 +64,10 @@ function killByPort(port) {
|
|
|
65
64
|
console.log(chalk.green(`Killed pid ${pid} on port ${port}`));
|
|
66
65
|
|
|
67
66
|
// If a project owned this port, clear its recorded pid so `status` reflects.
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
console.log(chalk.dim(`Cleared metroPid for ${path}`));
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
67
|
+
const owner = findProjectByMetroPort(port);
|
|
68
|
+
if (owner) {
|
|
69
|
+
setMetro(owner, port, null);
|
|
70
|
+
console.log(chalk.dim(`Cleared metroPid for ${owner}`));
|
|
75
71
|
}
|
|
76
72
|
}
|
|
77
73
|
|
|
78
|
-
function findPidListeningOnPort(port) {
|
|
79
|
-
const out = getExecutor().runQuiet(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`);
|
|
80
|
-
if (!out) return null;
|
|
81
|
-
const pid = parseInt(out.split('\n')[0], 10);
|
|
82
|
-
return Number.isFinite(pid) ? pid : null;
|
|
83
|
-
}
|
package/src/config.js
CHANGED
|
@@ -96,6 +96,14 @@ export function allMetroPorts() {
|
|
|
96
96
|
.filter(p => typeof p === 'number');
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
export function findProjectByMetroPort(port) {
|
|
100
|
+
const cfg = loadConfig();
|
|
101
|
+
for (const [path, proj] of Object.entries(cfg?.projects || {})) {
|
|
102
|
+
if (proj.metroPort === port) return path;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
99
107
|
export function allClaimedDevices() {
|
|
100
108
|
const cfg = loadConfig();
|
|
101
109
|
const result = {
|
|
@@ -103,10 +111,12 @@ export function allClaimedDevices() {
|
|
|
103
111
|
androidAvds: [],
|
|
104
112
|
androidConsolePorts: [],
|
|
105
113
|
// iosClaims: udid -> { label, path }. androidClaims: consolePort ->
|
|
106
|
-
// { label, path, avdName }.
|
|
107
|
-
//
|
|
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.
|
|
108
117
|
iosClaims: {},
|
|
109
118
|
androidClaims: {},
|
|
119
|
+
androidClaimsByAvd: {},
|
|
110
120
|
};
|
|
111
121
|
if (!cfg) return result;
|
|
112
122
|
for (const [path, proj] of Object.entries(cfg.projects || {})) {
|
|
@@ -117,7 +127,14 @@ export function allClaimedDevices() {
|
|
|
117
127
|
result.iosClaims[ios.deviceUdid] = { label, path };
|
|
118
128
|
}
|
|
119
129
|
const android = proj.platforms?.android;
|
|
120
|
-
if (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
|
+
}
|
|
121
138
|
if (typeof android?.consolePort === 'number') {
|
|
122
139
|
result.androidConsolePorts.push(android.consolePort);
|
|
123
140
|
result.androidClaims[android.consolePort] = {
|
package/src/metro.js
CHANGED
|
@@ -52,6 +52,13 @@ export function killMetroByPid(pid) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
export function findPidListeningOnPort(port) {
|
|
56
|
+
const out = getExecutor().runQuiet(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`);
|
|
57
|
+
if (!out) return null;
|
|
58
|
+
const pid = parseInt(out.split('\n')[0], 10);
|
|
59
|
+
return Number.isFinite(pid) ? pid : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
export function isPidAlive(pid) {
|
|
56
63
|
if (!pid) return false;
|
|
57
64
|
try {
|
package/src/sim/android.js
CHANGED
|
@@ -35,37 +35,45 @@ export function nextConsolePort(claimedPorts) {
|
|
|
35
35
|
return max + 2; // emulator console ports are even
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|