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/src/commands/ios.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import prompts from 'prompts';
|
|
4
4
|
import { findProjectRoot, detectIsExpo, detectBundleId, detectAndroidPackage } from '../project.js';
|
|
5
|
-
import { getProject, upsertProject, setMetro, setDevice, allClaimedDevices, recordSimUsage, getSimUsage } from '../config.js';
|
|
5
|
+
import { getProject, upsertProject, setMetro, setDevice, clearDevice, allClaimedDevices, recordSimUsage, getSimUsage } from '../config.js';
|
|
6
6
|
import { allocatePort, isMetroRunning } from '../ports.js';
|
|
7
7
|
import { selectIosDevice, bootIosSim, listIosRuntimes, createIosSim, parseRuntimeVersion, listAllIosSims, sortSims } from '../sim/ios.js';
|
|
8
8
|
import { buildIosCommand, detectPackageManager } from '../runner.js';
|
|
9
9
|
import { getExecutor } from '../exec.js';
|
|
10
|
+
import { resolveLabel } from '../labels.js';
|
|
10
11
|
|
|
11
12
|
export default function iosCommand(program) {
|
|
12
13
|
program
|
|
@@ -14,7 +15,8 @@ export default function iosCommand(program) {
|
|
|
14
15
|
.description('Ensure a dedicated iOS simulator + Metro server for the current project; build/install if needed')
|
|
15
16
|
.option('--device-type <name>', 'Explicit opt-in: create a NEW sim of this device type (e.g. "iPhone 17 Pro")')
|
|
16
17
|
.option('--runtime <version>', 'iOS runtime version when creating a new sim (e.g. "26.2"); defaults to latest')
|
|
17
|
-
.option('--auto', 'Non-interactive: pick the first unclaimed sim without prompting')
|
|
18
|
+
.option('--auto', 'Non-interactive: pick the first unclaimed sim without prompting (also implied when stdin is not a TTY)')
|
|
19
|
+
.option('--label <name>', 'Optional shortcut name; refer to the project as <name> in stop / release / etc.')
|
|
18
20
|
.option('--script <name>', 'package.json script to invoke for build/install (default: ios)', 'ios')
|
|
19
21
|
.option('--no-script', 'Skip the package.json script lookup; run expo/react-native CLI directly')
|
|
20
22
|
.option('--pm <name>', 'Package manager: npm, yarn, pnpm, bun (default: detected from lockfile)')
|
|
@@ -26,11 +28,21 @@ export default function iosCommand(program) {
|
|
|
26
28
|
process.exit(1);
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
// Treat non-TTY environments (agents, CI) as if --auto was passed.
|
|
32
|
+
const auto = opts.auto || !process.stdin.isTTY;
|
|
33
|
+
|
|
29
34
|
const bundleId = detectBundleId(root);
|
|
30
35
|
const androidPackage = detectAndroidPackage(root);
|
|
31
36
|
const isExpo = detectIsExpo(root);
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
const existing = getProject(root);
|
|
39
|
+
const label = await resolveLabel({ root, existingProject: existing, optsLabel: opts.label });
|
|
40
|
+
upsertProject(root, {
|
|
41
|
+
bundleId,
|
|
42
|
+
androidPackage,
|
|
43
|
+
isExpo,
|
|
44
|
+
...(label ? { label } : {}),
|
|
45
|
+
});
|
|
34
46
|
let proj = getProject(root);
|
|
35
47
|
|
|
36
48
|
if (!proj.metroPort) {
|
|
@@ -65,28 +77,59 @@ export default function iosCommand(program) {
|
|
|
65
77
|
console.log(chalk.dim(`Reusing assigned sim ${udid} (already booted)`));
|
|
66
78
|
}
|
|
67
79
|
} else if (selection.kind === 'allocate') {
|
|
68
|
-
const picked = (selection.candidates.length === 1 ||
|
|
69
|
-
? selection.candidates[0]
|
|
70
|
-
: await pickSim(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
80
|
+
const picked = (selection.candidates.length === 1 || auto)
|
|
81
|
+
? { sim: selection.candidates[0], prevClaim: null }
|
|
82
|
+
: await pickSim({
|
|
83
|
+
candidates: selection.candidates,
|
|
84
|
+
iosClaims: claimedDevices.iosClaims,
|
|
85
|
+
usage,
|
|
86
|
+
});
|
|
87
|
+
udid = picked.sim.udid;
|
|
88
|
+
releasePriorClaim(picked.prevClaim);
|
|
89
|
+
if (picked.sim.state !== 'Booted') {
|
|
90
|
+
console.log(chalk.dim(`Booting ${picked.sim.name} (${udid})...`));
|
|
74
91
|
bootIosSim(udid);
|
|
75
92
|
} else {
|
|
76
|
-
console.log(chalk.green(`Assigned ${picked.name} (${udid}, booted)`));
|
|
93
|
+
console.log(chalk.green(`Assigned ${picked.sim.name} (${udid}, booted)`));
|
|
94
|
+
}
|
|
95
|
+
} else if (selection.kind === 'allClaimed') {
|
|
96
|
+
// Sims exist but every one is claimed by another project. With
|
|
97
|
+
// --auto, refuse rather than silently stealing. Interactive: show
|
|
98
|
+
// picker so the user can confirm-steal one.
|
|
99
|
+
if (auto) {
|
|
100
|
+
if (opts.deviceType) {
|
|
101
|
+
udid = createNewSim({ deviceType: opts.deviceType, runtimeVersion: opts.runtime });
|
|
102
|
+
console.log(chalk.green(`Created and booted new sim ${udid}`));
|
|
103
|
+
} else {
|
|
104
|
+
console.error(chalk.red('All iOS simulators are claimed by other rn-iso projects.'));
|
|
105
|
+
console.error(chalk.dim('Re-run without --auto to confirm taking one over, or pass --device-type to create a new sim.'));
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
const picked = await pickSim({
|
|
110
|
+
candidates: [],
|
|
111
|
+
iosClaims: claimedDevices.iosClaims,
|
|
112
|
+
usage,
|
|
113
|
+
allClaimed: true,
|
|
114
|
+
});
|
|
115
|
+
udid = picked.sim.udid;
|
|
116
|
+
releasePriorClaim(picked.prevClaim);
|
|
117
|
+
if (picked.sim.state !== 'Booted') {
|
|
118
|
+
console.log(chalk.dim(`Booting ${picked.sim.name} (${udid})...`));
|
|
119
|
+
bootIosSim(udid);
|
|
120
|
+
} else {
|
|
121
|
+
console.log(chalk.green(`Took over ${picked.sim.name} (${udid}, booted)`));
|
|
122
|
+
}
|
|
77
123
|
}
|
|
78
124
|
} else {
|
|
79
|
-
//
|
|
125
|
+
// noSims: no iOS simulators exist on this machine at all.
|
|
80
126
|
if (opts.deviceType) {
|
|
81
127
|
udid = createNewSim({ deviceType: opts.deviceType, runtimeVersion: opts.runtime });
|
|
82
128
|
console.log(chalk.green(`Created and booted new sim ${udid}`));
|
|
83
129
|
} else {
|
|
84
|
-
console.error(chalk.red('No
|
|
85
|
-
console.error(chalk.dim('
|
|
86
|
-
console.error(chalk.dim('
|
|
87
|
-
console.error(chalk.dim(' - `rn-iso unreserve --all` if you have stale reservations'));
|
|
88
|
-
console.error(chalk.dim(' - Free another rn-iso project (`rn-iso release` from there)'));
|
|
89
|
-
console.error(chalk.dim(' - Pass --device-type "iPhone 17 Pro" [--runtime 26.2] to create a new sim'));
|
|
130
|
+
console.error(chalk.red('No iOS simulators found.'));
|
|
131
|
+
console.error(chalk.dim('Pass --device-type "iPhone 17 Pro" [--runtime 26.2] to create one,'));
|
|
132
|
+
console.error(chalk.dim('or install a simulator runtime via Xcode.'));
|
|
90
133
|
process.exit(1);
|
|
91
134
|
}
|
|
92
135
|
}
|
|
@@ -125,10 +168,10 @@ export default function iosCommand(program) {
|
|
|
125
168
|
});
|
|
126
169
|
}
|
|
127
170
|
|
|
128
|
-
async function pickSim(candidates, iosClaims = {}, usage = {}) {
|
|
129
|
-
// Show ALL sims
|
|
130
|
-
//
|
|
131
|
-
//
|
|
171
|
+
async function pickSim({ candidates, iosClaims = {}, usage = {}, allClaimed = false }) {
|
|
172
|
+
// Show ALL sims so the user has full context. Unclaimed candidates pick
|
|
173
|
+
// immediately; claimed sims are selectable too but require a confirm
|
|
174
|
+
// prompt so the steal is intentional.
|
|
132
175
|
const allSims = listAllIosSims();
|
|
133
176
|
const candidateUdids = new Set(candidates.map(s => s.udid));
|
|
134
177
|
const sorted = sortSims(allSims, usage);
|
|
@@ -140,37 +183,56 @@ async function pickSim(candidates, iosClaims = {}, usage = {}) {
|
|
|
140
183
|
const versionPart = version.padStart(6);
|
|
141
184
|
const claim = iosClaims[s.udid];
|
|
142
185
|
if (claim) {
|
|
143
|
-
const
|
|
144
|
-
? `[claimed by ${claim.label}]`
|
|
145
|
-
: `[reserved: ${claim.label}]`;
|
|
186
|
+
const stateTag = s.state === 'Booted' ? chalk.green(' [booted]') : '';
|
|
146
187
|
return {
|
|
147
|
-
title: chalk.
|
|
148
|
-
value:
|
|
149
|
-
disabled: true,
|
|
188
|
+
title: chalk.yellow(`${namePart} ${versionPart} [claimed by ${claim.label}]${stateTag}`),
|
|
189
|
+
value: { sim: s, claim },
|
|
150
190
|
};
|
|
151
191
|
}
|
|
152
192
|
const stateTag = s.state === 'Booted' ? chalk.green(' [booted]') : '';
|
|
153
193
|
const isCandidate = candidateUdids.has(s.udid);
|
|
154
194
|
if (!isCandidate) {
|
|
155
|
-
// Shouldn't happen with current selectIosDevice, but be safe.
|
|
156
195
|
return { title: chalk.dim(`${namePart} ${versionPart}`), value: null, disabled: true };
|
|
157
196
|
}
|
|
158
197
|
return {
|
|
159
198
|
title: `${namePart} ${chalk.dim(versionPart)}${stateTag}`,
|
|
160
|
-
value: s,
|
|
199
|
+
value: { sim: s, claim: null },
|
|
161
200
|
};
|
|
162
201
|
});
|
|
202
|
+
const message = allClaimed
|
|
203
|
+
? 'All sims are claimed. Pick one to take over:'
|
|
204
|
+
: 'Pick a simulator (claimed sims will prompt to confirm):';
|
|
163
205
|
const answer = await prompts({
|
|
164
206
|
type: 'select',
|
|
165
|
-
name: '
|
|
166
|
-
message
|
|
207
|
+
name: 'pick',
|
|
208
|
+
message,
|
|
167
209
|
choices,
|
|
168
210
|
});
|
|
169
|
-
if (!answer.
|
|
211
|
+
if (!answer.pick) {
|
|
170
212
|
console.error(chalk.red('Cancelled.'));
|
|
171
213
|
process.exit(1);
|
|
172
214
|
}
|
|
173
|
-
|
|
215
|
+
const { sim, claim } = answer.pick;
|
|
216
|
+
if (claim) {
|
|
217
|
+
const confirm = await prompts({
|
|
218
|
+
type: 'confirm',
|
|
219
|
+
name: 'ok',
|
|
220
|
+
message: `${sim.name} is currently held by project "${claim.label}". Take it over?`,
|
|
221
|
+
initial: false,
|
|
222
|
+
});
|
|
223
|
+
if (!confirm.ok) {
|
|
224
|
+
console.error(chalk.red('Cancelled.'));
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
return { sim, prevClaim: claim };
|
|
228
|
+
}
|
|
229
|
+
return { sim, prevClaim: null };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function releasePriorClaim(prevClaim) {
|
|
233
|
+
if (!prevClaim?.path) return;
|
|
234
|
+
clearDevice(prevClaim.path, 'ios');
|
|
235
|
+
console.log(chalk.dim(`Released prior assignment from "${prevClaim.label}"`));
|
|
174
236
|
}
|
|
175
237
|
|
|
176
238
|
function createNewSim({ deviceType, runtimeVersion }) {
|
package/src/commands/release.js
CHANGED
|
@@ -1,36 +1,20 @@
|
|
|
1
1
|
// src/commands/release.js
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { resolveRegisteredProject } from '../project.js';
|
|
4
|
-
import { getProject, clearDevice
|
|
4
|
+
import { getProject, clearDevice } from '../config.js';
|
|
5
|
+
import { shutdownIosSim } from '../sim/ios.js';
|
|
6
|
+
import { shutdownAndroidEmulator } from '../sim/android.js';
|
|
5
7
|
|
|
6
8
|
export default function releaseCommand(program) {
|
|
7
9
|
program
|
|
8
10
|
.command('release [target]')
|
|
9
|
-
.description('Free a project assignment
|
|
11
|
+
.description('Free a project assignment. [target] is an absolute project path; defaults to the current project.')
|
|
10
12
|
.option('--platform <platform>', 'ios or android (default: both)')
|
|
13
|
+
.option('--shutdown', 'Also shut down the simulator/emulator after releasing')
|
|
11
14
|
.action((target, opts) => {
|
|
12
|
-
// 1. If a target was given, try it as a reservation label / id first.
|
|
13
|
-
if (target) {
|
|
14
|
-
const matches = findReservations(target, opts.platform);
|
|
15
|
-
if (matches.length > 0) {
|
|
16
|
-
for (const m of matches) {
|
|
17
|
-
removeReservation(m.platform, m.id);
|
|
18
|
-
console.log(chalk.green(
|
|
19
|
-
`Released ${m.platform} reservation ${m.id}` +
|
|
20
|
-
(m.label ? ` (${m.label})` : '')
|
|
21
|
-
));
|
|
22
|
-
}
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// 2. Otherwise treat the target as a project path (or default to cwd).
|
|
28
15
|
const { found, error } = resolveRegisteredProject(target);
|
|
29
16
|
if (!found) {
|
|
30
17
|
console.error(chalk.red(error));
|
|
31
|
-
if (target) {
|
|
32
|
-
console.error(chalk.dim('(Also tried as reservation label/id; no match.)'));
|
|
33
|
-
}
|
|
34
18
|
process.exit(1);
|
|
35
19
|
}
|
|
36
20
|
const proj = getProject(found);
|
|
@@ -40,12 +24,22 @@ export default function releaseCommand(program) {
|
|
|
40
24
|
}
|
|
41
25
|
const platforms = opts.platform ? [opts.platform] : ['ios', 'android'];
|
|
42
26
|
for (const p of platforms) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
console.log(chalk.green(`Released ${p} assignment for ${found}.`));
|
|
46
|
-
} else {
|
|
27
|
+
const entry = proj.platforms?.[p];
|
|
28
|
+
if (!entry) {
|
|
47
29
|
console.log(chalk.dim(`No ${p} assignment to release for ${found}.`));
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (opts.shutdown) {
|
|
33
|
+
if (p === 'ios') {
|
|
34
|
+
shutdownIosSim(entry.deviceUdid);
|
|
35
|
+
console.log(chalk.green(`Shut down iOS sim ${entry.deviceUdid}`));
|
|
36
|
+
} else {
|
|
37
|
+
shutdownAndroidEmulator(`emulator-${entry.consolePort}`);
|
|
38
|
+
console.log(chalk.green(`Shut down emulator-${entry.consolePort}`));
|
|
39
|
+
}
|
|
48
40
|
}
|
|
41
|
+
clearDevice(found, p);
|
|
42
|
+
console.log(chalk.green(`Released ${p} assignment for ${found}.`));
|
|
49
43
|
}
|
|
50
44
|
});
|
|
51
45
|
}
|
package/src/commands/reserve.js
CHANGED
|
@@ -1,176 +1,173 @@
|
|
|
1
1
|
// src/commands/reserve.js
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import prompts from 'prompts';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { findProjectRoot, detectIsExpo, detectBundleId, detectAndroidPackage } from '../project.js';
|
|
5
|
+
import { getProject, upsertProject, setMetro, setDevice, clearDevice, allClaimedDevices } from '../config.js';
|
|
6
|
+
import { allocatePort } from '../ports.js';
|
|
7
|
+
import { listBootedIosSims, parseRuntimeVersion, sortSims } from '../sim/ios.js';
|
|
6
8
|
import { listAdbDevices, getAvdNameForSerial } from '../sim/android.js';
|
|
9
|
+
import { resolveLabel } from '../labels.js';
|
|
7
10
|
|
|
8
11
|
export default function reserveCommand(program) {
|
|
9
12
|
program
|
|
10
|
-
.command('reserve [platform]
|
|
11
|
-
.description('
|
|
12
|
-
.option('--label <
|
|
13
|
-
.
|
|
14
|
-
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Direct path: both platform and identifier provided.
|
|
21
|
-
if (platform && identifier) {
|
|
22
|
-
addOne(platform, identifier, opts.label);
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Interactive path: list running devices and multi-select.
|
|
27
|
-
if (platform && platform !== 'ios' && platform !== 'android') {
|
|
28
|
-
console.error(chalk.red(`Unknown platform: ${platform}. Use ios or android.`));
|
|
13
|
+
.command('reserve [platform]')
|
|
14
|
+
.description('Lock a manually-started sim/emulator to the current project (registers without building).')
|
|
15
|
+
.option('--label <name>', 'Optional shortcut name; refer to the project as <name> in stop / release / etc.')
|
|
16
|
+
.action(async (platform, opts) => {
|
|
17
|
+
const plat = platform || 'ios';
|
|
18
|
+
if (plat !== 'ios' && plat !== 'android') {
|
|
19
|
+
console.error(chalk.red(`Unknown platform: ${plat}. Use ios or android.`));
|
|
29
20
|
process.exit(1);
|
|
30
21
|
}
|
|
31
22
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const claimedIos = new Set(claimed.iosUdids);
|
|
36
|
-
const claimedAndroidPorts = new Set(claimed.androidConsolePorts);
|
|
37
|
-
|
|
38
|
-
const choices = [];
|
|
39
|
-
|
|
40
|
-
if (showIos) {
|
|
41
|
-
let iosSims = [];
|
|
42
|
-
try {
|
|
43
|
-
iosSims = listBootedIosSims();
|
|
44
|
-
} catch (e) {
|
|
45
|
-
console.error(chalk.dim(`(could not list iOS sims: ${e.message})`));
|
|
46
|
-
}
|
|
47
|
-
for (const sim of iosSims) {
|
|
48
|
-
const taken = claimedIos.has(sim.udid);
|
|
49
|
-
choices.push({
|
|
50
|
-
title: `${chalk.bold('ios')} ${sim.name.padEnd(22)} ${chalk.dim(sim.udid)}${taken ? chalk.yellow(' [reserved]') : ''}`,
|
|
51
|
-
value: { kind: 'ios', udid: sim.udid, name: sim.name },
|
|
52
|
-
disabled: taken,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (showAndroid) {
|
|
58
|
-
let emulators = [];
|
|
59
|
-
try {
|
|
60
|
-
emulators = listAdbDevices().emulators;
|
|
61
|
-
} catch (e) {
|
|
62
|
-
console.error(chalk.dim(`(could not list Android emulators: ${e.message})`));
|
|
63
|
-
}
|
|
64
|
-
for (const e of emulators) {
|
|
65
|
-
const taken = claimedAndroidPorts.has(e.consolePort);
|
|
66
|
-
const avdName = taken ? null : getAvdNameForSerial(e.serial);
|
|
67
|
-
const label = avdName ? `${avdName} on ${e.serial}` : e.serial;
|
|
68
|
-
choices.push({
|
|
69
|
-
title: `${chalk.bold('and')} ${label}${taken ? chalk.yellow(' [reserved]') : ''}`,
|
|
70
|
-
value: { kind: 'android', serial: e.serial, consolePort: e.consolePort, avdName },
|
|
71
|
-
disabled: taken,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (choices.length === 0) {
|
|
77
|
-
const what = !platform ? 'booted iOS sims or running Android emulators'
|
|
78
|
-
: platform === 'ios' ? 'booted iOS sims'
|
|
79
|
-
: 'running Android emulators';
|
|
80
|
-
console.error(chalk.red(`No ${what} found.`));
|
|
23
|
+
const root = findProjectRoot(process.cwd());
|
|
24
|
+
if (!root) {
|
|
25
|
+
console.error(chalk.red('Not in a React Native project (no package.json found).'));
|
|
81
26
|
process.exit(1);
|
|
82
27
|
}
|
|
83
28
|
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
29
|
+
const existing = getProject(root);
|
|
30
|
+
const label = await resolveLabel({ root, existingProject: existing, optsLabel: opts.label });
|
|
31
|
+
upsertProject(root, {
|
|
32
|
+
bundleId: detectBundleId(root),
|
|
33
|
+
androidPackage: detectAndroidPackage(root),
|
|
34
|
+
isExpo: detectIsExpo(root),
|
|
35
|
+
...(label ? { label } : {}),
|
|
91
36
|
});
|
|
92
|
-
|
|
93
|
-
if (!
|
|
94
|
-
|
|
95
|
-
|
|
37
|
+
let proj = getProject(root);
|
|
38
|
+
if (!proj.metroPort) {
|
|
39
|
+
const port = await allocatePort(root);
|
|
40
|
+
setMetro(root, port, null);
|
|
41
|
+
proj = getProject(root);
|
|
42
|
+
console.log(chalk.dim(`Allocated Metro port: ${port}`));
|
|
96
43
|
}
|
|
97
44
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
name: 'label',
|
|
103
|
-
message: 'Label (optional, e.g. "agent-1"):',
|
|
104
|
-
});
|
|
105
|
-
label = labelAnswer.label || undefined;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
for (const sel of answer.selected) {
|
|
109
|
-
if (sel.kind === 'ios') {
|
|
110
|
-
addReservation('ios', { udid: sel.udid, label });
|
|
111
|
-
console.log(chalk.green(`Reserved iOS ${sel.name} (${sel.udid})${label ? ` (${label})` : ''}`));
|
|
112
|
-
} else {
|
|
113
|
-
addReservation('android', {
|
|
114
|
-
serial: sel.serial,
|
|
115
|
-
consolePort: sel.consolePort,
|
|
116
|
-
avdName: sel.avdName,
|
|
117
|
-
label,
|
|
118
|
-
});
|
|
119
|
-
console.log(chalk.green(
|
|
120
|
-
`Reserved emulator ${sel.serial}` +
|
|
121
|
-
(sel.avdName ? ` (AVD: ${sel.avdName})` : '') +
|
|
122
|
-
(label ? ` (${label})` : '')
|
|
123
|
-
));
|
|
124
|
-
}
|
|
45
|
+
if (plat === 'ios') {
|
|
46
|
+
await reserveIos(root, proj);
|
|
47
|
+
} else {
|
|
48
|
+
await reserveAndroid(root, proj);
|
|
125
49
|
}
|
|
126
50
|
});
|
|
127
51
|
}
|
|
128
52
|
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
console.
|
|
133
|
-
|
|
53
|
+
async function reserveIos(root, proj) {
|
|
54
|
+
const booted = listBootedIosSims();
|
|
55
|
+
if (booted.length === 0) {
|
|
56
|
+
console.error(chalk.red('No booted iOS simulators found.'));
|
|
57
|
+
console.error(chalk.dim('Boot one (Simulator.app, Xcode, or `xcrun simctl boot`) and re-run.'));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const auto = !process.stdin.isTTY;
|
|
62
|
+
const claims = allClaimedDevices().iosClaims;
|
|
63
|
+
const sorted = sortSims(booted);
|
|
64
|
+
|
|
65
|
+
let pick;
|
|
66
|
+
if (sorted.length === 1 || auto) {
|
|
67
|
+
pick = sorted[0];
|
|
68
|
+
} else {
|
|
69
|
+
const nameWidth = Math.max(...sorted.map(s => s.name.length), 18);
|
|
70
|
+
const choices = sorted.map(s => {
|
|
71
|
+
const claim = claims[s.udid];
|
|
72
|
+
const tag = !claim ? ''
|
|
73
|
+
: claim.path === root ? chalk.dim(' [already yours]')
|
|
74
|
+
: chalk.yellow(` [claimed by ${claim.label}]`);
|
|
75
|
+
const title = `${s.name.padEnd(nameWidth)} ${chalk.dim(parseRuntimeVersion(s.runtime).padStart(6))}${tag}`;
|
|
76
|
+
return { title, value: s };
|
|
77
|
+
});
|
|
78
|
+
const answer = await prompts({
|
|
79
|
+
type: 'select',
|
|
80
|
+
name: 'sim',
|
|
81
|
+
message: 'Pick a booted simulator to lock to this project:',
|
|
82
|
+
choices,
|
|
83
|
+
});
|
|
84
|
+
if (!answer.sim) {
|
|
85
|
+
console.error(chalk.red('Cancelled.'));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
pick = answer.sim;
|
|
134
89
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
90
|
+
|
|
91
|
+
const claim = claims[pick.udid];
|
|
92
|
+
if (claim && claim.path !== root) {
|
|
93
|
+
if (auto) {
|
|
94
|
+
console.error(chalk.red(`${pick.name} is held by project "${claim.label}". Re-run interactively to confirm taking it over.`));
|
|
139
95
|
process.exit(1);
|
|
140
96
|
}
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
97
|
+
const ok = await prompts({
|
|
98
|
+
type: 'confirm',
|
|
99
|
+
name: 'ok',
|
|
100
|
+
message: `${pick.name} is currently held by project "${claim.label}". Take it over?`,
|
|
101
|
+
initial: false,
|
|
102
|
+
});
|
|
103
|
+
if (!ok.ok) {
|
|
104
|
+
console.error(chalk.red('Cancelled.'));
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
clearDevice(claim.path, 'ios');
|
|
108
|
+
console.log(chalk.dim(`Released prior assignment from "${claim.label}"`));
|
|
150
109
|
}
|
|
151
|
-
|
|
152
|
-
|
|
110
|
+
|
|
111
|
+
setDevice(root, 'ios', { deviceUdid: pick.udid });
|
|
112
|
+
console.log(chalk.green(`Locked iOS sim ${pick.name} (${pick.udid}) to ${root}`));
|
|
153
113
|
}
|
|
154
114
|
|
|
155
|
-
function
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return;
|
|
115
|
+
async function reserveAndroid(root, proj) {
|
|
116
|
+
const running = listAdbDevices().emulators;
|
|
117
|
+
if (running.length === 0) {
|
|
118
|
+
console.error(chalk.red('No running Android emulators found.'));
|
|
119
|
+
console.error(chalk.dim('Start one (Android Studio or `emulator -avd ...`) and re-run.'));
|
|
120
|
+
process.exit(1);
|
|
162
121
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
122
|
+
|
|
123
|
+
const auto = !process.stdin.isTTY;
|
|
124
|
+
const claims = allClaimedDevices().androidClaims;
|
|
125
|
+
|
|
126
|
+
let pick;
|
|
127
|
+
if (running.length === 1 || auto) {
|
|
128
|
+
pick = running[0];
|
|
129
|
+
} else {
|
|
130
|
+
const choices = running.map(e => {
|
|
131
|
+
const claim = claims[e.consolePort];
|
|
132
|
+
const tag = !claim ? ''
|
|
133
|
+
: claim.path === root ? chalk.dim(' [already yours]')
|
|
134
|
+
: chalk.yellow(` [claimed by ${claim.label}]`);
|
|
135
|
+
return { title: `${e.serial}${tag}`, value: e };
|
|
136
|
+
});
|
|
137
|
+
const answer = await prompts({
|
|
138
|
+
type: 'select',
|
|
139
|
+
name: 'emu',
|
|
140
|
+
message: 'Pick a running emulator to lock to this project:',
|
|
141
|
+
choices,
|
|
142
|
+
});
|
|
143
|
+
if (!answer.emu) {
|
|
144
|
+
console.error(chalk.red('Cancelled.'));
|
|
145
|
+
process.exit(1);
|
|
167
146
|
}
|
|
147
|
+
pick = answer.emu;
|
|
168
148
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
console.
|
|
149
|
+
|
|
150
|
+
const claim = claims[pick.consolePort];
|
|
151
|
+
if (claim && claim.path !== root) {
|
|
152
|
+
if (auto) {
|
|
153
|
+
console.error(chalk.red(`${pick.serial} is held by project "${claim.label}". Re-run interactively to confirm taking it over.`));
|
|
154
|
+
process.exit(1);
|
|
174
155
|
}
|
|
156
|
+
const ok = await prompts({
|
|
157
|
+
type: 'confirm',
|
|
158
|
+
name: 'ok',
|
|
159
|
+
message: `${pick.serial} is held by project "${claim.label}". Take it over?`,
|
|
160
|
+
initial: false,
|
|
161
|
+
});
|
|
162
|
+
if (!ok.ok) {
|
|
163
|
+
console.error(chalk.red('Cancelled.'));
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
clearDevice(claim.path, 'android');
|
|
167
|
+
console.log(chalk.dim(`Released prior assignment from "${claim.label}"`));
|
|
175
168
|
}
|
|
169
|
+
|
|
170
|
+
const avdName = getAvdNameForSerial(pick.serial) || `emulator-${pick.consolePort}`;
|
|
171
|
+
setDevice(root, 'android', { avdName, consolePort: pick.consolePort });
|
|
172
|
+
console.log(chalk.green(`Locked emulator ${pick.serial} (AVD: ${avdName}) to ${root}`));
|
|
176
173
|
}
|
package/src/commands/status.js
CHANGED
|
@@ -12,10 +12,7 @@ export default function statusCommand(program) {
|
|
|
12
12
|
.action(async () => {
|
|
13
13
|
const cfg = loadConfig();
|
|
14
14
|
const hasProjects = cfg && Object.keys(cfg.projects || {}).length > 0;
|
|
15
|
-
|
|
16
|
-
const hasReservations = (reservations.ios?.length || 0) + (reservations.android?.length || 0) > 0;
|
|
17
|
-
|
|
18
|
-
if (!hasProjects && !hasReservations) {
|
|
15
|
+
if (!hasProjects) {
|
|
19
16
|
console.log(chalk.dim('No projects registered.'));
|
|
20
17
|
return;
|
|
21
18
|
}
|
|
@@ -44,17 +41,6 @@ export default function statusCommand(program) {
|
|
|
44
41
|
const android = proj.platforms?.android;
|
|
45
42
|
if (android) console.log(` android: ${chalk.cyan(android.avdName)} on emulator-${android.consolePort}`);
|
|
46
43
|
}
|
|
47
|
-
|
|
48
|
-
if (hasReservations) {
|
|
49
|
-
console.log('\n' + chalk.bold('Reservations (external):'));
|
|
50
|
-
for (const r of reservations.ios || []) {
|
|
51
|
-
console.log(` ios: ${chalk.cyan(r.udid)}${r.label ? chalk.dim(` (${r.label})`) : ''}`);
|
|
52
|
-
}
|
|
53
|
-
for (const r of reservations.android || []) {
|
|
54
|
-
const tag = r.avdName ? `${r.avdName} on ${r.serial}` : r.serial;
|
|
55
|
-
console.log(` android: ${chalk.cyan(tag)}${r.label ? chalk.dim(` (${r.label})`) : ''}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
44
|
console.log('');
|
|
59
45
|
});
|
|
60
46
|
}
|