stim 1.8.0 → 1.10.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 (91) hide show
  1. package/README.md +23 -155
  2. package/dist/{android-DWssqHQ8.mjs → android-787HhP5w.mjs} +168 -39
  3. package/dist/{android-BRgBYzU0.mjs → android-DBkeHwpm.mjs} +250 -200
  4. package/dist/{android-DeYdjjBP.mjs → android-DPov-fv2.mjs} +2 -2
  5. package/dist/{android-cas-SWIjzeuL.mjs → android-cas-CITmIeWY.mjs} +22 -14
  6. package/dist/android-cas-compiler.mjs +2 -2
  7. package/dist/app-install-CrJ6t_bv.mjs +819 -0
  8. package/dist/budget-BP4jZZL4.mjs +620 -0
  9. package/dist/build-progress-BXx-ZBe4.mjs +512 -0
  10. package/dist/{cache-manifest-CYJi8-Hx.mjs → cache-manifest-Pubfp8wV.mjs} +4 -4
  11. package/dist/cache-manifest.mjs +1 -1
  12. package/dist/cli.mjs +22 -19
  13. package/dist/collector-run.d.mts +7 -1
  14. package/dist/collector-run.mjs +10 -9
  15. package/dist/{command-output-B3ycpsYv.mjs → command-output-D5z2_bcF.mjs} +11 -2
  16. package/dist/{config-B7iyKWkL.mjs → config-BEq0Qw_a.mjs} +134 -121
  17. package/dist/{dependency-state-D0M7oKA7.mjs → dependency-state-r9UesA9a.mjs} +2 -2
  18. package/dist/{deps-oCXVNEqQ.mjs → deps-DP4eWqea.mjs} +8 -5
  19. package/dist/{detached-entry-Bpo6ZY_I.mjs → detached-entry-DJHoca49.mjs} +1 -1
  20. package/dist/{device-BUoZyMoR.mjs → dev-client-92U593KT.mjs} +1090 -660
  21. package/dist/{device-RdujjHLB.mjs → device-DhTZe7L4.mjs} +13 -20
  22. package/dist/{build-lock-CgtsB6TV.mjs → device-lease-CbEaIk7b.mjs} +481 -27
  23. package/dist/{device-pool-BxRwdufO.mjs → device-pool-Di8Q-9Ro.mjs} +7 -7
  24. package/dist/{device-remote-ByN8P6CB.mjs → device-remote-Dy1jGj3U.mjs} +645 -119
  25. package/dist/{doctor-BMt156qe.mjs → doctor-B-inAJmt.mjs} +137 -113
  26. package/dist/{error-diagnostics-D1Kaf-eT.mjs → error-diagnostics-xxWKNO3-.mjs} +22 -17
  27. package/dist/exec-DK8KaVHK.mjs +133 -0
  28. package/dist/{gc-v2YcoJvm.mjs → gc-ChPFZMRC.mjs} +836 -813
  29. package/dist/{guide-BTAlD1_y.mjs → guide-DwmaD_Wa.mjs} +953 -209
  30. package/dist/guide-status-CWvti2Lg.mjs +125 -0
  31. package/dist/idle-DpRLp5lj.mjs +157 -0
  32. package/dist/{build-slots-C-oAHRT1.mjs → in-use-CjUluqIX.mjs} +102 -8
  33. package/dist/{ios-BcCiI4TS.mjs → ios-B2Fpqvlb.mjs} +60 -71
  34. package/dist/{ios-BP04pY5f.mjs → ios-Dd-hYJBh.mjs} +241 -234
  35. package/dist/{ios-device-CFXA46ez.mjs → ios-device-BiOsSVzd.mjs} +3 -2
  36. package/dist/{ios-device-CauFJiYx.mjs → ios-device-CtRdEVjx.mjs} +149 -52
  37. package/dist/launch-verify-CQS_Kg-C.mjs +365 -0
  38. package/dist/{logs-CakdBTtm.mjs → logs-MARIy6m9.mjs} +20 -15
  39. package/dist/metro-BWLOJ_Wr.mjs +409 -0
  40. package/dist/{named-ports-DzMOlmaX.mjs → named-ports-DiTfSz6p.mjs} +4 -4
  41. package/dist/{ndjson-BZwI1NBv.mjs → ndjson-CDY9IkVC.mjs} +23 -43
  42. package/dist/{ndjson-DvHSsNU9.d.mts → ndjson-Du3j0AQ9.d.mts} +1 -0
  43. package/dist/{ownership-Cp52QAK1.mjs → ownership-D2xbLVoz.mjs} +2 -3
  44. package/dist/{teardown-BJqEzPo_.mjs → ownership-DZjdx8Gq.mjs} +664 -612
  45. package/dist/{ports-BIbJaKNW.mjs → ports-CLSSFOuz.mjs} +3 -3
  46. package/dist/{process-output-Dl5uKE03.mjs → process-output-Fpl8PYi8.mjs} +1 -1
  47. package/dist/{project-DDZWf_16.mjs → project-BXZzOABt.mjs} +8 -3
  48. package/dist/{reload-BdypPbjM.mjs → reload-DoETJ3rl.mjs} +11 -13
  49. package/dist/{server-bare-BQkyxa_b.mjs → server-bare-DVbb7kXH.mjs} +2 -2
  50. package/dist/{server-expo-BVAA1OqJ.mjs → server-expo-BvOCeakp.mjs} +8 -7
  51. package/dist/server-expo-DBVTdult.mjs +2 -0
  52. package/dist/{settings-BXXorVM2.mjs → settings-DOugPlPz.mjs} +59 -159
  53. package/dist/settings-EWYNwe72.mjs +378 -0
  54. package/dist/settings.schema.json +1064 -0
  55. package/dist/simslim-Ca3487H6.mjs +436 -0
  56. package/dist/{slot-launch-ZUUe0TQP.mjs → slot-launch-C5cg0fmk.mjs} +4 -4
  57. package/dist/spawn-claims-CUleC2cM.mjs +49 -0
  58. package/dist/{start-CmxtOKjU.mjs → start-F9gv5dcC.mjs} +159 -114
  59. package/dist/start-VoFsPSes.mjs +2 -0
  60. package/dist/{state-DZobzxLW.mjs → state-WGdIbi_d.mjs} +5 -3
  61. package/dist/{stats-BYcZ6n1h.mjs → stats-BAgJaLA4.mjs} +66 -5
  62. package/dist/{stats-CLg5RuDS.mjs → stats-DBYJ5bxL.mjs} +4 -4
  63. package/dist/status-DLCD-o3s.mjs +764 -0
  64. package/dist/status-Dr4fRkym.mjs +216 -0
  65. package/dist/{guide-status-Craq9z0t.mjs → stim-installations-CINXy_su.mjs} +3 -122
  66. package/dist/{stop-C8JW6kKp.mjs → stop-Dj4pfmyv.mjs} +111 -31
  67. package/dist/{stop-B1kOpYei.mjs → stop-ZvOeTmKb.mjs} +2 -2
  68. package/dist/supervisor-run.d.mts +11 -2
  69. package/dist/supervisor-run.mjs +228 -42
  70. package/dist/{workspace-process-lock-BYfZz_S-.mjs → workspace-process-lock-BXA96myD.mjs} +14 -9
  71. package/dist/workspaces-BTUyFfkv.mjs +1003 -0
  72. package/dist/worktree-4aZg2NnI.mjs +2 -0
  73. package/dist/{worktree-CFLA-6MX.mjs → worktree-BMS7GeqB.mjs} +523 -61
  74. package/dist/{worktree-FG_ZKKdb.mjs → worktree-BlizKDpZ.mjs} +200 -54
  75. package/dist/{xcode-B6YaWeg0.mjs → xcode-B2tBkf0m.mjs} +66 -57
  76. package/package.json +6 -6
  77. package/skill/SKILL.md +2 -0
  78. package/dist/app-install-BDbsSgCl.mjs +0 -1175
  79. package/dist/dev-client-BmT-ioFI.mjs +0 -797
  80. package/dist/device-lease-Ffz_M5t0.mjs +0 -359
  81. package/dist/exec-B29XNuhP.mjs +0 -77
  82. package/dist/logs-query-BGL-Pl12.mjs +0 -290
  83. package/dist/metro-C7bcJ0uo.mjs +0 -233
  84. package/dist/ownership-wolE-0Do.mjs +0 -470
  85. package/dist/public-http-probe-CyMDrQ3c.mjs +0 -61
  86. package/dist/reclaim-KJ-0uWEF.mjs +0 -361
  87. package/dist/server-expo-YDDG3eP7.mjs +0 -2
  88. package/dist/spawn-entry-nCE6m770.mjs +0 -14
  89. package/dist/status-DdkLki47.mjs +0 -426
  90. package/dist/workspace-state-BT5E3SA2.mjs +0 -146
  91. package/dist/{metro-store-CDDZHtPl.mjs → metro-store-DzeWkfqW.mjs} +1 -1
@@ -1,19 +1,139 @@
1
- import { a as clearSupervisor, w as process_identity_exports, x as setSupervisor } from "./config-B7iyKWkL.mjs";
2
- import { r as createNdjsonWriter } from "./ndjson-BZwI1NBv.mjs";
3
- import { _ as workspaceLogsDir, a as withWorkspaceStateLock, o as writeWorkspaceState, r as readWorkspaceState } from "./workspace-state-BT5E3SA2.mjs";
4
- import { i as detectIsExpo } from "./project-DDZWf_16.mjs";
1
+ import { B as parseDeviceSlotKey, C as setSupervisor, D as process_identity_exports, H as projectDeviceSlots, a as clearSupervisor, f as getProject } from "./config-BEq0Qw_a.mjs";
2
+ import { r as createNdjsonWriter } from "./ndjson-CDY9IkVC.mjs";
3
+ import { _ as withWorkspaceStateLock, j as workspaceLogsDir, m as readWorkspaceState$1, p as clearWorkspaceStateKeys, y as writeWorkspaceState } from "./metro-BWLOJ_Wr.mjs";
4
+ import { a as detectIsExpo } from "./project-BXZzOABt.mjs";
5
5
  import { t as describeError } from "./errors-B6W7FkxQ.mjs";
6
- import { t as relaunchWithLogFile } from "./detached-entry-Bpo6ZY_I.mjs";
7
- import { c as readPidFile, f as writePidFile, n as MODE_EXPO, o as clearWorkspaceSupervisor, r as clearExpoMetroTunnel, t as MODE_BARE } from "./state-DZobzxLW.mjs";
8
- import { existsSync, realpathSync } from "node:fs";
9
- import { isAbsolute, join, resolve } from "node:path";
6
+ import { t as relaunchWithLogFile } from "./detached-entry-DJHoca49.mjs";
7
+ import { c as readPidFile, f as writePidFile, n as MODE_EXPO, o as clearWorkspaceSupervisor, r as clearExpoMetroTunnel, t as MODE_BARE } from "./state-WGdIbi_d.mjs";
8
+ import { t as withWorkspaceProcessLock } from "./workspace-process-lock-BXA96myD.mjs";
9
+ import { a as parseLease, r as leaseIsExpired, s as parseWorkspaceLeases, t as deviceLeasePath } from "./device-lease-CbEaIk7b.mjs";
10
+ import { c as createActivityReader } from "./build-progress-BXx-ZBe4.mjs";
11
+ import { N as ownedAvdSerialResolver } from "./android-787HhP5w.mjs";
12
+ import { s as workspaceBuildInProgress } from "./idle-DpRLp5lj.mjs";
13
+ import { n as withIdleWorkspace } from "./in-use-CjUluqIX.mjs";
14
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
15
+ import { LOG_ROTATE_BYTES } from "@stim-cli/core";
16
+ import { IDLE_STOP_KEY } from "@stim-cli/core/state";
17
+ import { dirname, isAbsolute, join, resolve } from "node:path";
10
18
  import { fileURLToPath } from "node:url";
19
+ //#region src/supervisor/idle-stop.ts
20
+ const IDLE_CHECK_MS = 6e4;
21
+ const MINUTE_MS = 6e4;
22
+ function trackDevServerActivity(now) {
23
+ let last = now();
24
+ const open = /* @__PURE__ */ new Set();
25
+ return {
26
+ record(entry) {
27
+ const { event, requestId } = entry ?? {};
28
+ if (event === "bundle_response_started") {
29
+ if (typeof requestId === "string") open.add(requestId);
30
+ } else if (event === "bundle_response_finished" || event === "bundle_response_failed") {
31
+ if (typeof requestId === "string") open.delete(requestId);
32
+ } else if (event !== "expo_stdout") return;
33
+ last = now();
34
+ },
35
+ lastActivityAt: () => open.size > 0 ? now() : last
36
+ };
37
+ }
38
+ function deviceTargets(root) {
39
+ const serialOf = ownedAvdSerialResolver({ timeoutMs: 5e3 });
40
+ return projectDeviceSlots(getProject(root)).flatMap(({ slot, platforms }) => {
41
+ const targets = [];
42
+ if (platforms.ios?.deviceUdid) targets.push({
43
+ platform: "ios",
44
+ id: platforms.ios.deviceUdid,
45
+ slot
46
+ });
47
+ const android = platforms.android;
48
+ const serial = android?.avdName ? serialOf(android.avdName).serial : android?.serial;
49
+ if (serial) targets.push({
50
+ platform: "android",
51
+ id: serial,
52
+ slot
53
+ });
54
+ return targets;
55
+ });
56
+ }
57
+ function heldDeviceLease(root, now) {
58
+ for (const [key, record] of Object.entries(parseWorkspaceLeases(readWorkspaceState$1(root)?.deviceLeases))) {
59
+ const platform = parseDeviceSlotKey(key)?.platform;
60
+ if (!platform) continue;
61
+ let raw;
62
+ try {
63
+ raw = readFileSync(deviceLeasePath(platform, record.id), "utf8");
64
+ } catch (error) {
65
+ if (error.code === "ENOENT") continue;
66
+ return `${platform} device ${record.id} has a lease that cannot be read (${describeError(error)})`;
67
+ }
68
+ const lease = parseLease(raw);
69
+ if (!lease) return `${platform} device ${record.id} has a lease that cannot be read`;
70
+ if (lease.token !== record.token || leaseIsExpired(lease, now)) continue;
71
+ const by = record.kind === "declared" ? "stim device lock" : "a stim ios or android run";
72
+ return `${platform} device ${record.id} is leased by ${by} until ${lease.expiresAt}`;
73
+ }
74
+ return null;
75
+ }
76
+ function workspaceIdleProbe(root, { platform = process.platform } = {}) {
77
+ return {
78
+ lastActivityAt() {
79
+ const used = Date.parse(String(readWorkspaceState$1(root)?.lastUsedAt ?? ""));
80
+ let clientLog = NaN;
81
+ try {
82
+ clientLog = statSync(join(workspaceLogsDir(root), "client.ndjson")).mtimeMs;
83
+ } catch {}
84
+ const times = [used, clientLog].filter(Number.isFinite);
85
+ return times.length ? Math.max(...times) : NaN;
86
+ },
87
+ blocker() {
88
+ if (workspaceBuildInProgress(root)) return "a build is in progress";
89
+ const lease = heldDeviceLease(root, Date.now());
90
+ if (lease) return lease;
91
+ const readActivity = createActivityReader();
92
+ for (const target of deviceTargets(root)) {
93
+ const activity = readActivity({
94
+ ...target,
95
+ workspace: root
96
+ });
97
+ if (activity.state === "driven") return `${target.platform} device ${target.id} is driven by ${activity.driver?.tool ?? "an unknown tool"}`;
98
+ const unknown = platform === "win32" ? activity.basis.filter((basis) => basis !== "driver-process") : activity.basis;
99
+ if (activity.state === "unknown" && unknown.length > 0) return `${target.platform} device ${target.id} has unknown activity (${unknown.join(", ")})`;
100
+ }
101
+ return null;
102
+ }
103
+ };
104
+ }
105
+ function watchIdleDevServer({ idleStopMs, now, serverActivityAt, probe, onIdle, checkMs = IDLE_CHECK_MS }) {
106
+ const idleMinutes = () => {
107
+ const last = Math.max(serverActivityAt(), ...[probe.lastActivityAt()].filter(Number.isFinite));
108
+ if (now() - last < idleStopMs) return null;
109
+ let blocker;
110
+ try {
111
+ blocker = probe.blocker();
112
+ } catch (error) {
113
+ blocker = describeError(error);
114
+ }
115
+ return blocker ? null : Math.floor((now() - last) / MINUTE_MS);
116
+ };
117
+ let deciding = false;
118
+ const timer = setInterval(() => {
119
+ if (deciding || idleMinutes() === null) return;
120
+ deciding = true;
121
+ onIdle(idleMinutes).finally(() => {
122
+ deciding = false;
123
+ });
124
+ }, Math.min(checkMs, idleStopMs));
125
+ timer.unref?.();
126
+ return () => clearInterval(timer);
127
+ }
128
+ //#endregion
11
129
  //#region src/supervisor/run.ts
130
+ const USAGE = "Usage: run.js --root <path> --port <n> [--tunnel] [--reset-cache] [--idle-stop-minutes <n>]";
12
131
  function parseArgs(argv) {
13
132
  let root;
14
133
  let port;
15
134
  let tunnel = false;
16
135
  let resetCache = false;
136
+ let idleStopMinutes = "0";
17
137
  for (let i = 0; i < argv.length; i++) {
18
138
  const arg = argv[i];
19
139
  if (arg === "--root") {
@@ -32,23 +152,29 @@ function parseArgs(argv) {
32
152
  resetCache = true;
33
153
  continue;
34
154
  }
35
- return { error: `Unknown supervisor argument "${arg}". Usage: run.js --root <path> --port <n> [--tunnel] [--reset-cache]` };
155
+ if (arg === "--idle-stop-minutes") {
156
+ idleStopMinutes = argv[++i] ?? "";
157
+ continue;
158
+ }
159
+ return { error: `Unknown supervisor argument "${arg}". ${USAGE}` };
36
160
  }
37
- if (!root) return { error: "Missing --root. Usage: run.js --root <path> --port <n> [--tunnel] [--reset-cache]" };
161
+ if (!root) return { error: `Missing --root. ${USAGE}` };
38
162
  if (!isAbsolute(root)) return { error: `--root must be an absolute path, got "${root}".` };
39
163
  const parsedPort = Number(port);
40
164
  if (!Number.isInteger(parsedPort) || parsedPort <= 0 || parsedPort > 65535) return { error: `--port must be a TCP port number, got "${port}".` };
165
+ if (!/^\d+$/.test(idleStopMinutes)) return { error: `--idle-stop-minutes must be a whole number of minutes, got "${idleStopMinutes}".` };
41
166
  return {
42
167
  root: resolve(root),
43
168
  port: parsedPort,
44
169
  tunnel,
45
- resetCache
170
+ resetCache,
171
+ idleStopMinutes: Number(idleStopMinutes)
46
172
  };
47
173
  }
48
- async function runSupervisor({ root, port, tunnel = false, resetCache = false, isExpo = detectIsExpo, startBare = null, startExpo = null, now = Date.now, onExit = (code) => process.exit(code), attachSignals = true, stderr = (line) => console.error(line) }) {
174
+ async function runSupervisor({ root, port, tunnel = false, resetCache = false, idleStopMinutes = 0, idleProbe, isExpo = detectIsExpo, startBare = null, startExpo = null, now = Date.now, onExit = (code) => process.exit(code), attachSignals = true, stderr = (line) => console.error(line) }) {
49
175
  root = realpathSync(root);
50
176
  const logsDir = workspaceLogsDir(root);
51
- const writer = createNdjsonWriter(join(logsDir, "metro.ndjson"));
177
+ const writer = createNdjsonWriter(join(logsDir, "metro.ndjson"), { maxBytes: LOG_ROTATE_BYTES });
52
178
  const mode = isExpo(root) ? MODE_EXPO : MODE_BARE;
53
179
  const startedAt = new Date(now()).toISOString();
54
180
  const processToken = (0, process_identity_exports.captureProcessToken)(process.pid);
@@ -68,6 +194,7 @@ async function runSupervisor({ root, port, tunnel = false, resetCache = false, i
68
194
  clearExpoMetroTunnel(root);
69
195
  writePidFile(root, process.pid);
70
196
  writeWorkspaceState(root, { supervisor: record });
197
+ clearWorkspaceStateKeys(root, [IDLE_STOP_KEY]);
71
198
  try {
72
199
  setSupervisor(root, record);
73
200
  } catch (err) {
@@ -86,7 +213,9 @@ async function runSupervisor({ root, port, tunnel = false, resetCache = false, i
86
213
  msg: `supervisor pid ${process.pid} starting the ${mode} dev server on port ${port} for ${root}`
87
214
  });
88
215
  let stopping = false;
216
+ let stopWatchingIdle = null;
89
217
  const finish = (code, event, level, msg) => {
218
+ stopWatchingIdle?.();
90
219
  writer.write({
91
220
  src: "metro",
92
221
  level,
@@ -104,18 +233,48 @@ async function runSupervisor({ root, port, tunnel = false, resetCache = false, i
104
233
  onExit(code);
105
234
  };
106
235
  let server;
236
+ const shutdown = async (code, event, msg) => {
237
+ if (stopping || !server) return;
238
+ stopping = true;
239
+ stopWatchingIdle?.();
240
+ try {
241
+ await server.close();
242
+ } catch (err) {
243
+ writer.write({
244
+ src: "metro",
245
+ level: "warn",
246
+ event: "server_close_failed",
247
+ msg: describeError(err)
248
+ });
249
+ }
250
+ finish(code, event, code === 0 ? "info" : "error", msg);
251
+ };
252
+ if (attachSignals) for (const signal of ["SIGTERM", "SIGINT"]) process.on(signal, () => {
253
+ if (server) {
254
+ shutdown(0, "supervisor_stopped", `received ${signal}; stopping the ${mode} dev server`);
255
+ return;
256
+ }
257
+ if (stopping) return;
258
+ stopping = true;
259
+ finish(signal === "SIGINT" ? 130 : 143, "supervisor_stopped", "warn", `received ${signal} before the ${mode} dev server started; stopping`);
260
+ });
261
+ const serverActivity = trackDevServerActivity(now);
262
+ const serverWriter = Object.assign(Object.create(writer), { write(entry) {
263
+ serverActivity.record(entry);
264
+ return writer.write(entry);
265
+ } });
107
266
  try {
108
- server = await (mode === "expo-child" ? startExpo || (await import("./server-expo-YDDG3eP7.mjs")).startExpoServer : startBare || (await import("./server-bare-BQkyxa_b.mjs")).startBareServer)({
267
+ server = await (mode === "expo-child" ? startExpo || (await import("./server-expo-DBVTdult.mjs")).startExpoServer : startBare || (await import("./server-bare-DVbb7kXH.mjs")).startBareServer)({
109
268
  root,
110
269
  port,
111
270
  logsDir,
112
- writer,
271
+ writer: serverWriter,
113
272
  tunnel,
114
273
  resetCache,
115
274
  onTunnelUrl: (url) => {
116
275
  try {
117
276
  withWorkspaceStateLock(root, () => {
118
- if (readWorkspaceState(root)?.supervisor?.processToken === processToken) writeWorkspaceState(root, { metroTunnel: {
277
+ if (readWorkspaceState$1(root)?.supervisor?.processToken === processToken) writeWorkspaceState(root, { metroTunnel: {
119
278
  kind: "expo",
120
279
  url
121
280
  } });
@@ -132,46 +291,72 @@ async function runSupervisor({ root, port, tunnel = false, resetCache = false, i
132
291
  }
133
292
  });
134
293
  } catch (err) {
294
+ if (stopping) return null;
135
295
  stderr(`Stim supervisor: failed to start the ${mode} dev server: ${describeError(err)}`);
136
296
  finish(1, "supervisor_failed", "fatal", `failed to start the ${mode} dev server: ${describeError(err)}`);
137
297
  return null;
138
298
  }
139
- const readyServer = server;
140
- if (readyServer.serverPid) withWorkspaceStateLock(root, () => {
141
- if (readWorkspaceState(root)?.supervisor?.processToken === processToken) writeWorkspaceState(root, { supervisor: {
142
- ...record,
143
- serverPid: readyServer.serverPid
144
- } });
145
- });
299
+ if (stopping) {
300
+ try {
301
+ await server.close();
302
+ } catch {}
303
+ return null;
304
+ }
305
+ const serverPid = server.serverPid;
306
+ if (serverPid) {
307
+ const serverProcessToken = (0, process_identity_exports.captureProcessToken)(serverPid);
308
+ withWorkspaceStateLock(root, () => {
309
+ if (readWorkspaceState$1(root)?.supervisor?.processToken === processToken) writeWorkspaceState(root, { supervisor: {
310
+ ...record,
311
+ serverPid,
312
+ ...serverProcessToken ? { serverProcessToken } : {}
313
+ } });
314
+ });
315
+ }
146
316
  writer.write({
147
317
  src: "metro",
148
318
  level: "info",
149
319
  event: "server_started",
150
320
  msg: `${mode} dev server listening on port ${port}`
151
321
  });
152
- const shutdown = async (code, event, msg) => {
153
- if (stopping) return;
154
- stopping = true;
155
- try {
156
- await readyServer.close();
157
- } catch (err) {
158
- writer.write({
159
- src: "metro",
160
- level: "warn",
161
- event: "server_close_failed",
162
- msg: describeError(err)
163
- });
322
+ if (idleStopMinutes > 0) stopWatchingIdle = watchIdleDevServer({
323
+ idleStopMs: idleStopMinutes * 6e4,
324
+ now,
325
+ serverActivityAt: serverActivity.lastActivityAt,
326
+ probe: idleProbe ?? workspaceIdleProbe(root),
327
+ onIdle: async (idleMinutesNow) => {
328
+ const stopIfStillIdle = async () => {
329
+ const idleMinutes = idleMinutesNow();
330
+ if (stopping || idleMinutes === null) return;
331
+ withWorkspaceStateLock(root, () => {
332
+ if (readWorkspaceState$1(root)?.supervisor?.processToken === processToken) writeWorkspaceState(root, { [IDLE_STOP_KEY]: {
333
+ reason: "idle",
334
+ at: new Date(now()).toISOString(),
335
+ idleMinutes
336
+ } });
337
+ });
338
+ await shutdown(0, "supervisor_idle_stopped", `no bundle request, client log or Stim command for ${idleMinutes} minutes (metro.idleStopMinutes is ${idleStopMinutes}); stopped the ${mode} dev server`);
339
+ };
340
+ try {
341
+ await withIdleWorkspace(root, () => withWorkspaceProcessLock(dirname(logsDir), "metro-start", stopIfStillIdle, {
342
+ external: true,
343
+ waitMs: 0,
344
+ ownerPurpose: "idle stop"
345
+ }), {
346
+ purpose: "idle stop",
347
+ supervisor: false,
348
+ managedLocks: false
349
+ });
350
+ } catch (err) {
351
+ stderr(`Stim supervisor: could not stop the idle dev server: ${describeError(err)}`);
352
+ }
164
353
  }
165
- finish(code, event, code === 0 ? "info" : "error", msg);
166
- };
167
- readyServer.onExit?.((info) => {
354
+ });
355
+ server.onExit?.((info) => {
168
356
  if (stopping) return;
169
357
  const detail = info?.signal ? `signal ${info.signal}` : `exit code ${info?.code ?? "unknown"}`;
170
358
  shutdown(1, "supervisor_stopped", `the ${mode} dev server exited unexpectedly (${detail}); shutting the supervisor down`);
171
359
  });
172
- if (attachSignals) for (const signal of ["SIGTERM", "SIGINT"]) process.on(signal, () => {
173
- shutdown(0, "supervisor_stopped", `received ${signal}; stopping the ${mode} dev server`);
174
- });
175
360
  return {
176
361
  mode,
177
362
  server,
@@ -201,7 +386,8 @@ async function main(argv = process.argv.slice(2)) {
201
386
  root,
202
387
  port: parsed.port,
203
388
  tunnel: parsed.tunnel ?? false,
204
- resetCache: parsed.resetCache ?? false
389
+ resetCache: parsed.resetCache ?? false,
390
+ idleStopMinutes: parsed.idleStopMinutes ?? 0
205
391
  });
206
392
  }
207
393
  function invokedDirectly() {
@@ -1,4 +1,5 @@
1
- import { E as ownership_claim_exports } from "./config-B7iyKWkL.mjs";
1
+ import { k as ownership_claim_exports } from "./config-BEq0Qw_a.mjs";
2
+ import { r as stopDeclaringSpawnsOn, t as declareSpawnsOn } from "./spawn-claims-CUleC2cM.mjs";
2
3
  import { join } from "node:path";
3
4
  //#region src/engine/workspace-process-lock.ts
4
5
  const DEFAULT_WAIT_MS = 6e4;
@@ -10,11 +11,11 @@ function workspaceProcessLockError(err) {
10
11
  if (code === "STIM_LOCK_TIMEOUT") return "timeout";
11
12
  return null;
12
13
  }
13
- function lockPath(root, name, external) {
14
+ function workspaceProcessLockPath(root, name, external) {
14
15
  return external ? join(root, `${name}.lock`) : join(root, ".stim", `${name}.lock`);
15
16
  }
16
- async function withWorkspaceProcessLock(root, name, fn, { now = Date.now, sleep = defaultSleep, waitMs = DEFAULT_WAIT_MS, ownerPurpose, rejectOwnerPurposes = [], external = false } = {}) {
17
- const path = lockPath(root, name, external);
17
+ async function withWorkspaceProcessLock(root, name, fn, { now = Date.now, sleep = defaultSleep, waitMs = DEFAULT_WAIT_MS, ownerPurpose, rejectOwnerPurposes = [], external = false, declareSpawns = false } = {}) {
18
+ const path = workspaceProcessLockPath(root, name, external);
18
19
  const deadline = now() + waitMs;
19
20
  for (;;) {
20
21
  const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
@@ -24,10 +25,14 @@ async function withWorkspaceProcessLock(root, name, fn, { now = Date.now, sleep
24
25
  details: ownerPurpose ? { purpose: ownerPurpose } : {}
25
26
  });
26
27
  if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
27
- if (attempt.acquired) try {
28
- return await fn();
29
- } finally {
30
- (0, ownership_claim_exports.releaseClaim)(attempt.acquired);
28
+ if (attempt.acquired) {
29
+ if (declareSpawns) declareSpawnsOn(attempt.acquired);
30
+ try {
31
+ return await fn(attempt.acquired);
32
+ } finally {
33
+ stopDeclaringSpawnsOn(attempt.acquired);
34
+ (0, ownership_claim_exports.releaseClaim)(attempt.acquired);
35
+ }
31
36
  }
32
37
  const purpose = (attempt.held ?? attempt.waitingFor?.[0])?.details.purpose;
33
38
  if (typeof purpose === "string" && rejectOwnerPurposes.includes(purpose)) {
@@ -44,4 +49,4 @@ async function withWorkspaceProcessLock(root, name, fn, { now = Date.now, sleep
44
49
  }
45
50
  }
46
51
  //#endregion
47
- export { workspaceProcessLockError as n, withWorkspaceProcessLock as t };
52
+ export { workspaceProcessLockError as n, workspaceProcessLockPath as r, withWorkspaceProcessLock as t };