stim 1.13.0 → 1.15.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 (133) hide show
  1. package/README.md +2 -0
  2. package/dist/{activity-DfVKZTEB.mjs → activity-B2-kb7k0.mjs} +281 -41
  3. package/dist/{android-BHfAu2mf.mjs → android-CYmq0jNi.mjs} +2 -2
  4. package/dist/{android-CD8pr8Q2.mjs → android-Cu_L4RpL.mjs} +524 -412
  5. package/dist/{android-qx_31wJo.mjs → android-Db2eEMaf.mjs} +152 -65
  6. package/dist/android-cas-PnKfAdB2.mjs +167 -0
  7. package/dist/android-cas-compiler.mjs +2 -2
  8. package/dist/{app-install-CDFY2nUL.mjs → app-install-CAAplXAt.mjs} +162 -173
  9. package/dist/{budget-E5o91NR1.mjs → budget-CMiqghig.mjs} +1048 -77
  10. package/dist/{build-plan-BTa93o_r.mjs → build-plan-aeGMFX7e.mjs} +193 -650
  11. package/dist/build-progress-C2Jv1AtA.mjs +872 -0
  12. package/dist/build-slots-Df1ApSuH.mjs +127 -0
  13. package/dist/{cache-manifest-DWPF4RKJ.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
  14. package/dist/cache-manifest.mjs +1 -1
  15. package/dist/{chrome-B5hWgQJS.mjs → chrome-BrcVWQyD.mjs} +4 -4
  16. package/dist/cli-0cX5s4NM.mjs +45 -0
  17. package/dist/cli.mjs +6 -42
  18. package/dist/client-C2IS36Qf.mjs +1333 -0
  19. package/dist/collector-run.mjs +8 -7
  20. package/dist/{command-output-BIL7rlX7.mjs → command-output-CliHHn7a.mjs} +1 -1
  21. package/dist/{config-0jCcMM0E.mjs → config-D0DbpZtM.mjs} +29 -29
  22. package/dist/{created-devices-Cd558h31.mjs → created-devices-BwVb3liH.mjs} +2 -2
  23. package/dist/{dependency-state-DNN0yfx6.mjs → dependency-state-BmsjKZ2H.mjs} +2 -2
  24. package/dist/{deps-2Wo81np9.mjs → deps-BFlKbOIp.mjs} +3 -3
  25. package/dist/{detached-entry-DJHoca49.mjs → detached-entry-CiTEgMR1.mjs} +1 -1
  26. package/dist/dev-client-CjzLoNs0.mjs +80 -0
  27. package/dist/{device-Cf7vl-so.mjs → device-BZ1IyS5A.mjs} +7 -7
  28. package/dist/device-capacity-DMuKC7nx.mjs +136 -0
  29. package/dist/device-host-worker.d.mts +1 -0
  30. package/dist/device-host-worker.mjs +769 -0
  31. package/dist/device-ios-DMQBUPus.mjs +381 -0
  32. package/dist/{device-lease-CL9dZf73.mjs → device-lease-W5Pi1q38.mjs} +43 -36
  33. package/dist/{device-pool-MuoiyRcu.mjs → device-pool-B2r38YjS.mjs} +4 -4
  34. package/dist/{device-remote-DT9ngZiD.mjs → device-remote-9ulPsdhY.mjs} +68 -162
  35. package/dist/{doctor-D2iysNNH.mjs → doctor-BcE5xsBn.mjs} +191 -99
  36. package/dist/{error-diagnostics-BoiOhck8.mjs → error-diagnostics-Cb0P1WLS.mjs} +12 -11
  37. package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
  38. package/dist/{gc-eA-PRBX8.mjs → gc-BuL1Genr.mjs} +430 -102
  39. package/dist/{android-cas-B9Kv9aIy.mjs → gradle-CmdOBq1d.mjs} +8 -294
  40. package/dist/{guide-BfZ0OmiP.mjs → guide-cCSGRFd3.mjs} +2052 -240
  41. package/dist/{guide-status-BaMYu55M.mjs → guide-status-CIoWK6mG.mjs} +3 -3
  42. package/dist/hosted-macos-DwowNU7W.mjs +447 -0
  43. package/dist/idle-shutdown-B5LPaNyY.mjs +122 -0
  44. package/dist/in-use-CndPMilN.mjs +110 -0
  45. package/dist/{ios-CmEzZiOj.mjs → ios-3CtCRhwr.mjs} +201 -10
  46. package/dist/{ios-CcfCeZ19.mjs → ios-Chob8ny7.mjs} +560 -259
  47. package/dist/{ios-device-BW-t_Lkv.mjs → ios-device-DERmMmk-.mjs} +12 -7
  48. package/dist/{ios-device-BiOsSVzd.mjs → ios-device-Ee7sDXfp.mjs} +1 -1
  49. package/dist/{launch-verify-CuJaBpEa.mjs → launch-verify-CLlofxRt.mjs} +42 -11
  50. package/dist/{logs-DcSRbC2t.mjs → logs-h9h6MUvE.mjs} +95 -13
  51. package/dist/machine-usage-D_YYMzfR.mjs +283 -0
  52. package/dist/machines-CYtOCqcj.mjs +259 -0
  53. package/dist/macos-BwtbUtTN.mjs +468 -0
  54. package/dist/macos-run.d.mts +5 -0
  55. package/dist/macos-run.mjs +2 -0
  56. package/dist/{merge-state-ytFQCeh4.mjs → merge-state-BnxLNQiM.mjs} +22 -12
  57. package/dist/{metro-BPeY1YMh.mjs → metro-Di8EN2Tb.mjs} +20 -120
  58. package/dist/{named-ports-n7tnJsI8.mjs → named-ports-Dj_XBNaL.mjs} +8 -64
  59. package/dist/native-run-DFDoIRbY.mjs +147 -0
  60. package/dist/{native-runtime-CiiFHt0e.mjs → native-runtime-i6OkVg-w.mjs} +11 -6
  61. package/dist/offload-worker.d.mts +121 -0
  62. package/dist/offload-worker.mjs +601 -0
  63. package/dist/{ownership-Bz2kETAS.mjs → ownership-BkYxUtuj.mjs} +3 -2
  64. package/dist/{page-BTu7I60K.mjs → page-QF_Ke10s.mjs} +7 -2
  65. package/dist/{ports-C5UutqpJ.mjs → ports-DddwttKM.mjs} +3 -3
  66. package/dist/prebuild-BvxDlD6C.mjs +213 -0
  67. package/dist/process-identity-BmRm1qV0.mjs +12 -0
  68. package/dist/{project-DGeclhvO.mjs → project-CV3tDFHl.mjs} +9 -18
  69. package/dist/pull-request-pxnDCQzn.mjs +322 -0
  70. package/dist/pull-requests.d.mts +120 -0
  71. package/dist/pull-requests.mjs +2 -0
  72. package/dist/recordings-GEXp4iBT.mjs +37 -0
  73. package/dist/{reload-C-cV5yy_.mjs → reload-ChPiwvot.mjs} +70 -22
  74. package/dist/{remote-cache--aehr4Pr.mjs → remote-cache-CO2elm_b.mjs} +4 -4
  75. package/dist/run-B-xqRrsg.mjs +119 -0
  76. package/dist/{server-bare-Bq8BpSIo.mjs → server-bare-X7PUgizL.mjs} +60 -13
  77. package/dist/{server-expo-DUCg52-K.mjs → server-expo-DhtX6bom.mjs} +6 -10
  78. package/dist/server-expo-KJeD109I.mjs +2 -0
  79. package/dist/{settings-xB0Frkg6.mjs → settings-CduVcoxj.mjs} +77 -26
  80. package/dist/{settings-ssVBToDR.mjs → settings-DbL6uhH8.mjs} +16 -10
  81. package/dist/settings.schema.json +357 -2
  82. package/dist/{simslim-Bbg9UWrt.mjs → simslim-Ciluxlq4.mjs} +8 -132
  83. package/dist/slot-launch-BY0BrOLh.mjs +65 -0
  84. package/dist/{spawn-claims-PT8aQpCa.mjs → spawn-claims-D0XfSe8u.mjs} +1 -1
  85. package/dist/spawn-entry-DncpJvWL.mjs +16 -0
  86. package/dist/stage-CzaiwFsI.mjs +143 -0
  87. package/dist/start-CPO4wRg7.mjs +2 -0
  88. package/dist/{start-BZhItkiL.mjs → start-CUD9nasA.mjs} +108 -61
  89. package/dist/state-B4PQFXMW.d.mts +12 -0
  90. package/dist/state-BxCgeTQu.mjs +41 -0
  91. package/dist/{state-DwAU2_-D.mjs → state-CaeaHbDA.mjs} +7 -4
  92. package/dist/{state-l1pYwOre.mjs → state-DFm77wRn.mjs} +67 -5
  93. package/dist/stats-DBLLFmKd.mjs +66 -0
  94. package/dist/{status-TJlL0HWo.mjs → status-DYXxNOT0.mjs} +425 -260
  95. package/dist/{status-sszP4XKU.mjs → status-l84Gbvp2.mjs} +122 -20
  96. package/dist/stdout-reader-C8yjre1x.mjs +23 -0
  97. package/dist/stim-desktop-Cnz8CPnl.mjs +63 -0
  98. package/dist/stim-footprint.swift +32 -0
  99. package/dist/{stim-installations-CINXy_su.mjs → stim-installations-QMEI32v1.mjs} +1 -1
  100. package/dist/{stop-BZ2qMiBP.mjs → stop-Bad61z_2.mjs} +128 -19
  101. package/dist/stop-Bvf1MflS.mjs +48 -0
  102. package/dist/{stop-DskDKNTU.mjs → stop-CceYh1yG.mjs} +2 -2
  103. package/dist/stop-cause-CZ_Mr-v9.mjs +665 -0
  104. package/dist/supervisor-run.d.mts +44 -12
  105. package/dist/supervisor-run.mjs +276 -33
  106. package/dist/support-CDQbDkL5.mjs +315 -0
  107. package/dist/support-WLOzVKUv.mjs +176 -0
  108. package/dist/teardown-BYuQafiA.mjs +718 -0
  109. package/dist/{xcode-LM6A8cNZ.mjs → toolchain-DmeYjXD9.mjs} +363 -13
  110. package/dist/warm-progress-DVHLN4uC.mjs +463 -0
  111. package/dist/{idle-DxLi5OkN.mjs → watchman-Bx0wceoF.mjs} +55 -8
  112. package/dist/{web-sWXd5iek.mjs → web-k6w8-HnE.mjs} +39 -22
  113. package/dist/web-run.mjs +326 -13
  114. package/dist/workspace-process-lock-BL7Q0HCO.mjs +57 -0
  115. package/dist/workspace-state-CL2V2Ons.mjs +371 -0
  116. package/dist/worktree-BT_uULFx.mjs +2 -0
  117. package/dist/{worktree-DeBQSVtR.mjs → worktree-C0c5Udwa.mjs} +55 -54
  118. package/dist/{worktree-D4Do_XiQ.mjs → worktree-qXKzADSC.mjs} +161 -255
  119. package/package.json +10 -5
  120. package/shim/bundle-response.cjs +55 -10
  121. package/shim/bundle-response.d.cts +1 -0
  122. package/shim/expo-metro-config.cjs +10 -2
  123. package/dist/build-progress-l7d-X-ZP.mjs +0 -247
  124. package/dist/in-use-wYEaKQM9.mjs +0 -249
  125. package/dist/ownership-PUP4Dwai.mjs +0 -1138
  126. package/dist/server-expo-BeYwP_Xy.mjs +0 -2
  127. package/dist/slot-launch-BIWEW-wI.mjs +0 -19
  128. package/dist/start-Rw2GD3nV.mjs +0 -2
  129. package/dist/stats-CaSwmsSZ.mjs +0 -62
  130. package/dist/stats-D8rUTPHd.mjs +0 -368
  131. package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
  132. package/dist/worktree-3cBa-bQt.mjs +0 -2
  133. package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
@@ -1,7 +1,9 @@
1
- import { U as projectDeviceSlots, z as deviceSlotKey } from "./config-0jCcMM0E.mjs";
2
- import { a as formatElapsed, o as formatLongDuration, r as clockTime, u as plural } from "./command-output-BIL7rlX7.mjs";
3
- import { r as cdpEndpoint } from "./state-l1pYwOre.mjs";
4
- import { sep } from "path";
1
+ import { H as projectDeviceSlots, R as deviceSlotKey } from "./config-D0DbpZtM.mjs";
2
+ import { a as formatElapsed, o as formatLongDuration, r as clockTime, u as plural } from "./command-output-CliHHn7a.mjs";
3
+ import { n as isInsideProject } from "./metro-Di8EN2Tb.mjs";
4
+ import { r as cdpEndpoint } from "./state-DFm77wRn.mjs";
5
+ import { basename, sep } from "path";
6
+ import { ACTIVITY_RECENCY_BASES } from "@stim-cli/core/state";
5
7
  //#region src/status.ts
6
8
  const IOS_SIM_MB = 1500;
7
9
  const ANDROID_EMULATOR_MB = 2500;
@@ -31,7 +33,7 @@ function avdExpected({ slot, serial, launches, leasedIds, running, now }) {
31
33
  if (!launch) return false;
32
34
  return running || now - Date.parse(launch.launchedAt) < 18e5;
33
35
  }
34
- function androidStatus(android, androidRuntime, androidDeviceProfile) {
36
+ function androidStatus(android, androidRuntime, androidDeviceProfile, facts) {
35
37
  if (!android) return null;
36
38
  return {
37
39
  name: android.avdName ?? android.serial,
@@ -41,10 +43,20 @@ function androidStatus(android, androidRuntime, androidDeviceProfile) {
41
43
  serial: androidRuntime.serial,
42
44
  state: androidRuntime.state
43
45
  } : {},
44
- ...androidDeviceProfile ? { deviceProfile: androidDeviceProfile } : {}
46
+ ...androidDeviceProfile ? { deviceProfile: androidDeviceProfile } : {},
47
+ ...facts
45
48
  };
46
49
  }
47
- function environmentState(project, { simsByUdid = {}, metro = null, worktrees = [], simsAvailable = true, androidRuntime = null, androidRuntimes = {}, androidDeviceProfile = null, androidDeviceProfiles = {}, supervisor = null, logs = null, remote = null, idleStop = null, launches = {}, leasedIds = /* @__PURE__ */ new Set(), now = Date.now(), slot = "default", workspaceRunning = false } = {}) {
50
+ function idleShutdownFact(stopped, record) {
51
+ return stopped && record ? { idleShutdown: record } : {};
52
+ }
53
+ function stoppedMetroFacts(idleStop, lastStop) {
54
+ return {
55
+ ...idleStop ? { idleStop } : {},
56
+ ...lastStop ? { lastStop } : {}
57
+ };
58
+ }
59
+ function environmentState(project, { simsByUdid = {}, metro = null, worktrees = [], simsAvailable = true, androidRuntime = null, androidRuntimes = {}, androidDeviceProfile = null, androidDeviceProfiles = {}, supervisor = null, logs = null, remote = null, idleStop = null, lastStop = null, idleShutdowns = {}, launches = {}, leasedIds = /* @__PURE__ */ new Set(), now = Date.now(), slot = "default", workspaceRunning = false, workspaces } = {}) {
48
60
  const ios = project.platforms?.ios;
49
61
  const android = project.platforms?.android;
50
62
  const sim = ios ? simsByUdid[ios.deviceUdid] : null;
@@ -67,9 +79,9 @@ function environmentState(project, { simsByUdid = {}, metro = null, worktrees =
67
79
  ...slot === "default" ? {} : { slot }
68
80
  });
69
81
  const slotFlag = slot === "default" ? "" : ` --slot ${slot}`;
70
- if (metro?.notOurs && slot === "default") add("port-not-ours", `port ${project.metroPort}: ${metro.notOurs}`, "stim guide errors teardown", "error");
71
- if (ios && !sim && simsAvailable) add("sim-missing", `recorded sim ${ios.deviceUdid} no longer exists`, `stim ios${slotFlag}`);
72
- if (simBooted && project.metroPort && !metroRunning) add("sim-without-metro", "simulator is booted with no Metro serving it", "stim start");
82
+ if (metro?.notOurs && slot === "default") add("port-not-ours", ...portNotOurs(project.metroPort, portHolder(metro.cwd, project.__path, workspaces), supervisor));
83
+ if (ios && !sim && simsAvailable) add("sim-missing", `recorded sim ${ios.deviceUdid} no longer exists`, `stim ios${slotFlag}`, "info");
84
+ if (simBooted && project.metroPort && !metroRunning) add("sim-without-metro", "simulator is booted with no Metro serving it", "stim start", "info");
73
85
  if (androidRuntime && android?.avdName) {
74
86
  const recordedSerial = android.consolePort ? `emulator-${android.consolePort}` : android.serial;
75
87
  const expected = avdExpected({
@@ -80,7 +92,7 @@ function environmentState(project, { simsByUdid = {}, metro = null, worktrees =
80
92
  running,
81
93
  now
82
94
  });
83
- for (const [code, message] of androidIssues(android, recordedSerial, androidRuntime, expected)) add(code, message, code === "avd-unchecked" ? "stim doctor" : `stim android${slotFlag}`);
95
+ for (const [code, message, severity] of androidIssues(android, recordedSerial, androidRuntime, expected)) add(code, message, code === "avd-unchecked" ? "stim doctor" : `stim android${slotFlag}`, severity);
84
96
  }
85
97
  if (supervisor?.status === "unverified") add("supervisor-unverified", `supervisor pid ${supervisor.pid} could not be verified${supervisor.reason ? `: ${supervisor.reason}` : ""}`, "stim guide errors teardown", "error");
86
98
  const slots = [];
@@ -95,6 +107,7 @@ function environmentState(project, { simsByUdid = {}, metro = null, worktrees =
95
107
  metro,
96
108
  androidRuntime: androidRuntimes[name],
97
109
  androidDeviceProfile: androidDeviceProfiles[name],
110
+ idleShutdowns,
98
111
  launches,
99
112
  leasedIds,
100
113
  now,
@@ -115,20 +128,23 @@ function environmentState(project, { simsByUdid = {}, metro = null, worktrees =
115
128
  ...slots.length ? { slots } : {},
116
129
  live,
117
130
  memoryMb,
118
- warnings: issues.map(issueText),
131
+ memorySource: "estimate",
132
+ warnings: warningTexts(issues),
119
133
  issues,
120
134
  ios: ios ? {
121
135
  name: sim?.name ?? null,
122
136
  udid: ios.deviceUdid,
123
137
  owned: Boolean(ios.owned),
124
- state: sim?.state ?? (simsAvailable ? "missing" : "unknown")
138
+ state: sim?.state ?? (simsAvailable ? "missing" : "unknown"),
139
+ ...idleShutdownFact(!simBooted && sim, idleShutdowns[deviceSlotKey("ios", slot)])
125
140
  } : null,
126
- android: androidStatus(android, androidRuntime, androidDeviceProfile),
141
+ android: androidStatus(android, androidRuntime, androidDeviceProfile, idleShutdownFact(!androidDetected, idleShutdowns[deviceSlotKey("android", slot)])),
127
142
  metro: project.metroPort ? {
128
143
  port: project.metroPort,
129
144
  running: metroRunning,
130
145
  pid: metro?.metro?.pid ?? null,
131
- ...idleStop && !metroRunning ? { idleStop } : {}
146
+ ...metroRunning ? {} : stoppedMetroFacts(idleStop, lastStop),
147
+ ...portHeldBy(metro)
132
148
  } : null,
133
149
  supervisor: supervisor && supervisor.status !== "stale" ? {
134
150
  pid: supervisor.pid ?? null,
@@ -144,6 +160,14 @@ function environmentState(project, { simsByUdid = {}, metro = null, worktrees =
144
160
  remoteDevices: remote ? [remote] : []
145
161
  };
146
162
  }
163
+ /** Sets each environment's `memoryMb` to the sum of its owners' footprints; without footprints it keeps the estimate. */
164
+ function applyMachineMemory(states, machine) {
165
+ if (machine.memorySource !== "footprint") return;
166
+ for (const state of states) {
167
+ state.memoryMb = machine.owners.reduce((sum, owner) => owner.workspace === state.path ? sum + owner.memoryMb : sum, 0);
168
+ state.memorySource = machine.memorySource;
169
+ }
170
+ }
147
171
  /** Adds the workspace's owned Chrome to its status: the web entry, its memory, and an unverifiable browser. */
148
172
  function withWebFacts(state, web) {
149
173
  if (!web) return state;
@@ -167,7 +191,7 @@ function withWebFacts(state, web) {
167
191
  ...state,
168
192
  live: state.live || running || web.status === "orphaned",
169
193
  memoryMb: state.memoryMb + (running || web.status === "orphaned" ? BROWSER_MB : 0),
170
- warnings: issues.map(issueText),
194
+ warnings: warningTexts(issues),
171
195
  issues,
172
196
  web: webBrowserState(web)
173
197
  };
@@ -184,20 +208,94 @@ function webBrowserState({ record, status }) {
184
208
  headless: record.headless,
185
209
  viewport: record.viewport,
186
210
  profile: record.profile,
187
- cdpEndpoint: running ? cdpEndpoint(record.cdpPort) : null
211
+ cdpEndpoint: running ? cdpEndpoint(record.cdpPort) : null,
212
+ targetId: running ? record.targetId ?? null : null
188
213
  };
189
214
  }
190
215
  function androidIssues(android, recordedSerial, runtime, expected) {
191
216
  const issues = [];
192
217
  if (runtime.serial && recordedSerial && runtime.serial !== recordedSerial) issues.push(["avd-serial-changed", `owned AVD ${android.avdName} changed serial (${recordedSerial} -> ${runtime.serial}), so Metro forwarding is lost; rerun with the same build options, then reopen agent-device on ${runtime.serial}`]);
193
- if (runtime.state === "missing") issues.push(["avd-missing", `recorded AVD ${android.avdName} no longer exists`]);
218
+ if (runtime.state === "missing") issues.push([
219
+ "avd-missing",
220
+ `recorded AVD ${android.avdName} no longer exists`,
221
+ "info"
222
+ ]);
194
223
  if (runtime.state === "not-detected" && expected) issues.push(["avd-not-detected", `owned AVD ${android.avdName} is not detected by adb`]);
195
224
  if (runtime.error) issues.push(["avd-unchecked", `could not check owned AVD ${android.avdName}: ${runtime.error}`]);
196
225
  return issues;
197
226
  }
227
+ /**
228
+ * The owned emulators worth an `adb reverse --list`: each runs this workspace's debug app, launched onto that serial
229
+ * against the Metro port that is serving now.
230
+ */
231
+ function metroReverseTargets(state, launches) {
232
+ const metroPort = state.metro?.running ? state.metro.port : null;
233
+ if (!metroPort) return [];
234
+ return [{
235
+ slot: "default",
236
+ android: state.android
237
+ }, ...state.slots ?? []].flatMap(({ slot, android }) => {
238
+ const launch = launches[deviceSlotKey("android", slot)];
239
+ if (!android?.owned || android.physical || !android.serial || android.app?.state !== "running") return [];
240
+ if (!launch || launch.release || launch.metroPort !== metroPort || launch.deviceId !== android.serial) return [];
241
+ return [{
242
+ slot,
243
+ serial: android.serial,
244
+ metroPort
245
+ }];
246
+ });
247
+ }
248
+ function addReverseMissingIssue(state, { slot, serial, metroPort }) {
249
+ const issue = {
250
+ code: "android-reverse-missing",
251
+ severity: "warning",
252
+ message: `the adb reverse for Metro port ${metroPort} is missing on ${serial}, so the app cannot reach Metro`,
253
+ remedy: "stim reload android",
254
+ workspace: state.path,
255
+ ...slot === "default" ? {} : { slot }
256
+ };
257
+ state.issues.push(issue);
258
+ state.warnings.push(issueText(issue));
259
+ }
260
+ function portNotOurs(port, holder, supervisor) {
261
+ if (supervisor?.status === "ours") return [
262
+ `port ${port} is in use by ${holder}, so this workspace's Metro cannot serve on it`,
263
+ "stim stop",
264
+ "warning"
265
+ ];
266
+ if (supervisor?.status === "unverified") return [
267
+ `port ${port} is in use by ${holder}`,
268
+ "stim guide errors teardown",
269
+ "warning"
270
+ ];
271
+ return [
272
+ `port ${port} is in use by ${holder}; stim start will choose a free port`,
273
+ "stim start",
274
+ "info"
275
+ ];
276
+ }
277
+ function portHeldBy(metro) {
278
+ return metro?.notOurs && metro.pid ? { heldBy: {
279
+ pid: metro.pid,
280
+ cwd: metro.cwd ?? null
281
+ } } : {};
282
+ }
283
+ function warningTexts(issues) {
284
+ return issues.filter((issue) => issue.severity !== "info").map(issueText);
285
+ }
286
+ function portHolder(cwd, self, workspaces = []) {
287
+ if (!cwd) return "another app";
288
+ const owner = workspaces.filter((path) => path !== self && isInsideProject(cwd, path)).toSorted((a, b) => b.length - a.length)[0];
289
+ if (owner) return `Metro for workspace ${basename(owner)}`;
290
+ return `a dev server in ${cwd.split(/[\\/]/).filter(Boolean).slice(-3).join("/")}`;
291
+ }
198
292
  function issueText(issue) {
199
293
  return `${issue.slot ? `${issue.slot}: ` : ""}${issue.message}; run \`${issue.remedy}\``;
200
294
  }
295
+ function toMinute(iso) {
296
+ const time = Date.parse(iso);
297
+ return (/* @__PURE__ */ new Date(time - time % 6e4)).toISOString();
298
+ }
201
299
  /**
202
300
  * Activity as status reports it: `lastActivityAt` rounded down to the minute, the precision its readers show, so a
203
301
  * new log record within the same minute does not change the payload.
@@ -207,7 +305,11 @@ function statusActivity(activity) {
207
305
  if (!Number.isFinite(at)) return activity;
208
306
  return {
209
307
  ...activity,
210
- lastActivityAt: (/* @__PURE__ */ new Date(at - at % 6e4)).toISOString()
308
+ lastActivityAt: toMinute(activity.lastActivityAt),
309
+ ...activity.recent ? { recent: Object.fromEntries(ACTIVITY_RECENCY_BASES.flatMap((basis) => {
310
+ const iso = activity.recent?.[basis];
311
+ return iso ? [[basis, toMinute(iso)]] : [];
312
+ })) } : {}
211
313
  };
212
314
  }
213
315
  function activityLabel(activity, now) {
@@ -329,4 +431,4 @@ function deviceLeaseLines(states, now) {
329
431
  return lines;
330
432
  }
331
433
  //#endregion
332
- export { withWebFacts as _, deviceLeaseStates as a, formatSpace as c, poolLine as d, remoteDeviceLine as f, unprovisionedWorktrees as g, tightVolumes as h, deviceLeaseLines as i, gitSummaryText as l, statusActivity as m, activityLabel as n, diskLine as o, remoteDeviceState as p, capacity as r, environmentState as s, RECENT_LAUNCH_MS as t, parseDfFree as u };
434
+ export { statusActivity as _, capacity as a, withWebFacts as b, diskLine as c, gitSummaryText as d, metroReverseTargets as f, remoteDeviceState as g, remoteDeviceLine as h, applyMachineMemory as i, environmentState as l, poolLine as m, activityLabel as n, deviceLeaseLines as o, parseDfFree as p, addReverseMissingIssue as r, deviceLeaseStates as s, RECENT_LAUNCH_MS as t, formatSpace as u, tightVolumes as v, unprovisionedWorktrees as y };
@@ -0,0 +1,23 @@
1
+ import { fstatSync } from "node:fs";
2
+ //#region src/stdout-reader.ts
3
+ const READER_CHECK_MS = 2e3;
4
+ /**
5
+ * Calls `onGone` once the process reading stdout is gone, for a streaming command that can stay quiet for a long
6
+ * time. Returns a function that stops watching.
7
+ */
8
+ function watchStdoutReader(onGone) {
9
+ const parent = process.ppid;
10
+ const stdout = fstatSync(1);
11
+ const linuxPipe = process.platform === "linux" && (stdout.isFIFO() || stdout.isSocket());
12
+ const timer = setInterval(() => {
13
+ if (linuxPipe && process.ppid !== parent) onGone();
14
+ else process.stdout.write("");
15
+ }, READER_CHECK_MS);
16
+ process.stdout.on("error", onGone);
17
+ return () => {
18
+ clearInterval(timer);
19
+ process.stdout.off("error", onGone);
20
+ };
21
+ }
22
+ //#endregion
23
+ export { watchStdoutReader as t };
@@ -0,0 +1,63 @@
1
+ import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
+ //#region src/devices/stim-desktop.ts
3
+ const LAUNCH_SERVICES_LOOKUP = `ObjC.import('AppKit'); const url = $.NSWorkspace.sharedWorkspace.URLForApplicationWithBundleIdentifier('dev.stim.desktop'); url.isNil() ? '' : url.path.js`;
4
+ const SCHEME_HANDLER_LOOKUP = `ObjC.import('AppKit'); const url = $.NSWorkspace.sharedWorkspace.URLForApplicationToOpenURL($.NSURL.URLWithString('stim-desktop://workspace')); url.isNil() ? '' : url.path.js`;
5
+ const STIM_DESKTOP_INSTALLED = "Stim Desktop installed";
6
+ /**
7
+ * macOS `open` passes its environment to an app it launches, so Stim Desktop
8
+ * started by a command run under a scoped `STIM_HOME` would serve that home.
9
+ */
10
+ const STIM_DESKTOP_OPEN_OPTIONS = {
11
+ timeoutMs: 5e3,
12
+ killSignal: "SIGKILL",
13
+ omitEnv: ["STIM_HOME"]
14
+ };
15
+ /** Stim Desktop sets `STIM_DESKTOP_APP` to its bundle path for every command it runs; a non-empty value means Desktop is installed. */
16
+ function stimDesktopInstalled(platform = process.platform, env = process.env) {
17
+ if (platform !== "darwin") return false;
18
+ if (env.STIM_DESKTOP_APP) return true;
19
+ return launchServicesFinds(LAUNCH_SERVICES_LOOKUP);
20
+ }
21
+ function launchServicesFinds(script) {
22
+ return Boolean(getExecutor().runFileQuiet("osascript", [
23
+ "-l",
24
+ "JavaScript",
25
+ "-e",
26
+ script
27
+ ], {
28
+ timeoutMs: 5e3,
29
+ killSignal: "SIGKILL"
30
+ }));
31
+ }
32
+ function stimDesktopHandlesLinks(platform = process.platform, env = process.env) {
33
+ if (platform !== "darwin") return false;
34
+ if (env.STIM_DESKTOP_APP) return true;
35
+ return launchServicesFinds(SCHEME_HANDLER_LOOKUP);
36
+ }
37
+ /**
38
+ * The links that open this workspace for a human, or undefined when no app on this Mac handles
39
+ * `stim-desktop://`. `target` names the device Stim Desktop focuses when the user opens the workspace.
40
+ */
41
+ function workspaceLinks(root, target, handlesLinks = stimDesktopHandlesLinks) {
42
+ if (!handlesLinks()) return void 0;
43
+ let desktop = `stim-desktop://workspace?path=${linkParam(root)}`;
44
+ if (target) desktop += `&platform=${target.platform}`;
45
+ if (target?.slot && target.slot !== "default") desktop += `&slot=${linkParam(target.slot)}`;
46
+ return { desktop };
47
+ }
48
+ const linkParam = (value) => encodeURIComponent(value).replaceAll("%2F", "/");
49
+ function workspaceLinkLine(links) {
50
+ return `Open in Stim Desktop: ${links.desktop}`;
51
+ }
52
+ function settingDefault(setting, desktopInstalled = stimDesktopInstalled) {
53
+ if (setting.desktopDefault !== void 0 && desktopInstalled()) return {
54
+ value: setting.desktopDefault,
55
+ reason: STIM_DESKTOP_INSTALLED
56
+ };
57
+ return {
58
+ value: setting.default,
59
+ reason: null
60
+ };
61
+ }
62
+ //#endregion
63
+ export { workspaceLinks as a, workspaceLinkLine as i, settingDefault as n, stimDesktopInstalled as r, STIM_DESKTOP_OPEN_OPTIONS as t };
@@ -0,0 +1,32 @@
1
+ // Prints "<pid> <bytes>" for each pid, the pid's physical footprint: the value Activity Monitor's Memory column and
2
+ // top's MEM column show. With no arguments it reads every pid proc_listallpids(3) returns.
3
+ //
4
+ // The footprint comes from proc_pid_rusage(pid, RUSAGE_INFO_V4, ...) in <libproc.h>, field ri_phys_footprint of
5
+ // struct rusage_info_v4 in <sys/resource.h> (macOS 10.14+). The call fails with EPERM for another user's process
6
+ // unless the caller is root, and with ESRCH for a pid that has exited; those pids are left out of the output.
7
+
8
+ import Darwin
9
+
10
+ func allPids() -> [pid_t] {
11
+ var capacity = Int(proc_listallpids(nil, 0)) + 64
12
+ while true {
13
+ var pids = [pid_t](repeating: 0, count: capacity)
14
+ let count = pids.withUnsafeMutableBytes { proc_listallpids($0.baseAddress, Int32($0.count)) }
15
+ if count < 0 { return [] }
16
+ if Int(count) < capacity { return Array(pids.prefix(Int(count))) }
17
+ capacity *= 2
18
+ }
19
+ }
20
+
21
+ let arguments = CommandLine.arguments.dropFirst()
22
+ let pids = arguments.isEmpty ? allPids() : arguments.compactMap { pid_t($0) }
23
+ var output = ""
24
+ output.reserveCapacity(pids.count * 20)
25
+ var info = rusage_info_v4()
26
+ for pid in pids where pid > 0 {
27
+ let status = withUnsafeMutablePointer(to: &info) {
28
+ $0.withMemoryRebound(to: rusage_info_t?.self, capacity: 1) { proc_pid_rusage(pid, RUSAGE_INFO_V4, $0) }
29
+ }
30
+ if status == 0 { output += "\(pid) \(info.ri_phys_footprint)\n" }
31
+ }
32
+ fputs(output, stdout)
@@ -1,4 +1,4 @@
1
- import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
1
+ import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
2
  import { accessSync, constants, realpathSync, statSync } from "node:fs";
3
3
  import { delimiter, join, resolve } from "node:path";
4
4
  //#region src/diagnostics/stim-installations.ts
@@ -1,14 +1,19 @@
1
- import { A as ownership_claim_exports, B as deviceSlotPlatforms, E as withConfigLock, G as validateDeviceSlot, H as parseDeviceSlotOption, O as process_identity_exports, T as upsertProject, U as projectDeviceSlots, V as parseDeviceSlotKey, o as clearSupervisor, p as getProject } from "./config-0jCcMM0E.mjs";
2
- import { a as pidExists, b as writeWorkspaceState, c as resolveProjectMetro, h as readWorkspaceState, k as workspaceDir, l as signalProcessTree, m as clearWorkspaceStateKeys, n as killMetroTree, v as withWorkspaceStateLock } from "./metro-BPeY1YMh.mjs";
3
- import { i as findCommandWorkspace } from "./project-DGeclhvO.mjs";
4
- import { a as clearRemoteSession, c as readMetroTunnel, i as clearManagedMetroTunnel, o as clearWorkspaceLaunches, s as clearWorkspaceSupervisor, u as readRemoteSession } from "./state-DwAU2_-D.mjs";
5
- import { a as withWorkspaceProcessLock } from "./named-ports-n7tnJsI8.mjs";
6
- import { f as refuseNoProject, l as phaseLine, p as releasedLeaseFact, u as plural } from "./command-output-BIL7rlX7.mjs";
7
- import { u as releaseWorkspaceLeases } from "./device-lease-CL9dZf73.mjs";
8
- import { f as teardownOwnedAvd, i as stopTunnel, m as teardownOwnedIosSim, p as teardownOwnedBrowser, t as resolveSupervisorTarget } from "./ownership-PUP4Dwai.mjs";
9
- import { n as verifyCollectorOwnership } from "./ownership-Bz2kETAS.mjs";
10
- import { f as resolveEasCliBin } from "./remote-cache--aehr4Pr.mjs";
11
- import { _ as describeNativeRunHolder, b as requestNativeRunCancel, f as NATIVE_RUN_LOCK, g as decideStopAction, h as clearNativeRunCancel, i as endRecordedSession, p as NATIVE_RUN_WAIT_MS, v as nativeRunHolder, y as nativeRunWaitNotice } from "./device-remote-DT9ngZiD.mjs";
1
+ import { B as parseDeviceSlotKey, D as withConfigLock, E as upsertProject, H as projectDeviceSlots, V as parseDeviceSlotOption, W as validateDeviceSlot, o as clearSupervisor, p as getProject, z as deviceSlotPlatforms } from "./config-D0DbpZtM.mjs";
2
+ import { n as ownership_claim_exports, t as process_identity_exports } from "./process-identity-BmRm1qV0.mjs";
3
+ import { b as workspaceDir, c as writeWorkspaceState, n as clearWorkspaceStateKeys, o as withWorkspaceStateLock, r as readWorkspaceState } from "./workspace-state-CL2V2Ons.mjs";
4
+ import { i as findCommandWorkspace } from "./project-CV3tDFHl.mjs";
5
+ import { a as clearRemoteSession, c as readMetroTunnel, i as clearManagedMetroTunnel, o as clearWorkspaceLaunches, s as clearWorkspaceSupervisor, u as readRemoteSession } from "./state-CaeaHbDA.mjs";
6
+ import { t as withWorkspaceProcessLock } from "./workspace-process-lock-BL7Q0HCO.mjs";
7
+ import { f as refuseNoProject, l as phaseLine, p as releasedLeaseFact, u as plural } from "./command-output-CliHHn7a.mjs";
8
+ import { n as fileLeaseIo, u as releaseWorkspaceLeases } from "./device-lease-W5Pi1q38.mjs";
9
+ import { l as resolveProjectMetro, o as pidExists, r as killMetroTree, u as signalProcessTree } from "./metro-Di8EN2Tb.mjs";
10
+ import { a as teardownOwnedBrowser, i as teardownOwnedAvd, o as teardownOwnedIosSim } from "./teardown-BYuQafiA.mjs";
11
+ import { a as logVanishedSupervisor, l as withdrawDevServerStopRequest, p as stopTunnel, s as requestDevServerStop, u as resolveSupervisorTarget } from "./stop-cause-CZ_Mr-v9.mjs";
12
+ import { n as verifyCollectorOwnership } from "./ownership-BkYxUtuj.mjs";
13
+ import { t as stopMacosApp } from "./stop-Bvf1MflS.mjs";
14
+ import { f as resolveEasCliBin } from "./remote-cache-CO2elm_b.mjs";
15
+ import { i as endRecordedSession } from "./device-remote-9ulPsdhY.mjs";
16
+ import { a as decideStopAction, c as nativeRunWaitNotice, i as clearNativeRunCancel, l as requestNativeRunCancel, n as NATIVE_RUN_WAIT_MS, o as describeNativeRunHolder, s as nativeRunHolder, t as NATIVE_RUN_LOCK } from "./native-run-DFDoIRbY.mjs";
12
17
  import chalk from "chalk";
13
18
  //#region src/commands/stop.ts
14
19
  const DEFAULT_WAIT_MS = 1e4;
@@ -121,6 +126,7 @@ function remoteEnded(entry) {
121
126
  }
122
127
  async function runStop(options) {
123
128
  if (options.slot === void 0) return stopWorkspace(options);
129
+ if (options.slot === "web") return stopBrowserOnly(options);
124
130
  const slot = validateDeviceSlot(options.slot);
125
131
  const root = options.root;
126
132
  const project = options.project === void 0 ? getProject(root) : options.project;
@@ -149,6 +155,7 @@ async function runStop(options) {
149
155
  },
150
156
  clearRegistration: async () => true,
151
157
  teardownBrowser: null,
158
+ stopMacos: false,
152
159
  releaseLeases: (projectRoot) => releaseWorkspaceLeases(projectRoot, { slot })
153
160
  });
154
161
  result.outcomes.port = {
@@ -163,7 +170,65 @@ async function runStop(options) {
163
170
  result.summary = summarize(root, result.outcomes, result.ok);
164
171
  return result;
165
172
  }
166
- async function stopWorkspace({ root, project = void 0, state = void 0, collectors = void 0, signalCollector = (pid) => signalProcessTree(pid), verifyCollector = verifyCollectorOwnership, clearCollectors = clearCollectorState, isAlive = pidExists, killGroup = killMetroTree, signalServer = (pid) => signalProcessTree(pid), inspectIdentity = process_identity_exports.inspectProcessIdentity, waitForDeath = void 0, waitMs = DEFAULT_WAIT_MS, resolveMetro = resolveProjectMetro, teardownIos = teardownOwnedIosSim, teardownAvd = teardownOwnedAvd, teardownBrowser = (projectRoot) => teardownOwnedBrowser(projectRoot), remoteDevice = void 0, teardownRemoteSession = defaultTeardownRemoteSession, metroTunnel = void 0, stopMetroTunnel = stopTunnel, releaseLeases = releaseWorkspaceLeases, freePort = defaultFreePort, clearRegistration = defaultClearRegistration, clearState = clearSupervisorState, report = (line) => console.error(line) }) {
173
+ async function stopBrowserOnly({ root, project = void 0, teardownBrowser = (projectRoot) => teardownOwnedBrowser(projectRoot), report = (line) => console.error(line) }) {
174
+ const kept = "Only the owned Chrome stops; the workspace server and devices keep running.";
175
+ const port = (project === void 0 ? getProject(root) : project)?.metroPort ?? null;
176
+ const outcomes = {
177
+ supervisor: {
178
+ status: "skipped",
179
+ reason: kept
180
+ },
181
+ collectors: {
182
+ status: "skipped",
183
+ entries: []
184
+ },
185
+ metro: {
186
+ status: "skipped",
187
+ reason: kept
188
+ },
189
+ device: {
190
+ ios: null,
191
+ android: null
192
+ },
193
+ port: port === null ? {
194
+ status: "none",
195
+ port
196
+ } : {
197
+ status: "kept",
198
+ port,
199
+ reason: kept
200
+ },
201
+ metroTunnel: { status: "none" },
202
+ releasedLeases: []
203
+ };
204
+ outcomes.device.web = await closeBrowser(root, teardownBrowser, report);
205
+ if (!outcomes.device.web) return {
206
+ ok: true,
207
+ outcomes,
208
+ summary: `Stopped: no owned Chrome was running (${root})`
209
+ };
210
+ const ok = outcomes.device.web.status === "shut-down";
211
+ return {
212
+ ok,
213
+ outcomes,
214
+ summary: summarize(root, outcomes, ok)
215
+ };
216
+ }
217
+ async function stopWorkspaceMacos(root, failed, enabled) {
218
+ if (enabled === false || readWorkspaceState(root)?.macos === void 0) return void 0;
219
+ try {
220
+ await stopMacosApp(root);
221
+ return { status: "stopped" };
222
+ } catch (error) {
223
+ const reason = error instanceof Error ? error.message : String(error);
224
+ failed(reason);
225
+ return {
226
+ status: "failed",
227
+ reason
228
+ };
229
+ }
230
+ }
231
+ async function stopWorkspace({ root, project = void 0, state = void 0, collectors = void 0, signalCollector = (pid) => signalProcessTree(pid), verifyCollector = verifyCollectorOwnership, clearCollectors = clearCollectorState, isAlive = pidExists, killGroup = killMetroTree, signalServer = (pid) => signalProcessTree(pid), inspectIdentity = process_identity_exports.inspectProcessIdentity, waitForDeath = void 0, waitMs = DEFAULT_WAIT_MS, resolveMetro = resolveProjectMetro, teardownIos = teardownOwnedIosSim, teardownAvd = teardownOwnedAvd, teardownBrowser = (projectRoot) => teardownOwnedBrowser(projectRoot), stopMacos, remoteDevice = void 0, teardownRemoteSession = defaultTeardownRemoteSession, metroTunnel = void 0, stopMetroTunnel = stopTunnel, releaseLeases = releaseWorkspaceLeases, freePort = defaultFreePort, clearRegistration = defaultClearRegistration, clearState = clearSupervisorState, report = (line) => console.error(line) }) {
167
232
  const proj = project === void 0 ? getProject(root) : project;
168
233
  const sup = state === void 0 ? readSupervisorState(root) : state;
169
234
  const collectorRecords = collectors === void 0 ? readCollectorState(root) : collectors;
@@ -202,6 +267,7 @@ async function stopWorkspace({ root, project = void 0, state = void 0, collector
202
267
  });
203
268
  if (target.status === "none") report(chalk.dim(phaseLine("stop", "no supervisor recorded")));
204
269
  else if (target.status === "stale") {
270
+ logVanishedSupervisor(root, sup);
205
271
  outcomes.supervisor = {
206
272
  status: "already-stopped",
207
273
  pid: target.pid,
@@ -219,6 +285,7 @@ async function stopWorkspace({ root, project = void 0, state = void 0, collector
219
285
  stillHolding = `supervisor pid ${target.pid} could not be verified`;
220
286
  } else {
221
287
  outcomes.supervisor = await stopSupervisor(target, {
288
+ root,
222
289
  killGroup,
223
290
  waiter,
224
291
  report
@@ -318,6 +385,10 @@ async function stopWorkspace({ root, project = void 0, state = void 0, collector
318
385
  }
319
386
  outcomes.device.web = await closeBrowser(root, teardownBrowser, report);
320
387
  if (outcomes.device.web && outcomes.device.web.status !== "shut-down") ok = false;
388
+ outcomes.macos = await stopWorkspaceMacos(root, (reason) => {
389
+ ok = false;
390
+ report(reason);
391
+ }, stopMacos);
321
392
  const remote = remoteDevice === void 0 ? readRemoteSession(root) : remoteDevice;
322
393
  const sessionId = typeof remote?.sessionId === "string" ? remote.sessionId : null;
323
394
  if (sessionId) {
@@ -470,8 +541,9 @@ async function reapCollectors(root, collectors, { isAlive, signal, report, verif
470
541
  entries
471
542
  };
472
543
  }
473
- async function stopSupervisor(target, { killGroup, waiter, report }) {
544
+ async function stopSupervisor(target, { root, killGroup, waiter, report }) {
474
545
  report(chalk.dim(phaseLine("stop", `sending SIGTERM to supervisor process group ${target.pid}`)));
546
+ requestDevServerStop(root, target.processToken, { by: "stim stop" });
475
547
  let signalled = false;
476
548
  try {
477
549
  signalled = killGroup(target.pid, target.processToken);
@@ -480,6 +552,7 @@ async function stopSupervisor(target, { killGroup, waiter, report }) {
480
552
  report(chalk.red(phaseLine("stop", `could not signal supervisor pid ${target.pid}: ${String(e?.message || e)}`)));
481
553
  }
482
554
  if (!signalled) {
555
+ withdrawDevServerStopRequest(root, target.processToken);
483
556
  const reason = `could not signal supervisor pid ${target.pid}`;
484
557
  report(chalk.red(phaseLine("stop", reason)));
485
558
  return {
@@ -691,6 +764,8 @@ async function closeBrowser(root, teardownBrowser, report) {
691
764
  }
692
765
  function summarize(root, outcomes, ok) {
693
766
  const parts = [];
767
+ if (outcomes.macos?.status === "stopped") parts.push("macOS app stopped");
768
+ if (outcomes.macos?.status === "failed") parts.push("macOS app could not be stopped");
694
769
  if (outcomes.supervisor.status === "stopped") parts.push(`supervisor pid ${outcomes.supervisor.pid} stopped`);
695
770
  if (outcomes.supervisor.status === "already-stopped") parts.push("supervisor already stopped");
696
771
  if (outcomes.supervisor.status === "timeout") parts.push(`supervisor pid ${outcomes.supervisor.pid} still running`);
@@ -752,6 +827,17 @@ function ownerGoneRefusal(holder, at) {
752
827
  remedy: `Wait for the build tool${child} to exit, or end it yourself, then run \`stim stop\` again.`
753
828
  });
754
829
  }
830
+ function recordedDeviceSlots(root) {
831
+ const slots = new Set(projectDeviceSlots(getProject(root)).map(({ slot }) => slot));
832
+ for (const key of Object.keys({
833
+ ...readCollectorState(root),
834
+ ...fileLeaseIo.readHolder(root)
835
+ })) {
836
+ const parsed = parseDeviceSlotKey(key);
837
+ if (parsed) slots.add(parsed.slot);
838
+ }
839
+ return [...slots].toSorted();
840
+ }
755
841
  function workspaceDeviceSlots(root) {
756
842
  const slots = projectDeviceSlots(getProject(root)).filter(({ platforms }) => Object.values(platforms).some(Boolean)).map(({ slot }) => slot);
757
843
  if (typeof readRemoteSession(root)?.sessionId === "string" && !slots.includes("default")) slots.push("default");
@@ -772,7 +858,21 @@ function readRemoteSessionEntry(root, report) {
772
858
  * SIGINT and given INTERRUPT_WAIT_MS to exit; a build for another slot that stays is left running while
773
859
  * the slot stops without the lock; any other holder is waited on with a visible notice.
774
860
  */
775
- async function stopWorkspaceNow({ root, slot, report = (line) => console.error(line), now = Date.now, sleep, waitMs = NATIVE_RUN_WAIT_MS, interruptWaitMs = INTERRUPT_WAIT_MS, inspectIdentity = process_identity_exports.inspectProcessIdentity, interrupt = (pid) => process.kill(pid, "SIGINT"), endRemote = (projectRoot) => readRemoteSessionEntry(projectRoot, report), deviceSlots = workspaceDeviceSlots, stop = (options) => runStop(options) }) {
861
+ async function stopWorkspaceNow({ root, slot, report = (line) => console.error(line), now = Date.now, sleep, waitMs = NATIVE_RUN_WAIT_MS, interruptWaitMs = INTERRUPT_WAIT_MS, inspectIdentity = process_identity_exports.inspectProcessIdentity, interrupt = (pid) => process.kill(pid, "SIGINT"), endRemote = (projectRoot) => readRemoteSessionEntry(projectRoot, report), deviceSlots = workspaceDeviceSlots, recordedSlots = recordedDeviceSlots, stop = (options) => runStop(options) }) {
862
+ if (slot === "web") return stop({
863
+ root,
864
+ slot
865
+ });
866
+ let unknownSlot = slot !== void 0 && !recordedSlots(root).includes(slot);
867
+ const stillUnknown = () => {
868
+ const known = recordedSlots(root);
869
+ if (known.includes(slot)) return null;
870
+ return new StopBlocked({
871
+ code: "STIM_BAD_ARG",
872
+ message: `This workspace has no device slot named ${slot}. Its slots are ${known.join(", ")}.`,
873
+ remedy: `Run \`stim stop --slot <name>\` with one of those slots, \`stim stop --slot web\` to close only the owned Chrome, or \`stim stop\` for the whole workspace.`
874
+ });
875
+ };
776
876
  let remote = null;
777
877
  let remoteHandled = slot !== void 0;
778
878
  const endRemoteNow = () => {
@@ -802,6 +902,11 @@ async function stopWorkspaceNow({ root, slot, report = (line) => console.error(l
802
902
  }
803
903
  endRemoteNow();
804
904
  const target = nativeRunHolder(holder);
905
+ if (unknownSlot) {
906
+ const refusal = target.slot === slot ? null : stillUnknown();
907
+ if (refusal) throw refusal;
908
+ unknownSlot = false;
909
+ }
805
910
  const { action } = decideStopAction({
806
911
  stopSlot: slot,
807
912
  holder: target,
@@ -845,10 +950,14 @@ async function stopWorkspaceNow({ root, slot, report = (line) => console.error(l
845
950
  return result;
846
951
  };
847
952
  try {
848
- return withRemote(await withWorkspaceProcessLock(workspaceDir(root), NATIVE_RUN_LOCK, () => stop({
849
- root,
850
- slot
851
- }), {
953
+ return withRemote(await withWorkspaceProcessLock(workspaceDir(root), NATIVE_RUN_LOCK, () => {
954
+ const refusal = unknownSlot ? stillUnknown() : null;
955
+ if (refusal) throw refusal;
956
+ return stop({
957
+ root,
958
+ slot
959
+ });
960
+ }, {
852
961
  external: true,
853
962
  waitMs,
854
963
  now,
@@ -875,7 +984,7 @@ async function stopWorkspaceNow({ root, slot, report = (line) => console.error(l
875
984
  }
876
985
  }
877
986
  function stopCommand(program) {
878
- 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. Use 'default' for the workspace's default device.", parseDeviceSlotOption).option("--json", "print the per-step outcomes as JSON").action(async (opts) => {
987
+ 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. Use 'default' for the workspace's default device, or 'web' to close only the owned Chrome.", (slot) => slot === "web" ? slot : parseDeviceSlotOption(slot)).option("--json", "print the per-step outcomes as JSON").action(async (opts) => {
879
988
  const root = findCommandWorkspace(process.cwd());
880
989
  if (!root) {
881
990
  refuseNoProject({ json: Boolean(opts.json) });