stim 1.2.0 → 1.3.1

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 (59) hide show
  1. package/README.md +34 -3
  2. package/dist/{android-Bcmev2s2.mjs → android-CT-PkXUh.mjs} +157 -62
  3. package/dist/{android-cas-D6HPZ6UQ.mjs → android-cas-DXorPCi_.mjs} +5 -5
  4. package/dist/android-hMj83VpN.mjs +1374 -0
  5. package/dist/{app-install-NFxUahnH.mjs → app-install-DIp9riNx.mjs} +6 -6
  6. package/dist/{build-slots-D0b6sJyo.mjs → build-slots-C4AouyNz.mjs} +2 -2
  7. package/dist/{cache-manifest-Cu4_Znw9.mjs → cache-manifest-CXR-Y8pH.mjs} +1 -1
  8. package/dist/cache-manifest.mjs +1 -1
  9. package/dist/cli.mjs +13 -13
  10. package/dist/collector-run.d.mts +3 -1
  11. package/dist/collector-run.mjs +33 -8
  12. package/dist/{config-CMtIaTk9.mjs → config-CcefMY2Q.mjs} +93 -12
  13. package/dist/{deps-BLBsTNDS.mjs → deps-0Yt0Inly.mjs} +1 -1
  14. package/dist/{dev-client-RpvXxCXV.mjs → dev-client-VfSZNLT4.mjs} +186 -9
  15. package/dist/{device-CoCPmazS.mjs → device-Bbv45Z9Z.mjs} +19 -11
  16. package/dist/{device-lease-Dpik41RY.mjs → device-lease-CJwK7OZr.mjs} +107 -83
  17. package/dist/{device-pool-B5pEVa3t.mjs → device-pool-CcBNTvM7.mjs} +18 -12
  18. package/dist/{device-remote-E81KAdXf.mjs → device-remote-DAA0Zm6Z.mjs} +7 -7
  19. package/dist/{doctor-CqPeMsSm.mjs → doctor-B4EITgQB.mjs} +11 -12
  20. package/dist/{doctor-D_CX7ib5.mjs → doctor-BueDfLYG.mjs} +4 -4
  21. package/dist/{error-diagnostics-Dw8Gn3yw.mjs → error-diagnostics-DwhNBFDU.mjs} +62 -52
  22. package/dist/{gc-B7KkDinF.mjs → gc-BRu62v4o.mjs} +66 -57
  23. package/dist/{guide-BS2Xc24o.mjs → guide-D95E0u3i.mjs} +307 -74
  24. package/dist/{ios-B7KLgIp5.mjs → ios-DAju3TVy.mjs} +183 -54
  25. package/dist/{ios-D1h1Di39.mjs → ios-Deh5js5V.mjs} +78 -40
  26. package/dist/{ios-device-xNt0Lg6v.mjs → ios-device-CWOM_S44.mjs} +1 -1
  27. package/dist/{logs-DcX9AvRF.mjs → logs-CAdwW6c2.mjs} +34 -6
  28. package/dist/{logs-query-oqn47Ffx.mjs → logs-query-Cdp5aaCH.mjs} +23 -6
  29. package/dist/{metro-COslRK_F.mjs → metro-CORqXyTL.mjs} +2 -2
  30. package/dist/{ndjson-DcAtEx_K.mjs → ndjson-BZwI1NBv.mjs} +5 -2
  31. package/dist/{ownership-CGPart6T.mjs → ownership-8O073GIC.mjs} +3 -3
  32. package/dist/{ownership-CYOyqxUo.mjs → ownership-DXcol4oU.mjs} +3 -3
  33. package/dist/{ownership-claim-7zRxxTfZ.mjs → ownership-claim-BtPtJVmq.mjs} +38 -23
  34. package/dist/{project-BBTVABUz.mjs → project-fKmuIaMI.mjs} +2 -2
  35. package/dist/{reclaim-C5Jmuahx.mjs → reclaim-CUmwucWG.mjs} +80 -74
  36. package/dist/{reload-Z8nKifAk.mjs → reload-CWpXoOgN.mjs} +32 -24
  37. package/dist/{server-bare-DEk37Mz2.mjs → server-bare-DEzPZLvf.mjs} +2 -2
  38. package/dist/server-expo-44em5Pmg.mjs +2 -0
  39. package/dist/{server-expo-C3ktAwUG.mjs → server-expo-DizT4OMk.mjs} +4 -4
  40. package/dist/{settings-DwU1_U_q.mjs → settings-DEJf6_kJ.mjs} +2 -2
  41. package/dist/slot-launch-CZklomj6.mjs +19 -0
  42. package/dist/{start-BrY4EuIg.mjs → start-B5G4NjiA.mjs} +11 -11
  43. package/dist/{state-CZ-ER-8d.mjs → state-B3XA2wp2.mjs} +9 -9
  44. package/dist/{stats-BCWEVLrM.mjs → stats-BR-kwj0B.mjs} +1 -1
  45. package/dist/{stats-BsF15Bti.mjs → stats-CaaQr4yN.mjs} +3 -3
  46. package/dist/{status-DFI1Hfpg.mjs → status-CI9clRSb.mjs} +55 -24
  47. package/dist/{stop-B7DR4awK.mjs → stop-CQhffpmX.mjs} +2 -2
  48. package/dist/{stop-Btg770vV.mjs → stop-CjlgDk0a.mjs} +93 -42
  49. package/dist/supervisor-run.d.mts +1 -2
  50. package/dist/supervisor-run.mjs +5 -5
  51. package/dist/{remote-cache-DDmJ1Roq.mjs → teardown--gESs9ck.mjs} +985 -887
  52. package/dist/{workspace-process-lock-BKfdys1q.mjs → workspace-process-lock-IBj1Towr.mjs} +1 -1
  53. package/dist/{worktree-BPTbI-LN.mjs → worktree-DtQGO9-s.mjs} +121 -111
  54. package/dist/{worktree-C79xx_kM.mjs → worktree-Eq6TLH8N.mjs} +1 -1
  55. package/dist/{xcode-DeKCribl.mjs → xcode-xnxyvJkD.mjs} +159 -96
  56. package/package.json +5 -5
  57. package/dist/android-K63_0UfF.mjs +0 -727
  58. package/dist/build-lock-BsVoLDaq.mjs +0 -645
  59. package/dist/server-expo-Drd7ksHu.mjs +0 -2
@@ -1,17 +1,17 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { P as volumeRootFor, g as loadConfig, u as getConfigDir } from "./config-CMtIaTk9.mjs";
3
- import { D as workspaceLogsDir } from "./state-CZ-ER-8d.mjs";
4
- import { f as listWorktrees } from "./worktree-C79xx_kM.mjs";
5
- import { a as findProjectRoot, c as projectShortcut } from "./project-BBTVABUz.mjs";
6
- import { a as isMetroRunning, i as resolveProjectMetro } from "./metro-COslRK_F.mjs";
7
- import { S as readParked, v as POOL_SETTING_REMEDY, x as parkedMaxSetting } from "./remote-cache-DDmJ1Roq.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 { u as listAllIosSims } from "./ios-D1h1Di39.mjs";
10
- import { i as listLeaseFiles } from "./device-lease-Dpik41RY.mjs";
11
- import { N as resolveOwnedAvdSerial } from "./android-K63_0UfF.mjs";
12
- import { s as queryLogs } from "./logs-query-oqn47Ffx.mjs";
13
- import { t as resolveSupervisorTarget } from "./ownership-CYOyqxUo.mjs";
14
- import { i as readSupervisorState } from "./stop-Btg770vV.mjs";
8
+ import { N as resolveOwnedAvdSerial } from "./android-hMj83VpN.mjs";
9
+ import { a as POOL_SETTING_REMEDY, c as parkedMaxSetting, l as readParked } from "./teardown--gESs9ck.mjs";
10
+ import { u as listAllIosSims } from "./ios-Deh5js5V.mjs";
11
+ import { i as listLeaseFiles } from "./device-lease-CJwK7OZr.mjs";
12
+ import { c as queryLogs } from "./logs-query-Cdp5aaCH.mjs";
13
+ import { t as resolveSupervisorTarget } from "./ownership-DXcol4oU.mjs";
14
+ import { i as readSupervisorState } from "./stop-CjlgDk0a.mjs";
15
15
  import { existsSync } from "fs";
16
16
  import { totalmem } from "os";
17
17
  import chalk from "chalk";
@@ -24,14 +24,14 @@ function poolLine({ platform, parked, max }) {
24
24
  const what = plural(parked, `parked ${platform === "ios" ? "iOS simulator" : "Android emulator"}`);
25
25
  return max > 0 ? `pool: ${what} (max ${max})` : `pool: ${what} (parking off; gc --delete removes them)`;
26
26
  }
27
- function environmentState(project, { simsByUdid = {}, metro = null, worktrees = [], simsAvailable = true, androidRuntime = null, supervisor = null, logs = null } = {}) {
27
+ function environmentState(project, { simsByUdid = {}, metro = null, worktrees = [], simsAvailable = true, androidRuntime = null, androidRuntimes = {}, supervisor = null, logs = null } = {}) {
28
28
  const ios = project.platforms?.ios;
29
29
  const android = project.platforms?.android;
30
30
  const sim = ios ? simsByUdid[ios.deviceUdid] : null;
31
31
  const simBooted = Boolean(sim && sim.state === "Booted");
32
32
  const metroRunning = Boolean(metro?.metro);
33
33
  const androidDetected = androidRuntime ? Boolean(androidRuntime.serial) : Boolean(android?.serial);
34
- const live = simBooted || metroRunning || androidDetected;
34
+ let live = simBooted || metroRunning || androidDetected;
35
35
  let memoryMb = 0;
36
36
  if (simBooted) memoryMb += IOS_SIM_MB;
37
37
  if (androidDetected) memoryMb += ANDROID_EMULATOR_MB;
@@ -48,8 +48,30 @@ function environmentState(project, { simsByUdid = {}, metro = null, worktrees =
48
48
  if (androidRuntime.error) warnings.push(`could not check owned AVD ${android.avdName}: ${androidRuntime.error}`);
49
49
  }
50
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
+ }
51
72
  return {
52
73
  path: project.__path,
74
+ ...slots.length ? { slots } : {},
53
75
  live,
54
76
  memoryMb,
55
77
  warnings,
@@ -143,6 +165,7 @@ function deviceLeaseStates(entries, { root, now }) {
143
165
  return {
144
166
  path: entry.path,
145
167
  platform: entry.platform,
168
+ ...lease?.slot ? { slot: lease.slot } : {},
146
169
  id: entry.id,
147
170
  deviceName: lease?.deviceName ?? null,
148
171
  holder: lease?.holder ?? null,
@@ -158,7 +181,7 @@ function deviceLeaseLines(states, now) {
158
181
  if (states.length === 0) return [];
159
182
  const lines = [`Device leases (${states.length}):`];
160
183
  for (const state of states) {
161
- 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})` : ""}`;
162
185
  if (!state.parsed || state.expiresAt === null) {
163
186
  lines.push(` ${device} -- unreadable lease file, so nothing may take the device: ${state.path}`);
164
187
  continue;
@@ -204,6 +227,7 @@ function statusCommand(program) {
204
227
  metro,
205
228
  worktrees,
206
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])),
207
231
  androidRuntime: proj.platforms?.android?.owned && proj.platforms.android.avdName ? readAndroidRuntime(proj.platforms.android.avdName) : null,
208
232
  supervisor,
209
233
  logs: logFacts(path)
@@ -274,15 +298,22 @@ function statusCommand(program) {
274
298
  const errs = n > 0 ? chalk.yellow(` (${n} error${n === 1 ? "" : "s"} since the last marker)`) : "";
275
299
  console.log(chalk.dim(` logs: ${state.logs.dir}`) + errs);
276
300
  }
277
- if (state.ios) {
278
- const booted = state.ios.state === "Booted" ? chalk.green("booted") : chalk.dim(state.ios.state.toLowerCase());
279
- const owned = state.ios.owned ? chalk.dim(" (owned)") : "";
280
- console.log(` ios: ${chalk.cyan(state.ios.name ?? state.ios.udid)} ${booted}${owned}`);
281
- }
282
- if (state.android) {
283
- const kind = state.android.physical ? chalk.dim("(physical)") : chalk.dim("(emulator)");
284
- const observed = state.android.state ? ` ${state.android.state}${state.android.serial ? ` (${state.android.serial})` : ""}` : "";
285
- console.log(` android: ${chalk.cyan(state.android.name)} ${kind}${observed}${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
+ }
286
317
  }
287
318
  for (const w of state.warnings) console.log(chalk.yellow(` ! ${w}`));
288
319
  }
@@ -1,3 +1,3 @@
1
- import { t as resolveSupervisorTarget } from "./ownership-CYOyqxUo.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-Btg770vV.mjs";
1
+ import { t as resolveSupervisorTarget } from "./ownership-DXcol4oU.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-CjlgDk0a.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-CMtIaTk9.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-CZ-ER-8d.mjs";
3
- import { a as findProjectRoot } from "./project-BBTVABUz.mjs";
4
- import { i as resolveProjectMetro, n as killMetroTree, r as pidExists } from "./metro-COslRK_F.mjs";
5
- import { n as verifyCollectorOwnership } from "./ownership-CGPart6T.mjs";
6
- import { d as resolveEasCliBin, h as teardownOwnedIosSim, m as teardownOwnedAvd } from "./remote-cache-DDmJ1Roq.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-Dpik41RY.mjs";
9
- import { i as endRecordedSession } from "./device-remote-E81KAdXf.mjs";
10
- import { r as stopTunnel, t as resolveSupervisorTarget } from "./ownership-CYOyqxUo.mjs";
7
+ import { n as teardownOwnedIosSim, t as teardownOwnedAvd, x as resolveEasCliBin } from "./teardown--gESs9ck.mjs";
8
+ import { t as withWorkspaceProcessLock } from "./workspace-process-lock-IBj1Towr.mjs";
9
+ import { l as releaseWorkspaceLeases } from "./device-lease-CJwK7OZr.mjs";
10
+ import { i as endRecordedSession } from "./device-remote-DAA0Zm6Z.mjs";
11
+ import { r as stopTunnel, t as resolveSupervisorTarget } from "./ownership-DXcol4oU.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-CMtIaTk9.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-CZ-ER-8d.mjs";
4
- import { i as detectIsExpo } from "./project-BBTVABUz.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-Drd7ksHu.mjs")).startExpoServer : startBare || (await import("./server-bare-DEk37Mz2.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,