stim 1.2.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 (59) hide show
  1. package/README.md +30 -0
  2. package/dist/android-BqVIJoG7.mjs +1368 -0
  3. package/dist/{android-Bcmev2s2.mjs → android-CqcW1hrP.mjs} +148 -56
  4. package/dist/{android-cas-D6HPZ6UQ.mjs → android-cas-BR5YJBYv.mjs} +5 -5
  5. package/dist/{app-install-NFxUahnH.mjs → app-install-DIp9riNx.mjs} +6 -6
  6. package/dist/{build-slots-D0b6sJyo.mjs → build-slots-C3-R4XQr.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-Spz8ozgo.mjs} +1 -1
  14. package/dist/{dev-client-RpvXxCXV.mjs → dev-client-BjisHDB5.mjs} +186 -9
  15. package/dist/{device-CoCPmazS.mjs → device-BkiW-gkk.mjs} +19 -11
  16. package/dist/{device-lease-Dpik41RY.mjs → device-lease-BJYybF-2.mjs} +107 -83
  17. package/dist/{device-pool-B5pEVa3t.mjs → device-pool-CPbLGfrB.mjs} +18 -12
  18. package/dist/{device-remote-E81KAdXf.mjs → device-remote-AkYCKS8U.mjs} +7 -7
  19. package/dist/{doctor-CqPeMsSm.mjs → doctor-Bmv86UUW.mjs} +11 -12
  20. package/dist/{doctor-D_CX7ib5.mjs → doctor-fAMXC4vl.mjs} +4 -4
  21. package/dist/{error-diagnostics-Dw8Gn3yw.mjs → error-diagnostics-DHC31rXA.mjs} +62 -52
  22. package/dist/{gc-B7KkDinF.mjs → gc-GGu-8ULN.mjs} +66 -57
  23. package/dist/{guide-BS2Xc24o.mjs → guide-92CPA21s.mjs} +196 -13
  24. package/dist/{ios-B7KLgIp5.mjs → ios-BSXFeO5H.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-DPNF4C9s.mjs} +9 -6
  28. package/dist/{logs-query-oqn47Ffx.mjs → logs-query-R-Cqc6Qv.mjs} +22 -5
  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-BpCKiRlQ.mjs} +3 -3
  33. package/dist/{ownership-claim-7zRxxTfZ.mjs → ownership-claim-Bf2CAfw3.mjs} +4 -3
  34. package/dist/{project-BBTVABUz.mjs → project-fKmuIaMI.mjs} +2 -2
  35. package/dist/{reclaim-C5Jmuahx.mjs → reclaim-iW9Cad7k.mjs} +80 -74
  36. package/dist/{reload-Z8nKifAk.mjs → reload-DazHDSFA.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-cci0xvmC.mjs +19 -0
  42. package/dist/{start-BrY4EuIg.mjs → start-mObjQH2W.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-BxQNqu9t.mjs} +55 -24
  47. package/dist/{stop-B7DR4awK.mjs → stop-CrHGcjrV.mjs} +2 -2
  48. package/dist/{stop-Btg770vV.mjs → stop-DimGXEKY.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-ClFp1Aax.mjs} +887 -888
  52. package/dist/{workspace-process-lock-BKfdys1q.mjs → workspace-process-lock-3NcQnZ0X.mjs} +1 -1
  53. package/dist/{worktree-BPTbI-LN.mjs → worktree-DxTgN1CJ.mjs} +12 -12
  54. package/dist/{worktree-C79xx_kM.mjs → worktree-Eq6TLH8N.mjs} +1 -1
  55. package/dist/{xcode-DeKCribl.mjs → xcode-D7sAp-Zy.mjs} +82 -56
  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,11 +1,12 @@
1
- import { f as getProject } from "./config-CMtIaTk9.mjs";
2
- import { f as readWorkspaceLaunches } from "./state-CZ-ER-8d.mjs";
3
- import { a as findProjectRoot } from "./project-BBTVABUz.mjs";
4
- import { i as resolveProjectMetro } from "./metro-COslRK_F.mjs";
1
+ import { L as deviceSlotPlatforms, R as parseDeviceSlotKey, f as getProject } from "./config-CcefMY2Q.mjs";
2
+ import { f as readWorkspaceLaunches } from "./state-B3XA2wp2.mjs";
3
+ import { a as findProjectRoot } from "./project-fKmuIaMI.mjs";
4
+ import { i as resolveProjectMetro } from "./metro-CORqXyTL.mjs";
5
5
  import { l as phaseLine } from "./command-output-8wHMEde0.mjs";
6
- import { u as androidAppProcess, y as iosAppProcess } from "./app-install-NFxUahnH.mjs";
7
- import { x as resolveOwnedIosSim } from "./ios-D1h1Di39.mjs";
8
- import { N as resolveOwnedAvdSerial } from "./android-K63_0UfF.mjs";
6
+ import { N as resolveOwnedAvdSerial } from "./android-BqVIJoG7.mjs";
7
+ import { u as androidAppProcess, y as iosAppProcess } from "./app-install-DIp9riNx.mjs";
8
+ import { x as resolveOwnedIosSim } from "./ios-Deh5js5V.mjs";
9
+ import { n as nativeRunCommand } from "./slot-launch-cci0xvmC.mjs";
9
10
  import chalk from "chalk";
10
11
  import { WebSocket } from "ws";
11
12
  //#region src/engine/reload.ts
@@ -134,21 +135,22 @@ function failure(code, message, remedy) {
134
135
  function describe(error) {
135
136
  return error instanceof Error ? error.message : String(error);
136
137
  }
137
- function processFailure(platform, record, d) {
138
+ function processFailure(platform, record, d, slot = "default") {
138
139
  const process = platform === "ios" ? d.iosProcess(record.deviceId, record.appId) : d.androidProcess(record.deviceId, record.appId);
139
140
  if (process === void 0) {
140
141
  const probe = platform === "ios" ? `xcrun simctl spawn ${record.deviceId} launchctl list` : `adb -s ${record.deviceId} shell pidof ${record.appId}`;
141
142
  return failure("STIM_RELOAD_PROBE_FAILED", `Stim could not determine whether ${record.appId} is running on ${record.deviceId}.`, `Run \`${probe}\` and retry when the device tool responds.`);
142
143
  }
143
- if (process === null) return failure("STIM_RELOAD_STOPPED", `${record.appId} is not running on ${record.deviceId}.`, `Run \`stim ${platform}\` to launch it.`);
144
+ if (process === null) return failure("STIM_RELOAD_STOPPED", `${record.appId} is not running on ${record.deviceId}.`, `Run \`${nativeRunCommand(platform, slot)}\` to launch it.`);
144
145
  return null;
145
146
  }
146
- function inspectTarget(platform, record, project, d) {
147
+ function inspectTarget(platform, record, project, d, slot = "default") {
148
+ const runCommand = nativeRunCommand(platform, slot);
147
149
  if (platform === "ios") {
148
150
  const configured = project.platforms?.ios;
149
151
  if (!configured?.owned || configured.deviceUdid !== record.deviceId) return {
150
152
  platform,
151
- error: failure("STIM_RELOAD_UNOWNED", `The recorded iOS launch on ${record.deviceId} is not this workspace's current owned simulator.`, "Run `stim ios` to launch the app on this workspace's owned simulator.")
153
+ error: failure("STIM_RELOAD_UNOWNED", `The recorded iOS launch on ${record.deviceId} is not this workspace's current owned simulator.`, `Run \`${runCommand}\` to launch the app on this workspace's owned simulator.`)
152
154
  };
153
155
  let resolved;
154
156
  try {
@@ -161,15 +163,16 @@ function inspectTarget(platform, record, project, d) {
161
163
  }
162
164
  if (!resolved.sim || resolved.sim.state !== "Booted") return {
163
165
  platform,
164
- error: failure("STIM_RELOAD_STOPPED", `The recorded iOS app is not running on a booted owned simulator.`, "Run `stim ios` to boot, install, and launch it.")
166
+ error: failure("STIM_RELOAD_STOPPED", `The recorded iOS app is not running on a booted owned simulator.`, `Run \`${runCommand}\` to boot, install, and launch it.`)
165
167
  };
166
- const stopped = processFailure(platform, record, d);
168
+ const stopped = processFailure(platform, record, d, slot);
167
169
  if (stopped) return {
168
170
  platform,
169
171
  error: stopped
170
172
  };
171
173
  return {
172
174
  platform,
175
+ slot,
173
176
  record,
174
177
  deviceName: resolved.sim.name
175
178
  };
@@ -177,7 +180,7 @@ function inspectTarget(platform, record, project, d) {
177
180
  const configured = project.platforms?.android;
178
181
  if (!configured?.owned || !configured.avdName) return {
179
182
  platform,
180
- error: failure("STIM_RELOAD_UNOWNED", `The recorded Android launch on ${record.deviceId} is not this workspace's current owned emulator.`, "Run `stim android` to launch the app on this workspace's owned emulator.")
183
+ error: failure("STIM_RELOAD_UNOWNED", `The recorded Android launch on ${record.deviceId} is not this workspace's current owned emulator.`, `Run \`${runCommand}\` to launch the app on this workspace's owned emulator.`)
181
184
  };
182
185
  let resolved;
183
186
  try {
@@ -190,15 +193,16 @@ function inspectTarget(platform, record, project, d) {
190
193
  }
191
194
  if (!resolved.serial || resolved.serial !== record.deviceId) return {
192
195
  platform,
193
- error: failure("STIM_RELOAD_STOPPED", `The recorded Android app is not running on this workspace's owned emulator.`, "Run `stim android` to boot, install, and launch it.")
196
+ error: failure("STIM_RELOAD_STOPPED", `The recorded Android app is not running on this workspace's owned emulator.`, `Run \`${runCommand}\` to boot, install, and launch it.`)
194
197
  };
195
- const stopped = processFailure(platform, record, d);
198
+ const stopped = processFailure(platform, record, d, slot);
196
199
  if (stopped) return {
197
200
  platform,
198
201
  error: stopped
199
202
  };
200
203
  return {
201
204
  platform,
205
+ slot,
202
206
  record,
203
207
  deviceName: configured.avdName
204
208
  };
@@ -217,12 +221,16 @@ async function runReload({ root, platform = null, deps = {} }) {
217
221
  error: failure("STIM_NO_PROJECT", `No Stim environment is registered for ${root}.`, "Run `stim start` first.")
218
222
  };
219
223
  const launches = d.readLaunches(root);
220
- const inspected = (platform ? [platform] : ["ios", "android"]).flatMap((candidate) => {
221
- const record = launches[candidate];
222
- return record ? [inspectTarget(candidate, record, project, d)] : [];
224
+ const inspected = Object.entries(launches).flatMap(([key, record]) => {
225
+ const parsed = parseDeviceSlotKey(key);
226
+ if (!parsed || platform && parsed.platform !== platform) return [];
227
+ return [inspectTarget(parsed.platform, record, {
228
+ ...project,
229
+ platforms: deviceSlotPlatforms(project, parsed.slot)
230
+ }, d, parsed.slot)];
223
231
  });
224
232
  const live = inspected.filter((target) => !isTargetFailure(target));
225
- if (live.length > 1) return {
233
+ if (new Set(live.map((target) => target.platform)).size > 1) return {
226
234
  ok: false,
227
235
  error: failure("STIM_RELOAD_AMBIGUOUS", "Both the iOS and Android apps are running.", "Choose one with `stim reload ios` or `stim reload android`.")
228
236
  };
@@ -230,10 +238,10 @@ async function runReload({ root, platform = null, deps = {} }) {
230
238
  ok: false,
231
239
  error: inspected.find(isTargetFailure)?.error ?? failure("STIM_RELOAD_STOPPED", platform ? `No ${platform} app launch is recorded for this workspace.` : "No live app launch is recorded.", platform ? `Run \`stim ${platform}\` first.` : "Run `stim ios` or `stim android` first.")
232
240
  };
233
- const target = live[0];
241
+ const target = live.find((candidate) => !candidate.record.release && candidate.record.metroPort === project.metroPort) ?? live[0];
234
242
  if (target.record.release) return {
235
243
  ok: false,
236
- error: failure("STIM_RELOAD_RELEASE", `${target.record.appId} was launched with an embedded release bundle.`, `Run \`stim ${target.platform}\` with a Debug configuration or variant before reloading JavaScript.`)
244
+ error: failure("STIM_RELOAD_RELEASE", `${target.record.appId} was launched with an embedded release bundle.`, `Run \`${nativeRunCommand(target.platform, target.slot)}\` with a Debug configuration or variant before reloading JavaScript.`)
237
245
  };
238
246
  const port = target.record.metroPort;
239
247
  if (!port || project.metroPort !== port) return {
@@ -244,7 +252,7 @@ async function runReload({ root, platform = null, deps = {} }) {
244
252
  ok: false,
245
253
  error: failure("STIM_NO_METRO", `Port ${port} is not serving this workspace's Metro.`, "Run `stim start`, then retry.")
246
254
  };
247
- const stopped = processFailure(target.platform, target.record, d);
255
+ const stopped = processFailure(target.platform, target.record, d, target.slot);
248
256
  if (stopped) return {
249
257
  ok: false,
250
258
  error: stopped
@@ -254,7 +262,7 @@ async function runReload({ root, platform = null, deps = {} }) {
254
262
  appId: target.record.appId
255
263
  });
256
264
  if (!reloaded.ok) {
257
- const stoppedAfterMetro = processFailure(target.platform, target.record, d);
265
+ const stoppedAfterMetro = processFailure(target.platform, target.record, d, target.slot);
258
266
  if (stoppedAfterMetro) return {
259
267
  ok: false,
260
268
  error: stoppedAfterMetro
@@ -1,6 +1,6 @@
1
- import { p as readWorkspaceState } from "./state-CZ-ER-8d.mjs";
1
+ import { p as readWorkspaceState } from "./state-B3XA2wp2.mjs";
2
2
  import { n as supervisorError } from "./errors-B6W7FkxQ.mjs";
3
- import { C as projectMetroSharedCache } from "./settings-DwU1_U_q.mjs";
3
+ import { C as projectMetroSharedCache } from "./settings-DEJf6_kJ.mjs";
4
4
  import { a as metroStoreRoot, o as registerMetroStore, t as appendCacheStore } from "./metro-store-CDDZHtPl.mjs";
5
5
  import { createRequire } from "node:module";
6
6
  import { dirname, isAbsolute, join, relative, sep } from "node:path";
@@ -0,0 +1,2 @@
1
+ import { p as startExpoServer } from "./server-expo-DizT4OMk.mjs";
2
+ export { startExpoServer };
@@ -1,10 +1,10 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { r as createNdjsonWriter } from "./ndjson-DcAtEx_K.mjs";
3
- import { p as readWorkspaceState } from "./state-CZ-ER-8d.mjs";
4
- import { l as resolvePackageJson } from "./project-BBTVABUz.mjs";
2
+ import { r as createNdjsonWriter } from "./ndjson-BZwI1NBv.mjs";
3
+ import { p as readWorkspaceState } from "./state-B3XA2wp2.mjs";
4
+ import { l as resolvePackageJson } from "./project-fKmuIaMI.mjs";
5
5
  import { n as supervisorError } from "./errors-B6W7FkxQ.mjs";
6
6
  import { n as stripAnsi, t as createLineReader } from "./process-output-C7IlIzd7.mjs";
7
- import { C as projectMetroSharedCache } from "./settings-DwU1_U_q.mjs";
7
+ import { C as projectMetroSharedCache } from "./settings-DEJf6_kJ.mjs";
8
8
  import { a as metroStoreRoot, i as metroStoreConfirmedRoot, n as expoMetroConfigPath, o as registerMetroStore, r as expoMetroStoreEnv } from "./metro-store-CDDZHtPl.mjs";
9
9
  import { accessSync, constants, existsSync, readFileSync } from "node:fs";
10
10
  import { dirname, join, parse } from "node:path";
@@ -1,5 +1,5 @@
1
- import { d as getConfigPath, g as loadConfig, m as getRepoSettings, p as getProjectSettings } from "./config-CMtIaTk9.mjs";
2
- import { g as repoRoot, s as gitCommonDir } from "./worktree-C79xx_kM.mjs";
1
+ import { d as getConfigPath, g as loadConfig, m as getRepoSettings, p as getProjectSettings } from "./config-CcefMY2Q.mjs";
2
+ import { g as repoRoot, s as gitCommonDir } from "./worktree-Eq6TLH8N.mjs";
3
3
  import { createHash } from "node:crypto";
4
4
  import { isAbsolute } from "node:path";
5
5
  import { existsSync, readFileSync, realpathSync, statSync } from "fs";
@@ -0,0 +1,19 @@
1
+ import { f as getProject } from "./config-CcefMY2Q.mjs";
2
+ import { p as readWorkspaceState } from "./state-B3XA2wp2.mjs";
3
+ import { h as deviceShellArg } from "./app-install-DIp9riNx.mjs";
4
+ import { n as fileLeaseIo } from "./device-lease-BJYybF-2.mjs";
5
+ //#region src/engine/slot-launch.ts
6
+ function launchSlotScope(root, slot = "default") {
7
+ if (slot !== "default") return slot;
8
+ if (Object.keys(getProject(root)?.deviceSlots ?? {}).length) return slot;
9
+ const state = readWorkspaceState(root);
10
+ if (Object.keys({
11
+ ...state?.collectors,
12
+ ...fileLeaseIo.readHolder(root)
13
+ }).some((key) => key.includes(":"))) return slot;
14
+ }
15
+ function nativeRunCommand(platform, slot = "default", { physical, deviceId } = {}) {
16
+ return `stim ${platform}${slot === "default" ? "" : ` --slot ${slot}`}${physical && deviceId ? ` --device ${deviceShellArg(deviceId)}` : ""}`;
17
+ }
18
+ //#endregion
19
+ export { nativeRunCommand as n, launchSlotScope as t };
@@ -1,23 +1,23 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { E as inspectProcessIdentity, O as waitForProcessExit, S as upsertProject, T as captureProcessToken, a as clearSupervisor, f as getProject } from "./config-CMtIaTk9.mjs";
3
- import { C as supervisorLogFile, D as workspaceLogsDir, _ as writeWorkspaceState, c as readMetroTunnel, i as clearManagedMetroTunnel, p as readWorkspaceState, s as clearWorkspaceSupervisor, y as ensureWorkspaceStorage } from "./state-CZ-ER-8d.mjs";
4
- import { g as repoRoot, s as gitCommonDir } from "./worktree-C79xx_kM.mjs";
5
- import { a as findProjectRoot, i as detectIsExpo, n as detectAndroidPackage, r as detectBundleId, t as appProjectProblem } from "./project-BBTVABUz.mjs";
2
+ import { E as inspectProcessIdentity, O as waitForProcessExit, S as upsertProject, T as captureProcessToken, a as clearSupervisor, f as getProject } from "./config-CcefMY2Q.mjs";
3
+ import { C as supervisorLogFile, D as workspaceLogsDir, _ as writeWorkspaceState, c as readMetroTunnel, i as clearManagedMetroTunnel, p as readWorkspaceState, s as clearWorkspaceSupervisor, y as ensureWorkspaceStorage } from "./state-B3XA2wp2.mjs";
4
+ import { g as repoRoot, s as gitCommonDir } from "./worktree-Eq6TLH8N.mjs";
5
+ import { a as findProjectRoot, i as detectIsExpo, n as detectAndroidPackage, r as detectBundleId, t as appProjectProblem } from "./project-fKmuIaMI.mjs";
6
6
  import { n as supervisorError } from "./errors-B6W7FkxQ.mjs";
7
- import { i as resolveProjectMetro, n as killMetroTree, o as reserveMetroPort, r as pidExists } from "./metro-COslRK_F.mjs";
8
- import { A as resolveSettings, D as remoteDeviceSettingError, E as remoteAndroidSetting, F as tunnelModeSetting, I as unknownSettingKeys, L as PUBLIC_METRO_ENV, N as settingShapeErrors, O as remoteIosSetting, R as detectProviders, S as ngrokUrlSetting, T as publicUrlSetting, a as SETTING_SHAPE_REMEDY, b as metroTunnelSettingError, d as cacheProviderSettingError, k as resolveCacheProviderConfig, z as planMetroReach } from "./settings-DwU1_U_q.mjs";
7
+ import { i as resolveProjectMetro, n as killMetroTree, o as reserveMetroPort, r as pidExists } from "./metro-CORqXyTL.mjs";
9
8
  import { g as stepTimer, l as phaseLine } from "./command-output-8wHMEde0.mjs";
10
- import { n as workspaceProcessLockError, t as withWorkspaceProcessLock } from "./workspace-process-lock-BKfdys1q.mjs";
9
+ import { A as resolveSettings, D as remoteDeviceSettingError, E as remoteAndroidSetting, F as tunnelModeSetting, I as unknownSettingKeys, L as PUBLIC_METRO_ENV, N as settingShapeErrors, O as remoteIosSetting, R as detectProviders, S as ngrokUrlSetting, T as publicUrlSetting, a as SETTING_SHAPE_REMEDY, b as metroTunnelSettingError, d as cacheProviderSettingError, k as resolveCacheProviderConfig, z as planMetroReach } from "./settings-DEJf6_kJ.mjs";
11
10
  import { n as expoMetroConfigPath } from "./metro-store-CDDZHtPl.mjs";
12
- import { o as expoSdkMajor } from "./server-expo-C3ktAwUG.mjs";
11
+ import { o as expoSdkMajor } from "./server-expo-DizT4OMk.mjs";
12
+ import { n as workspaceProcessLockError, t as withWorkspaceProcessLock } from "./workspace-process-lock-3NcQnZ0X.mjs";
13
13
  import { t as spawnEntry } from "./spawn-entry-nCE6m770.mjs";
14
- import { s as queryLogs } from "./logs-query-oqn47Ffx.mjs";
15
- import { a as withManagedRemoteWorktreeLock, i as terminateChild, n as startTunnelSequence, r as stopTunnel, s as withManagedTunnelLock, t as resolveSupervisorTarget } from "./ownership-CYOyqxUo.mjs";
14
+ import { s as queryLogs } from "./logs-query-R-Cqc6Qv.mjs";
15
+ import { a as withManagedRemoteWorktreeLock, i as terminateChild, n as startTunnelSequence, r as stopTunnel, s as withManagedTunnelLock, t as resolveSupervisorTarget } from "./ownership-BpCKiRlQ.mjs";
16
16
  import { mkdirSync, openSync, readFileSync } from "node:fs";
17
17
  import { randomUUID } from "node:crypto";
18
18
  import { dirname } from "node:path";
19
- import chalk from "chalk";
20
19
  import { CACHE_PROVIDER_ENV, cacheProviderEnv } from "@stim-cli/cache";
20
+ import chalk from "chalk";
21
21
  //#region src/supervisor/cache-reset.ts
22
22
  async function resetMetroCache(root) {
23
23
  if (detectIsExpo(root) && ((expoSdkMajor(root) ?? 0) < 54 || !expoMetroConfigPath())) throw supervisorError("STIM_BAD_ARG", "An isolated Metro cache reset requires Expo SDK 54 or newer and the Stim Metro config adapter.", "Upgrade Expo or repair the Stim installation before retrying. The running server was not changed.");
@@ -1,4 +1,4 @@
1
- import { D as sameProcessRecord, k as withDirLock, u as getConfigDir } from "./config-CMtIaTk9.mjs";
1
+ import { D as sameProcessRecord, I as deviceSlotKey, R as parseDeviceSlotKey, k as withDirLock, u as getConfigDir } from "./config-CcefMY2Q.mjs";
2
2
  import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { dirname } from "node:path";
4
4
  import { join as join$1, resolve as resolve$1 } from "path";
@@ -122,14 +122,14 @@ function parseWorkspaceLaunchRecord(value) {
122
122
  function readWorkspaceLaunches(root) {
123
123
  const launches = readWorkspaceState(root)?.launches;
124
124
  if (!launches || typeof launches !== "object" || Array.isArray(launches)) return {};
125
- const ios = parseWorkspaceLaunchRecord(launches.ios);
126
- const android = parseWorkspaceLaunchRecord(launches.android);
127
- return {
128
- ...ios ? { ios } : {},
129
- ...android ? { android } : {}
130
- };
125
+ const records = {};
126
+ for (const [key, value] of Object.entries(launches)) {
127
+ const record = parseWorkspaceLaunchRecord(value);
128
+ if (parseDeviceSlotKey(key) && record) records[key] = record;
129
+ }
130
+ return records;
131
131
  }
132
- function writeWorkspaceLaunch(root, platform, record) {
132
+ function writeWorkspaceLaunch(root, platform, record, slot = "default") {
133
133
  withWorkspaceStateLock(root, () => {
134
134
  const state = readWorkspaceState(root) ?? {};
135
135
  const launches = state.launches && typeof state.launches === "object" ? state.launches : {};
@@ -137,7 +137,7 @@ function writeWorkspaceLaunch(root, platform, record) {
137
137
  ...state,
138
138
  launches: {
139
139
  ...launches,
140
- [platform]: record
140
+ [deviceSlotKey(platform, slot)]: record
141
141
  }
142
142
  });
143
143
  });
@@ -1,4 +1,4 @@
1
- import { C as withConfigLock, u as getConfigDir } from "./config-CMtIaTk9.mjs";
1
+ import { C as withConfigLock, u as getConfigDir } from "./config-CcefMY2Q.mjs";
2
2
  import { readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { basename, dirname, join, relative, resolve } from "node:path";
4
4
  const PLATFORMS = ["ios", "android"];
@@ -1,7 +1,7 @@
1
- import { g as repoRoot, s as gitCommonDir } from "./worktree-C79xx_kM.mjs";
2
- import { a as findProjectRoot } from "./project-BBTVABUz.mjs";
1
+ import { g as repoRoot, s as gitCommonDir } from "./worktree-Eq6TLH8N.mjs";
2
+ import { a as findProjectRoot } from "./project-fKmuIaMI.mjs";
3
3
  import { o as formatLongDuration } from "./command-output-8wHMEde0.mjs";
4
- import { a as statsProjectKey, r as readStats } from "./stats-BCWEVLrM.mjs";
4
+ import { a as statsProjectKey, r as readStats } from "./stats-BR-kwj0B.mjs";
5
5
  import chalk from "chalk";
6
6
  //#region src/commands/stats.ts
7
7
  const PLATFORMS = ["ios", "android"];
@@ -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-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";
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-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-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-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,