stim 1.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 (63) hide show
  1. package/README.md +22 -10
  2. package/dist/android-CC5bUer7.mjs +204 -0
  3. package/dist/android-DnZlT3aU.mjs +2279 -0
  4. package/dist/android-cas-DGlTStWu.mjs +1078 -0
  5. package/dist/android-cas-compiler.mjs +1 -1
  6. package/dist/android-xmyGBNy5.mjs +665 -0
  7. package/dist/app-install-oZIQRbdD.mjs +1141 -0
  8. package/dist/build-lock-vE45ZzJZ.mjs +645 -0
  9. package/dist/build-slots-CgacA9zQ.mjs +152 -0
  10. package/dist/{cache-manifest-4tH64LQ9.mjs → cache-manifest-oeH18ERr.mjs} +2 -2
  11. package/dist/cache-manifest.mjs +1 -1
  12. package/dist/cli.mjs +24 -27166
  13. package/dist/collector-run.mjs +6 -4
  14. package/dist/command-output-8wHMEde0.mjs +289 -0
  15. package/dist/{config-7kmtuhO2.mjs → config-D7uu8Gbq.mjs} +43 -12
  16. package/dist/deps-64wVnpZc.mjs +435 -0
  17. package/dist/dev-client-Bvky1hya.mjs +557 -0
  18. package/dist/device-DJKv5Qej.mjs +250 -0
  19. package/dist/device-lease-D-G3SgVe.mjs +335 -0
  20. package/dist/device-pool-BlHXnYYs.mjs +408 -0
  21. package/dist/device-remote-CCgkG82J.mjs +1209 -0
  22. package/dist/doctor-BRF9aAJ3.mjs +530 -0
  23. package/dist/doctor-DlVxTtMq.mjs +1137 -0
  24. package/dist/error-diagnostics-B5IJwv3-.mjs +636 -0
  25. package/dist/gc-DhXdgSdt.mjs +1651 -0
  26. package/dist/guide-DrhrDBo5.mjs +4319 -0
  27. package/dist/ios-BZxKPuqP.mjs +382 -0
  28. package/dist/ios-CK1Vrk6Q.mjs +2721 -0
  29. package/dist/ios-device-CYbuRNXR.mjs +81 -0
  30. package/dist/ios-device-CdSm0GDw.mjs +413 -0
  31. package/dist/logs-Dxoy16Qv.mjs +176 -0
  32. package/dist/logs-query-oqn47Ffx.mjs +273 -0
  33. package/dist/metro-B01H3EuD.mjs +184 -0
  34. package/dist/metro-store-CDDZHtPl.mjs +92 -0
  35. package/dist/ownership-B9QIdQOw.mjs +475 -0
  36. package/dist/ownership-D8HmSv2E.mjs +60 -0
  37. package/dist/ownership-claim-CeJLEpjD.mjs +569 -0
  38. package/dist/{project-Dit1CckW.mjs → project-COj6nzzh.mjs} +12 -3
  39. package/dist/reclaim-BPvN2HlP.mjs +347 -0
  40. package/dist/reload-DYZEBbZy.mjs +332 -0
  41. package/dist/remote-cache-DOi5-zLj.mjs +934 -0
  42. package/dist/{server-bare-BmTVVmBB.mjs → server-bare-BYHqEeyw.mjs} +3 -2
  43. package/dist/{server-expo-CHJe1r6j.mjs → server-expo-BEMr_m6p.mjs} +5 -4
  44. package/dist/server-expo-rb0k-0w7.mjs +2 -0
  45. package/dist/settings-BSwrY4FZ.mjs +687 -0
  46. package/dist/spawn-entry-nCE6m770.mjs +14 -0
  47. package/dist/start-ByCylYtB.mjs +822 -0
  48. package/dist/{state-2nush3MK.mjs → state-IYEY-zi8.mjs} +21 -22
  49. package/dist/stats-DZ4Q5J-7.mjs +62 -0
  50. package/dist/stats-wqxPm7Tv.mjs +307 -0
  51. package/dist/status-Dia0XTqG.mjs +363 -0
  52. package/dist/stop-BApyaF21.mjs +591 -0
  53. package/dist/stop-D6o5zAdm.mjs +3 -0
  54. package/dist/supervisor-run.d.mts +0 -1
  55. package/dist/supervisor-run.mjs +4 -4
  56. package/dist/workspace-process-lock-CxOO9-pf.mjs +47 -0
  57. package/dist/worktree-Dx3ZC5lS.mjs +539 -0
  58. package/dist/worktree-DxEmUI_n.mjs +1322 -0
  59. package/dist/xcode-CDB6fytw.mjs +1705 -0
  60. package/package.json +5 -5
  61. package/dist/android-jc8MnUyL.mjs +0 -514
  62. package/dist/metro-store-COLl1pOk.mjs +0 -1249
  63. package/dist/server-expo-BZJm--wA.mjs +0 -2
@@ -1,9 +1,11 @@
1
1
  import { t as getExecutor } from "./exec-bsN9MJXb.mjs";
2
- import { S as captureProcessToken } from "./config-7kmtuhO2.mjs";
3
- import { t as createLineReader } from "./process-output-C7IlIzd7.mjs";
4
- import { a as watchAppPid, c as parseDeviceConsoleLine, f as readCollectors, i as waitForAppPid, l as startIosDeviceConsole, m as unregisterCollector, n as parseLogcatLine, p as registerCollector, r as startAndroidLogcat, t as androidClockOffset, u as collectorProcessTitle } from "./android-jc8MnUyL.mjs";
5
- import { E as workspaceLogsDir } from "./state-2nush3MK.mjs";
2
+ import { T as captureProcessToken } from "./config-D7uu8Gbq.mjs";
6
3
  import { r as createNdjsonWriter } from "./ndjson-DcAtEx_K.mjs";
4
+ import { D as workspaceLogsDir } from "./state-IYEY-zi8.mjs";
5
+ import { t as createLineReader } from "./process-output-C7IlIzd7.mjs";
6
+ import { i as startIosDeviceConsole, r as parseDeviceConsoleLine } from "./ios-device-CYbuRNXR.mjs";
7
+ import { a as unregisterCollector, i as registerCollector, r as readCollectors, t as collectorProcessTitle } from "./ownership-D8HmSv2E.mjs";
8
+ import { a as watchAppPid, i as waitForAppPid, n as parseLogcatLine, r as startAndroidLogcat, t as androidClockOffset } from "./android-CC5bUer7.mjs";
7
9
  import { existsSync } from "node:fs";
8
10
  import { isAbsolute, join, resolve } from "node:path";
9
11
  import { fileURLToPath } from "node:url";
@@ -0,0 +1,289 @@
1
+ import { n as stripAnsi } from "./process-output-C7IlIzd7.mjs";
2
+ //#region src/launch-error-preview.ts
3
+ function framePriority(frame, root) {
4
+ const normalized = frame.replaceAll("\\", "/");
5
+ if (/^\s*at (?:android\.|java\.|com\.android\.|dalvik\.)/.test(normalized)) return 2;
6
+ if (/(?:^|[/(\s])node_modules\//.test(normalized) || /\bnode:internal\//.test(normalized)) return 2;
7
+ const paths = [...normalized.matchAll(/(?:\(|@|\bat )([^()]+?\.(?:[cm]?[jt]sx?|swift|kt|java|mm?|cpp|cc|c|h))(?::\d+(?::\d+)?)?(?:\)|$)/g)];
8
+ for (const [, file] of paths) {
9
+ if (!file || /^https?:/.test(file)) continue;
10
+ if (!(file.startsWith("/") || /^[A-Za-z]:\//.test(file)) && !file.startsWith("../")) return 0;
11
+ if (root && file.startsWith(`${root.replaceAll("\\", "/").replace(/\/$/, "")}/`)) return 0;
12
+ }
13
+ if (/\(<anonymous>\)\s*$/.test(frame)) return 2;
14
+ return 1;
15
+ }
16
+ function decodeStack(value) {
17
+ return value.replace(/\\([nrt\\'"])/g, (_escape, char) => {
18
+ return {
19
+ n: "\n",
20
+ r: "\r",
21
+ t: " "
22
+ }[char] ?? char;
23
+ });
24
+ }
25
+ function expandStackFields(message) {
26
+ return message.replace(/(?<!\w)(['"]?)(componentStack|stack)\1:\s*(['"])((?:\\.|(?!\3)[^\\])*)\3,?/g, (_match, _keyQuote, field, _quote, value) => {
27
+ return `\n${field === "componentStack" ? "Component" : "Error"} stack:\n${decodeStack(value)}\n`;
28
+ }).replace(/(^|\n|[,{])[ \t]*(['"]?)(componentStack|stack)\2:[ \t]*(['"])((?:\\.|[^\\\n])*)$/gm, (match, prefix, _keyQuote, field, _quote, value) => {
29
+ if (!/\\n\s+at\s/.test(value)) return match;
30
+ return `${prefix}\n${field === "componentStack" ? "Component" : "Error"} stack:\n${decodeStack(value)}`;
31
+ });
32
+ }
33
+ function jsonStringField(blob, field) {
34
+ const match = new RegExp(`"${field}":"((?:[^"\\\\]|\\\\.)*)"`).exec(blob);
35
+ if (!match) return null;
36
+ try {
37
+ return JSON.parse(`"${match[1]}"`);
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+ function jsonObjectComplete(blob) {
43
+ let depth = 0;
44
+ let inString = false;
45
+ for (let i = 0; i < blob.length; i++) {
46
+ const char = blob[i];
47
+ if (inString) {
48
+ if (char === "\\") i++;
49
+ else if (char === "\"") inString = false;
50
+ } else if (char === "\"") inString = true;
51
+ else if (char === "{") depth++;
52
+ else if (char === "}" && --depth === 0) return true;
53
+ }
54
+ return false;
55
+ }
56
+ /**
57
+ * React Native's DebugServerException embeds Metro's serialized bundling error
58
+ * after a `Body:` line, and logcat splits that JSON across lines past 4 KiB.
59
+ */
60
+ function compactMetroErrorBody(text, root, printed) {
61
+ if (!text.includes("DebugServerException")) return null;
62
+ const lines = text.split(/\r?\n/);
63
+ const start = lines.findIndex((line) => line.includes("{\"type\":\""));
64
+ if (start < 0) return null;
65
+ const prefix = lines[start].slice(0, lines[start].indexOf("{\"type\":\""));
66
+ let blob = lines[start].slice(prefix.length);
67
+ let end = start + 1;
68
+ while (end < lines.length && !jsonObjectComplete(blob) && lines[end].trim() && !/^(?:\tat\s|\s*Caused by:)/.test(lines[end])) blob += lines[end++];
69
+ const type = /^\{"type":"([^"]*)"/.exec(blob)?.[1];
70
+ const message = jsonStringField(blob, "message");
71
+ if (!type || !message) return null;
72
+ const stripRoot = (value) => root ? value.replaceAll(`${root}/`, "") : value;
73
+ const [firstLine = "", ...rest] = stripRoot(stripAnsi(message)).trim().split(/\r?\n/);
74
+ const first = firstLine.trim();
75
+ const importStack = stripRoot(stripAnsi(jsonStringField(blob, "_expoImportStack") ?? "")).trim();
76
+ const diagnosis = [
77
+ first,
78
+ ...rest,
79
+ ...importStack ? importStack.split(/\r?\n/) : []
80
+ ];
81
+ const printedLines = new Set(printed.flatMap((line) => stripRoot(stripAnsi(line)).split(/\r?\n/).map((part) => part.trim())));
82
+ const replacement = first && diagnosis.every((line) => !line.trim() || printedLines.has(line.trim())) ? [`${type}: ${first} (diagnosis above)`] : [`${type}: ${first}`, ...diagnosis.slice(1)];
83
+ return {
84
+ head: [...lines.slice(0, start), ...prefix.trim() ? [prefix] : []].join("\n"),
85
+ lines: [...replacement, "[Metro error body compacted; full text in stim logs]"],
86
+ tail: lines.slice(end).join("\n")
87
+ };
88
+ }
89
+ function shortFrame(frame, root) {
90
+ return (root ? frame.replaceAll(`${root}/`, "") : frame).trim().replace(/ \(BuildId: [0-9a-f]+\)/gi, "").replace(" [captured native frame]", "").replace(/ \(in ([^()]+)\) \(\/?<compiler-generated>:0\) \(\1\)$/, " ($1; compiler-generated)").replace(/https?:\/\/[^\s)]+/g, (location) => {
91
+ const match = /^(.*\.bundle)(?:[^\s]*?)(:\d+:\d+)$/.exec(location);
92
+ if (!match) {
93
+ const bundle = /\/([^/]+\.bundle)/.exec(location)?.[1];
94
+ return bundle ? `${bundle} [unsymbolicated]` : location;
95
+ }
96
+ return `${match[1].split("/").at(-1)}${match[2]} [unsymbolicated]`;
97
+ }).replace(/\((\/(?:apex|system)\/[^()]+)\)/g, (_match, path) => `(${path.split("/").at(-1)})`);
98
+ }
99
+ function structuredFrames(value) {
100
+ if (!Array.isArray(value)) return [];
101
+ return value.flatMap((frame) => {
102
+ if (frame === null || typeof frame !== "object") return [];
103
+ const { file, line, column, fn } = frame;
104
+ const location = [
105
+ typeof file === "string" ? file : null,
106
+ line,
107
+ column
108
+ ].filter((part) => typeof part === "string" || typeof part === "number" && Number.isFinite(part)).join(":");
109
+ const name = typeof fn === "string" ? fn : "";
110
+ return name && location ? [`at ${name} (${location})`] : name || location ? [`at ${name || location}`] : [];
111
+ });
112
+ }
113
+ /** Formats captured stacks for humans; full mode removes preview limits and preserves frame text. */
114
+ function launchErrorPreview(records, root, { full = false } = {}) {
115
+ const lines = [];
116
+ let kind = "Error";
117
+ let frames = [];
118
+ let hadStack = false;
119
+ let source = "";
120
+ const flush = () => {
121
+ if (frames.length) {
122
+ hadStack = true;
123
+ const ranked = frames.map((frame, index) => ({
124
+ frame,
125
+ index,
126
+ rank: framePriority(frame, root)
127
+ }));
128
+ const selected = ranked.toSorted((a, b) => a.rank - b.rank || a.index - b.index).slice(0, full ? void 0 : 10).toSorted((a, b) => a.index - b.index);
129
+ const prioritized = !full && selected.some(({ index, rank }) => index >= 10 && rank === 0);
130
+ lines.push(`${kind} stack${prioritized ? " (app frames prioritized)" : ""}:`, ...selected.map(({ frame }) => ` ${full ? frame.trim() : shortFrame(frame, root)}`));
131
+ if (frames.length > selected.length) {
132
+ const frameworkCount = ranked.filter((entry) => entry.rank === 2 && !selected.includes(entry)).length;
133
+ lines.push(` ... ${frames.length - selected.length} more frames${frameworkCount ? ` (${frameworkCount} dependency/native)` : ""}`);
134
+ }
135
+ }
136
+ frames = [];
137
+ kind = "Error";
138
+ };
139
+ const consume = (line) => {
140
+ if ((hadStack || frames.length > 0) && /^\s*isComponentError:\s*(?:true|false)\s*}?,?\s*$/.test(line)) return;
141
+ const header = /^\s*(Component stack|Error stack|Call Stack):?\s*$/i.exec(line);
142
+ if (header) {
143
+ flush();
144
+ kind = /^component/i.test(header[1]) ? "Component" : "Error";
145
+ } else if (/^\s*at\s+\S/.test(line) || /^\s*\S[^\n]*@\S+:\d+:\d+\s*$/.test(line) || /^\s+\S[^\n]*\([^()]+:\d+:\d+\)\s*$/.test(line)) {
146
+ if (!full && /\(https?:\/\/[^\s)]*$/.test(line)) return;
147
+ frames.push(line);
148
+ } else if (line.trim()) {
149
+ flush();
150
+ if (/^\s*Caused by:/.test(line)) kind = "Caused-by";
151
+ lines.push(!full && line.length > 1e3 ? `${line.slice(0, 1e3)} ... [preview shortened; full text in stim logs]` : line);
152
+ }
153
+ };
154
+ for (const record of records) {
155
+ const nextSource = JSON.stringify([
156
+ record.src,
157
+ record.platform,
158
+ record.proc
159
+ ]);
160
+ if (source !== nextSource) flush();
161
+ source = nextSource;
162
+ if (record.msg != null) {
163
+ const msg = String(record.msg);
164
+ const compacted = full ? null : compactMetroErrorBody(msg, root, lines);
165
+ const chunks = compacted ? [
166
+ expandStackFields(compacted.head),
167
+ ...compacted.lines,
168
+ expandStackFields(compacted.tail)
169
+ ] : [expandStackFields(msg)];
170
+ for (const chunk of chunks) for (const line of chunk.split(/\r?\n/)) consume(line);
171
+ }
172
+ const stack = typeof record.stack === "string" ? record.stack.split(/\r?\n/) : structuredFrames(record.stack);
173
+ if (stack.length) {
174
+ flush();
175
+ if (record.event === "native_crash") kind = "Native";
176
+ for (const line of stack) consume(line);
177
+ }
178
+ const components = typeof record.componentStack === "string" ? record.componentStack.split(/\r?\n/) : structuredFrames(record.componentStack);
179
+ if (components.length) {
180
+ flush();
181
+ kind = "Component";
182
+ for (const line of components) consume(line);
183
+ }
184
+ if (typeof record.codeFrame === "string") {
185
+ flush();
186
+ lines.push(record.codeFrame);
187
+ }
188
+ if (typeof record.symbolicationNote === "string") {
189
+ flush();
190
+ lines.push(...record.symbolicationNote.split("\n").filter((line) => full || !line.startsWith("Same error captured by ")));
191
+ }
192
+ }
193
+ flush();
194
+ if (hadStack && !full) lines.push("Full captured logs: stim logs --source all");
195
+ return lines;
196
+ }
197
+ //#endregion
198
+ //#region src/command-output.ts
199
+ const LABEL_WIDTH = 11;
200
+ function formatDuration(ms) {
201
+ const value = Number(ms);
202
+ if (!Number.isFinite(value) || value < 0) return "unknown";
203
+ if (value < 1e3) return `${Math.round(value)}ms`;
204
+ const totalSeconds = Math.round(value / 1e3);
205
+ if (totalSeconds >= 60) {
206
+ const minutes = Math.floor(totalSeconds / 60);
207
+ const seconds = totalSeconds - minutes * 60;
208
+ return `${minutes}m${String(seconds).padStart(2, "0")}s`;
209
+ }
210
+ return `${Math.round(value / 100) / 10}s`;
211
+ }
212
+ function formatLongDuration(ms) {
213
+ const value = Number(ms);
214
+ if (!Number.isFinite(value) || value < 0) return "unknown";
215
+ const totalSeconds = Math.round(value / 1e3);
216
+ const hours = Math.floor(totalSeconds / 3600);
217
+ const minutes = Math.floor((totalSeconds - hours * 3600) / 60);
218
+ const seconds = totalSeconds - hours * 3600 - minutes * 60;
219
+ if (hours > 0) return minutes > 0 ? `${hours}h${String(minutes).padStart(2, "0")}m` : `${hours}h`;
220
+ if (minutes > 0) return seconds > 0 ? `${minutes}m${String(seconds).padStart(2, "0")}s` : `${minutes}m`;
221
+ return `${seconds}s`;
222
+ }
223
+ function formatElapsed(ms) {
224
+ const totalSeconds = Math.max(0, Math.round((Number(ms) || 0) / 1e3));
225
+ const minutes = Math.floor(totalSeconds / 60);
226
+ const seconds = totalSeconds % 60;
227
+ return minutes > 0 ? `${minutes}m${String(seconds).padStart(2, "0")}s` : `${seconds}s`;
228
+ }
229
+ function plural(count, singular, many = `${singular}s`) {
230
+ return `${count} ${count === 1 ? singular : many}`;
231
+ }
232
+ function clockTime(iso) {
233
+ const at = new Date(iso);
234
+ if (Number.isNaN(at.getTime())) return iso;
235
+ return [
236
+ at.getHours(),
237
+ at.getMinutes(),
238
+ at.getSeconds()
239
+ ].map((n) => String(n).padStart(2, "0")).join(":");
240
+ }
241
+ function releasedLeaseFact(lease) {
242
+ return `released the ${lease.platform} lease on ${lease.id} (it ran until ${clockTime(lease.expiresAt)})`;
243
+ }
244
+ function phaseLine(label, text) {
245
+ return ` ${String(label).padEnd(LABEL_WIDTH)} ${text}`;
246
+ }
247
+ /** Quote a path for a shell command Stim prints for a human to copy and run. */
248
+ function quotedPath(path) {
249
+ return `'${path.replaceAll("'", "'\\''")}'`;
250
+ }
251
+ function shortUdid(udid) {
252
+ const text = String(udid ?? "");
253
+ return text.length > 4 ? `${text.slice(0, 4)}..` : text;
254
+ }
255
+ function shortHash(hash) {
256
+ const text = String(hash ?? "");
257
+ return text.length > 8 ? `${text.slice(0, 6)}..` : text;
258
+ }
259
+ function isAppLaunchError(record) {
260
+ return record.src !== "device" || record.level === "fatal" || record.platform === "android" && typeof record.proc === "string" && /^ReactNativeJS\(\d+\)$/.test(record.proc) || record.platform === "ios" && record.subsystem === "com.facebook.react.log" && record.category === "javascript";
261
+ }
262
+ /**
263
+ * Splits the error-level records a verified launch collected into the device
264
+ * log the run counts and the records it still prints one by one.
265
+ */
266
+ function launchErrorReport(records, root) {
267
+ const fromDevice = records.filter((record) => record.src === "device" && !isAppLaunchError(record));
268
+ const lines = launchErrorPreview(records.filter(isAppLaunchError), root);
269
+ return {
270
+ summary: fromDevice.length === 0 ? null : `${plural(fromDevice.length, "general device error-level record")} (not confirmed app errors); inspect with stim logs --errors --source device`,
271
+ lines
272
+ };
273
+ }
274
+ function appReadinessMessage(status, waitedMs) {
275
+ if (status === "ready") return `app reported ready (${formatDuration(waitedMs)} verification total)`;
276
+ if (status === "error") return "not confirmed: an app error or process exit interrupted the readiness wait";
277
+ return "not confirmed: no ready log within 30s of bundle load; inspect the UI and logs";
278
+ }
279
+ const SLOW_STEP_MS = 2e3;
280
+ function stepClock(now = Date.now) {
281
+ const t0 = now();
282
+ return () => now() - t0;
283
+ }
284
+ function stepTimer(now = Date.now) {
285
+ const elapsed = stepClock(now);
286
+ return () => `(${formatDuration(elapsed())})`;
287
+ }
288
+ //#endregion
289
+ export { launchErrorPreview as _, formatElapsed as a, launchErrorReport as c, quotedPath as d, releasedLeaseFact as f, stepTimer as g, stepClock as h, formatDuration as i, phaseLine as l, shortUdid as m, appReadinessMessage as n, formatLongDuration as o, shortHash as p, clockTime as r, isAppLaunchError as s, SLOW_STEP_MS as t, plural as u };
@@ -1,7 +1,7 @@
1
1
  import { t as getExecutor } from "./exec-bsN9MJXb.mjs";
2
2
  import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
3
- import { dirname, isAbsolute, join, normalize } from "path";
4
3
  import { homedir } from "os";
4
+ import { dirname, isAbsolute, join, normalize } from "path";
5
5
  import { withDirLock } from "@stim-cli/core";
6
6
  import { capture, check, decode } from "unique-pid";
7
7
  //#region src/fs-util.ts
@@ -101,9 +101,20 @@ function sameProcessRecord(a, b) {
101
101
  if (!a || !b) return !a && !b;
102
102
  return a.pid === b.pid && a.processToken === b.processToken;
103
103
  }
104
- function captureProcessToken(pid) {
104
+ function captureProcessIdentity(pid) {
105
105
  const result = capture(pid);
106
- return result.ok ? result.value : null;
106
+ if (result.ok) return {
107
+ ok: true,
108
+ token: result.value
109
+ };
110
+ return {
111
+ ok: false,
112
+ reason: `${result.error.code} (${result.error.message})`
113
+ };
114
+ }
115
+ function captureProcessToken(pid) {
116
+ const captured = captureProcessIdentity(pid);
117
+ return captured.ok ? captured.token : null;
107
118
  }
108
119
  function inspectProcessIdentity(record) {
109
120
  if (!record || typeof record.processToken !== "string") return "unknown";
@@ -141,6 +152,15 @@ function lockPath() {
141
152
  function withConfigLock(fn) {
142
153
  return withDirLock(lockPath(), fn, { ensureParent: ensureDir });
143
154
  }
155
+ function configCorruptRepair(path = getConfigPath()) {
156
+ return `Repair the file, or move it aside to start over: mv "${path}" "${path}.broken"`;
157
+ }
158
+ function configCorrupt(reason, path = getConfigPath()) {
159
+ const corrupt = /* @__PURE__ */ new Error(`Stim config at ${path} ${reason}\nIt holds the records of the simulators and emulators Stim owns, so it is never reset automatically.
160
+ ` + configCorruptRepair(path));
161
+ corrupt.code = "STIM_CONFIG_CORRUPT";
162
+ return corrupt;
163
+ }
144
164
  function loadConfig() {
145
165
  const p = getConfigPath();
146
166
  if (!existsSync(p)) return null;
@@ -148,11 +168,24 @@ function loadConfig() {
148
168
  try {
149
169
  return JSON.parse(raw);
150
170
  } catch (err) {
151
- const corrupt = /* @__PURE__ */ new Error(`Stim config at ${p} is not valid JSON: ${err.message}\nIt holds the records of the simulators and emulators Stim owns, so it is never reset automatically.
152
- Repair the file, or move it aside to start over: mv "${p}" "${p}.broken"`);
153
- corrupt.code = "STIM_CONFIG_CORRUPT";
154
- throw corrupt;
171
+ throw configCorrupt(`is not valid JSON: ${err.message}`, p);
172
+ }
173
+ }
174
+ function isRecord(value) {
175
+ return typeof value === "object" && value !== null && !Array.isArray(value);
176
+ }
177
+ /**
178
+ * An array or scalar container swallows every write silently: `JSON.stringify` drops a string key set on
179
+ * an array, so the record is gone by the time the file is written. Absent is created; unusable is refused.
180
+ */
181
+ function adoptContainer(cfg, key) {
182
+ const value = cfg[key];
183
+ if (value === void 0 || value === null) {
184
+ cfg[key] = {};
185
+ return true;
155
186
  }
187
+ if (!isRecord(value)) throw configCorrupt(`has a ${key} that is not an object: ${JSON.stringify(value)}`);
188
+ return false;
156
189
  }
157
190
  function saveConfig(config) {
158
191
  ensureDir();
@@ -174,10 +207,8 @@ function ensureConfig() {
174
207
  const existing = loadConfig();
175
208
  if (existing) {
176
209
  let changed = false;
177
- if (!existing.repos) {
178
- existing.repos = {};
179
- changed = true;
180
- }
210
+ if (adoptContainer(existing, "projects")) changed = true;
211
+ if (adoptContainer(existing, "repos")) changed = true;
181
212
  if (existing.version !== CONFIG_VERSION) {
182
213
  existing.version = CONFIG_VERSION;
183
214
  changed = true;
@@ -347,4 +378,4 @@ function allConsolePortsAndSerials({ isMounted = isOnMountedVolume } = {}) {
347
378
  return result;
348
379
  }
349
380
  //#endregion
350
- export { listMountedVolumes as A, inspectProcessIdentity as C, directorySize as D, withDirLock as E, formatBytes as O, captureProcessToken as S, waitForProcessExit as T, saveConfig as _, clearSupervisor as a, upsertProject as b, getConcurrencyLimits as c, getProjectSettings as d, getRepoSettings as f, removeProject as g, releaseAndroidConsolePort as h, clearDevice as i, volumeRootFor as j, isOnMountedVolume as k, getConfigDir as l, loadConfig as m, allMetroPorts as n, ensureConfig as o, isPathPrefix as p, claimMetroPort as r, findEnclosingWorktreeRoot as s, allConsolePortsAndSerials as t, getProject as u, setDevice as v, sameProcessRecord as w, withConfigLock as x, setSupervisor as y };
381
+ export { directorySize as A, withConfigLock as C, sameProcessRecord as D, inspectProcessIdentity as E, isOnMountedVolume as M, listMountedVolumes as N, waitForProcessExit as O, volumeRootFor as P, upsertProject as S, captureProcessToken as T, releaseAndroidConsolePort as _, clearSupervisor as a, setDevice as b, findEnclosingWorktreeRoot as c, getConfigPath as d, getProject as f, loadConfig as g, isPathPrefix as h, clearDevice as i, formatBytes as j, withDirLock as k, getConcurrencyLimits as l, getRepoSettings as m, allMetroPorts as n, configCorruptRepair as o, getProjectSettings as p, claimMetroPort as r, ensureConfig as s, allConsolePortsAndSerials as t, getConfigDir as u, removeProject as v, captureProcessIdentity as w, setSupervisor as x, saveConfig as y };