stim 1.1.0 → 1.3.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.
Files changed (64) hide show
  1. package/README.md +37 -0
  2. package/dist/{android-CC5bUer7.mjs → android-BW8i_YxU.mjs} +1 -1
  3. package/dist/android-BqVIJoG7.mjs +1368 -0
  4. package/dist/{android-DnZlT3aU.mjs → android-CqcW1hrP.mjs} +205 -58
  5. package/dist/{android-cas-DGlTStWu.mjs → android-cas-BR5YJBYv.mjs} +6 -6
  6. package/dist/android-cas-compiler.mjs +1 -1
  7. package/dist/{app-install-oZIQRbdD.mjs → app-install-DIp9riNx.mjs} +29 -21
  8. package/dist/{build-slots-CgacA9zQ.mjs → build-slots-C3-R4XQr.mjs} +3 -3
  9. package/dist/{cache-manifest-oeH18ERr.mjs → cache-manifest-CXR-Y8pH.mjs} +1 -1
  10. package/dist/cache-manifest.mjs +1 -1
  11. package/dist/cli.mjs +13 -13
  12. package/dist/collector-run.d.mts +4 -1
  13. package/dist/collector-run.mjs +36 -11
  14. package/dist/{config-D7uu8Gbq.mjs → config-CcefMY2Q.mjs} +94 -13
  15. package/dist/{deps-64wVnpZc.mjs → deps-Spz8ozgo.mjs} +2 -2
  16. package/dist/{dev-client-Bvky1hya.mjs → dev-client-BjisHDB5.mjs} +250 -10
  17. package/dist/{device-DJKv5Qej.mjs → device-BkiW-gkk.mjs} +19 -11
  18. package/dist/{device-lease-D-G3SgVe.mjs → device-lease-BJYybF-2.mjs} +107 -83
  19. package/dist/{device-pool-BlHXnYYs.mjs → device-pool-CPbLGfrB.mjs} +19 -13
  20. package/dist/{device-remote-CCgkG82J.mjs → device-remote-AkYCKS8U.mjs} +8 -8
  21. package/dist/{doctor-DlVxTtMq.mjs → doctor-Bmv86UUW.mjs} +18 -26
  22. package/dist/{doctor-BRF9aAJ3.mjs → doctor-fAMXC4vl.mjs} +10 -10
  23. package/dist/{error-diagnostics-B5IJwv3-.mjs → error-diagnostics-DHC31rXA.mjs} +65 -55
  24. package/dist/{exec-bsN9MJXb.mjs → exec-CyylIdq9.mjs} +4 -2
  25. package/dist/{gc-DhXdgSdt.mjs → gc-GGu-8ULN.mjs} +90 -67
  26. package/dist/{guide-DrhrDBo5.mjs → guide-92CPA21s.mjs} +414 -47
  27. package/dist/{ios-CK1Vrk6Q.mjs → ios-BSXFeO5H.mjs} +200 -59
  28. package/dist/{ios-BZxKPuqP.mjs → ios-Deh5js5V.mjs} +147 -40
  29. package/dist/{ios-device-CdSm0GDw.mjs → ios-device-CWOM_S44.mjs} +2 -2
  30. package/dist/{ios-device-CYbuRNXR.mjs → ios-device-D3pzTKQE.mjs} +1 -1
  31. package/dist/{logs-Dxoy16Qv.mjs → logs-DPNF4C9s.mjs} +9 -6
  32. package/dist/{logs-query-oqn47Ffx.mjs → logs-query-R-Cqc6Qv.mjs} +22 -5
  33. package/dist/{metro-B01H3EuD.mjs → metro-CORqXyTL.mjs} +3 -3
  34. package/dist/{ndjson-DcAtEx_K.mjs → ndjson-BZwI1NBv.mjs} +5 -2
  35. package/dist/{ownership-D8HmSv2E.mjs → ownership-8O073GIC.mjs} +3 -3
  36. package/dist/{ownership-B9QIdQOw.mjs → ownership-BpCKiRlQ.mjs} +4 -4
  37. package/dist/{ownership-claim-CeJLEpjD.mjs → ownership-claim-Bf2CAfw3.mjs} +4 -3
  38. package/dist/{project-COj6nzzh.mjs → project-fKmuIaMI.mjs} +2 -2
  39. package/dist/{reclaim-BPvN2HlP.mjs → reclaim-iW9Cad7k.mjs} +80 -73
  40. package/dist/{reload-DYZEBbZy.mjs → reload-DazHDSFA.mjs} +32 -24
  41. package/dist/{server-bare-BYHqEeyw.mjs → server-bare-DEzPZLvf.mjs} +7 -2
  42. package/dist/server-expo-44em5Pmg.mjs +2 -0
  43. package/dist/{server-expo-BEMr_m6p.mjs → server-expo-DizT4OMk.mjs} +5 -5
  44. package/dist/{settings-BSwrY4FZ.mjs → settings-DEJf6_kJ.mjs} +26 -5
  45. package/dist/slot-launch-cci0xvmC.mjs +19 -0
  46. package/dist/{start-ByCylYtB.mjs → start-mObjQH2W.mjs} +12 -12
  47. package/dist/{state-IYEY-zi8.mjs → state-B3XA2wp2.mjs} +9 -9
  48. package/dist/{stats-wqxPm7Tv.mjs → stats-BR-kwj0B.mjs} +1 -1
  49. package/dist/{stats-DZ4Q5J-7.mjs → stats-CaaQr4yN.mjs} +3 -3
  50. package/dist/{status-Dia0XTqG.mjs → status-BxQNqu9t.mjs} +86 -25
  51. package/dist/{stop-D6o5zAdm.mjs → stop-CrHGcjrV.mjs} +2 -2
  52. package/dist/{stop-BApyaF21.mjs → stop-DimGXEKY.mjs} +93 -42
  53. package/dist/supervisor-run.d.mts +1 -2
  54. package/dist/supervisor-run.mjs +5 -5
  55. package/dist/{remote-cache-DOi5-zLj.mjs → teardown-ClFp1Aax.mjs} +889 -830
  56. package/dist/{workspace-process-lock-CxOO9-pf.mjs → workspace-process-lock-3NcQnZ0X.mjs} +1 -1
  57. package/dist/{worktree-DxEmUI_n.mjs → worktree-DxTgN1CJ.mjs} +16 -16
  58. package/dist/{worktree-Dx3ZC5lS.mjs → worktree-Eq6TLH8N.mjs} +7 -3
  59. package/dist/{xcode-CDB6fytw.mjs → xcode-D7sAp-Zy.mjs} +256 -103
  60. package/package.json +5 -5
  61. package/shim/bundle-response.cjs +5 -0
  62. package/dist/android-xmyGBNy5.mjs +0 -665
  63. package/dist/build-lock-vE45ZzJZ.mjs +0 -645
  64. package/dist/server-expo-rb0k-0w7.mjs +0 -2
@@ -1,16 +1,17 @@
1
- import { t as getExecutor } from "./exec-bsN9MJXb.mjs";
2
- import { P as volumeRootFor, g as loadConfig, u as getConfigDir } from "./config-D7uu8Gbq.mjs";
3
- import { D as workspaceLogsDir } from "./state-IYEY-zi8.mjs";
4
- import { f as listWorktrees } from "./worktree-Dx3ZC5lS.mjs";
5
- import { a as findProjectRoot, c as projectShortcut } from "./project-COj6nzzh.mjs";
6
- import { a as isMetroRunning, i as resolveProjectMetro } from "./metro-B01H3EuD.mjs";
7
- import { S as readParked, v as POOL_SETTING_REMEDY, x as parkedMaxSetting } from "./remote-cache-DOi5-zLj.mjs";
1
+ import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
+ import { P as volumeRootFor, g as loadConfig, u as getConfigDir, z as projectDeviceSlots } from "./config-CcefMY2Q.mjs";
3
+ import { D as workspaceLogsDir } from "./state-B3XA2wp2.mjs";
4
+ import { f as listWorktrees } from "./worktree-Eq6TLH8N.mjs";
5
+ import { a as findProjectRoot, c as projectShortcut } from "./project-fKmuIaMI.mjs";
6
+ import { a as isMetroRunning, i as resolveProjectMetro } from "./metro-CORqXyTL.mjs";
8
7
  import { a as formatElapsed, r as clockTime, u as plural } from "./command-output-8wHMEde0.mjs";
9
- import { l as listAllIosSims } from "./ios-BZxKPuqP.mjs";
10
- import { i as listLeaseFiles } from "./device-lease-D-G3SgVe.mjs";
11
- import { s as queryLogs } from "./logs-query-oqn47Ffx.mjs";
12
- import { t as resolveSupervisorTarget } from "./ownership-B9QIdQOw.mjs";
13
- import { i as readSupervisorState } from "./stop-BApyaF21.mjs";
8
+ import { N as resolveOwnedAvdSerial } from "./android-BqVIJoG7.mjs";
9
+ import { a as POOL_SETTING_REMEDY, c as parkedMaxSetting, l as readParked } from "./teardown-ClFp1Aax.mjs";
10
+ import { u as listAllIosSims } from "./ios-Deh5js5V.mjs";
11
+ import { i as listLeaseFiles } from "./device-lease-BJYybF-2.mjs";
12
+ import { s as queryLogs } from "./logs-query-R-Cqc6Qv.mjs";
13
+ import { t as resolveSupervisorTarget } from "./ownership-BpCKiRlQ.mjs";
14
+ import { i as readSupervisorState } from "./stop-DimGXEKY.mjs";
14
15
  import { existsSync } from "fs";
15
16
  import { totalmem } from "os";
16
17
  import chalk from "chalk";
@@ -23,24 +24,54 @@ function poolLine({ platform, parked, max }) {
23
24
  const what = plural(parked, `parked ${platform === "ios" ? "iOS simulator" : "Android emulator"}`);
24
25
  return max > 0 ? `pool: ${what} (max ${max})` : `pool: ${what} (parking off; gc --delete removes them)`;
25
26
  }
26
- function environmentState(project, { simsByUdid = {}, metro = null, worktrees = [], simsAvailable = true, supervisor = null, logs = null } = {}) {
27
+ function environmentState(project, { simsByUdid = {}, metro = null, worktrees = [], simsAvailable = true, androidRuntime = null, androidRuntimes = {}, supervisor = null, logs = null } = {}) {
27
28
  const ios = project.platforms?.ios;
28
29
  const android = project.platforms?.android;
29
30
  const sim = ios ? simsByUdid[ios.deviceUdid] : null;
30
31
  const simBooted = Boolean(sim && sim.state === "Booted");
31
32
  const metroRunning = Boolean(metro?.metro);
32
- const live = simBooted || metroRunning || Boolean(android?.serial);
33
+ const androidDetected = androidRuntime ? Boolean(androidRuntime.serial) : Boolean(android?.serial);
34
+ let live = simBooted || metroRunning || androidDetected;
33
35
  let memoryMb = 0;
34
36
  if (simBooted) memoryMb += IOS_SIM_MB;
35
- if (android?.serial) memoryMb += ANDROID_EMULATOR_MB;
37
+ if (androidDetected) memoryMb += ANDROID_EMULATOR_MB;
36
38
  if (metroRunning) memoryMb += METRO_MB;
37
39
  const warnings = [];
38
40
  if (metro?.notOurs) warnings.push(`port ${project.metroPort}: ${metro.notOurs}`);
39
41
  if (ios && !sim && simsAvailable) warnings.push(`recorded sim ${ios.deviceUdid} no longer exists`);
40
42
  if (simBooted && project.metroPort && !metroRunning) warnings.push("simulator is booted with no Metro serving it");
43
+ if (androidRuntime && android?.avdName) {
44
+ const recordedSerial = android.consolePort ? `emulator-${android.consolePort}` : android.serial;
45
+ if (androidRuntime.serial && recordedSerial && androidRuntime.serial !== recordedSerial) warnings.push(`owned AVD ${android.avdName} changed serial (${recordedSerial} -> ${androidRuntime.serial}); rerun your \`stim android\` command in this workspace to restore Metro forwarding, then reopen agent-device on ${androidRuntime.serial}`);
46
+ if (androidRuntime.state === "missing") warnings.push(`recorded AVD ${android.avdName} no longer exists`);
47
+ if (androidRuntime.state === "not-detected") warnings.push(`owned AVD ${android.avdName} is not detected by adb; rerun your \`stim android\` command in this workspace to reconnect`);
48
+ if (androidRuntime.error) warnings.push(`could not check owned AVD ${android.avdName}: ${androidRuntime.error}`);
49
+ }
41
50
  if (supervisor && supervisor.alive === false) warnings.push(`stale supervisor record for ${project.__path}`);
51
+ const slots = [];
52
+ for (const { slot, platforms } of projectDeviceSlots(project).slice(1)) {
53
+ const deviceState = environmentState({
54
+ ...project,
55
+ platforms,
56
+ deviceSlots: void 0
57
+ }, {
58
+ simsByUdid,
59
+ simsAvailable,
60
+ metro,
61
+ androidRuntime: androidRuntimes[slot]
62
+ });
63
+ slots.push({
64
+ slot,
65
+ ios: deviceState.ios,
66
+ android: deviceState.android
67
+ });
68
+ memoryMb += deviceState.memoryMb - (metroRunning ? METRO_MB : 0);
69
+ live ||= deviceState.live;
70
+ warnings.push(...deviceState.warnings.map((warning) => `${slot}: ${warning}`));
71
+ }
42
72
  return {
43
73
  path: project.__path,
74
+ ...slots.length ? { slots } : {},
44
75
  live,
45
76
  memoryMb,
46
77
  warnings,
@@ -53,7 +84,11 @@ function environmentState(project, { simsByUdid = {}, metro = null, worktrees =
53
84
  android: android ? {
54
85
  name: android.avdName ?? android.serial,
55
86
  owned: Boolean(android.owned),
56
- physical: Boolean(android.serial && !android.avdName)
87
+ physical: Boolean(android.serial && !android.avdName),
88
+ ...androidRuntime ? {
89
+ serial: androidRuntime.serial,
90
+ state: androidRuntime.state
91
+ } : {}
57
92
  } : null,
58
93
  metro: project.metroPort ? {
59
94
  port: project.metroPort,
@@ -130,6 +165,7 @@ function deviceLeaseStates(entries, { root, now }) {
130
165
  return {
131
166
  path: entry.path,
132
167
  platform: entry.platform,
168
+ ...lease?.slot ? { slot: lease.slot } : {},
133
169
  id: entry.id,
134
170
  deviceName: lease?.deviceName ?? null,
135
171
  holder: lease?.holder ?? null,
@@ -145,7 +181,7 @@ function deviceLeaseLines(states, now) {
145
181
  if (states.length === 0) return [];
146
182
  const lines = [`Device leases (${states.length}):`];
147
183
  for (const state of states) {
148
- const device = `${state.platform} ${state.id ?? state.path}${state.deviceName ? ` (${state.deviceName})` : ""}`;
184
+ const device = `${state.platform}${state.slot ? ` [${state.slot}]` : ""} ${state.id ?? state.path}${state.deviceName ? ` (${state.deviceName})` : ""}`;
149
185
  if (!state.parsed || state.expiresAt === null) {
150
186
  lines.push(` ${device} -- unreadable lease file, so nothing may take the device: ${state.path}`);
151
187
  continue;
@@ -191,6 +227,8 @@ function statusCommand(program) {
191
227
  metro,
192
228
  worktrees,
193
229
  simsAvailable,
230
+ androidRuntimes: Object.fromEntries(projectDeviceSlots(proj).slice(1).map(({ slot, platforms }) => [slot, platforms.android?.owned && platforms.android.avdName ? readAndroidRuntime(platforms.android.avdName) : null])),
231
+ androidRuntime: proj.platforms?.android?.owned && proj.platforms.android.avdName ? readAndroidRuntime(proj.platforms.android.avdName) : null,
194
232
  supervisor,
195
233
  logs: logFacts(path)
196
234
  }));
@@ -260,14 +298,22 @@ function statusCommand(program) {
260
298
  const errs = n > 0 ? chalk.yellow(` (${n} error${n === 1 ? "" : "s"} since the last marker)`) : "";
261
299
  console.log(chalk.dim(` logs: ${state.logs.dir}`) + errs);
262
300
  }
263
- if (state.ios) {
264
- const booted = state.ios.state === "Booted" ? chalk.green("booted") : chalk.dim(state.ios.state.toLowerCase());
265
- const owned = state.ios.owned ? chalk.dim(" (owned)") : "";
266
- console.log(` ios: ${chalk.cyan(state.ios.name ?? state.ios.udid)} ${booted}${owned}`);
267
- }
268
- if (state.android) {
269
- const kind = state.android.physical ? chalk.dim("(physical)") : chalk.dim("(emulator)");
270
- console.log(` android: ${chalk.cyan(state.android.name)} ${kind}${state.android.owned ? chalk.dim(" (owned)") : ""}`);
301
+ for (const deviceState of [{
302
+ slot: "default",
303
+ ios: state.ios,
304
+ android: state.android
305
+ }, ...state.slots ?? []]) {
306
+ const slotLabel = deviceState.slot === "default" ? "" : ` [${deviceState.slot}]`;
307
+ if (deviceState.ios) {
308
+ const booted = deviceState.ios.state === "Booted" ? chalk.green("booted") : chalk.dim(deviceState.ios.state.toLowerCase());
309
+ const owned = deviceState.ios.owned ? chalk.dim(" (owned)") : "";
310
+ console.log(` ios${slotLabel}: ${chalk.cyan(deviceState.ios.name ?? deviceState.ios.udid)} ${booted}${owned}`);
311
+ }
312
+ if (deviceState.android) {
313
+ const kind = deviceState.android.physical ? chalk.dim("(physical)") : chalk.dim("(emulator)");
314
+ const observed = deviceState.android.state ? ` ${deviceState.android.state}${deviceState.android.serial ? ` (${deviceState.android.serial})` : ""}` : "";
315
+ console.log(` android${slotLabel}: ${chalk.cyan(deviceState.android.name)} ${kind}${observed}${deviceState.android.owned ? chalk.dim(" (owned)") : ""}`);
316
+ }
271
317
  }
272
318
  for (const w of state.warnings) console.log(chalk.yellow(` ! ${w}`));
273
319
  }
@@ -297,6 +343,21 @@ function statusCommand(program) {
297
343
  if (cap.overCapacity) console.log(chalk.yellow("Over comfortable capacity. A machine that swaps is slower than one working in sequence -- release one before starting another."));
298
344
  });
299
345
  }
346
+ function readAndroidRuntime(avdName) {
347
+ try {
348
+ const resolved = resolveOwnedAvdSerial(avdName, { timeoutMs: 5e3 });
349
+ return {
350
+ serial: resolved.serial ?? null,
351
+ state: resolved.serial ? "detected" : resolved.missing ? "missing" : resolved.notRunning ? "not-detected" : "unknown"
352
+ };
353
+ } catch (error) {
354
+ return {
355
+ serial: null,
356
+ state: "unknown",
357
+ error: String(error?.message || error).split("\n")[0]
358
+ };
359
+ }
360
+ }
300
361
  function readVolumes(projectPath) {
301
362
  const roots = [.../* @__PURE__ */ new Set([
302
363
  "/",
@@ -1,3 +1,3 @@
1
- import { t as resolveSupervisorTarget } from "./ownership-B9QIdQOw.mjs";
2
- import { a as resolveCollectorTargets, i as readSupervisorState, n as clearSupervisorState, o as runStop, r as readCollectorState, s as stopCommand, t as clearCollectorState } from "./stop-BApyaF21.mjs";
1
+ import { t as resolveSupervisorTarget } from "./ownership-BpCKiRlQ.mjs";
2
+ import { a as resolveCollectorTargets, i as readSupervisorState, n as clearSupervisorState, o as runStop, r as readCollectorState, s as stopCommand, t as clearCollectorState } from "./stop-DimGXEKY.mjs";
3
3
  export { clearCollectorState, clearSupervisorState, stopCommand as default, readCollectorState, readSupervisorState, resolveCollectorTargets, resolveSupervisorTarget, runStop };
@@ -1,13 +1,14 @@
1
- import { C as withConfigLock, D as sameProcessRecord, E as inspectProcessIdentity, O as waitForProcessExit, S as upsertProject, a as clearSupervisor, f as getProject } from "./config-D7uu8Gbq.mjs";
2
- import { _ as writeWorkspaceState, a as clearRemoteSession, c as readMetroTunnel, i as clearManagedMetroTunnel, m as withWorkspaceStateLock, o as clearWorkspaceStateKeys, p as readWorkspaceState, s as clearWorkspaceSupervisor, u as readRemoteSession } from "./state-IYEY-zi8.mjs";
3
- import { a as findProjectRoot } from "./project-COj6nzzh.mjs";
4
- import { i as resolveProjectMetro, n as killMetroTree, r as pidExists } from "./metro-B01H3EuD.mjs";
5
- import { n as verifyCollectorOwnership } from "./ownership-D8HmSv2E.mjs";
6
- import { d as resolveEasCliBin, h as teardownOwnedIosSim, m as teardownOwnedAvd } from "./remote-cache-DOi5-zLj.mjs";
1
+ import { C as withConfigLock, D as sameProcessRecord, E as inspectProcessIdentity, L as deviceSlotPlatforms, O as waitForProcessExit, R as parseDeviceSlotKey, S as upsertProject, V as validateDeviceSlot, a as clearSupervisor, f as getProject, z as projectDeviceSlots } from "./config-CcefMY2Q.mjs";
2
+ import { T as workspaceDir, _ as writeWorkspaceState, a as clearRemoteSession, c as readMetroTunnel, i as clearManagedMetroTunnel, m as withWorkspaceStateLock, o as clearWorkspaceStateKeys, p as readWorkspaceState, s as clearWorkspaceSupervisor, u as readRemoteSession } from "./state-B3XA2wp2.mjs";
3
+ import { a as findProjectRoot } from "./project-fKmuIaMI.mjs";
4
+ import { i as resolveProjectMetro, n as killMetroTree, r as pidExists } from "./metro-CORqXyTL.mjs";
5
+ import { n as verifyCollectorOwnership } from "./ownership-8O073GIC.mjs";
7
6
  import { f as releasedLeaseFact, l as phaseLine, u as plural } from "./command-output-8wHMEde0.mjs";
8
- import { l as releaseWorkspaceLeases } from "./device-lease-D-G3SgVe.mjs";
9
- import { i as endRecordedSession } from "./device-remote-CCgkG82J.mjs";
10
- import { r as stopTunnel, t as resolveSupervisorTarget } from "./ownership-B9QIdQOw.mjs";
7
+ import { n as teardownOwnedIosSim, t as teardownOwnedAvd, x as resolveEasCliBin } from "./teardown-ClFp1Aax.mjs";
8
+ import { t as withWorkspaceProcessLock } from "./workspace-process-lock-3NcQnZ0X.mjs";
9
+ import { l as releaseWorkspaceLeases } from "./device-lease-BJYybF-2.mjs";
10
+ import { i as endRecordedSession } from "./device-remote-AkYCKS8U.mjs";
11
+ import { r as stopTunnel, t as resolveSupervisorTarget } from "./ownership-BpCKiRlQ.mjs";
11
12
  import chalk from "chalk";
12
13
  //#region src/commands/stop.ts
13
14
  const DEFAULT_WAIT_MS = 1e4;
@@ -101,7 +102,47 @@ function defaultTeardownRemoteSession(root, sessionId) {
101
102
  easBin: resolveEasCliBin(root)?.file ?? null
102
103
  });
103
104
  }
104
- async function runStop({ root, project = void 0, state = void 0, collectors = void 0, signalCollector = (pid) => process.kill(pid, "SIGTERM"), verifyCollector = verifyCollectorOwnership, clearCollectors = clearCollectorState, isAlive = pidExists, killGroup = killMetroTree, inspectIdentity = inspectProcessIdentity, waitForDeath = void 0, waitMs = DEFAULT_WAIT_MS, resolveMetro = resolveProjectMetro, teardownIos = teardownOwnedIosSim, teardownAvd = teardownOwnedAvd, remoteDevice = void 0, teardownRemoteSession = defaultTeardownRemoteSession, metroTunnel = void 0, stopMetroTunnel = stopTunnel, releaseLeases = releaseWorkspaceLeases, freePort = defaultFreePort, clearRegistration = defaultClearRegistration, clearState = clearSupervisorState, report = (line) => console.error(line) }) {
105
+ async function runStop(options) {
106
+ if (options.slot === void 0) return stopWorkspace(options);
107
+ const slot = validateDeviceSlot(options.slot);
108
+ const root = options.root;
109
+ const project = options.project === void 0 ? getProject(root) : options.project;
110
+ const records = options.collectors === void 0 ? readCollectorState(root) : options.collectors;
111
+ const collectors = Object.fromEntries(Object.entries(records ?? {}).filter(([key]) => parseDeviceSlotKey(key)?.slot === slot));
112
+ const result = await stopWorkspace({
113
+ ...options,
114
+ project: project ? {
115
+ ...project,
116
+ platforms: deviceSlotPlatforms(project, slot),
117
+ deviceSlots: void 0,
118
+ supervisor: void 0,
119
+ metroPort: null
120
+ } : null,
121
+ state: null,
122
+ collectors,
123
+ remoteDevice: null,
124
+ metroTunnel: null,
125
+ clearCollectors: (projectRoot, expected) => withWorkspaceStateLock(projectRoot, () => {
126
+ clearCollectorState(projectRoot, expected);
127
+ return !Object.keys(readCollectorState(projectRoot)).some((key) => parseDeviceSlotKey(key)?.slot === slot);
128
+ }),
129
+ clearState: () => true,
130
+ clearRegistration: async () => true,
131
+ releaseLeases: (projectRoot) => releaseWorkspaceLeases(projectRoot, { slot })
132
+ });
133
+ result.outcomes.port = {
134
+ status: "kept",
135
+ port: project?.metroPort ?? null,
136
+ reason: "The workspace server is shared by all slots."
137
+ };
138
+ result.outcomes.metro = {
139
+ status: "skipped",
140
+ reason: "The workspace server is shared by all slots."
141
+ };
142
+ result.summary = summarize(root, result.outcomes, result.ok);
143
+ return result;
144
+ }
145
+ async function stopWorkspace({ root, project = void 0, state = void 0, collectors = void 0, signalCollector = (pid) => process.kill(pid, "SIGTERM"), verifyCollector = verifyCollectorOwnership, clearCollectors = clearCollectorState, isAlive = pidExists, killGroup = killMetroTree, inspectIdentity = inspectProcessIdentity, waitForDeath = void 0, waitMs = DEFAULT_WAIT_MS, resolveMetro = resolveProjectMetro, teardownIos = teardownOwnedIosSim, teardownAvd = teardownOwnedAvd, remoteDevice = void 0, teardownRemoteSession = defaultTeardownRemoteSession, metroTunnel = void 0, stopMetroTunnel = stopTunnel, releaseLeases = releaseWorkspaceLeases, freePort = defaultFreePort, clearRegistration = defaultClearRegistration, clearState = clearSupervisorState, report = (line) => console.error(line) }) {
105
146
  const proj = project === void 0 ? getProject(root) : project;
106
147
  const sup = state === void 0 ? readSupervisorState(root) : state;
107
148
  const collectorRecords = collectors === void 0 ? readCollectorState(root) : collectors;
@@ -235,7 +276,7 @@ async function runStop({ root, project = void 0, state = void 0, collectors = vo
235
276
  teardownAvd,
236
277
  report
237
278
  });
238
- if ([outcomes.device.ios, outcomes.device.android].some((device) => device?.status === "failed")) ok = false;
279
+ if (Object.values(outcomes.device).some((device) => device?.status === "failed")) ok = false;
239
280
  }
240
281
  const remote = remoteDevice === void 0 ? readRemoteSession(root) : remoteDevice;
241
282
  const sessionId = typeof remote?.sessionId === "string" ? remote.sessionId : null;
@@ -461,34 +502,38 @@ function shutDownDevices(project, { teardownIos, teardownAvd, report }) {
461
502
  ios: null,
462
503
  android: null
463
504
  };
464
- const ios = project?.platforms?.ios;
465
- const iosUdid = ios?.deviceUdid;
466
- const iosName = ios?.deviceName;
467
- if (iosUdid) {
468
- if (!ios?.owned) {
469
- device.ios = {
470
- status: "skipped",
471
- kind: "not-owned",
472
- label: iosUdid,
473
- reason: "Stim does not own this device"
474
- };
475
- report(chalk.dim(phaseLine("device", `${iosUdid} is not Stim-owned, leaving it running`)));
476
- } else device.ios = reportDevice(iosUdid, teardownIos(iosUdid, {
477
- del: false,
478
- label: iosName
479
- }), report);
480
- }
481
- const android = project?.platforms?.android;
482
- if (android?.avdName) {
483
- if (!android.owned) {
484
- device.android = {
485
- status: "skipped",
486
- kind: "not-owned",
487
- label: android.avdName,
488
- reason: "Stim does not own this device"
489
- };
490
- report(chalk.dim(phaseLine("device", `${android.avdName} is not Stim-owned, leaving it running`)));
491
- } else device.android = reportDevice(android.avdName, teardownAvd(android.avdName, { del: false }), report);
505
+ for (const { slot, platforms } of projectDeviceSlots(project)) {
506
+ const iosKey = slot === "default" ? "ios" : `ios:${slot}`;
507
+ const androidKey = slot === "default" ? "android" : `android:${slot}`;
508
+ const ios = platforms.ios;
509
+ const iosUdid = ios?.deviceUdid;
510
+ const iosName = ios?.deviceName;
511
+ if (iosUdid) {
512
+ if (!ios?.owned) {
513
+ device[iosKey] = {
514
+ status: "skipped",
515
+ kind: "not-owned",
516
+ label: iosUdid,
517
+ reason: "Stim does not own this device"
518
+ };
519
+ report(chalk.dim(phaseLine("device", `${iosUdid} is not Stim-owned, leaving it running`)));
520
+ } else device[iosKey] = reportDevice(iosUdid, teardownIos(iosUdid, {
521
+ del: false,
522
+ label: iosName
523
+ }), report);
524
+ }
525
+ const android = platforms.android;
526
+ if (android?.avdName) {
527
+ if (!android.owned) {
528
+ device[androidKey] = {
529
+ status: "skipped",
530
+ kind: "not-owned",
531
+ label: android.avdName,
532
+ reason: "Stim does not own this device"
533
+ };
534
+ report(chalk.dim(phaseLine("device", `${android.avdName} is not Stim-owned, leaving it running`)));
535
+ } else device[androidKey] = reportDevice(android.avdName, teardownAvd(android.avdName, { del: false }), report);
536
+ }
492
537
  }
493
538
  return device;
494
539
  }
@@ -539,7 +584,7 @@ function summarize(root, outcomes, ok) {
539
584
  if (outcomes.metro.status === "not-managed") parts.push(`external server on port ${outcomes.metro.port} left alone`);
540
585
  if (outcomes.metro.status === "refused") parts.push(`port ${outcomes.metro.port} refused`);
541
586
  if (outcomes.metro.status === "failed") parts.push(`port ${outcomes.metro.port} could not be freed`);
542
- const devicesByPlatform = [["ios", outcomes.device.ios], ["android", outcomes.device.android]];
587
+ const devicesByPlatform = Object.entries(outcomes.device).filter(([key]) => key !== "remote");
543
588
  for (const [platform, o] of devicesByPlatform) {
544
589
  if (!o) continue;
545
590
  if (o.status === "shut-down") parts.push(`${platform} ${o.label} shut down`);
@@ -571,13 +616,19 @@ async function defaultClearRegistration(root, expected) {
571
616
  }
572
617
  }
573
618
  function stopCommand(program) {
574
- program.command("stop").description("The inverse of `start`: halt this workspace's supervisor, shut the owned device down (never deleted), and free the reserved port. Non-destructive -- the device stays assigned, so coming back costs a boot. Acts on the current workspace.").option("--json", "print the per-step outcomes as JSON").action(async (opts) => {
619
+ program.command("stop").description("The inverse of `start`: halt this workspace's supervisor, shut the owned device down (never deleted), and free the reserved port. Non-destructive -- the device stays assigned, so coming back costs a boot. Acts on the current workspace.").option("--slot <name>", "Stop only this device slot, keeping the shared server running", validateDeviceSlot).option("--json", "print the per-step outcomes as JSON").action(async (opts) => {
575
620
  const root = findProjectRoot(process.cwd());
576
621
  if (!root) {
577
622
  console.error(chalk.red("Not inside a project (no package.json found above the current directory)."));
578
623
  process.exit(1);
579
624
  }
580
- const { ok, outcomes, summary } = await runStop({ root });
625
+ const { ok, outcomes, summary } = await withWorkspaceProcessLock(workspaceDir(root), "native-run", () => runStop({
626
+ root,
627
+ slot: opts.slot
628
+ }), {
629
+ external: true,
630
+ waitMs: 18e5
631
+ });
581
632
  if (opts.json) console.log(JSON.stringify({
582
633
  root,
583
634
  ok,
@@ -13,12 +13,11 @@ interface WorkspaceState {
13
13
  supervisor?: Record<string, unknown>;
14
14
  collectors?: Record<string, unknown>;
15
15
  lastBuild?: Record<string, unknown>;
16
- launches?: Partial<Record<WorkspaceLaunchPlatform, WorkspaceLaunchRecord>>;
16
+ launches?: Record<string, WorkspaceLaunchRecord>;
17
17
  remoteDevice?: Record<string, unknown>;
18
18
  metroTunnel?: Record<string, unknown>;
19
19
  [key: string]: unknown;
20
20
  }
21
- type WorkspaceLaunchPlatform = "ios" | "android";
22
21
  interface WorkspaceLaunchRecord {
23
22
  appId: string;
24
23
  deviceId: string;
@@ -1,7 +1,7 @@
1
- import { T as captureProcessToken, a as clearSupervisor, x as setSupervisor } from "./config-D7uu8Gbq.mjs";
2
- import { r as createNdjsonWriter } from "./ndjson-DcAtEx_K.mjs";
3
- import { D as workspaceLogsDir, _ as writeWorkspaceState, h as writePidFile, l as readPidFile, m as withWorkspaceStateLock, n as MODE_EXPO, p as readWorkspaceState, r as clearExpoMetroTunnel, s as clearWorkspaceSupervisor, t as MODE_BARE } from "./state-IYEY-zi8.mjs";
4
- import { i as detectIsExpo } from "./project-COj6nzzh.mjs";
1
+ import { T as captureProcessToken, a as clearSupervisor, x as setSupervisor } from "./config-CcefMY2Q.mjs";
2
+ import { r as createNdjsonWriter } from "./ndjson-BZwI1NBv.mjs";
3
+ import { D as workspaceLogsDir, _ as writeWorkspaceState, h as writePidFile, l as readPidFile, m as withWorkspaceStateLock, n as MODE_EXPO, p as readWorkspaceState, r as clearExpoMetroTunnel, s as clearWorkspaceSupervisor, t as MODE_BARE } from "./state-B3XA2wp2.mjs";
4
+ import { i as detectIsExpo } from "./project-fKmuIaMI.mjs";
5
5
  import { t as describeError } from "./errors-B6W7FkxQ.mjs";
6
6
  import { existsSync, realpathSync } from "node:fs";
7
7
  import { isAbsolute, join, resolve } from "node:path";
@@ -97,7 +97,7 @@ async function runSupervisor({ root, port, tunnel = false, isExpo = detectIsExpo
97
97
  };
98
98
  let server;
99
99
  try {
100
- server = await (mode === "expo-child" ? startExpo || (await import("./server-expo-rb0k-0w7.mjs")).startExpoServer : startBare || (await import("./server-bare-BYHqEeyw.mjs")).startBareServer)({
100
+ server = await (mode === "expo-child" ? startExpo || (await import("./server-expo-44em5Pmg.mjs")).startExpoServer : startBare || (await import("./server-bare-DEzPZLvf.mjs")).startBareServer)({
101
101
  root,
102
102
  port,
103
103
  logsDir,