stim 0.0.0 → 1.1.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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +162 -0
  3. package/dist/android-CC5bUer7.mjs +204 -0
  4. package/dist/android-DnZlT3aU.mjs +2279 -0
  5. package/dist/android-cas-DGlTStWu.mjs +1078 -0
  6. package/dist/android-cas-compiler.d.mts +1 -0
  7. package/dist/android-cas-compiler.mjs +71 -0
  8. package/dist/android-xmyGBNy5.mjs +665 -0
  9. package/dist/app-install-oZIQRbdD.mjs +1141 -0
  10. package/dist/build-lock-vE45ZzJZ.mjs +645 -0
  11. package/dist/build-slots-CgacA9zQ.mjs +152 -0
  12. package/dist/cache-manifest-oeH18ERr.mjs +66 -0
  13. package/dist/cache-manifest.d.mts +27 -0
  14. package/dist/cache-manifest.mjs +2 -0
  15. package/dist/cli.d.mts +1 -0
  16. package/dist/cli.mjs +40 -0
  17. package/dist/collector-run.d.mts +119 -0
  18. package/dist/collector-run.mjs +541 -0
  19. package/dist/command-output-8wHMEde0.mjs +289 -0
  20. package/dist/config-D7uu8Gbq.mjs +381 -0
  21. package/dist/deps-64wVnpZc.mjs +435 -0
  22. package/dist/dev-client-Bvky1hya.mjs +557 -0
  23. package/dist/device-DJKv5Qej.mjs +250 -0
  24. package/dist/device-lease-D-G3SgVe.mjs +335 -0
  25. package/dist/device-pool-BlHXnYYs.mjs +408 -0
  26. package/dist/device-remote-CCgkG82J.mjs +1209 -0
  27. package/dist/doctor-BRF9aAJ3.mjs +530 -0
  28. package/dist/doctor-DlVxTtMq.mjs +1137 -0
  29. package/dist/error-diagnostics-B5IJwv3-.mjs +636 -0
  30. package/dist/errors-B6W7FkxQ.mjs +15 -0
  31. package/dist/exec-bsN9MJXb.mjs +63 -0
  32. package/dist/gc-DhXdgSdt.mjs +1651 -0
  33. package/dist/guide-DrhrDBo5.mjs +4319 -0
  34. package/dist/ios-BZxKPuqP.mjs +382 -0
  35. package/dist/ios-CK1Vrk6Q.mjs +2721 -0
  36. package/dist/ios-device-CYbuRNXR.mjs +81 -0
  37. package/dist/ios-device-CdSm0GDw.mjs +413 -0
  38. package/dist/logs-Dxoy16Qv.mjs +176 -0
  39. package/dist/logs-query-oqn47Ffx.mjs +273 -0
  40. package/dist/metro-B01H3EuD.mjs +184 -0
  41. package/dist/metro-store-CDDZHtPl.mjs +92 -0
  42. package/dist/ndjson-DcAtEx_K.mjs +131 -0
  43. package/dist/ndjson-DvHSsNU9.d.mts +16 -0
  44. package/dist/ownership-B9QIdQOw.mjs +475 -0
  45. package/dist/ownership-D8HmSv2E.mjs +60 -0
  46. package/dist/ownership-claim-CeJLEpjD.mjs +569 -0
  47. package/dist/process-output-C7IlIzd7.mjs +40 -0
  48. package/dist/project-COj6nzzh.mjs +243 -0
  49. package/dist/reclaim-BPvN2HlP.mjs +347 -0
  50. package/dist/reload-DYZEBbZy.mjs +332 -0
  51. package/dist/remote-cache-DOi5-zLj.mjs +934 -0
  52. package/dist/server-bare-BYHqEeyw.mjs +352 -0
  53. package/dist/server-expo-BEMr_m6p.mjs +312 -0
  54. package/dist/server-expo-rb0k-0w7.mjs +2 -0
  55. package/dist/settings-BSwrY4FZ.mjs +687 -0
  56. package/dist/spawn-entry-nCE6m770.mjs +14 -0
  57. package/dist/start-ByCylYtB.mjs +822 -0
  58. package/dist/state-IYEY-zi8.mjs +268 -0
  59. package/dist/stats-DZ4Q5J-7.mjs +62 -0
  60. package/dist/stats-wqxPm7Tv.mjs +307 -0
  61. package/dist/status-Dia0XTqG.mjs +363 -0
  62. package/dist/stop-BApyaF21.mjs +591 -0
  63. package/dist/stop-D6o5zAdm.mjs +3 -0
  64. package/dist/supervisor-run.d.mts +84 -0
  65. package/dist/supervisor-run.mjs +210 -0
  66. package/dist/workspace-process-lock-CxOO9-pf.mjs +47 -0
  67. package/dist/worktree-Dx3ZC5lS.mjs +539 -0
  68. package/dist/worktree-DxEmUI_n.mjs +1322 -0
  69. package/dist/xcode-CDB6fytw.mjs +1705 -0
  70. package/package.json +61 -8
  71. package/shim/android-cas-pch.cmake +35 -0
  72. package/shim/android-cas.gradle +29 -0
  73. package/shim/android-cas.toolchain.cmake +12 -0
  74. package/shim/android-optimizations.gradle +30 -0
  75. package/shim/bundle-response.cjs +46 -0
  76. package/shim/bundle-response.d.cts +5 -0
  77. package/shim/expo-metro-config.cjs +188 -0
  78. package/shim/metro-cache-generation.cjs +20 -0
  79. package/shim/metro-cache-generation.d.cts +5 -0
  80. package/skill/SKILL.md +14 -0
@@ -0,0 +1,591 @@
1
+ import { C as withConfigLock, D as sameProcessRecord, E as inspectProcessIdentity, O as waitForProcessExit, S as upsertProject, a as clearSupervisor, f as getProject } from "./config-D7uu8Gbq.mjs";
2
+ import { _ as writeWorkspaceState, a as clearRemoteSession, c as readMetroTunnel, i as clearManagedMetroTunnel, m as withWorkspaceStateLock, o as clearWorkspaceStateKeys, p as readWorkspaceState, s as clearWorkspaceSupervisor, u as readRemoteSession } from "./state-IYEY-zi8.mjs";
3
+ import { a as findProjectRoot } from "./project-COj6nzzh.mjs";
4
+ import { i as resolveProjectMetro, n as killMetroTree, r as pidExists } from "./metro-B01H3EuD.mjs";
5
+ import { n as verifyCollectorOwnership } from "./ownership-D8HmSv2E.mjs";
6
+ import { d as resolveEasCliBin, h as teardownOwnedIosSim, m as teardownOwnedAvd } from "./remote-cache-DOi5-zLj.mjs";
7
+ import { f as releasedLeaseFact, l as phaseLine, u as plural } from "./command-output-8wHMEde0.mjs";
8
+ import { l as releaseWorkspaceLeases } from "./device-lease-D-G3SgVe.mjs";
9
+ import { i as endRecordedSession } from "./device-remote-CCgkG82J.mjs";
10
+ import { r as stopTunnel, t as resolveSupervisorTarget } from "./ownership-B9QIdQOw.mjs";
11
+ import chalk from "chalk";
12
+ //#region src/commands/stop.ts
13
+ const DEFAULT_WAIT_MS = 1e4;
14
+ function readSupervisorState(root) {
15
+ const sup = readWorkspaceState(root)?.supervisor;
16
+ return sup && typeof sup === "object" ? sup : null;
17
+ }
18
+ function readCollectorState(root) {
19
+ const collectors = readWorkspaceState(root)?.collectors;
20
+ return collectors && typeof collectors === "object" ? collectors : {};
21
+ }
22
+ function clearSupervisorState(root, expected) {
23
+ return withWorkspaceStateLock(root, () => {
24
+ if (!clearWorkspaceSupervisor(root, expected)) return false;
25
+ clearWorkspaceStateKeys(root, ["launches"]);
26
+ return true;
27
+ });
28
+ }
29
+ function clearCollectorState(root, expected) {
30
+ return withWorkspaceStateLock(root, () => {
31
+ const current = { ...readWorkspaceState(root)?.collectors };
32
+ if (expected === void 0) {
33
+ clearWorkspaceStateKeys(root, ["collectors"]);
34
+ return true;
35
+ }
36
+ for (const [platform, record] of Object.entries(expected ?? {})) if (sameProcessRecord(current[platform], record)) delete current[platform];
37
+ if (Object.keys(current).length) writeWorkspaceState(root, { collectors: current });
38
+ else clearWorkspaceStateKeys(root, ["collectors"]);
39
+ return Object.keys(current).length === 0;
40
+ });
41
+ }
42
+ function numberOrNull(v) {
43
+ return typeof v === "number" && Number.isSafeInteger(v) && v > 0 ? v : null;
44
+ }
45
+ function resolveCollectorTargets({ root, collectors, isAlive = pidExists, selfPid = process.pid, verify = verifyCollectorOwnership }) {
46
+ const targets = [];
47
+ for (const [platform, record] of Object.entries(collectors || {})) {
48
+ const pid = numberOrNull(Number(record?.pid));
49
+ if (!pid || pid === selfPid) {
50
+ targets.push({
51
+ platform,
52
+ pid: pid ?? null,
53
+ status: "invalid"
54
+ });
55
+ continue;
56
+ }
57
+ if (!isAlive(pid)) {
58
+ targets.push({
59
+ platform,
60
+ pid,
61
+ status: "stale"
62
+ });
63
+ continue;
64
+ }
65
+ const ownership = verify({
66
+ pid,
67
+ platform,
68
+ root,
69
+ isAlive,
70
+ expected: record
71
+ });
72
+ if (ownership.status === "gone") {
73
+ targets.push({
74
+ platform,
75
+ pid,
76
+ status: "stale"
77
+ });
78
+ continue;
79
+ }
80
+ if (ownership.status === "unverified") {
81
+ targets.push({
82
+ platform,
83
+ pid,
84
+ status: "unverified",
85
+ reason: ownership.reason
86
+ });
87
+ continue;
88
+ }
89
+ targets.push({
90
+ platform,
91
+ pid,
92
+ status: "running"
93
+ });
94
+ }
95
+ return targets;
96
+ }
97
+ function defaultTeardownRemoteSession(root, sessionId) {
98
+ return endRecordedSession({
99
+ root,
100
+ sessionId,
101
+ easBin: resolveEasCliBin(root)?.file ?? null
102
+ });
103
+ }
104
+ async function runStop({ root, project = void 0, state = void 0, collectors = void 0, signalCollector = (pid) => process.kill(pid, "SIGTERM"), verifyCollector = verifyCollectorOwnership, clearCollectors = clearCollectorState, isAlive = pidExists, killGroup = killMetroTree, inspectIdentity = inspectProcessIdentity, waitForDeath = void 0, waitMs = DEFAULT_WAIT_MS, resolveMetro = resolveProjectMetro, teardownIos = teardownOwnedIosSim, teardownAvd = teardownOwnedAvd, remoteDevice = void 0, teardownRemoteSession = defaultTeardownRemoteSession, metroTunnel = void 0, stopMetroTunnel = stopTunnel, releaseLeases = releaseWorkspaceLeases, freePort = defaultFreePort, clearRegistration = defaultClearRegistration, clearState = clearSupervisorState, report = (line) => console.error(line) }) {
105
+ const proj = project === void 0 ? getProject(root) : project;
106
+ const sup = state === void 0 ? readSupervisorState(root) : state;
107
+ const collectorRecords = collectors === void 0 ? readCollectorState(root) : collectors;
108
+ const reservedPort = typeof proj?.metroPort === "number" ? proj.metroPort : null;
109
+ const waiter = waitForDeath ?? ((pid, processToken) => waitForProcessExit({
110
+ pid,
111
+ processToken
112
+ }, waitMs));
113
+ const outcomes = {
114
+ supervisor: { status: "none" },
115
+ collectors: {
116
+ status: "none",
117
+ entries: []
118
+ },
119
+ metro: { status: "none" },
120
+ device: {
121
+ ios: null,
122
+ android: null
123
+ },
124
+ port: {
125
+ status: "none",
126
+ port: reservedPort
127
+ },
128
+ metroTunnel: { status: "none" },
129
+ releasedLeases: []
130
+ };
131
+ let ok = true;
132
+ let stillHolding = null;
133
+ const target = resolveSupervisorTarget({
134
+ state: sup,
135
+ record: proj?.supervisor ?? null,
136
+ reservedPort,
137
+ isAlive,
138
+ inspectIdentity
139
+ });
140
+ if (target.status === "none") report(chalk.dim(phaseLine("stop", "no supervisor recorded")));
141
+ else if (target.status === "stale") {
142
+ outcomes.supervisor = {
143
+ status: "already-stopped",
144
+ pid: target.pid,
145
+ reason: `recorded pid ${target.pid} is not running`
146
+ };
147
+ report(chalk.dim(phaseLine("stop", `supervisor pid ${target.pid} is already gone (stale record)`)));
148
+ } else if (target.status === "unverified") {
149
+ outcomes.supervisor = {
150
+ status: "unverified",
151
+ pid: target.pid,
152
+ reason: target.reason
153
+ };
154
+ report(chalk.yellow(phaseLine("stop", `refusing to signal supervisor pid ${target.pid}: ${target.reason}`)));
155
+ ok = false;
156
+ stillHolding = `supervisor pid ${target.pid} could not be verified`;
157
+ } else {
158
+ outcomes.supervisor = await stopSupervisor(target, {
159
+ killGroup,
160
+ waiter,
161
+ report
162
+ });
163
+ if (outcomes.supervisor.status !== "stopped") {
164
+ ok = false;
165
+ stillHolding = outcomes.supervisor.reason;
166
+ }
167
+ }
168
+ outcomes.collectors = await reapCollectors(root, collectorRecords, {
169
+ isAlive,
170
+ signal: signalCollector,
171
+ report,
172
+ verify: verifyCollector,
173
+ waiter
174
+ });
175
+ const unverifiedCollectors = outcomes.collectors.entries.filter((e) => e.status === "unverified");
176
+ if (unverifiedCollectors.length) {
177
+ report(chalk.dim(phaseLine("stop", `keeping the collector records; ${plural(unverifiedCollectors.length, "pid")} could not be verified, and a later \`ios\` / \`android\` run replaces them`)));
178
+ ok = false;
179
+ stillHolding ??= "collector process identity could not be verified";
180
+ } else if (outcomes.collectors.entries.some((entry) => entry.status === "failed")) {
181
+ ok = false;
182
+ stillHolding ??= "a collector could not be stopped";
183
+ } else if (outcomes.collectors.entries.length && clearCollectors(root, collectorRecords) === false) {
184
+ ok = false;
185
+ stillHolding ??= "a replacement collector appeared during cleanup";
186
+ }
187
+ if (outcomes.supervisor.status === "stopped" || outcomes.supervisor.status === "timeout" || outcomes.supervisor.status === "unverified" || outcomes.supervisor.status === "failed") outcomes.metro = {
188
+ status: "skipped",
189
+ reason: "the supervisor owns the dev server on this port"
190
+ };
191
+ else if (reservedPort === null) report(chalk.dim(phaseLine("metro", "no port reserved")));
192
+ else {
193
+ outcomes.metro = await stopMetro(reservedPort, root, {
194
+ resolveMetro,
195
+ report
196
+ });
197
+ if (outcomes.metro.status === "refused" || outcomes.metro.status === "failed") {
198
+ ok = false;
199
+ stillHolding = outcomes.metro.reason;
200
+ }
201
+ }
202
+ const releaseStoppedSupervisor = async () => {
203
+ const stateCleared = clearState(root, sup);
204
+ const registrationCleared = await clearRegistration(root, proj?.supervisor ?? null);
205
+ if (stateCleared === false || registrationCleared === false) {
206
+ ok = false;
207
+ outcomes.port = {
208
+ status: "kept",
209
+ port: reservedPort,
210
+ reason: "a replacement supervisor appeared during cleanup"
211
+ };
212
+ return;
213
+ }
214
+ if (reservedPort !== null) {
215
+ if (freePort(root, reservedPort) === false) {
216
+ ok = false;
217
+ outcomes.port = {
218
+ status: "kept",
219
+ port: reservedPort,
220
+ reason: "the port reservation changed during cleanup"
221
+ };
222
+ return;
223
+ }
224
+ outcomes.port = {
225
+ status: "freed",
226
+ port: reservedPort
227
+ };
228
+ report(chalk.dim(phaseLine("port", `released ${reservedPort}`)));
229
+ }
230
+ };
231
+ if (stillHolding) report(chalk.dim(phaseLine("device", "left alone (something is still running)")));
232
+ else {
233
+ outcomes.device = shutDownDevices(proj, {
234
+ teardownIos,
235
+ teardownAvd,
236
+ report
237
+ });
238
+ if ([outcomes.device.ios, outcomes.device.android].some((device) => device?.status === "failed")) ok = false;
239
+ }
240
+ const remote = remoteDevice === void 0 ? readRemoteSession(root) : remoteDevice;
241
+ const sessionId = typeof remote?.sessionId === "string" ? remote.sessionId : null;
242
+ if (sessionId) {
243
+ const result = teardownRemoteSession(root, sessionId);
244
+ outcomes.device.remote = {
245
+ status: result.status,
246
+ label: sessionId,
247
+ reason: result.reason
248
+ };
249
+ if (result.status === "failed") {
250
+ ok = false;
251
+ report(chalk.red(phaseLine("device", result.reason ?? `could not stop remote session ${sessionId}`)));
252
+ } else {
253
+ report(chalk.dim(phaseLine("device", `stopped remote session ${sessionId}`)));
254
+ if (result.reason) {
255
+ ok = false;
256
+ report(chalk.yellow(phaseLine("device", result.reason)));
257
+ } else clearRemoteSession(root, sessionId);
258
+ }
259
+ }
260
+ const tunnel = metroTunnel === void 0 ? readMetroTunnel(root) : metroTunnel;
261
+ let tunnelHolding = null;
262
+ if (tunnel?.kind === "managed") {
263
+ const result = await stopMetroTunnel(tunnel);
264
+ outcomes.metroTunnel = {
265
+ status: result.status,
266
+ provider: tunnel.provider,
267
+ reason: result.reason
268
+ };
269
+ if (result.status === "failed") {
270
+ ok = false;
271
+ tunnelHolding = result.reason ?? `could not stop the ${tunnel.provider} tunnel`;
272
+ if (!stillHolding) stillHolding = tunnelHolding;
273
+ report(chalk.red(phaseLine("lan", tunnelHolding)));
274
+ } else if (!clearManagedMetroTunnel(root, tunnel)) {
275
+ tunnelHolding = "a replacement managed tunnel record appeared during cleanup and remains active";
276
+ stillHolding = stillHolding ?? tunnelHolding;
277
+ ok = false;
278
+ outcomes.metroTunnel = {
279
+ status: "failed",
280
+ provider: tunnel.provider,
281
+ reason: tunnelHolding
282
+ };
283
+ report(chalk.red(phaseLine("lan", tunnelHolding)));
284
+ } else report(chalk.dim(phaseLine("lan", result.status === "missing" ? "tunnel already gone" : `stopped the ${tunnel.provider} tunnel`)));
285
+ } else if (tunnel?.kind === "expo") outcomes.metroTunnel = { status: "not-managed" };
286
+ outcomes.releasedLeases = releaseLeases(root);
287
+ for (const lease of outcomes.releasedLeases) report(chalk.dim(phaseLine("lease", releasedLeaseFact(lease))));
288
+ if (stillHolding) {
289
+ outcomes.port = {
290
+ status: "kept",
291
+ port: reservedPort,
292
+ reason: stillHolding
293
+ };
294
+ report(chalk.yellow(phaseLine("port", `keeping reservation ${reservedPort ?? "(none)"} -- ${stillHolding}`)));
295
+ const supervisorIsDown = outcomes.supervisor.status === "none" || outcomes.supervisor.status === "already-stopped" || outcomes.supervisor.status === "stopped";
296
+ if (tunnelHolding && supervisorIsDown) {
297
+ clearState(root, sup);
298
+ await clearRegistration(root, proj?.supervisor ?? null);
299
+ }
300
+ } else await releaseStoppedSupervisor();
301
+ return {
302
+ ok,
303
+ outcomes,
304
+ summary: summarize(root, outcomes, ok)
305
+ };
306
+ }
307
+ async function reapCollectors(root, collectors, { isAlive, signal, report, verify = verifyCollectorOwnership, waiter }) {
308
+ const targets = resolveCollectorTargets({
309
+ root,
310
+ collectors,
311
+ isAlive,
312
+ verify
313
+ });
314
+ const entries = [];
315
+ for (const target of targets) {
316
+ if (target.status === "invalid") {
317
+ entries.push({
318
+ platform: target.platform,
319
+ pid: target.pid,
320
+ status: "invalid"
321
+ });
322
+ report(chalk.dim(phaseLine("stop", `ignoring an unusable ${target.platform} collector record`)));
323
+ continue;
324
+ }
325
+ if (target.status === "stale") {
326
+ entries.push({
327
+ platform: target.platform,
328
+ pid: target.pid,
329
+ status: "already-stopped"
330
+ });
331
+ report(chalk.dim(phaseLine("stop", `collector ${target.platform} pid ${target.pid} is already gone`)));
332
+ continue;
333
+ }
334
+ if (target.status === "unverified") {
335
+ entries.push({
336
+ platform: target.platform,
337
+ pid: target.pid,
338
+ status: "unverified",
339
+ reason: target.reason
340
+ });
341
+ report(chalk.yellow(phaseLine("stop", `refusing to signal collector ${target.platform} pid ${target.pid}: ${target.reason}`)));
342
+ continue;
343
+ }
344
+ try {
345
+ const ownership = verify({
346
+ pid: target.pid,
347
+ platform: target.platform,
348
+ root,
349
+ isAlive,
350
+ expected: collectors?.[target.platform]
351
+ });
352
+ if (ownership.status !== "ours") {
353
+ entries.push({
354
+ platform: target.platform,
355
+ pid: target.pid,
356
+ status: ownership.status === "gone" ? "already-stopped" : "unverified",
357
+ ...ownership.status === "unverified" ? { reason: ownership.reason } : {}
358
+ });
359
+ continue;
360
+ }
361
+ signal(target.pid);
362
+ if (!await waiter(target.pid, collectors?.[target.platform]?.processToken)) {
363
+ entries.push({
364
+ platform: target.platform,
365
+ pid: target.pid,
366
+ status: "failed",
367
+ reason: "collector exit could not be confirmed"
368
+ });
369
+ continue;
370
+ }
371
+ entries.push({
372
+ platform: target.platform,
373
+ pid: target.pid,
374
+ status: "stopped"
375
+ });
376
+ report(chalk.green(phaseLine("stop", `collector ${target.platform} pid ${target.pid}`)));
377
+ } catch (error) {
378
+ const gone = error.code === "ESRCH";
379
+ entries.push({
380
+ platform: target.platform,
381
+ pid: target.pid,
382
+ status: gone ? "already-stopped" : "failed"
383
+ });
384
+ report(chalk.dim(phaseLine("stop", gone ? `collector ${target.platform} pid ${target.pid} exited before it could be signalled` : `could not signal collector ${target.platform} pid ${target.pid}`)));
385
+ }
386
+ }
387
+ if (entries.length === 0) {
388
+ report(chalk.dim(phaseLine("stop", "no collectors recorded")));
389
+ return {
390
+ status: "none",
391
+ entries
392
+ };
393
+ }
394
+ return {
395
+ status: "stopped",
396
+ entries
397
+ };
398
+ }
399
+ async function stopSupervisor(target, { killGroup, waiter, report }) {
400
+ report(chalk.dim(phaseLine("stop", `sending SIGTERM to supervisor process group ${target.pid}`)));
401
+ let signalled = false;
402
+ try {
403
+ signalled = killGroup(target.pid, target.processToken);
404
+ } catch (e) {
405
+ signalled = false;
406
+ report(chalk.red(phaseLine("stop", `could not signal supervisor pid ${target.pid}: ${String(e?.message || e)}`)));
407
+ }
408
+ if (!signalled) {
409
+ const reason = `could not signal supervisor pid ${target.pid}`;
410
+ report(chalk.red(phaseLine("stop", reason)));
411
+ return {
412
+ status: "failed",
413
+ pid: target.pid,
414
+ port: target.port ?? null,
415
+ reason
416
+ };
417
+ }
418
+ if (await waiter(target.pid, target.processToken)) {
419
+ report(chalk.green(phaseLine("stop", `supervisor pid ${target.pid}`)));
420
+ return {
421
+ status: "stopped",
422
+ pid: target.pid,
423
+ port: target.port ?? null,
424
+ mode: target.mode ?? null
425
+ };
426
+ }
427
+ const reason = `supervisor pid ${target.pid} did not exit within ${Math.round(DEFAULT_WAIT_MS / 1e3)}s of SIGTERM`;
428
+ report(chalk.red(phaseLine("stop", reason)));
429
+ report(chalk.dim(phaseLine("", `inspect it with \`ps -p ${target.pid}\`, or signal it yourself: kill -9 -${target.pid}`)));
430
+ return {
431
+ status: "timeout",
432
+ pid: target.pid,
433
+ port: target.port ?? null,
434
+ reason
435
+ };
436
+ }
437
+ async function stopMetro(port, root, { resolveMetro, report }) {
438
+ const resolution = await resolveMetro(port, root);
439
+ if (resolution.missing) {
440
+ report(chalk.dim(phaseLine("metro", `nothing listening on port ${port}`)));
441
+ return {
442
+ status: "missing",
443
+ port
444
+ };
445
+ }
446
+ const reason = resolution.notOurs || "no recorded Stim supervisor owns this server; stop it with the tool that started it";
447
+ report(chalk.yellow(phaseLine("metro", `leaving port ${port} alone: ${reason}`)));
448
+ return {
449
+ status: "not-managed",
450
+ port,
451
+ reason
452
+ };
453
+ }
454
+ const OCCUPANCY_HINT = "often a UI-test runner or device tool still attached";
455
+ function occupiedSkipReason(reason, holders) {
456
+ const named = (holders || []).filter(Boolean);
457
+ return named.length ? `${reason} -- held by UI-test runner ${named.join(", ")}` : `${reason} -- ${OCCUPANCY_HINT}`;
458
+ }
459
+ function shutDownDevices(project, { teardownIos, teardownAvd, report }) {
460
+ const device = {
461
+ ios: null,
462
+ android: null
463
+ };
464
+ const ios = project?.platforms?.ios;
465
+ const iosUdid = ios?.deviceUdid;
466
+ const iosName = ios?.deviceName;
467
+ if (iosUdid) {
468
+ if (!ios?.owned) {
469
+ device.ios = {
470
+ status: "skipped",
471
+ kind: "not-owned",
472
+ label: iosUdid,
473
+ reason: "Stim does not own this device"
474
+ };
475
+ report(chalk.dim(phaseLine("device", `${iosUdid} is not Stim-owned, leaving it running`)));
476
+ } else device.ios = reportDevice(iosUdid, teardownIos(iosUdid, {
477
+ del: false,
478
+ label: iosName
479
+ }), report);
480
+ }
481
+ const android = project?.platforms?.android;
482
+ if (android?.avdName) {
483
+ if (!android.owned) {
484
+ device.android = {
485
+ status: "skipped",
486
+ kind: "not-owned",
487
+ label: android.avdName,
488
+ reason: "Stim does not own this device"
489
+ };
490
+ report(chalk.dim(phaseLine("device", `${android.avdName} is not Stim-owned, leaving it running`)));
491
+ } else device.android = reportDevice(android.avdName, teardownAvd(android.avdName, { del: false }), report);
492
+ }
493
+ return device;
494
+ }
495
+ function reportDevice(label, r, report) {
496
+ if (r.status === "torn-down") {
497
+ report(chalk.green(phaseLine("device", `shut down ${r.label ?? label}`)));
498
+ return {
499
+ status: "shut-down",
500
+ label: r.label ?? label
501
+ };
502
+ }
503
+ if (r.status === "missing") {
504
+ report(chalk.dim(phaseLine("device", `${label} is already gone`)));
505
+ return {
506
+ status: "missing",
507
+ label
508
+ };
509
+ }
510
+ if (r.status === "skipped") {
511
+ const reason = r.kind === "occupied" ? occupiedSkipReason(r.reason, r.holders) : r.reason;
512
+ report(chalk.yellow(phaseLine("device", `skipped ${label}: ${reason}`)));
513
+ return {
514
+ status: "skipped",
515
+ kind: r.kind ?? null,
516
+ label,
517
+ reason
518
+ };
519
+ }
520
+ report(chalk.red(phaseLine("device", `failed to shut down ${label}: ${r.reason}`)));
521
+ return {
522
+ status: "failed",
523
+ label,
524
+ reason: r.reason
525
+ };
526
+ }
527
+ function summarize(root, outcomes, ok) {
528
+ const parts = [];
529
+ if (outcomes.supervisor.status === "stopped") parts.push(`supervisor pid ${outcomes.supervisor.pid} stopped`);
530
+ if (outcomes.supervisor.status === "already-stopped") parts.push("supervisor already stopped");
531
+ if (outcomes.supervisor.status === "timeout") parts.push(`supervisor pid ${outcomes.supervisor.pid} still running`);
532
+ if (outcomes.supervisor.status === "unverified") parts.push(`supervisor pid ${outcomes.supervisor.pid} unverified`);
533
+ if (outcomes.supervisor.status === "failed") parts.push(`supervisor pid ${outcomes.supervisor.pid} could not be signalled`);
534
+ const reaped = outcomes.collectors.entries.filter((e) => e.status === "stopped").length;
535
+ if (reaped) parts.push(`${reaped} collector${reaped === 1 ? "" : "s"} stopped`);
536
+ const unverified = outcomes.collectors.entries.filter((e) => e.status === "unverified").length;
537
+ if (unverified) parts.push(`${unverified} collector${unverified === 1 ? "" : "s"} left unsignalled`);
538
+ if (outcomes.metro.status === "stopped") parts.push(`metro on port ${outcomes.metro.port} stopped`);
539
+ if (outcomes.metro.status === "not-managed") parts.push(`external server on port ${outcomes.metro.port} left alone`);
540
+ if (outcomes.metro.status === "refused") parts.push(`port ${outcomes.metro.port} refused`);
541
+ if (outcomes.metro.status === "failed") parts.push(`port ${outcomes.metro.port} could not be freed`);
542
+ const devicesByPlatform = [["ios", outcomes.device.ios], ["android", outcomes.device.android]];
543
+ for (const [platform, o] of devicesByPlatform) {
544
+ if (!o) continue;
545
+ if (o.status === "shut-down") parts.push(`${platform} ${o.label} shut down`);
546
+ if (o.status === "skipped") parts.push(`${platform} ${o.label} skipped`);
547
+ if (o.status === "failed") parts.push(`${platform} ${o.label} failed`);
548
+ }
549
+ if (outcomes.port.status === "freed") parts.push(`port ${outcomes.port.port} freed`);
550
+ if (outcomes.port.status === "kept") parts.push(`port ${outcomes.port.port} kept`);
551
+ if (outcomes.metroTunnel.status === "stopped") parts.push(`${outcomes.metroTunnel.provider} tunnel stopped`);
552
+ if (outcomes.metroTunnel.status === "failed") parts.push(`${outcomes.metroTunnel.provider} tunnel could not be stopped`);
553
+ for (const lease of outcomes.releasedLeases) parts.push(`${lease.platform} lease on ${lease.id} released`);
554
+ const what = parts.length ? parts.join(", ") : "nothing was running";
555
+ return `${ok ? "Stopped" : "Stopped with problems"}: ${what} (${root})`;
556
+ }
557
+ function defaultFreePort(root, port) {
558
+ return withConfigLock(() => {
559
+ const current = getProject(root);
560
+ if (!current) return true;
561
+ if (current.supervisor || current.metroPort !== port) return false;
562
+ upsertProject(root, { metroPort: null });
563
+ return true;
564
+ });
565
+ }
566
+ async function defaultClearRegistration(root, expected) {
567
+ try {
568
+ return clearSupervisor(root, expected);
569
+ } catch {
570
+ return false;
571
+ }
572
+ }
573
+ function stopCommand(program) {
574
+ program.command("stop").description("The inverse of `start`: halt this workspace's supervisor, shut the owned device down (never deleted), and free the reserved port. Non-destructive -- the device stays assigned, so coming back costs a boot. Acts on the current workspace.").option("--json", "print the per-step outcomes as JSON").action(async (opts) => {
575
+ const root = findProjectRoot(process.cwd());
576
+ if (!root) {
577
+ console.error(chalk.red("Not inside a project (no package.json found above the current directory)."));
578
+ process.exit(1);
579
+ }
580
+ const { ok, outcomes, summary } = await runStop({ root });
581
+ if (opts.json) console.log(JSON.stringify({
582
+ root,
583
+ ok,
584
+ ...outcomes
585
+ }));
586
+ else console.log(ok ? chalk.green(summary) : chalk.yellow(summary));
587
+ if (!ok) process.exit(1);
588
+ });
589
+ }
590
+ //#endregion
591
+ export { resolveCollectorTargets as a, readSupervisorState as i, clearSupervisorState as n, runStop as o, readCollectorState as r, stopCommand as s, clearCollectorState as t };
@@ -0,0 +1,3 @@
1
+ import { t as resolveSupervisorTarget } from "./ownership-B9QIdQOw.mjs";
2
+ import { a as resolveCollectorTargets, i as readSupervisorState, n as clearSupervisorState, o as runStop, r as readCollectorState, s as stopCommand, t as clearCollectorState } from "./stop-BApyaF21.mjs";
3
+ export { clearCollectorState, clearSupervisorState, stopCommand as default, readCollectorState, readSupervisorState, resolveCollectorTargets, resolveSupervisorTarget, runStop };
@@ -0,0 +1,84 @@
1
+ import { t as NdjsonWriter } from "./ndjson-DvHSsNU9.mjs";
2
+ //#region src/process-identity.d.ts
3
+ interface ProcessRecord {
4
+ pid?: unknown;
5
+ processToken?: unknown;
6
+ }
7
+ //#endregion
8
+ //#region src/supervisor/state.d.ts
9
+ declare const MODE_BARE = "bare-inproc";
10
+ declare const MODE_EXPO = "expo-child";
11
+ interface WorkspaceState {
12
+ metroCacheGeneration?: string;
13
+ supervisor?: Record<string, unknown>;
14
+ collectors?: Record<string, unknown>;
15
+ lastBuild?: Record<string, unknown>;
16
+ launches?: Partial<Record<WorkspaceLaunchPlatform, WorkspaceLaunchRecord>>;
17
+ remoteDevice?: Record<string, unknown>;
18
+ metroTunnel?: Record<string, unknown>;
19
+ [key: string]: unknown;
20
+ }
21
+ type WorkspaceLaunchPlatform = "ios" | "android";
22
+ interface WorkspaceLaunchRecord {
23
+ appId: string;
24
+ deviceId: string;
25
+ metroPort: number | null;
26
+ release: boolean;
27
+ launchedAt: string;
28
+ }
29
+ declare function readWorkspaceState(root: string): WorkspaceState | null;
30
+ declare function withWorkspaceStateLock<T>(root: string, fn: () => T): T;
31
+ declare function writeWorkspaceState(root: string, patch: WorkspaceState): WorkspaceState;
32
+ declare function clearWorkspaceSupervisor(root: string, expected?: ProcessRecord | null): boolean;
33
+ declare function clearExpoMetroTunnel(root: string): void;
34
+ declare function writePidFile(root: string, pid: number): string;
35
+ declare function readPidFile(root: string): number | null;
36
+ //#endregion
37
+ //#region src/supervisor/run.d.ts
38
+ interface ParsedSupervisorArgs {
39
+ root?: string;
40
+ port?: number;
41
+ tunnel?: boolean;
42
+ error?: string;
43
+ }
44
+ declare function parseArgs(argv: string[]): ParsedSupervisorArgs;
45
+ interface ServerExitInfo {
46
+ code?: number | null;
47
+ signal?: NodeJS.Signals | null;
48
+ reason?: string;
49
+ error?: Error;
50
+ }
51
+ interface ServerHandle {
52
+ close(): Promise<void> | void;
53
+ onExit?(cb: (info?: ServerExitInfo | null) => void): void;
54
+ serverPid?: number | null;
55
+ }
56
+ type ServerStarter = (opts: {
57
+ root: string;
58
+ port: number;
59
+ logsDir: string;
60
+ writer?: NdjsonWriter | null;
61
+ tunnel?: boolean;
62
+ onTunnelUrl?: ((url: string) => void) | null;
63
+ }) => Promise<ServerHandle>;
64
+ interface RunSupervisorOptions {
65
+ root: string;
66
+ port: number;
67
+ tunnel?: boolean;
68
+ isExpo?: (projectRoot: string) => boolean;
69
+ startBare?: ServerStarter | null;
70
+ startExpo?: ServerStarter | null;
71
+ now?: () => number;
72
+ onExit?: (code: number) => void;
73
+ attachSignals?: boolean;
74
+ stderr?: (line: string) => void;
75
+ }
76
+ declare function runSupervisor({ root, port, tunnel, isExpo, startBare, startExpo, now, onExit, attachSignals, stderr }: RunSupervisorOptions): Promise<{
77
+ mode: string;
78
+ server: ServerHandle | undefined;
79
+ shutdown: (code: number, event: string, msg: string) => Promise<void>;
80
+ startedAt: string;
81
+ } | null>;
82
+ declare function main(argv?: string[]): Promise<void>;
83
+ //#endregion
84
+ export { MODE_BARE, MODE_EXPO, RunSupervisorOptions, ServerExitInfo, ServerHandle, type WorkspaceState, clearExpoMetroTunnel, clearWorkspaceSupervisor, main, parseArgs, readPidFile, readWorkspaceState, runSupervisor, withWorkspaceStateLock, writePidFile, writeWorkspaceState };