stim 1.14.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 (130) hide show
  1. package/README.md +2 -0
  2. package/dist/{activity-CYVDcrt3.mjs → activity-B2-kb7k0.mjs} +281 -41
  3. package/dist/{android-XpvCrV7q.mjs → android-CYmq0jNi.mjs} +2 -2
  4. package/dist/{android-LbPFRWVL.mjs → android-Cu_L4RpL.mjs} +502 -407
  5. package/dist/{android-d6rGq1pA.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-BpgSgkJj.mjs → app-install-CAAplXAt.mjs} +95 -161
  9. package/dist/{budget-CGDkhvv_.mjs → budget-CMiqghig.mjs} +1038 -73
  10. package/dist/{build-plan-Dcr-3ie0.mjs → build-plan-aeGMFX7e.mjs} +193 -653
  11. package/dist/build-progress-C2Jv1AtA.mjs +872 -0
  12. package/dist/build-slots-Df1ApSuH.mjs +127 -0
  13. package/dist/{cache-manifest-C68QQ0ua.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
  14. package/dist/cache-manifest.mjs +1 -1
  15. package/dist/{chrome-BPCiC3ZX.mjs → chrome-BrcVWQyD.mjs} +1 -1
  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-DVcpw6eE.mjs → command-output-CliHHn7a.mjs} +1 -1
  21. package/dist/{config-lvc7d3C2.mjs → config-D0DbpZtM.mjs} +28 -29
  22. package/dist/{created-devices-CllH_8eb.mjs → created-devices-BwVb3liH.mjs} +2 -2
  23. package/dist/{dependency-state-DsT7aKeo.mjs → dependency-state-BmsjKZ2H.mjs} +2 -2
  24. package/dist/{deps-D9IcKqXS.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-CjuuAJjQ.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-5c3WrVRm.mjs → device-lease-W5Pi1q38.mjs} +43 -36
  33. package/dist/{device-pool-BiyDyPP-.mjs → device-pool-B2r38YjS.mjs} +4 -4
  34. package/dist/{device-remote-D87zEzaq.mjs → device-remote-9ulPsdhY.mjs} +68 -162
  35. package/dist/{doctor-x9pnU9fq.mjs → doctor-BcE5xsBn.mjs} +191 -99
  36. package/dist/{error-diagnostics-C5HzcwCW.mjs → error-diagnostics-Cb0P1WLS.mjs} +12 -11
  37. package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
  38. package/dist/{gc-ko6mUAYD.mjs → gc-BuL1Genr.mjs} +429 -101
  39. package/dist/{android-cas-iZK3DOCr.mjs → gradle-CmdOBq1d.mjs} +8 -294
  40. package/dist/{guide-6R0VNFqI.mjs → guide-cCSGRFd3.mjs} +1959 -222
  41. package/dist/{guide-status-BZrFPIA_.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-BwDPrnYQ.mjs → ios-3CtCRhwr.mjs} +201 -10
  46. package/dist/{ios-P1f5nm2n.mjs → ios-Chob8ny7.mjs} +512 -263
  47. package/dist/{ios-device-DGglU3E_.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-Deg4tB3Z.mjs → launch-verify-CLlofxRt.mjs} +4 -4
  50. package/dist/{logs-Civ4gbBH.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-CcEo8Fl8.mjs → metro-Di8EN2Tb.mjs} +20 -120
  58. package/dist/{named-ports-Blg8IxOh.mjs → named-ports-Dj_XBNaL.mjs} +6 -59
  59. package/dist/native-run-DFDoIRbY.mjs +147 -0
  60. package/dist/{native-runtime-DjBHRg5m.mjs → native-runtime-i6OkVg-w.mjs} +9 -5
  61. package/dist/offload-worker.d.mts +121 -0
  62. package/dist/offload-worker.mjs +601 -0
  63. package/dist/{ownership-DyYDF5ww.mjs → ownership-BkYxUtuj.mjs} +3 -2
  64. package/dist/{page-BHUzTDFt.mjs → page-QF_Ke10s.mjs} +7 -2
  65. package/dist/{ports-BguS83ex.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-CWlvEaeN.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-BiniwsSZ.mjs → reload-ChPiwvot.mjs} +70 -22
  74. package/dist/{remote-cache-CcR6o6lI.mjs → remote-cache-CO2elm_b.mjs} +4 -4
  75. package/dist/run-B-xqRrsg.mjs +119 -0
  76. package/dist/{server-bare-DGfgyJ5J.mjs → server-bare-X7PUgizL.mjs} +33 -15
  77. package/dist/{server-expo-BVuwQ65v.mjs → server-expo-DhtX6bom.mjs} +6 -10
  78. package/dist/server-expo-KJeD109I.mjs +2 -0
  79. package/dist/{settings-D61iNOsB.mjs → settings-CduVcoxj.mjs} +77 -26
  80. package/dist/{settings-B-fsC-Om.mjs → settings-DbL6uhH8.mjs} +16 -10
  81. package/dist/settings.schema.json +357 -2
  82. package/dist/{simslim-DrDN4WuV.mjs → simslim-Ciluxlq4.mjs} +8 -132
  83. package/dist/{slot-launch-D5t8EiMf.mjs → slot-launch-BY0BrOLh.mjs} +6 -6
  84. package/dist/{spawn-claims-Drw_o-Fb.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-s4RXp1wy.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-DHqvsbqC.mjs → state-CaeaHbDA.mjs} +7 -4
  92. package/dist/{state-Dk9RVXI7.mjs → state-DFm77wRn.mjs} +67 -5
  93. package/dist/stats-DBLLFmKd.mjs +66 -0
  94. package/dist/{status-BsPAnfOI.mjs → status-DYXxNOT0.mjs} +423 -268
  95. package/dist/{status-BgAuxESg.mjs → status-l84Gbvp2.mjs} +116 -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-BzreOWJp.mjs → stop-Bad61z_2.mjs} +38 -12
  101. package/dist/stop-Bvf1MflS.mjs +48 -0
  102. package/dist/{stop-BWIF_6BK.mjs → stop-CceYh1yG.mjs} +2 -2
  103. package/dist/stop-cause-CZ_Mr-v9.mjs +665 -0
  104. package/dist/supervisor-run.d.mts +43 -11
  105. package/dist/supervisor-run.mjs +227 -19
  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-Cp5KvK9x.mjs → toolchain-DmeYjXD9.mjs} +363 -13
  110. package/dist/warm-progress-DVHLN4uC.mjs +463 -0
  111. package/dist/{idle-DXtx3RbX.mjs → watchman-Bx0wceoF.mjs} +55 -8
  112. package/dist/{web-BsQs1WlO.mjs → web-k6w8-HnE.mjs} +37 -20
  113. package/dist/web-run.mjs +309 -12
  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-BVrHsYZG.mjs → worktree-C0c5Udwa.mjs} +55 -54
  118. package/dist/{worktree-UrQWXSeb.mjs → worktree-qXKzADSC.mjs} +154 -252
  119. package/package.json +10 -5
  120. package/shim/bundle-response.cjs +28 -12
  121. package/dist/build-progress-DxKpwnaS.mjs +0 -247
  122. package/dist/in-use-DlqBDKqY.mjs +0 -249
  123. package/dist/server-expo-DIHDhvGg.mjs +0 -2
  124. package/dist/start-Cetfvy8l.mjs +0 -2
  125. package/dist/stats-CFpFGlJV.mjs +0 -62
  126. package/dist/stats-CX1Z8Tca.mjs +0 -368
  127. package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
  128. package/dist/stop-cause-hjGBOZTG.mjs +0 -1231
  129. package/dist/worktree-CHWWvm5v.mjs +0 -2
  130. package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
@@ -0,0 +1,665 @@
1
+ import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
+ import { d as getConfigDir } from "./config-D0DbpZtM.mjs";
3
+ import { n as ownership_claim_exports, t as process_identity_exports } from "./process-identity-BmRm1qV0.mjs";
4
+ import { r as createNdjsonWriter } from "./ndjson-CDY9IkVC.mjs";
5
+ import { C as workspaceLogsDir, c as writeWorkspaceState, t as clearWorkspaceStateKey } from "./workspace-state-CL2V2Ons.mjs";
6
+ import { r as workspaceProcessLockPath, t as withWorkspaceProcessLock } from "./workspace-process-lock-BL7Q0HCO.mjs";
7
+ import { t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
8
+ import { o as pidExists, u as signalProcessTree } from "./metro-Di8EN2Tb.mjs";
9
+ import { G as resolveTailscaleBinary } from "./settings-CduVcoxj.mjs";
10
+ import { createHash, randomUUID } from "node:crypto";
11
+ import { closeSync, mkdirSync, openSync, readFileSync, unlinkSync } from "node:fs";
12
+ import { basename, join, resolve, sep } from "node:path";
13
+ import { LOG_ROTATE_BYTES } from "@stim-cli/core";
14
+ import { DEV_SERVER_STOP_REQUEST_KEY, readDevServerStop, readDevServerStopRequest } from "@stim-cli/core/state";
15
+ import { resolve4 } from "node:dns/promises";
16
+ import { request } from "node:https";
17
+ //#region src/engine/public-http-probe.ts
18
+ const DNS_TIMEOUT_MS = 5e3;
19
+ async function resolveAddresses(hostname, signal) {
20
+ if (signal.aborted) throw signal.reason;
21
+ let timer;
22
+ let onAbort;
23
+ const unavailable = new Promise((_resolve, reject) => {
24
+ onAbort = () => reject(signal.reason);
25
+ signal.addEventListener("abort", onAbort, { once: true });
26
+ if (signal.aborted) onAbort();
27
+ else timer = setTimeout(() => reject(/* @__PURE__ */ new Error("DNS lookup timed out")), DNS_TIMEOUT_MS);
28
+ });
29
+ try {
30
+ return await Promise.race([resolve4(hostname), unavailable]);
31
+ } finally {
32
+ clearTimeout(timer);
33
+ if (onAbort) signal.removeEventListener("abort", onAbort);
34
+ }
35
+ }
36
+ async function probePublicHttp(url, signal) {
37
+ try {
38
+ return (await fetch(url, {
39
+ signal,
40
+ redirect: "follow"
41
+ })).status;
42
+ } catch (error) {
43
+ const cause = error?.cause;
44
+ if (cause?.code !== "ENOTFOUND" && cause?.code !== "EAI_AGAIN") return null;
45
+ }
46
+ const hostname = new URL(url).hostname;
47
+ if (!hostname.endsWith(".trycloudflare.com")) return null;
48
+ let addresses;
49
+ try {
50
+ addresses = await resolveAddresses(hostname, signal);
51
+ } catch {
52
+ return null;
53
+ }
54
+ const firstAddress = addresses[0];
55
+ if (!firstAddress) return null;
56
+ return new Promise((resolve) => {
57
+ const request$1 = request(url, {
58
+ signal,
59
+ lookup: (_hostname, options, callback) => {
60
+ if (options.all) callback(null, addresses.map((address) => ({
61
+ address,
62
+ family: 4
63
+ })));
64
+ else callback(null, firstAddress, 4);
65
+ }
66
+ }, (response) => {
67
+ response.resume();
68
+ resolve(response.statusCode ?? null);
69
+ });
70
+ request$1.on("error", () => resolve(null));
71
+ request$1.end();
72
+ });
73
+ }
74
+ //#endregion
75
+ //#region src/engine/tunnel.ts
76
+ function tunnelArgv(provider, port, ngrokUrl, logFile, tailscaleBinary = "tailscale") {
77
+ if (provider === "tailscale") return {
78
+ bin: tailscaleBinary,
79
+ args: [
80
+ "serve",
81
+ `--https=${port}`,
82
+ `http://127.0.0.1:${port}`
83
+ ]
84
+ };
85
+ if (provider === "cloudflared") return {
86
+ bin: "cloudflared",
87
+ args: [
88
+ "tunnel",
89
+ "--url",
90
+ `http://127.0.0.1:${port}`,
91
+ ...logFile ? ["--logfile", logFile] : []
92
+ ]
93
+ };
94
+ return {
95
+ bin: "ngrok",
96
+ args: [
97
+ "http",
98
+ String(port),
99
+ `--log=${logFile || "stdout"}`,
100
+ "--log-format=json",
101
+ ...ngrokUrl ? ["--url", ngrokUrl] : []
102
+ ]
103
+ };
104
+ }
105
+ const CLOUDFLARED_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
106
+ function parseCloudflaredLine(line) {
107
+ const match = line.match(CLOUDFLARED_URL_RE);
108
+ return match ? match[0] : null;
109
+ }
110
+ function parseNgrokLine(line) {
111
+ let data;
112
+ try {
113
+ data = JSON.parse(line);
114
+ } catch {
115
+ return null;
116
+ }
117
+ if (typeof data !== "object" || data === null) return null;
118
+ const url = data.url;
119
+ return typeof url === "string" && /^https?:\/\//i.test(url) ? url : null;
120
+ }
121
+ function parseTailscaleLine(line) {
122
+ return /^https:\/\/[^\s/]+\.ts\.net(:\d+)?\/?$/.test(line) ? line : null;
123
+ }
124
+ function parserFor(provider) {
125
+ if (provider === "tailscale") return parseTailscaleLine;
126
+ return provider === "cloudflared" ? parseCloudflaredLine : parseNgrokLine;
127
+ }
128
+ const URL_TIMEOUT_MS = 15e3;
129
+ const REACHABLE_TIMEOUT_MS = 24e4;
130
+ const REACHABLE_POLL_MS = 2e3;
131
+ const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
132
+ async function withManagedTunnelLock(root, fn, options = {}) {
133
+ return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
134
+ ...options,
135
+ rejectOwnerPurposes: ["workspace removal"]
136
+ });
137
+ }
138
+ async function withManagedTunnelRemovalLock(root, fn, options = {}) {
139
+ return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
140
+ ...options,
141
+ ownerPurpose: "workspace removal"
142
+ });
143
+ }
144
+ async function withManagedRemoteWorktreeLock(worktreeRoot, fn, options = {}) {
145
+ return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
146
+ ...options,
147
+ external: true,
148
+ rejectOwnerPurposes: ["worktree removal"]
149
+ });
150
+ }
151
+ async function withManagedRemoteWorktreeRemovalLock(worktreeRoot, fn, options = {}) {
152
+ return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
153
+ ...options,
154
+ external: true,
155
+ ownerPurpose: "worktree removal"
156
+ });
157
+ }
158
+ function managedLockHolders(root) {
159
+ const locks = [{
160
+ label: "managed tunnel",
161
+ path: workspaceProcessLockPath(managedTunnelLockRoot(root), "metro-tunnel", false)
162
+ }, {
163
+ label: "managed remote",
164
+ path: workspaceProcessLockPath(managedRemoteWorktreeLockRoot(root), "managed-remote", true)
165
+ }];
166
+ const reasons = [];
167
+ for (const { label, path } of locks) {
168
+ const claims = (0, ownership_claim_exports.readClaimSet)(path);
169
+ const holder = claims.live[0];
170
+ if (holder) {
171
+ const purpose = typeof holder.details.purpose === "string" ? ` for ${holder.details.purpose}` : "";
172
+ reasons.push(`the ${label} lock is held${purpose}`);
173
+ } else if (claims.unresolved[0]) reasons.push(`the ${label} lock cannot be resolved: ${claims.unresolved[0].reason}`);
174
+ }
175
+ return reasons;
176
+ }
177
+ function managedTunnelLockRoot(root) {
178
+ const key = createHash("sha256").update(resolve(root)).digest("hex");
179
+ return join(getConfigDir(), "process-locks", key);
180
+ }
181
+ function managedRemoteWorktreeLockRoot(worktreeRoot) {
182
+ const key = createHash("sha256").update(resolve(worktreeRoot)).digest("hex");
183
+ return join(getConfigDir(), "process-locks", "worktrees", key);
184
+ }
185
+ async function defaultProbeReachable(url, signal) {
186
+ const status = await probePublicHttp(url, signal);
187
+ return status !== null && status > 0;
188
+ }
189
+ function waitForUrl(child, parseLine, timeoutMs) {
190
+ return new Promise((resolve) => {
191
+ let settled = false;
192
+ let lastLine = null;
193
+ let timer;
194
+ let onOut;
195
+ let onErr;
196
+ let onError;
197
+ let onExit;
198
+ const finish = (url, exited = false) => {
199
+ if (settled) return;
200
+ settled = true;
201
+ clearTimeout(timer);
202
+ child.stdout?.removeListener("data", onOut);
203
+ child.stderr?.removeListener("data", onErr);
204
+ child.removeListener("error", onError);
205
+ child.removeListener("exit", onExit);
206
+ resolve({
207
+ url,
208
+ exited,
209
+ lastLine
210
+ });
211
+ };
212
+ const onLine = (line) => {
213
+ if (settled) return;
214
+ if (line.trim()) lastLine = line.trim();
215
+ const url = parseLine(line);
216
+ if (url) finish(url);
217
+ };
218
+ const outReader = createLineReader(onLine);
219
+ const errReader = createLineReader(onLine);
220
+ onOut = (chunk) => outReader.push(chunk);
221
+ onErr = (chunk) => errReader.push(chunk);
222
+ onError = () => finish(null);
223
+ onExit = () => {
224
+ outReader.flush();
225
+ errReader.flush();
226
+ finish(null, true);
227
+ };
228
+ child.stdout?.setEncoding?.("utf-8");
229
+ child.stderr?.setEncoding?.("utf-8");
230
+ child.stdout?.on("data", onOut);
231
+ child.stderr?.on("data", onErr);
232
+ child.on("error", onError);
233
+ child.on("exit", onExit);
234
+ timer = setTimeout(() => finish(null), timeoutMs);
235
+ });
236
+ }
237
+ function waitForFileUrl(child, logFile, parseLine, timeoutMs) {
238
+ return new Promise((resolve) => {
239
+ let settled = false;
240
+ const finish = (url, exited = false) => {
241
+ if (settled) return;
242
+ settled = true;
243
+ clearInterval(interval);
244
+ clearTimeout(timer);
245
+ child.removeListener("error", onError);
246
+ child.removeListener("exit", onExit);
247
+ resolve({
248
+ url,
249
+ exited
250
+ });
251
+ };
252
+ const probe = () => {
253
+ try {
254
+ for (const line of readFileSync(logFile, "utf-8").split(/\r?\n/)) {
255
+ const url = parseLine(line);
256
+ if (url) return finish(url);
257
+ }
258
+ } catch {}
259
+ };
260
+ const onError = () => finish(null);
261
+ const onExit = () => finish(null, true);
262
+ child.on("error", onError);
263
+ child.on("exit", onExit);
264
+ const interval = setInterval(probe, 25);
265
+ const timer = setTimeout(() => finish(null), timeoutMs);
266
+ probe();
267
+ });
268
+ }
269
+ function createTunnelLogFile(provider) {
270
+ const dir = join(getConfigDir(), "tunnel-logs");
271
+ mkdirSync(dir, { recursive: true });
272
+ const path = join(dir, `${provider}-${randomUUID()}.log`);
273
+ closeSync(openSync(path, "wx", 384));
274
+ return path;
275
+ }
276
+ function removeTunnelLogFile(path) {
277
+ if (!path) return;
278
+ try {
279
+ unlinkSync(path);
280
+ } catch {}
281
+ }
282
+ function removeRecordedTunnelLogFile(path) {
283
+ if (!path) return;
284
+ const dir = resolve(join(getConfigDir(), "tunnel-logs"));
285
+ const resolved = resolve(path);
286
+ if (!resolved.startsWith(`${dir}${sep}`) || resolve(join(dir, basename(resolved))) !== resolved) return;
287
+ removeTunnelLogFile(resolved);
288
+ }
289
+ async function waitUntilReachable({ url, now, sleep, probe, timeoutMs }) {
290
+ const deadline = now() + timeoutMs;
291
+ for (;;) {
292
+ if (await probe(url, new AbortController().signal).catch(() => false)) return true;
293
+ if (now() >= deadline) return false;
294
+ await sleep(REACHABLE_POLL_MS);
295
+ }
296
+ }
297
+ const CLEANUP_TIMEOUT_MS = 1e3;
298
+ const CLEANUP_POLL_MS = 25;
299
+ async function startTunnel({ provider, port, spawnFn = null, urlTimeoutMs = URL_TIMEOUT_MS, reachableTimeoutMs = REACHABLE_TIMEOUT_MS, probeReachable = defaultProbeReachable, now = Date.now, sleep = defaultSleep, ngrokUrl = null, requireReachable = true, cleanupTimeoutMs = CLEANUP_TIMEOUT_MS, isChildAlive = pidExists, readProcessToken = process_identity_exports.captureProcessToken, logFile = null }) {
300
+ const spawn = spawnFn || ((cmd, args, opts) => getExecutor().spawn(cmd, args, opts));
301
+ const outputFile = provider === "tailscale" ? null : logFile || (spawnFn ? null : createTunnelLogFile(provider));
302
+ let child;
303
+ try {
304
+ const binary = provider === "tailscale" ? resolveTailscaleBinary((bin) => getExecutor().findExecutable(bin)) : provider;
305
+ if (!binary) throw new Error("Install Tailscale and sign in to your tailnet.");
306
+ const { bin, args } = tunnelArgv(provider, port, ngrokUrl, outputFile, binary);
307
+ child = spawn(bin, args, {
308
+ stdio: outputFile ? [
309
+ "ignore",
310
+ "ignore",
311
+ "ignore"
312
+ ] : [
313
+ "ignore",
314
+ "pipe",
315
+ "pipe"
316
+ ],
317
+ detached: true
318
+ });
319
+ } catch (err) {
320
+ removeTunnelLogFile(outputFile);
321
+ return {
322
+ failed: true,
323
+ reason: `Could not start ${provider}: ${describe(err)}`
324
+ };
325
+ }
326
+ let childExited = false;
327
+ child.once("exit", () => {
328
+ childExited = true;
329
+ });
330
+ const cleanup = async (alreadyExited = childExited) => {
331
+ const stopped = await terminateChild(child, {
332
+ alreadyExited,
333
+ timeoutMs: cleanupTimeoutMs,
334
+ now,
335
+ sleep,
336
+ isAlive: isChildAlive
337
+ });
338
+ if (stopped) removeTunnelLogFile(outputFile);
339
+ return stopped ? { status: "stopped" } : {
340
+ status: "failed",
341
+ reason: `Sent SIGKILL but could not confirm that pid ${child.pid ?? "unknown"} exited.`
342
+ };
343
+ };
344
+ const failAfterCleanup = async (reason, alreadyExited = childExited) => {
345
+ const stopped = await cleanup(alreadyExited);
346
+ return stopped.status === "stopped" ? {
347
+ failed: true,
348
+ reason
349
+ } : {
350
+ failed: true,
351
+ reason: `${reason} ${stopped.reason}`,
352
+ cleanupFailed: true
353
+ };
354
+ };
355
+ const pid = child.pid;
356
+ if (!pid) return failAfterCleanup(`${provider} started but reported no pid.`);
357
+ const captureChildIdentity = () => {
358
+ try {
359
+ return readProcessToken(pid);
360
+ } catch {
361
+ return null;
362
+ }
363
+ };
364
+ const initialProcessToken = captureChildIdentity();
365
+ if (!initialProcessToken) return failAfterCleanup(`${provider} started but its process identity token could not be read.`);
366
+ const { url, exited, lastLine } = outputFile ? await waitForFileUrl(child, outputFile, parserFor(provider), urlTimeoutMs) : await waitForUrl(child, parserFor(provider), urlTimeoutMs);
367
+ if (!url) return failAfterCleanup(exited ? provider === "tailscale" && lastLine ? `${provider} exited before printing a tunnel URL: ${lastLine}` : `${provider} exited before printing a tunnel URL.` : `${provider} did not print a tunnel URL within ${urlTimeoutMs}ms.`, exited || childExited);
368
+ if (!outputFile) resumeChildPipes(child);
369
+ if (requireReachable) {
370
+ if (!await waitUntilReachable({
371
+ url,
372
+ now,
373
+ sleep,
374
+ probe: probeReachable,
375
+ timeoutMs: reachableTimeoutMs
376
+ })) return failAfterCleanup(`${url} did not become reachable within ${reachableTimeoutMs}ms; ${provider} may still be registering, or the network path is blocked.`);
377
+ }
378
+ const finalProcessToken = captureChildIdentity();
379
+ if (!finalProcessToken || finalProcessToken !== initialProcessToken) return failAfterCleanup(`${provider} process identity changed before its tunnel could be recorded.`);
380
+ if (!outputFile) unrefChildPipes(child);
381
+ child.unref?.();
382
+ if (childExited) return failAfterCleanup(`${provider} exited before its tunnel could be recorded.`, true);
383
+ return {
384
+ url,
385
+ pid,
386
+ processToken: initialProcessToken,
387
+ logFile: outputFile,
388
+ cleanup: () => cleanup()
389
+ };
390
+ }
391
+ async function startTunnelSequence({ providers, port, ngrokUrl = null, requireReachable = true, start = startTunnel }) {
392
+ const failures = [];
393
+ for (const provider of providers) {
394
+ const result = await start({
395
+ provider,
396
+ port,
397
+ ngrokUrl: provider === "ngrok" ? ngrokUrl : null,
398
+ requireReachable
399
+ });
400
+ if (!("failed" in result)) return {
401
+ provider,
402
+ ...result
403
+ };
404
+ failures.push(`${provider}: ${result.reason}`);
405
+ if (result.cleanupFailed) return {
406
+ failed: true,
407
+ reason: failures.join(" ")
408
+ };
409
+ }
410
+ return {
411
+ failed: true,
412
+ reason: failures.join(" ") || "No managed tunnel provider was selected."
413
+ };
414
+ }
415
+ function closeChildPipes(child) {
416
+ child.stdout?.destroy?.();
417
+ child.stderr?.destroy?.();
418
+ }
419
+ function unrefChildPipes(child) {
420
+ resumeChildPipes(child);
421
+ for (const stream of [child.stdout, child.stderr]) stream?.unref?.();
422
+ }
423
+ function resumeChildPipes(child) {
424
+ child.stdout?.resume?.();
425
+ child.stderr?.resume?.();
426
+ }
427
+ async function signalAndWaitForExit(child, signal, { timeoutMs, now, sleep, isAlive, platform }) {
428
+ let exited = false;
429
+ const onExit = () => {
430
+ exited = true;
431
+ };
432
+ child.once("exit", onExit);
433
+ try {
434
+ if (platform === "win32" && child.pid) signalProcessTree(child.pid, signal, { platform });
435
+ child.kill(signal);
436
+ } catch (err) {
437
+ child.removeListener("exit", onExit);
438
+ return isEsrch(err);
439
+ }
440
+ const hasExited = () => exited;
441
+ const deadline = now() + timeoutMs;
442
+ while (!hasExited() && now() < deadline) await sleep(CLEANUP_POLL_MS);
443
+ if (!hasExited() && child.pid && !isAlive(child.pid)) exited = true;
444
+ child.removeListener("exit", onExit);
445
+ return hasExited();
446
+ }
447
+ async function terminateChild(child, { alreadyExited, timeoutMs, now, sleep, isAlive, platform = process.platform }) {
448
+ if (alreadyExited) {
449
+ closeChildPipes(child);
450
+ return true;
451
+ }
452
+ if (await signalAndWaitForExit(child, "SIGTERM", {
453
+ timeoutMs,
454
+ now,
455
+ sleep,
456
+ isAlive,
457
+ platform
458
+ })) {
459
+ closeChildPipes(child);
460
+ return true;
461
+ }
462
+ const killed = await signalAndWaitForExit(child, "SIGKILL", {
463
+ timeoutMs,
464
+ now,
465
+ sleep,
466
+ isAlive,
467
+ platform
468
+ });
469
+ closeChildPipes(child);
470
+ return killed;
471
+ }
472
+ function describe(err) {
473
+ return err?.message || String(err);
474
+ }
475
+ const STOP_TIMEOUT_MS = 5e3;
476
+ const STOP_POLL_MS = 100;
477
+ function isEsrch(err) {
478
+ return err?.code === "ESRCH";
479
+ }
480
+ async function stopTunnel(record, { isAlive = pidExists, inspectIdentity = process_identity_exports.inspectProcessIdentity, platform = process.platform, kill = (pid) => signalProcessTree(pid, "SIGTERM", { platform }), now = Date.now, sleep = defaultSleep, timeoutMs = STOP_TIMEOUT_MS } = {}) {
481
+ const missing = () => {
482
+ removeRecordedTunnelLogFile(record?.logFile);
483
+ return { status: "missing" };
484
+ };
485
+ const pid = record?.pid;
486
+ if (!pid || !isAlive(pid)) return missing();
487
+ if (!record.processToken) return {
488
+ status: "failed",
489
+ reason: `tunnel pid ${pid} has no process identity token, so Stim cannot verify ownership. Keep the record. Inspect the process, stop it with the provider tooling, remove the stale metroTunnel state, and retry.`
490
+ };
491
+ const identity = inspectIdentity(record);
492
+ if (identity === "gone" || identity === "different") return missing();
493
+ if (identity !== "same") return {
494
+ status: "failed",
495
+ reason: `could not verify the process identity for tunnel pid ${pid}; refusing to signal it.`
496
+ };
497
+ try {
498
+ kill(pid);
499
+ } catch (err) {
500
+ return isEsrch(err) ? missing() : {
501
+ status: "failed",
502
+ reason: describe(err)
503
+ };
504
+ }
505
+ const deadline = now() + timeoutMs;
506
+ const exited = () => {
507
+ if (!isAlive(pid)) return true;
508
+ const current = inspectIdentity(record);
509
+ return current === "gone" || current === "different";
510
+ };
511
+ while (now() < deadline && !exited()) await sleep(STOP_POLL_MS);
512
+ if (!exited()) return {
513
+ status: "failed",
514
+ reason: `pid ${pid} did not exit within ${timeoutMs}ms.`
515
+ };
516
+ removeRecordedTunnelLogFile(record.logFile);
517
+ return { status: "stopped" };
518
+ }
519
+ //#endregion
520
+ //#region src/supervisor/ownership.ts
521
+ function resolveSupervisorTarget({ state, record, reservedPort, isAlive = pidExists, inspectIdentity = process_identity_exports.inspectProcessIdentity } = {}) {
522
+ const statePid = numberOrNull(state?.pid);
523
+ const recordPid = numberOrNull(record?.pid);
524
+ const pid = statePid ?? recordPid;
525
+ if (!pid) return { status: "none" };
526
+ if (statePid && recordPid && statePid !== recordPid) return {
527
+ status: "unverified",
528
+ pid,
529
+ reason: `workspace state.json records supervisor pid ${statePid} but the registry records pid ${recordPid}`
530
+ };
531
+ if (!isAlive(pid)) return {
532
+ status: "stale",
533
+ pid
534
+ };
535
+ const port = numberOrNull(state?.port) ?? numberOrNull(record?.port);
536
+ if (reservedPort !== null && reservedPort !== void 0 && port !== null && port !== reservedPort) return {
537
+ status: "unverified",
538
+ pid,
539
+ reason: `supervisor pid ${pid} records port ${port}, but this project reserved port ${reservedPort}`
540
+ };
541
+ if (reservedPort !== null && reservedPort !== void 0 && port === null) return {
542
+ status: "unverified",
543
+ pid,
544
+ reason: `supervisor pid ${pid} has no recorded port, so it cannot be matched against reserved port ${reservedPort}`
545
+ };
546
+ const identityRecord = statePid ? state : record;
547
+ if (statePid && recordPid && state?.processToken !== record?.processToken) return {
548
+ status: "unverified",
549
+ pid,
550
+ reason: "workspace state and registry process identities disagree"
551
+ };
552
+ const identity = inspectIdentity(identityRecord);
553
+ if (identity === "gone" || identity === "different") return {
554
+ status: "stale",
555
+ pid
556
+ };
557
+ if (identity !== "same") return {
558
+ status: "unverified",
559
+ pid,
560
+ reason: "the recorded process identity could not be verified"
561
+ };
562
+ return {
563
+ status: "ours",
564
+ processToken: identityRecord.processToken,
565
+ pid,
566
+ port,
567
+ mode: state?.mode ?? record?.mode ?? null,
568
+ startedAt: state?.startedAt ?? record?.startedAt ?? null
569
+ };
570
+ }
571
+ function numberOrNull(v) {
572
+ return typeof v === "number" && Number.isSafeInteger(v) && v > 0 ? v : null;
573
+ }
574
+ //#endregion
575
+ //#region src/supervisor/stop-cause.ts
576
+ function requestDevServerStop(root, processToken, requester) {
577
+ if (!processToken) return;
578
+ try {
579
+ writeWorkspaceState(root, { [DEV_SERVER_STOP_REQUEST_KEY]: {
580
+ processToken,
581
+ by: requester.by,
582
+ pid: process.pid,
583
+ at: (/* @__PURE__ */ new Date()).toISOString(),
584
+ ...requester.workspace ? { workspace: requester.workspace } : {}
585
+ } });
586
+ } catch {}
587
+ }
588
+ function withdrawDevServerStopRequest(root, processToken) {
589
+ if (!processToken) return;
590
+ try {
591
+ clearWorkspaceStateKey(root, DEV_SERVER_STOP_REQUEST_KEY, (value) => {
592
+ return readDevServerStopRequest({ [DEV_SERVER_STOP_REQUEST_KEY]: value })?.processToken === processToken;
593
+ });
594
+ } catch {}
595
+ }
596
+ function devServerStopRecord(trigger, request, at) {
597
+ if (request) return {
598
+ reason: "requested",
599
+ at,
600
+ by: request.by,
601
+ byPid: request.pid,
602
+ ...request.workspace ? { byWorkspace: request.workspace } : {}
603
+ };
604
+ return trigger.kind === "signal" ? {
605
+ reason: "signal",
606
+ at,
607
+ signal: trigger.signal
608
+ } : {
609
+ reason: "server-exited",
610
+ at,
611
+ mode: trigger.mode,
612
+ code: trigger.code,
613
+ signal: trigger.signal
614
+ };
615
+ }
616
+ function describeDevServerStop(record) {
617
+ if (record.reason === "idle") return `idle for ${record.idleMinutes} minutes (metro.idleStopMinutes)`;
618
+ if (record.reason === "requested") return `stop requested by ${record.by} (pid ${record.byPid}${record.byWorkspace ? ` in ${record.byWorkspace}` : ""})`;
619
+ if (record.reason === "signal") return `received ${record.signal} with no Stim stop request recorded, so it likely came from outside Stim`;
620
+ const detail = record.signal ? `signal ${record.signal}` : `exit code ${record.code ?? "unknown"}`;
621
+ const expoSignal = record.mode === "expo-child" && record.code === 0 ? "; Expo CLI also exits 0 on SIGTERM, and no Stim stop request was recorded" : "";
622
+ return `the ${record.mode} dev server exited unexpectedly (${detail})${expoSignal}`;
623
+ }
624
+ function devServerStopLevel(record) {
625
+ if (record.reason === "server-exited") return "error";
626
+ return record.reason === "signal" ? "warn" : "info";
627
+ }
628
+ /** The log line for a recorded supervisor whose process is proven gone, or null while it may still run. */
629
+ function vanishedSupervisorMessage(record) {
630
+ if (!record || typeof record.pid !== "number") return null;
631
+ const identity = (0, process_identity_exports.inspectProcessIdentity)(record);
632
+ if (identity !== "gone" && identity !== "different") return null;
633
+ const started = typeof record.startedAt === "string" ? ` (started ${record.startedAt})` : "";
634
+ return `supervisor pid ${record.pid}${started} ${VANISHED}`;
635
+ }
636
+ const VANISHED = "is gone and recorded no cause: usually SIGKILL, a crash or a machine restart";
637
+ function logVanishedSupervisor(root, record) {
638
+ const msg = vanishedSupervisorMessage(record);
639
+ if (!msg) return;
640
+ const writer = createNdjsonWriter(join(workspaceLogsDir(root), "metro.ndjson"), { maxBytes: LOG_ROTATE_BYTES });
641
+ writer.write({
642
+ src: "metro",
643
+ level: "warn",
644
+ event: "supervisor_vanished",
645
+ msg
646
+ });
647
+ writer.close();
648
+ }
649
+ /** The recorded stop, else a vanished supervisor when status proved the recorded one gone. */
650
+ function metroLastStop(state, supervisor) {
651
+ const recorded = readDevServerStop(state);
652
+ if (recorded) return recorded;
653
+ if (supervisor?.status !== "stale" || typeof supervisor.pid !== "number") return null;
654
+ const startedAt = typeof supervisor.startedAt === "string" ? supervisor.startedAt : null;
655
+ return {
656
+ reason: "vanished",
657
+ pid: supervisor.pid,
658
+ startedAt
659
+ };
660
+ }
661
+ function describeMetroLastStop(stop) {
662
+ return stop.reason === "vanished" ? `supervisor pid ${stop.pid} ${VANISHED}` : describeDevServerStop(stop);
663
+ }
664
+ //#endregion
665
+ export { withManagedTunnelLock as _, logVanishedSupervisor as a, vanishedSupervisorMessage as c, managedLockHolders as d, startTunnelSequence as f, withManagedRemoteWorktreeRemovalLock as g, withManagedRemoteWorktreeLock as h, devServerStopRecord as i, withdrawDevServerStopRequest as l, terminateChild as m, describeMetroLastStop as n, metroLastStop as o, stopTunnel as p, devServerStopLevel as r, requestDevServerStop as s, describeDevServerStop as t, resolveSupervisorTarget as u, withManagedTunnelRemovalLock as v, probePublicHttp as y };