stim 1.8.0 → 1.9.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/README.md +23 -155
  2. package/dist/{android-BRgBYzU0.mjs → android-BPGizezh.mjs} +112 -135
  3. package/dist/{android-DeYdjjBP.mjs → android-DEgo4oCo.mjs} +2 -2
  4. package/dist/{android-DWssqHQ8.mjs → android-_y4Ohi5z.mjs} +54 -15
  5. package/dist/android-cas-compiler.mjs +2 -2
  6. package/dist/{android-cas-SWIjzeuL.mjs → android-cas-eiExNLf7.mjs} +21 -14
  7. package/dist/app-install-Uf3a83Tv.mjs +819 -0
  8. package/dist/{build-lock-CgtsB6TV.mjs → build-lock-DPXPLgTc.mjs} +120 -24
  9. package/dist/build-progress-CVqkfPC6.mjs +188 -0
  10. package/dist/{cache-manifest-CYJi8-Hx.mjs → cache-manifest-Dg7OnMrp.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 +2 -0
  14. package/dist/collector-run.mjs +10 -9
  15. package/dist/{command-output-B3ycpsYv.mjs → command-output-gByVmIjy.mjs} +11 -2
  16. package/dist/{config-B7iyKWkL.mjs → config-Cp9lM3wX.mjs} +187 -57
  17. package/dist/{dependency-state-D0M7oKA7.mjs → dependency-state-Bq5DcLGn.mjs} +2 -2
  18. package/dist/{deps-oCXVNEqQ.mjs → deps-Cip5vUm1.mjs} +8 -5
  19. package/dist/{detached-entry-Bpo6ZY_I.mjs → detached-entry-KEu0_AXL.mjs} +1 -1
  20. package/dist/{device-BUoZyMoR.mjs → dev-client-HyqISM0h.mjs} +1178 -635
  21. package/dist/{device-RdujjHLB.mjs → device-D4U-xLty.mjs} +13 -20
  22. package/dist/{device-lease-Ffz_M5t0.mjs → device-lease-Crs-HkJY.mjs} +16 -9
  23. package/dist/{device-pool-BxRwdufO.mjs → device-pool-CnTAz8X1.mjs} +7 -7
  24. package/dist/{device-remote-ByN8P6CB.mjs → device-remote-AQ5rQPqr.mjs} +482 -35
  25. package/dist/{doctor-BMt156qe.mjs → doctor-B_Ei3bZu.mjs} +95 -107
  26. package/dist/{error-diagnostics-D1Kaf-eT.mjs → error-diagnostics-Vfz83GCF.mjs} +22 -16
  27. package/dist/exec-jIAlgilk.mjs +132 -0
  28. package/dist/{gc-v2YcoJvm.mjs → gc-Ckw8RQHi.mjs} +763 -126
  29. package/dist/{guide-BTAlD1_y.mjs → guide-BiAbhs1J.mjs} +891 -501
  30. package/dist/{guide-status-Craq9z0t.mjs → guide-status-BAMVU0ZA.mjs} +5 -3
  31. package/dist/{ios-BcCiI4TS.mjs → ios-BL2NFckr.mjs} +43 -20
  32. package/dist/{ios-BP04pY5f.mjs → ios-BzUJWk1K.mjs} +141 -175
  33. package/dist/{ios-device-CauFJiYx.mjs → ios-device-D972i9wf.mjs} +149 -52
  34. package/dist/{ios-device-CFXA46ez.mjs → ios-device-DPsSm0v_.mjs} +3 -2
  35. package/dist/launch-verify-Q_OtW1Rf.mjs +365 -0
  36. package/dist/{logs-CakdBTtm.mjs → logs-CI9NQSK7.mjs} +18 -13
  37. package/dist/{logs-query-BGL-Pl12.mjs → logs-query-Cqma3WIu.mjs} +36 -28
  38. package/dist/{metro-C7bcJ0uo.mjs → metro-DrLHaMn2.mjs} +78 -8
  39. package/dist/{named-ports-DzMOlmaX.mjs → named-ports-CmLQSIRj.mjs} +4 -4
  40. package/dist/{ndjson-BZwI1NBv.mjs → ndjson-9uFNRwfe.mjs} +36 -3
  41. package/dist/{ownership-Cp52QAK1.mjs → ownership-D15Op8vN.mjs} +3 -3
  42. package/dist/{ownership-wolE-0Do.mjs → ownership-DCQPuWEc.mjs} +26 -7
  43. package/dist/{ports-BIbJaKNW.mjs → ports-BNKoWVWZ.mjs} +3 -3
  44. package/dist/{process-output-Dl5uKE03.mjs → process-output-Fpl8PYi8.mjs} +1 -1
  45. package/dist/{project-DDZWf_16.mjs → project-DS112DYB.mjs} +8 -3
  46. package/dist/{reload-BdypPbjM.mjs → reload-ZNJUo1pl.mjs} +12 -13
  47. package/dist/{server-bare-BQkyxa_b.mjs → server-bare-DWkkcIxV.mjs} +2 -2
  48. package/dist/{server-expo-BVAA1OqJ.mjs → server-expo-B_lctBR3.mjs} +8 -7
  49. package/dist/server-expo-CeILEu86.mjs +2 -0
  50. package/dist/settings-BKNh5rl4.mjs +376 -0
  51. package/dist/{settings-BXXorVM2.mjs → settings-D030k0t4.mjs} +464 -152
  52. package/dist/settings.schema.json +989 -0
  53. package/dist/{teardown-BJqEzPo_.mjs → sim-pool-KaIsp2Ft.mjs} +14 -419
  54. package/dist/simslim-B_LsGLuJ.mjs +259 -0
  55. package/dist/{slot-launch-ZUUe0TQP.mjs → slot-launch-DU9A-gYw.mjs} +4 -4
  56. package/dist/spawn-claims-Dn3C_N6w.mjs +49 -0
  57. package/dist/{start-CmxtOKjU.mjs → start-DYkkKF0s.mjs} +27 -31
  58. package/dist/{state-DZobzxLW.mjs → state-NcJzMEBF.mjs} +5 -3
  59. package/dist/{stats-CLg5RuDS.mjs → stats-B63-JVAY.mjs} +4 -4
  60. package/dist/{stats-BYcZ6n1h.mjs → stats-DG9e66Rz.mjs} +66 -5
  61. package/dist/status-S6ehL4Ui.mjs +932 -0
  62. package/dist/{stop-C8JW6kKp.mjs → stop-BO28HGqF.mjs} +101 -25
  63. package/dist/{stop-B1kOpYei.mjs → stop-T1R-cZRz.mjs} +2 -2
  64. package/dist/supervisor-run.mjs +52 -34
  65. package/dist/{workspace-process-lock-BYfZz_S-.mjs → workspace-process-lock-DWaiRD0e.mjs} +14 -9
  66. package/dist/{workspace-state-BT5E3SA2.mjs → workspace-state-DF2gCFhx.mjs} +52 -29
  67. package/dist/worktree-DKhSF0UX.mjs +2 -0
  68. package/dist/{worktree-CFLA-6MX.mjs → worktree-LGB-UwgG.mjs} +603 -59
  69. package/dist/{worktree-FG_ZKKdb.mjs → worktree-R_cUlE7s.mjs} +62 -41
  70. package/dist/{xcode-B6YaWeg0.mjs → xcode-BZqyxh3e.mjs} +216 -58
  71. package/package.json +5 -6
  72. package/skill/SKILL.md +2 -0
  73. package/dist/app-install-BDbsSgCl.mjs +0 -1175
  74. package/dist/build-slots-C-oAHRT1.mjs +0 -151
  75. package/dist/dev-client-BmT-ioFI.mjs +0 -797
  76. package/dist/exec-B29XNuhP.mjs +0 -77
  77. package/dist/reclaim-KJ-0uWEF.mjs +0 -361
  78. package/dist/server-expo-YDDG3eP7.mjs +0 -2
  79. package/dist/status-DdkLki47.mjs +0 -426
  80. package/dist/{metro-store-CDDZHtPl.mjs → metro-store-DzeWkfqW.mjs} +1 -1
@@ -0,0 +1,932 @@
1
+ import { K as projectDeviceSlots, g as loadConfig, u as getConfigDir, z as volumeRootFor } from "./config-Cp9lM3wX.mjs";
2
+ import { t as getExecutor } from "./exec-jIAlgilk.mjs";
3
+ import { a as parseNdjsonLine } from "./ndjson-9uFNRwfe.mjs";
4
+ import { b as workspaceLogsDir, r as readWorkspaceState, y as workspaceLogErrorIndex } from "./workspace-state-DF2gCFhx.mjs";
5
+ import { _ as repoRoot, s as gitCommonDir, y as resolveSourceCheckout } from "./worktree-R_cUlE7s.mjs";
6
+ import { l as projectShortcut, o as findProjectRoot } from "./project-DS112DYB.mjs";
7
+ import { l as readRemoteSession } from "./state-NcJzMEBF.mjs";
8
+ import { c as resolveProjectMetro, i as listeningPidsByPort, o as processCwd, r as listeningPids, s as processCwds, u as isMetroRunning } from "./metro-DrLHaMn2.mjs";
9
+ import { M as ownedAvdSerialResolver, c as androidToolPath } from "./android-_y4Ohi5z.mjs";
10
+ import { a as formatElapsed, r as clockTime, u as plural } from "./command-output-gByVmIjy.mjs";
11
+ import { o as parkedMaxSetting, s as readParked, t as POOL_SETTING_REMEDY } from "./sim-pool-KaIsp2Ft.mjs";
12
+ import { a as buildStatusLine, i as buildReport, o as parseActiveBuild, r as activeBuildState, t as ACTIVE_BUILD_KEY } from "./build-progress-CVqkfPC6.mjs";
13
+ import { d as listAllIosSimsAsync, v as parseSimctlList } from "./ios-BL2NFckr.mjs";
14
+ import { i as listLeaseFiles } from "./device-lease-Crs-HkJY.mjs";
15
+ import { a as statsProjectKey, r as readStats } from "./stats-DG9e66Rz.mjs";
16
+ import { f as easMachineStateRoot, p as readEasSessionLedger } from "./device-remote-AQ5rQPqr.mjs";
17
+ import { d as recordMatches, l as queryLogs, s as logFiles, t as ERROR_SOURCES } from "./logs-query-Cqma3WIu.mjs";
18
+ import { t as resolveSupervisorTarget } from "./ownership-DCQPuWEc.mjs";
19
+ import { i as readSupervisorState } from "./stop-BO28HGqF.mjs";
20
+ import { readdirSync, watch } from "node:fs";
21
+ import { closeSync as closeSync$1, existsSync as existsSync$1, fstatSync, openSync as openSync$1, readFileSync as readFileSync$1, readSync as readSync$1, renameSync as renameSync$1, rmSync as rmSync$1, writeFileSync as writeFileSync$1 } from "fs";
22
+ import { join } from "path";
23
+ import { rotatedLogPath } from "@stim-cli/core";
24
+ import { join as join$1 } from "node:path";
25
+ import { totalmem } from "os";
26
+ import chalk from "chalk";
27
+ //#region src/status-watch.ts
28
+ const WATCH_FALLBACK_MS = 3e4;
29
+ const WATCH_SIMCTL_INTERVAL_MS = 2e3;
30
+ const ADB_RESTART_MIN_MS = 1e3;
31
+ const ADB_RESTART_MAX_MS = 6e4;
32
+ const SIMCTL_TIMEOUT_MS = 1e4;
33
+ /**
34
+ * Coalesces change signals into refreshes: a burst of triggers runs `run` once
35
+ * after `debounceMs` of quiet, at most one run is in flight, and a trigger that
36
+ * arrives during a run causes exactly one more run after it.
37
+ */
38
+ function createRefreshScheduler({ debounceMs, run }) {
39
+ let timer = null;
40
+ let running = false;
41
+ let pending = false;
42
+ let stopped = false;
43
+ const fire = async () => {
44
+ timer = null;
45
+ running = true;
46
+ try {
47
+ await run();
48
+ } finally {
49
+ running = false;
50
+ if (pending) {
51
+ pending = false;
52
+ trigger();
53
+ }
54
+ }
55
+ };
56
+ function trigger(delayMs = debounceMs) {
57
+ if (stopped) return;
58
+ if (running) {
59
+ pending = true;
60
+ return;
61
+ }
62
+ if (timer) clearTimeout(timer);
63
+ timer = setTimeout(() => void fire(), delayMs);
64
+ }
65
+ return {
66
+ trigger,
67
+ stop() {
68
+ stopped = true;
69
+ if (timer) clearTimeout(timer);
70
+ timer = null;
71
+ }
72
+ };
73
+ }
74
+ /**
75
+ * Whether a change to `name` in a watched `$STIM_HOME` directory can change
76
+ * the status payload. A null name means the platform did not report one.
77
+ */
78
+ function changeAffectsStatus(dir, name) {
79
+ if (name === null) return true;
80
+ if (name.includes(".lock")) return false;
81
+ switch (dir) {
82
+ case "home": return name.startsWith("config.json") || name === "workspaces" || name === "device-locks";
83
+ case "workspace": return name.startsWith("state.json") || name === "logs";
84
+ case "eas": return name.startsWith("sessions.json");
85
+ default: return true;
86
+ }
87
+ }
88
+ /** A stable summary of every simulator's state, or null when the listing is unreadable. */
89
+ function simulatorSignature(simctlJson) {
90
+ try {
91
+ return parseSimctlList(simctlJson, { includeUnavailable: true }).map((sim) => `${sim.udid}:${sim.state}`).toSorted().join(",");
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+ /**
97
+ * Calls `onChange` when anything `stim status` reports may have changed:
98
+ * `$STIM_HOME` files, adb device arrivals and departures, and simulator
99
+ * state. simctl has no push API reachable from Node, so it is polled.
100
+ */
101
+ function watchStatusSources({ home, onChange, platform = process.platform, simctlIntervalMs = WATCH_SIMCTL_INTERVAL_MS }) {
102
+ let stopped = false;
103
+ const watchers = /* @__PURE__ */ new Map();
104
+ const desiredDirs = () => {
105
+ const dirs = /* @__PURE__ */ new Map([
106
+ [home, "home"],
107
+ [join$1(home, "workspaces"), "workspaces"],
108
+ [join$1(home, "device-locks"), "leases"],
109
+ [easMachineStateRoot(), "eas"]
110
+ ]);
111
+ let names = [];
112
+ try {
113
+ names = readdirSync(join$1(home, "workspaces"));
114
+ } catch {}
115
+ for (const name of names) {
116
+ const dir = join$1(home, "workspaces", name);
117
+ dirs.set(dir, "workspace");
118
+ dirs.set(join$1(dir, "logs"), "logs");
119
+ }
120
+ return dirs;
121
+ };
122
+ const reconcile = () => {
123
+ if (stopped) return;
124
+ const desired = desiredDirs();
125
+ for (const [dir, watcher] of watchers) if (!desired.has(dir)) {
126
+ watcher.close();
127
+ watchers.delete(dir);
128
+ }
129
+ for (const [dir, kind] of desired) {
130
+ if (watchers.has(dir)) continue;
131
+ try {
132
+ const watcher = watch(dir, (_event, name) => {
133
+ if (!changeAffectsStatus(kind, name === null ? null : String(name))) return;
134
+ if (kind === "home" || kind === "workspaces" || kind === "workspace") reconcile();
135
+ onChange();
136
+ });
137
+ watcher.on("error", () => {
138
+ watcher.close();
139
+ watchers.delete(dir);
140
+ });
141
+ watchers.set(dir, watcher);
142
+ } catch {}
143
+ }
144
+ };
145
+ const adb = trackAdbDevices(onChange);
146
+ let simctl = null;
147
+ let simTimer = null;
148
+ if (platform === "darwin") {
149
+ let last = null;
150
+ const check = () => {
151
+ if (simctl || stopped) return;
152
+ let out = "";
153
+ const child = getExecutor().spawn("xcrun", [
154
+ "simctl",
155
+ "list",
156
+ "devices",
157
+ "--json"
158
+ ], {
159
+ stdio: [
160
+ "ignore",
161
+ "pipe",
162
+ "ignore"
163
+ ],
164
+ timeout: SIMCTL_TIMEOUT_MS,
165
+ killSignal: "SIGKILL"
166
+ });
167
+ simctl = child;
168
+ child.stdout?.on("data", (chunk) => out += chunk.toString());
169
+ child.on("error", () => {
170
+ simctl = null;
171
+ });
172
+ child.on("close", () => {
173
+ simctl = null;
174
+ const signature = simulatorSignature(out);
175
+ if (signature === null || stopped) return;
176
+ if (last !== null && signature !== last) onChange();
177
+ last = signature;
178
+ });
179
+ };
180
+ check();
181
+ simTimer = setInterval(check, simctlIntervalMs);
182
+ }
183
+ reconcile();
184
+ return {
185
+ reconcile,
186
+ stop() {
187
+ stopped = true;
188
+ for (const watcher of watchers.values()) watcher.close();
189
+ watchers.clear();
190
+ if (simTimer) clearInterval(simTimer);
191
+ simctl?.kill("SIGKILL");
192
+ adb.stop();
193
+ }
194
+ };
195
+ }
196
+ /**
197
+ * Runs `adb track-devices` and calls `onChange` on every device list it
198
+ * reports. adb exits when its server restarts, so the tracker restarts with a
199
+ * doubling delay that resets once a run lasts a minute.
200
+ */
201
+ function trackAdbDevices(onChange) {
202
+ const exec = getExecutor();
203
+ const resolved = androidToolPath("adb");
204
+ const adb = resolved === "adb" ? exec.findExecutable("adb") : resolved;
205
+ if (!adb) return { stop() {} };
206
+ let child = null;
207
+ let restart = null;
208
+ let delay = ADB_RESTART_MIN_MS;
209
+ let stopped = false;
210
+ const start = () => {
211
+ restart = null;
212
+ if (stopped) return;
213
+ const startedAt = Date.now();
214
+ const current = exec.spawn(adb, ["track-devices"], { stdio: [
215
+ "ignore",
216
+ "pipe",
217
+ "ignore"
218
+ ] });
219
+ child = current;
220
+ current.stdout?.on("data", () => onChange());
221
+ current.on("error", () => {});
222
+ current.on("close", () => {
223
+ if (child === current) child = null;
224
+ if (stopped) return;
225
+ if (Date.now() - startedAt >= ADB_RESTART_MAX_MS) delay = ADB_RESTART_MIN_MS;
226
+ restart = setTimeout(start, delay);
227
+ delay = Math.min(delay * 2, ADB_RESTART_MAX_MS);
228
+ });
229
+ };
230
+ start();
231
+ return { stop() {
232
+ stopped = true;
233
+ if (restart) clearTimeout(restart);
234
+ child?.kill("SIGTERM");
235
+ } };
236
+ }
237
+ //#endregion
238
+ //#region src/diagnostics/error-index.ts
239
+ const INDEX_VERSION = 1;
240
+ const HEAD_BYTES = 1024;
241
+ const TAIL_BYTES = 256;
242
+ const CHUNK_BYTES = 4194304;
243
+ const NEWLINE = 10;
244
+ const CANDIDATE_PATTERNS = [
245
+ "\"level\":\"error\"",
246
+ "\"level\":\"fatal\"",
247
+ "\"marker\":true"
248
+ ].map((p) => Buffer.from(p));
249
+ const ERROR_CRITERIA = {
250
+ errorsOnly: true,
251
+ sources: ERROR_SOURCES,
252
+ includeNativeCrashes: true
253
+ };
254
+ /**
255
+ * The number of records `queryLogs({ dir, errorsOnly: true })` returns, without parsing every
256
+ * line on every call. `indexFile` keeps, per log file generation, the markers and error records
257
+ * already read and the byte offset they were read through; a later call reads only what was
258
+ * appended since. The index is a cache: losing it or a concurrent writer only costs a rescan.
259
+ */
260
+ function countErrorsSinceMarker(dir, indexFile) {
261
+ const previous = readIndex(indexFile);
262
+ const files = {};
263
+ let changed = false;
264
+ for (const name of logFiles(dir)) {
265
+ const current = join(dir, name);
266
+ for (const path of [rotatedLogPath(current), current]) {
267
+ const result = summarize(path, previous.files);
268
+ if (!result) continue;
269
+ files[result.key] = result.summary;
270
+ if (result.changed) changed = true;
271
+ }
272
+ }
273
+ if (changed || Object.keys(files).length !== Object.keys(previous.files).length) writeIndex(indexFile, {
274
+ version: INDEX_VERSION,
275
+ files
276
+ });
277
+ const records = [];
278
+ for (const summary of Object.values(files)) records.push(...Object.values(summary.markers), ...summary.errors);
279
+ return queryLogs({
280
+ records,
281
+ errorsOnly: true
282
+ }).length;
283
+ }
284
+ function summarize(path, known) {
285
+ let fd;
286
+ try {
287
+ fd = openSync$1(path, "r");
288
+ } catch {
289
+ return null;
290
+ }
291
+ try {
292
+ const { dev, ino, size } = fstatSync(fd);
293
+ const key = `${dev}:${ino}`;
294
+ const cached = known[key];
295
+ const base = cached && stillExtends(fd, cached, size) ? cached : null;
296
+ const start = base ?? {
297
+ offset: 0,
298
+ head: "",
299
+ tail: "",
300
+ markers: {},
301
+ errors: []
302
+ };
303
+ if (size <= start.offset) return {
304
+ key,
305
+ summary: start,
306
+ changed: !base
307
+ };
308
+ const summary = readFrom(fd, start, size);
309
+ return {
310
+ key,
311
+ summary,
312
+ changed: !base || summary.offset !== start.offset
313
+ };
314
+ } catch {
315
+ return null;
316
+ } finally {
317
+ closeSync$1(fd);
318
+ }
319
+ }
320
+ function stillExtends(fd, summary, size) {
321
+ if (size < summary.offset) return false;
322
+ const head = Buffer.from(summary.head, "base64");
323
+ const tail = Buffer.from(summary.tail, "base64");
324
+ return readAt(fd, 0, head.length).equals(head) && readAt(fd, summary.offset - tail.length, tail.length).equals(tail);
325
+ }
326
+ function readAt(fd, position, length) {
327
+ const buf = Buffer.alloc(length);
328
+ const read = length > 0 ? readSync$1(fd, buf, 0, length, position) : 0;
329
+ return buf.subarray(0, read);
330
+ }
331
+ function readFrom(fd, start, size) {
332
+ const markers = { ...start.markers };
333
+ const errors = [...start.errors];
334
+ let offset = start.offset;
335
+ let chunk = CHUNK_BYTES;
336
+ while (offset < size) {
337
+ const buf = readAt(fd, offset, Math.min(chunk, size - offset));
338
+ if (buf.length === 0) break;
339
+ const end = buf.lastIndexOf(NEWLINE);
340
+ if (end === -1) {
341
+ if (offset + buf.length >= size) break;
342
+ chunk *= 2;
343
+ continue;
344
+ }
345
+ for (const record of candidateRecords(buf.subarray(0, end + 1))) collect(record, markers, errors);
346
+ offset += end + 1;
347
+ }
348
+ if (offset === start.offset) return start;
349
+ return {
350
+ offset,
351
+ head: start.offset >= HEAD_BYTES ? start.head : readAt(fd, 0, Math.min(HEAD_BYTES, offset)).toString("base64"),
352
+ tail: readAt(fd, Math.max(0, offset - TAIL_BYTES), Math.min(TAIL_BYTES, offset)).toString("base64"),
353
+ markers,
354
+ errors
355
+ };
356
+ }
357
+ function candidateRecords(region) {
358
+ const lineStarts = /* @__PURE__ */ new Set();
359
+ for (const pattern of CANDIDATE_PATTERNS) for (let hit = region.indexOf(pattern); hit !== -1; hit = region.indexOf(pattern, hit + pattern.length)) lineStarts.add(region.lastIndexOf(NEWLINE, hit) + 1);
360
+ const records = [];
361
+ for (const lineStart of [...lineStarts].toSorted((a, b) => a - b)) {
362
+ const record = parseNdjsonLine(region.toString("utf-8", lineStart, region.indexOf(NEWLINE, lineStart)));
363
+ if (record) records.push(record);
364
+ }
365
+ return records;
366
+ }
367
+ function collect(record, markers, errors) {
368
+ const ts = typeof record.ts === "number" && Number.isFinite(record.ts) ? record.ts : null;
369
+ if (record.marker === true && ts !== null) {
370
+ const metro = record.src === "metro";
371
+ const slot = record.slot ?? "default";
372
+ const key = metro ? "metro" : JSON.stringify(["launch", slot]);
373
+ if ((markers[key]?.ts ?? -Infinity) < ts) markers[key] = metro ? {
374
+ src: "metro",
375
+ marker: true,
376
+ ts
377
+ } : {
378
+ marker: true,
379
+ ts,
380
+ slot
381
+ };
382
+ }
383
+ if (recordMatches(record, ERROR_CRITERIA)) errors.push({
384
+ src: record.src,
385
+ level: record.level,
386
+ event: record.event,
387
+ ts: ts ?? void 0,
388
+ slot: record.slot
389
+ });
390
+ }
391
+ function readIndex(file) {
392
+ try {
393
+ const parsed = JSON.parse(readFileSync$1(file, "utf-8"));
394
+ if (parsed?.version === INDEX_VERSION && parsed.files && typeof parsed.files === "object") return parsed;
395
+ } catch {}
396
+ return {
397
+ version: INDEX_VERSION,
398
+ files: {}
399
+ };
400
+ }
401
+ function writeIndex(file, index) {
402
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
403
+ try {
404
+ writeFileSync$1(tmp, JSON.stringify(index));
405
+ renameSync$1(tmp, file);
406
+ } catch {
407
+ rmSync$1(tmp, { force: true });
408
+ }
409
+ }
410
+ //#endregion
411
+ //#region src/status.ts
412
+ const IOS_SIM_MB = 1500;
413
+ const ANDROID_EMULATOR_MB = 2500;
414
+ const METRO_MB = 700;
415
+ function remoteDeviceState(record, ledger, root) {
416
+ if (!record) return null;
417
+ const claim = ledger.claims.get(record.sessionId);
418
+ return {
419
+ platform: record.platform,
420
+ backend: "eas",
421
+ sessionId: record.sessionId,
422
+ state: !ledger.safe ? "unknown" : claim?.workspaceRoot === root ? "claimed" : "unclaimed",
423
+ startedAt: record.startedAt,
424
+ webPreviewUrl: record.webPreviewUrl
425
+ };
426
+ }
427
+ function poolLine({ platform, parked, max }) {
428
+ if (parked <= 0) return null;
429
+ const what = plural(parked, `parked ${platform === "ios" ? "iOS simulator" : "Android emulator"}`);
430
+ return max > 0 ? `pool: ${what} (max ${max})` : `pool: ${what} (parking off; gc --delete removes them)`;
431
+ }
432
+ function environmentState(project, { simsByUdid = {}, metro = null, worktrees = [], simsAvailable = true, androidRuntime = null, androidRuntimes = {}, supervisor = null, logs = null, remote = null } = {}) {
433
+ const ios = project.platforms?.ios;
434
+ const android = project.platforms?.android;
435
+ const sim = ios ? simsByUdid[ios.deviceUdid] : null;
436
+ const simBooted = Boolean(sim && sim.state === "Booted");
437
+ const metroRunning = Boolean(metro?.metro);
438
+ const androidDetected = androidRuntime ? Boolean(androidRuntime.serial) : Boolean(android?.serial);
439
+ let live = simBooted || metroRunning || androidDetected || Boolean(remote);
440
+ let memoryMb = 0;
441
+ if (simBooted) memoryMb += IOS_SIM_MB;
442
+ if (androidDetected) memoryMb += ANDROID_EMULATOR_MB;
443
+ if (metroRunning) memoryMb += METRO_MB;
444
+ const warnings = [];
445
+ if (metro?.notOurs) warnings.push(`port ${project.metroPort}: ${metro.notOurs}`);
446
+ if (ios && !sim && simsAvailable) warnings.push(`recorded sim ${ios.deviceUdid} no longer exists`);
447
+ if (simBooted && project.metroPort && !metroRunning) warnings.push("simulator is booted with no Metro serving it");
448
+ if (androidRuntime && android?.avdName) {
449
+ const recordedSerial = android.consolePort ? `emulator-${android.consolePort}` : android.serial;
450
+ if (androidRuntime.serial && recordedSerial && androidRuntime.serial !== recordedSerial) warnings.push(`owned AVD ${android.avdName} changed serial (${recordedSerial} -> ${androidRuntime.serial}); rerun your \`stim android\` command in this workspace to restore Metro forwarding, then reopen agent-device on ${androidRuntime.serial}`);
451
+ if (androidRuntime.state === "missing") warnings.push(`recorded AVD ${android.avdName} no longer exists`);
452
+ if (androidRuntime.state === "not-detected") warnings.push(`owned AVD ${android.avdName} is not detected by adb; rerun your \`stim android\` command in this workspace to reconnect`);
453
+ if (androidRuntime.error) warnings.push(`could not check owned AVD ${android.avdName}: ${androidRuntime.error}`);
454
+ }
455
+ if (supervisor && supervisor.alive === false) warnings.push(`stale supervisor record for ${project.__path}`);
456
+ const slots = [];
457
+ for (const { slot, platforms } of projectDeviceSlots(project).slice(1)) {
458
+ const deviceState = environmentState({
459
+ ...project,
460
+ platforms,
461
+ deviceSlots: void 0
462
+ }, {
463
+ simsByUdid,
464
+ simsAvailable,
465
+ metro,
466
+ androidRuntime: androidRuntimes[slot]
467
+ });
468
+ slots.push({
469
+ slot,
470
+ ios: deviceState.ios,
471
+ android: deviceState.android
472
+ });
473
+ memoryMb += deviceState.memoryMb - (metroRunning ? METRO_MB : 0);
474
+ live ||= deviceState.live;
475
+ warnings.push(...deviceState.warnings.map((warning) => `${slot}: ${warning}`));
476
+ }
477
+ return {
478
+ path: project.__path,
479
+ ...slots.length ? { slots } : {},
480
+ live,
481
+ memoryMb,
482
+ warnings,
483
+ ios: ios ? {
484
+ name: sim?.name ?? null,
485
+ udid: ios.deviceUdid,
486
+ owned: Boolean(ios.owned),
487
+ state: sim?.state ?? (simsAvailable ? "missing" : "unknown")
488
+ } : null,
489
+ android: android ? {
490
+ name: android.avdName ?? android.serial,
491
+ owned: Boolean(android.owned),
492
+ physical: Boolean(android.serial && !android.avdName),
493
+ ...androidRuntime ? {
494
+ serial: androidRuntime.serial,
495
+ state: androidRuntime.state
496
+ } : {}
497
+ } : null,
498
+ metro: project.metroPort ? {
499
+ port: project.metroPort,
500
+ running: metroRunning,
501
+ pid: metro?.metro?.pid ?? null
502
+ } : null,
503
+ supervisor: supervisor ? {
504
+ pid: supervisor.pid ?? null,
505
+ mode: supervisor.mode ?? null,
506
+ startedAt: supervisor.startedAt ?? null,
507
+ healthy: Boolean(supervisor.healthy)
508
+ } : null,
509
+ logs: logs ? {
510
+ dir: logs.dir,
511
+ errorsSinceMarker: logs.errorsSinceMarker ?? 0
512
+ } : null,
513
+ worktree: worktrees.find((w) => w.path === project.__path) ?? null,
514
+ remoteDevices: remote ? [remote] : []
515
+ };
516
+ }
517
+ function remoteDeviceLine(remote) {
518
+ const claim = remote.state === "claimed" ? "" : ` (${remote.state})`;
519
+ const watch = remote.webPreviewUrl ? ` -- watch: ${remote.webPreviewUrl}` : "";
520
+ return `remote ${remote.platform ?? "?"}: EAS session ${remote.sessionId} billable${claim}${watch}`;
521
+ }
522
+ function capacity(states, totalMemoryMb) {
523
+ const committedMb = states.reduce((n, s) => n + s.memoryMb, 0);
524
+ return {
525
+ liveCount: states.filter((s) => s.live).length,
526
+ committedMb,
527
+ totalMemoryMb,
528
+ overCapacity: Boolean(totalMemoryMb && committedMb > totalMemoryMb * .6)
529
+ };
530
+ }
531
+ function parseDfFree(output) {
532
+ const lines = String(output || "").trim().split("\n");
533
+ if (lines.length < 2) return null;
534
+ const lastLine = lines[lines.length - 1];
535
+ if (lastLine === void 0) return null;
536
+ const m = /\s(\d+)\s+(\d+)\s+(\d+)\s+(\d+)%/.exec(lastLine);
537
+ if (!m) return null;
538
+ const totalKb = Number(m[1]);
539
+ const availableKb = Number(m[3]);
540
+ if (!Number.isFinite(totalKb) || !Number.isFinite(availableKb) || totalKb <= 0) return null;
541
+ return {
542
+ availableMb: Math.round(availableKb / 1024),
543
+ totalMb: Math.round(totalKb / 1024)
544
+ };
545
+ }
546
+ function diskIsTight(disk) {
547
+ return Boolean(disk && disk.availableMb < 25600);
548
+ }
549
+ function formatSpace(mb) {
550
+ if (!Number.isFinite(mb)) return "?";
551
+ if (mb >= 1048576) return `${(mb / 1048576).toFixed(1)} TB`;
552
+ if (mb >= 1024) return `${Math.round(mb / 1024)} GB`;
553
+ return `${Math.round(mb)} MB`;
554
+ }
555
+ function diskLine(volumes) {
556
+ const usable = (volumes || []).filter((v) => Boolean(v && v.disk));
557
+ if (usable.length === 0) return null;
558
+ if (usable.length === 1) {
559
+ const first = usable[0];
560
+ if (!first) return null;
561
+ const { disk } = first;
562
+ return `${formatSpace(disk.availableMb)} free of ${formatSpace(disk.totalMb)} on disk.`;
563
+ }
564
+ return `${usable.map((v) => `${formatSpace(v.disk.availableMb)} free on ${v.volume}`).join(", ")}.`;
565
+ }
566
+ function tightVolumes(volumes) {
567
+ return (volumes || []).filter((v) => v && diskIsTight(v.disk));
568
+ }
569
+ function unprovisionedWorktrees(worktrees, projectPaths) {
570
+ const known = new Set(projectPaths);
571
+ return worktrees.filter((w) => !known.has(w.path));
572
+ }
573
+ function deviceLeaseStates(entries, { root, now }) {
574
+ return entries.map((entry) => {
575
+ const lease = entry.lease;
576
+ return {
577
+ path: entry.path,
578
+ platform: entry.platform,
579
+ ...lease?.slot ? { slot: lease.slot } : {},
580
+ id: entry.id,
581
+ deviceName: lease?.deviceName ?? null,
582
+ holder: lease?.holder ?? null,
583
+ grantedAt: lease?.grantedAt ?? null,
584
+ expiresAt: lease?.expiresAt ?? null,
585
+ mine: Boolean(lease && root && lease.holder === root),
586
+ expired: Boolean(lease && Date.parse(lease.expiresAt) <= now),
587
+ parsed: Boolean(lease)
588
+ };
589
+ });
590
+ }
591
+ function deviceLeaseLines(states, now) {
592
+ if (states.length === 0) return [];
593
+ const lines = [`Device leases (${states.length}):`];
594
+ for (const state of states) {
595
+ const device = `${state.platform}${state.slot ? ` [${state.slot}]` : ""} ${state.id ?? state.path}${state.deviceName ? ` (${state.deviceName})` : ""}`;
596
+ if (!state.parsed || state.expiresAt === null) {
597
+ lines.push(` ${device} -- unreadable lease file, so nothing may take the device: ${state.path}`);
598
+ continue;
599
+ }
600
+ const when = clockTime(state.expiresAt);
601
+ const remaining = Date.parse(state.expiresAt) - now;
602
+ const expiry = state.expired ? `expired at ${when}` : `until ${when} (${formatElapsed(remaining)} left)`;
603
+ lines.push(` ${device} -- ${state.holder} ${expiry}${state.mine ? " [this workspace]" : ""}`);
604
+ }
605
+ return lines;
606
+ }
607
+ //#endregion
608
+ //#region src/commands/status.ts
609
+ function formatGb(mb) {
610
+ return `${(mb / 1024).toFixed(1)} GB`;
611
+ }
612
+ function statusCommand(program) {
613
+ program.command("status").description("Show every environment on this machine: devices, ports, what is actually running, and anything stuck.").option("--json", "print the state as JSON").option("--watch", "keep running and print the state again each time it changes").action(async (opts) => {
614
+ if (opts.watch) return watchStatus(Boolean(opts.json));
615
+ for (const line of await statusLines(Boolean(opts.json))) console.log(line);
616
+ });
617
+ }
618
+ async function statusLines(json) {
619
+ const out = [];
620
+ const cfg = loadConfig();
621
+ const projects = Object.entries(cfg?.projects || {});
622
+ const cwdRoot = findProjectRoot(process.cwd());
623
+ const simsRead = listAllIosSimsAsync();
624
+ simsRead.catch(() => {});
625
+ const ports = await portLookup(projects.flatMap(([, proj]) => proj.metroPort ? [proj.metroPort] : []));
626
+ const processes = Promise.all(projects.map(async ([path, proj]) => {
627
+ const metro = proj.metroPort ? await resolveOnPort(proj.metroPort, path, ports) : null;
628
+ return {
629
+ metro,
630
+ supervisor: await supervisorFacts(path, proj, metro, ports)
631
+ };
632
+ }));
633
+ processes.catch(() => {});
634
+ const source = resolveSourceCheckout(process.cwd());
635
+ const sourcePath = "path" in source ? source.path : null;
636
+ const worktrees = source.entries.filter((entry) => !entry.bare && entry.path !== sourcePath);
637
+ const androidRuntimeOf = androidRuntimeReader();
638
+ const devices = projects.map(([, proj]) => ({
639
+ androidRuntimes: Object.fromEntries(projectDeviceSlots(proj).slice(1).map(({ slot, platforms }) => [slot, platforms.android?.owned && platforms.android.avdName ? androidRuntimeOf(platforms.android.avdName) : null])),
640
+ androidRuntime: proj.platforms?.android?.owned && proj.platforms.android.avdName ? androidRuntimeOf(proj.platforms.android.avdName) : null
641
+ }));
642
+ const logs = projects.map(([path]) => logFacts(path));
643
+ const simsByUdid = {};
644
+ let simsAvailable = true;
645
+ let simctlError = null;
646
+ try {
647
+ for (const sim of await simsRead) simsByUdid[sim.udid] = sim;
648
+ } catch (e) {
649
+ simsAvailable = false;
650
+ simctlError = String(e?.message || e).split("\n")[0] ?? "";
651
+ }
652
+ const running = await processes;
653
+ const history = readStats().record?.history;
654
+ const states = [];
655
+ const labelOnlyRoots = [];
656
+ const easLedger = readEasSessionLedger();
657
+ for (const [i, [path, proj]] of projects.entries()) {
658
+ const { metro, supervisor } = running[i];
659
+ states.push(environmentState({
660
+ ...proj,
661
+ __path: path
662
+ }, {
663
+ simsByUdid,
664
+ metro,
665
+ worktrees,
666
+ simsAvailable,
667
+ ...devices[i],
668
+ supervisor,
669
+ logs: logs[i],
670
+ remote: remoteDeviceState(readRemoteSession(path), easLedger, path)
671
+ }));
672
+ const state = states[states.length - 1];
673
+ if (state) state.build = workspaceBuild(path, history);
674
+ labelOnlyRoots.push(Boolean(proj.worktreeRoot && !proj.bundleId && !state?.metro && !state?.ios && !state?.android));
675
+ }
676
+ const leaseNow = Date.now();
677
+ const leases = deviceLeaseStates(listLeaseFiles(), {
678
+ root: cwdRoot,
679
+ now: leaseNow
680
+ });
681
+ const cap = capacity(states, Math.round(totalmem() / 1048576));
682
+ const orphanWorktrees = unprovisionedWorktrees(worktrees, projects.map(([p]) => p));
683
+ const pools = ["ios", "android"].map((platform) => {
684
+ const { max, error } = parkedMaxSetting(platform);
685
+ return {
686
+ error,
687
+ line: poolLine({
688
+ platform,
689
+ parked: readParked(platform).length,
690
+ max
691
+ })
692
+ };
693
+ });
694
+ if (json) {
695
+ out.push(JSON.stringify({
696
+ environments: states.map((state, i) => labelOnlyRoots[i] ? {
697
+ ...state,
698
+ labelOnly: true
699
+ } : state),
700
+ capacity: cap,
701
+ deviceLeases: leases,
702
+ unprovisionedWorktrees: orphanWorktrees,
703
+ simctlAvailable: simsAvailable
704
+ }));
705
+ return out;
706
+ }
707
+ if (projects.length === 0 && orphanWorktrees.length === 0) {
708
+ out.push(chalk.dim("No projects registered."));
709
+ for (const pool of pools) {
710
+ if (pool.error) out.push(chalk.yellow(`${pool.error} ${POOL_SETTING_REMEDY}`));
711
+ if (pool.line) out.push(chalk.dim(pool.line));
712
+ }
713
+ for (const line of deviceLeaseLines(leases, leaseNow)) out.push(line);
714
+ return out;
715
+ }
716
+ if (!simsAvailable) out.push(chalk.yellow(`simctl could not be read (${simctlError}), so no iOS sim below could be checked.`));
717
+ for (const [i, [path, proj]] of projects.entries()) {
718
+ const state = states[i];
719
+ if (!state) continue;
720
+ const shortcut = projectShortcut(path, proj);
721
+ const marker = path === cwdRoot ? chalk.bold.cyan(`* ${shortcut}`) : shortcut;
722
+ const idle = state.live ? "" : chalk.dim(" [idle]");
723
+ out.push(`\n${marker}${idle} ${chalk.dim(`(${path})`)}`);
724
+ out.push(labelOnlyRoots[i] ? chalk.dim(" worktree root (holds the label; the app registers its own entry)") : chalk.dim(` app: ${proj.bundleId ?? "?"} (${proj.isExpo ? "expo" : "bare"})`));
725
+ if (state.metro) {
726
+ const label = state.metro.running ? chalk.green(`running (pid ${state.metro.pid})`) : chalk.dim("not running");
727
+ out.push(` metro: port ${state.metro.port} ${label}`);
728
+ }
729
+ if (state.supervisor) {
730
+ const health = state.supervisor.healthy ? chalk.green("healthy") : chalk.yellow("not answering");
731
+ const mode = state.supervisor.mode ? chalk.dim(` (${state.supervisor.mode})`) : "";
732
+ out.push(` supervisor: pid ${state.supervisor.pid}${mode} ${health}`);
733
+ }
734
+ if (state.build) {
735
+ const line = ` ${buildStatusLine(state.build, Date.now())}`;
736
+ out.push(state.build.state === "running" ? line : chalk.yellow(line));
737
+ }
738
+ if (state.logs) {
739
+ const n = state.logs.errorsSinceMarker;
740
+ const errs = n > 0 ? chalk.yellow(` (${n} error${n === 1 ? "" : "s"} since the last marker)`) : "";
741
+ out.push(chalk.dim(` logs: ${state.logs.dir}`) + errs);
742
+ }
743
+ for (const deviceState of [{
744
+ slot: "default",
745
+ ios: state.ios,
746
+ android: state.android
747
+ }, ...state.slots ?? []]) {
748
+ const slotLabel = deviceState.slot === "default" ? "" : ` [${deviceState.slot}]`;
749
+ if (deviceState.ios) {
750
+ const booted = deviceState.ios.state === "Booted" ? chalk.green("booted") : chalk.dim(deviceState.ios.state.toLowerCase());
751
+ const owned = deviceState.ios.owned ? chalk.dim(" (owned)") : "";
752
+ out.push(` ios${slotLabel}: ${chalk.cyan(deviceState.ios.name ?? deviceState.ios.udid)} ${booted}${owned}`);
753
+ }
754
+ if (deviceState.android) {
755
+ const kind = deviceState.android.physical ? chalk.dim("(physical)") : chalk.dim("(emulator)");
756
+ const observed = deviceState.android.state ? ` ${deviceState.android.state}${deviceState.android.serial ? ` (${deviceState.android.serial})` : ""}` : "";
757
+ out.push(` android${slotLabel}: ${chalk.cyan(deviceState.android.name)} ${kind}${observed}${deviceState.android.owned ? chalk.dim(" (owned)") : ""}`);
758
+ }
759
+ }
760
+ for (const remote of state.remoteDevices ?? []) out.push(` ${remoteDeviceLine(remote)}`);
761
+ for (const w of state.warnings) out.push(chalk.yellow(` ! ${w}`));
762
+ }
763
+ for (const pool of pools) {
764
+ if (pool.error) out.push(chalk.yellow(`\n${pool.error} ${POOL_SETTING_REMEDY}`));
765
+ if (pool.line) out.push(chalk.dim(`\n${pool.line}`));
766
+ }
767
+ const leaseLines = deviceLeaseLines(leases, leaseNow);
768
+ if (leaseLines.length) {
769
+ out.push("");
770
+ for (const line of leaseLines) out.push(line);
771
+ }
772
+ if (orphanWorktrees.length) {
773
+ out.push(chalk.dim(`\nWorktrees with no environment (${orphanWorktrees.length}):`));
774
+ for (const w of orphanWorktrees) out.push(chalk.dim(` ${w.path}${w.branch ? ` [${w.branch}]` : ""}`));
775
+ }
776
+ out.push(chalk.dim(`\n${cap.liveCount} live environment(s), roughly ${formatGb(cap.committedMb)} of ${formatGb(cap.totalMemoryMb)} committed.`));
777
+ const volumes = readVolumes(cwdRoot || process.cwd());
778
+ const line = diskLine(volumes);
779
+ if (line) {
780
+ const tight = tightVolumes(volumes);
781
+ if (tight.length) {
782
+ const which = tight.map((v) => v.volume).join(" and ");
783
+ out.push(chalk.yellow(`${line} A single iOS build can exhaust ${which} -- run \`stim gc\` before starting another environment.`));
784
+ } else out.push(chalk.dim(line));
785
+ }
786
+ if (cap.overCapacity) out.push(chalk.yellow("Over comfortable capacity. A machine that swaps is slower than one working in sequence -- release one before starting another."));
787
+ return out;
788
+ }
789
+ async function watchStatus(json) {
790
+ let last = null;
791
+ let sources = null;
792
+ const scheduler = createRefreshScheduler({
793
+ debounceMs: 250,
794
+ run: async () => {
795
+ let text;
796
+ try {
797
+ text = (await statusLines(json)).join("\n");
798
+ } catch (error) {
799
+ console.error(chalk.red(String(error?.message || error)));
800
+ return;
801
+ } finally {
802
+ sources?.reconcile();
803
+ }
804
+ if (text === last) return;
805
+ last = text;
806
+ process.stdout.write(`${!json && process.stdout.isTTY ? "\x1B[2J\x1B[H" : ""}${text}\n`);
807
+ }
808
+ });
809
+ const fallback = setInterval(() => scheduler.trigger(), WATCH_FALLBACK_MS);
810
+ const finish = () => {
811
+ clearInterval(fallback);
812
+ scheduler.stop();
813
+ sources?.stop();
814
+ process.exit(0);
815
+ };
816
+ process.on("SIGINT", finish);
817
+ process.on("SIGTERM", finish);
818
+ process.stdout.on("error", finish);
819
+ sources = watchStatusSources({
820
+ home: getConfigDir(),
821
+ onChange: () => scheduler.trigger()
822
+ });
823
+ scheduler.trigger(0);
824
+ await new Promise(() => {});
825
+ }
826
+ function workspaceBuild(path, history) {
827
+ const record = parseActiveBuild(readWorkspaceState(path)?.[ACTIVE_BUILD_KEY]);
828
+ if (!record) return null;
829
+ const projectKey = statsProjectKey({
830
+ root: path,
831
+ commonDir: gitCommonDir(path),
832
+ repoRoot: repoRoot(path)
833
+ });
834
+ return buildReport(record, {
835
+ state: activeBuildState(record.claim),
836
+ history: history?.[projectKey]
837
+ });
838
+ }
839
+ function androidRuntimeReader() {
840
+ const resolve = ownedAvdSerialResolver({ timeoutMs: 5e3 });
841
+ return (avdName) => readAndroidRuntime(() => resolve(avdName));
842
+ }
843
+ function readAndroidRuntime(resolveSerial) {
844
+ try {
845
+ const resolved = resolveSerial();
846
+ return {
847
+ serial: resolved.serial ?? null,
848
+ state: resolved.serial ? "detected" : resolved.missing ? "missing" : resolved.notRunning ? "not-detected" : "unknown"
849
+ };
850
+ } catch (error) {
851
+ return {
852
+ serial: null,
853
+ state: "unknown",
854
+ error: String(error?.message || error).split("\n")[0]
855
+ };
856
+ }
857
+ }
858
+ function readVolumes(projectPath) {
859
+ const roots = [.../* @__PURE__ */ new Set([
860
+ "/",
861
+ volumeRootFor(getConfigDir()),
862
+ volumeRootFor(projectPath)
863
+ ])];
864
+ const volumes = [];
865
+ for (const volume of roots) {
866
+ const quoted = `'${volume.replace(/'/g, "'\\''")}'`;
867
+ const disk = parseDfFree(getExecutor().runQuiet(`df -k ${quoted}`, { timeoutMs: 5e3 }));
868
+ if (disk) volumes.push({
869
+ volume,
870
+ disk
871
+ });
872
+ }
873
+ return volumes;
874
+ }
875
+ async function portLookup(metroPorts) {
876
+ const ports = [...new Set(metroPorts)];
877
+ const answering = new Map(await Promise.all(ports.map(async (port) => [port, await isMetroRunning(port)])));
878
+ const listeners = listeningPidsByPort(ports.filter((port) => answering.get(port)));
879
+ const cwds = listeners.then((byPort) => processCwds([...byPort.values()].flatMap((pids) => pids.slice(0, 1))));
880
+ return {
881
+ answering,
882
+ pidsOf: async (port) => (await listeners).get(port) ?? listeningPids(port),
883
+ cwdOf: async (pid) => {
884
+ const known = await cwds;
885
+ return known.has(pid) ? known.get(pid) ?? null : processCwd(pid);
886
+ }
887
+ };
888
+ }
889
+ async function resolveOnPort(port, path, lookup) {
890
+ return lookup.answering.get(port) ?? await isMetroRunning(port) ? resolveProjectMetro(port, path, lookup) : { missing: true };
891
+ }
892
+ async function supervisorFacts(path, proj, metroResolution, lookup) {
893
+ const state = readSupervisorState(path);
894
+ const record = proj?.supervisor ?? null;
895
+ const pid = state?.pid ?? record?.pid ?? null;
896
+ if (!pid) return null;
897
+ const port = state?.port ?? record?.port ?? null;
898
+ const alive = resolveSupervisorTarget({
899
+ state,
900
+ record,
901
+ reservedPort: proj?.metroPort
902
+ }).status === "ours";
903
+ let healthy = false;
904
+ if (alive && port) {
905
+ const resolution = port === proj?.metroPort && metroResolution ? metroResolution : await resolveOnPort(port, path, lookup);
906
+ healthy = Boolean(resolution?.metro);
907
+ }
908
+ return {
909
+ pid,
910
+ mode: state?.mode ?? record?.mode ?? null,
911
+ startedAt: state?.startedAt ?? record?.startedAt ?? null,
912
+ alive,
913
+ healthy
914
+ };
915
+ }
916
+ function logFacts(path) {
917
+ const dir = workspaceLogsDir(path);
918
+ if (!existsSync$1(dir)) return null;
919
+ try {
920
+ return {
921
+ dir,
922
+ errorsSinceMarker: countErrorsSinceMarker(dir, workspaceLogErrorIndex(path))
923
+ };
924
+ } catch {
925
+ return {
926
+ dir,
927
+ errorsSinceMarker: 0
928
+ };
929
+ }
930
+ }
931
+ //#endregion
932
+ export { statusCommand as default, readVolumes };