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.
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/dist/android-CC5bUer7.mjs +204 -0
- package/dist/android-DnZlT3aU.mjs +2279 -0
- package/dist/android-cas-DGlTStWu.mjs +1078 -0
- package/dist/android-cas-compiler.d.mts +1 -0
- package/dist/android-cas-compiler.mjs +71 -0
- package/dist/android-xmyGBNy5.mjs +665 -0
- package/dist/app-install-oZIQRbdD.mjs +1141 -0
- package/dist/build-lock-vE45ZzJZ.mjs +645 -0
- package/dist/build-slots-CgacA9zQ.mjs +152 -0
- package/dist/cache-manifest-oeH18ERr.mjs +66 -0
- package/dist/cache-manifest.d.mts +27 -0
- package/dist/cache-manifest.mjs +2 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +40 -0
- package/dist/collector-run.d.mts +119 -0
- package/dist/collector-run.mjs +541 -0
- package/dist/command-output-8wHMEde0.mjs +289 -0
- package/dist/config-D7uu8Gbq.mjs +381 -0
- package/dist/deps-64wVnpZc.mjs +435 -0
- package/dist/dev-client-Bvky1hya.mjs +557 -0
- package/dist/device-DJKv5Qej.mjs +250 -0
- package/dist/device-lease-D-G3SgVe.mjs +335 -0
- package/dist/device-pool-BlHXnYYs.mjs +408 -0
- package/dist/device-remote-CCgkG82J.mjs +1209 -0
- package/dist/doctor-BRF9aAJ3.mjs +530 -0
- package/dist/doctor-DlVxTtMq.mjs +1137 -0
- package/dist/error-diagnostics-B5IJwv3-.mjs +636 -0
- package/dist/errors-B6W7FkxQ.mjs +15 -0
- package/dist/exec-bsN9MJXb.mjs +63 -0
- package/dist/gc-DhXdgSdt.mjs +1651 -0
- package/dist/guide-DrhrDBo5.mjs +4319 -0
- package/dist/ios-BZxKPuqP.mjs +382 -0
- package/dist/ios-CK1Vrk6Q.mjs +2721 -0
- package/dist/ios-device-CYbuRNXR.mjs +81 -0
- package/dist/ios-device-CdSm0GDw.mjs +413 -0
- package/dist/logs-Dxoy16Qv.mjs +176 -0
- package/dist/logs-query-oqn47Ffx.mjs +273 -0
- package/dist/metro-B01H3EuD.mjs +184 -0
- package/dist/metro-store-CDDZHtPl.mjs +92 -0
- package/dist/ndjson-DcAtEx_K.mjs +131 -0
- package/dist/ndjson-DvHSsNU9.d.mts +16 -0
- package/dist/ownership-B9QIdQOw.mjs +475 -0
- package/dist/ownership-D8HmSv2E.mjs +60 -0
- package/dist/ownership-claim-CeJLEpjD.mjs +569 -0
- package/dist/process-output-C7IlIzd7.mjs +40 -0
- package/dist/project-COj6nzzh.mjs +243 -0
- package/dist/reclaim-BPvN2HlP.mjs +347 -0
- package/dist/reload-DYZEBbZy.mjs +332 -0
- package/dist/remote-cache-DOi5-zLj.mjs +934 -0
- package/dist/server-bare-BYHqEeyw.mjs +352 -0
- package/dist/server-expo-BEMr_m6p.mjs +312 -0
- package/dist/server-expo-rb0k-0w7.mjs +2 -0
- package/dist/settings-BSwrY4FZ.mjs +687 -0
- package/dist/spawn-entry-nCE6m770.mjs +14 -0
- package/dist/start-ByCylYtB.mjs +822 -0
- package/dist/state-IYEY-zi8.mjs +268 -0
- package/dist/stats-DZ4Q5J-7.mjs +62 -0
- package/dist/stats-wqxPm7Tv.mjs +307 -0
- package/dist/status-Dia0XTqG.mjs +363 -0
- package/dist/stop-BApyaF21.mjs +591 -0
- package/dist/stop-D6o5zAdm.mjs +3 -0
- package/dist/supervisor-run.d.mts +84 -0
- package/dist/supervisor-run.mjs +210 -0
- package/dist/workspace-process-lock-CxOO9-pf.mjs +47 -0
- package/dist/worktree-Dx3ZC5lS.mjs +539 -0
- package/dist/worktree-DxEmUI_n.mjs +1322 -0
- package/dist/xcode-CDB6fytw.mjs +1705 -0
- package/package.json +61 -8
- package/shim/android-cas-pch.cmake +35 -0
- package/shim/android-cas.gradle +29 -0
- package/shim/android-cas.toolchain.cmake +12 -0
- package/shim/android-optimizations.gradle +30 -0
- package/shim/bundle-response.cjs +46 -0
- package/shim/bundle-response.d.cts +5 -0
- package/shim/expo-metro-config.cjs +188 -0
- package/shim/metro-cache-generation.cjs +20 -0
- package/shim/metro-cache-generation.d.cts +5 -0
- package/skill/SKILL.md +14 -0
|
@@ -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 };
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { t as getExecutor } from "./exec-bsN9MJXb.mjs";
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { dirname, isAbsolute, join, normalize } from "path";
|
|
5
|
+
import { withDirLock } from "@stim-cli/core";
|
|
6
|
+
import { capture, check, decode } from "unique-pid";
|
|
7
|
+
//#region src/fs-util.ts
|
|
8
|
+
function volumeRootFor(path) {
|
|
9
|
+
const m = normalize(String(path)).match(/^\/volumes\/([^/]+)/i);
|
|
10
|
+
return m ? `/Volumes/${m[1]}` : "/";
|
|
11
|
+
}
|
|
12
|
+
function resolveWorkspaceRealish(workspacePath) {
|
|
13
|
+
const raw = String(workspacePath);
|
|
14
|
+
if (!raw.startsWith("/")) return null;
|
|
15
|
+
let current = raw;
|
|
16
|
+
const MAX_HOPS = 40;
|
|
17
|
+
for (let hops = 0; hops < MAX_HOPS; hops++) {
|
|
18
|
+
const normalized = normalize(current);
|
|
19
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
20
|
+
let prefix = "";
|
|
21
|
+
let followedSymlink = false;
|
|
22
|
+
for (const segment of segments) {
|
|
23
|
+
prefix += `/${segment}`;
|
|
24
|
+
let st;
|
|
25
|
+
try {
|
|
26
|
+
st = lstatSync(prefix);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (err && err.code === "ENOENT") return normalized;
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (st.isSymbolicLink()) {
|
|
32
|
+
let target;
|
|
33
|
+
try {
|
|
34
|
+
target = readlinkSync(prefix);
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
current = `${target.startsWith("/") ? target : normalize(join(dirname(prefix), target))}${normalized.slice(prefix.length)}`;
|
|
39
|
+
followedSymlink = true;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!followedSymlink) return normalized;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function isRealMount(entryDev, rootDev) {
|
|
48
|
+
return entryDev != null && rootDev != null && entryDev !== rootDev;
|
|
49
|
+
}
|
|
50
|
+
function listMountedVolumes({ statFn = statSync } = {}) {
|
|
51
|
+
const roots = ["/"];
|
|
52
|
+
let rootDev;
|
|
53
|
+
try {
|
|
54
|
+
rootDev = statFn("/").dev;
|
|
55
|
+
} catch {
|
|
56
|
+
return roots;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
for (const name of readdirSync("/Volumes")) {
|
|
60
|
+
const path = join("/Volumes", name);
|
|
61
|
+
let entryDev;
|
|
62
|
+
try {
|
|
63
|
+
entryDev = statFn(path).dev;
|
|
64
|
+
} catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (isRealMount(entryDev, rootDev)) roots.push(path);
|
|
68
|
+
}
|
|
69
|
+
} catch {}
|
|
70
|
+
return roots;
|
|
71
|
+
}
|
|
72
|
+
function resolveVolumeRoot(path) {
|
|
73
|
+
const realish = resolveWorkspaceRealish(path);
|
|
74
|
+
return realish === null ? null : volumeRootFor(realish);
|
|
75
|
+
}
|
|
76
|
+
function isOnMountedVolume(path, mountedVolumes) {
|
|
77
|
+
const mounted = new Set(mountedVolumes || listMountedVolumes());
|
|
78
|
+
const volume = resolveVolumeRoot(path);
|
|
79
|
+
if (volume === null) return false;
|
|
80
|
+
return mounted.has(volume);
|
|
81
|
+
}
|
|
82
|
+
function directorySize(dir, { timeoutMs } = {}) {
|
|
83
|
+
let out;
|
|
84
|
+
try {
|
|
85
|
+
out = getExecutor().runFile("du", ["-sk", dir], { timeoutMs });
|
|
86
|
+
} catch {
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
if (!out) return 0;
|
|
90
|
+
const kb = parseInt(out.split(/\s+/)[0] ?? "", 10);
|
|
91
|
+
return isNaN(kb) ? 0 : kb * 1024;
|
|
92
|
+
}
|
|
93
|
+
function formatBytes(bytes) {
|
|
94
|
+
if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)}G`;
|
|
95
|
+
if (bytes >= 1024 ** 2) return `${Math.round(bytes / 1024 ** 2)}M`;
|
|
96
|
+
return `${Math.round(bytes / 1024)}K`;
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/process-identity.ts
|
|
100
|
+
function sameProcessRecord(a, b) {
|
|
101
|
+
if (!a || !b) return !a && !b;
|
|
102
|
+
return a.pid === b.pid && a.processToken === b.processToken;
|
|
103
|
+
}
|
|
104
|
+
function captureProcessIdentity(pid) {
|
|
105
|
+
const result = capture(pid);
|
|
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;
|
|
118
|
+
}
|
|
119
|
+
function inspectProcessIdentity(record) {
|
|
120
|
+
if (!record || typeof record.processToken !== "string") return "unknown";
|
|
121
|
+
const decoded = decode(record.processToken);
|
|
122
|
+
if (!decoded.ok || decoded.value.pid !== record.pid) return "unknown";
|
|
123
|
+
const result = check(record.processToken);
|
|
124
|
+
return result.ok ? result.value : "unknown";
|
|
125
|
+
}
|
|
126
|
+
async function waitForProcessExit(record, timeoutMs) {
|
|
127
|
+
const deadline = Date.now() + timeoutMs;
|
|
128
|
+
while (Date.now() <= deadline) {
|
|
129
|
+
const identity = inspectProcessIdentity(record);
|
|
130
|
+
if (identity === "different" || identity === "gone") return true;
|
|
131
|
+
if (Date.now() >= deadline) return false;
|
|
132
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/config.ts
|
|
138
|
+
function getConfigDir() {
|
|
139
|
+
return process.env.STIM_HOME || join(homedir(), ".stim");
|
|
140
|
+
}
|
|
141
|
+
function getConfigPath() {
|
|
142
|
+
return join(getConfigDir(), "config.json");
|
|
143
|
+
}
|
|
144
|
+
function ensureDir() {
|
|
145
|
+
const dir = getConfigDir();
|
|
146
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
147
|
+
}
|
|
148
|
+
const LOCK_DIR_NAME = "config.lock";
|
|
149
|
+
function lockPath() {
|
|
150
|
+
return join(getConfigDir(), LOCK_DIR_NAME);
|
|
151
|
+
}
|
|
152
|
+
function withConfigLock(fn) {
|
|
153
|
+
return withDirLock(lockPath(), fn, { ensureParent: ensureDir });
|
|
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
|
+
}
|
|
164
|
+
function loadConfig() {
|
|
165
|
+
const p = getConfigPath();
|
|
166
|
+
if (!existsSync(p)) return null;
|
|
167
|
+
const raw = readFileSync(p, "utf-8");
|
|
168
|
+
try {
|
|
169
|
+
return JSON.parse(raw);
|
|
170
|
+
} catch (err) {
|
|
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;
|
|
186
|
+
}
|
|
187
|
+
if (!isRecord(value)) throw configCorrupt(`has a ${key} that is not an object: ${JSON.stringify(value)}`);
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
function saveConfig(config) {
|
|
191
|
+
ensureDir();
|
|
192
|
+
const target = getConfigPath();
|
|
193
|
+
const tmp = `${target}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
194
|
+
writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n");
|
|
195
|
+
try {
|
|
196
|
+
renameSync(tmp, target);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
try {
|
|
199
|
+
unlinkSync(tmp);
|
|
200
|
+
} catch {}
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const CONFIG_VERSION = 2;
|
|
205
|
+
function ensureConfig() {
|
|
206
|
+
return withConfigLock(() => {
|
|
207
|
+
const existing = loadConfig();
|
|
208
|
+
if (existing) {
|
|
209
|
+
let changed = false;
|
|
210
|
+
if (adoptContainer(existing, "projects")) changed = true;
|
|
211
|
+
if (adoptContainer(existing, "repos")) changed = true;
|
|
212
|
+
if (existing.version !== CONFIG_VERSION) {
|
|
213
|
+
existing.version = CONFIG_VERSION;
|
|
214
|
+
changed = true;
|
|
215
|
+
}
|
|
216
|
+
if (changed) saveConfig(existing);
|
|
217
|
+
return existing;
|
|
218
|
+
}
|
|
219
|
+
const fresh = {
|
|
220
|
+
version: CONFIG_VERSION,
|
|
221
|
+
projects: {},
|
|
222
|
+
repos: {}
|
|
223
|
+
};
|
|
224
|
+
saveConfig(fresh);
|
|
225
|
+
return fresh;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function getProject(projectPath) {
|
|
229
|
+
return loadConfig()?.projects?.[projectPath] || null;
|
|
230
|
+
}
|
|
231
|
+
function requireAbsoluteProjectPath(projectPath) {
|
|
232
|
+
if (isAbsolute(projectPath)) return;
|
|
233
|
+
throw new Error(`Project key "${projectPath}" is not an absolute path. The registry is keyed by the realpath of a workspace root, so a relative key can never resolve.`);
|
|
234
|
+
}
|
|
235
|
+
function upsertProject(projectPath, fields) {
|
|
236
|
+
requireAbsoluteProjectPath(projectPath);
|
|
237
|
+
return withConfigLock(() => {
|
|
238
|
+
const cfg = ensureConfig();
|
|
239
|
+
const existing = cfg.projects[projectPath] || {
|
|
240
|
+
metroPort: null,
|
|
241
|
+
platforms: {}
|
|
242
|
+
};
|
|
243
|
+
cfg.projects[projectPath] = {
|
|
244
|
+
...existing,
|
|
245
|
+
...fields
|
|
246
|
+
};
|
|
247
|
+
saveConfig(cfg);
|
|
248
|
+
return cfg.projects[projectPath];
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function removeProject(projectPath) {
|
|
252
|
+
withConfigLock(() => {
|
|
253
|
+
const cfg = loadConfig();
|
|
254
|
+
if (!cfg?.projects?.[projectPath]) return;
|
|
255
|
+
delete cfg.projects[projectPath];
|
|
256
|
+
saveConfig(cfg);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
function claimMetroPort(projectPath, port) {
|
|
260
|
+
return withConfigLock(() => {
|
|
261
|
+
const cfg = ensureConfig();
|
|
262
|
+
if (!cfg.projects[projectPath]) throw new Error(`Project not registered: ${projectPath}`);
|
|
263
|
+
for (const [path, proj] of Object.entries(cfg.projects)) if (path !== projectPath && proj?.metroPort === port) return null;
|
|
264
|
+
cfg.projects[projectPath].metroPort = port;
|
|
265
|
+
saveConfig(cfg);
|
|
266
|
+
return port;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
function setDevice(projectPath, platform, deviceFields) {
|
|
270
|
+
requireAbsoluteProjectPath(projectPath);
|
|
271
|
+
withConfigLock(() => {
|
|
272
|
+
const cfg = ensureConfig();
|
|
273
|
+
if (!cfg.projects[projectPath]) throw new Error(`Project not registered: ${projectPath}`);
|
|
274
|
+
cfg.projects[projectPath].platforms = cfg.projects[projectPath].platforms || {};
|
|
275
|
+
cfg.projects[projectPath].platforms[platform] = deviceFields;
|
|
276
|
+
saveConfig(cfg);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
function releaseAndroidConsolePort(projectPath, consolePort) {
|
|
280
|
+
return withConfigLock(() => {
|
|
281
|
+
const cfg = loadConfig();
|
|
282
|
+
const android = cfg?.projects?.[projectPath]?.platforms?.android;
|
|
283
|
+
if (!cfg || !android || android.consolePort !== consolePort) return false;
|
|
284
|
+
delete android.consolePort;
|
|
285
|
+
saveConfig(cfg);
|
|
286
|
+
return true;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
function clearDevice(projectPath, platform) {
|
|
290
|
+
withConfigLock(() => {
|
|
291
|
+
const cfg = loadConfig();
|
|
292
|
+
if (!cfg?.projects?.[projectPath]?.platforms) return;
|
|
293
|
+
delete cfg.projects[projectPath].platforms[platform];
|
|
294
|
+
saveConfig(cfg);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
function setSupervisor(projectPath, { pid, port, startedAt, processToken }) {
|
|
298
|
+
requireAbsoluteProjectPath(projectPath);
|
|
299
|
+
return withConfigLock(() => {
|
|
300
|
+
const cfg = ensureConfig();
|
|
301
|
+
if (!cfg.projects[projectPath]) cfg.projects[projectPath] = {
|
|
302
|
+
metroPort: null,
|
|
303
|
+
platforms: {}
|
|
304
|
+
};
|
|
305
|
+
cfg.projects[projectPath].supervisor = {
|
|
306
|
+
pid,
|
|
307
|
+
port,
|
|
308
|
+
startedAt,
|
|
309
|
+
processToken
|
|
310
|
+
};
|
|
311
|
+
saveConfig(cfg);
|
|
312
|
+
return cfg.projects[projectPath].supervisor;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
function clearSupervisor(projectPath, expected) {
|
|
316
|
+
return withConfigLock(() => {
|
|
317
|
+
const cfg = loadConfig();
|
|
318
|
+
const current = cfg?.projects?.[projectPath]?.supervisor;
|
|
319
|
+
if (!current) return true;
|
|
320
|
+
if (expected !== void 0 && !sameProcessRecord(current, expected)) return false;
|
|
321
|
+
delete cfg.projects[projectPath].supervisor;
|
|
322
|
+
saveConfig(cfg);
|
|
323
|
+
return true;
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
function getConcurrencyLimits({ env = process.env } = {}) {
|
|
327
|
+
const c = loadConfig()?.concurrency || {};
|
|
328
|
+
return {
|
|
329
|
+
maxBuilds: resolveLimit(env.STIM_MAX_BUILDS, c.maxBuilds),
|
|
330
|
+
maxDevices: resolveLimit(env.STIM_MAX_DEVICES, c.maxDevices)
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function resolveLimit(envVal, cfgVal) {
|
|
334
|
+
const raw = envVal !== void 0 && envVal !== null && envVal !== "" ? Number(envVal) : typeof cfgVal === "number" ? cfgVal : NaN;
|
|
335
|
+
if (!Number.isFinite(raw) || raw <= 0) return 0;
|
|
336
|
+
return Math.floor(raw);
|
|
337
|
+
}
|
|
338
|
+
function getProjectSettings(projectPath) {
|
|
339
|
+
return getProject(projectPath)?.settings || {};
|
|
340
|
+
}
|
|
341
|
+
function getRepoSettings(gitCommonDir) {
|
|
342
|
+
return loadConfig()?.repos?.[gitCommonDir]?.settings || {};
|
|
343
|
+
}
|
|
344
|
+
function isPathPrefix(prefix, path) {
|
|
345
|
+
if (prefix === path) return true;
|
|
346
|
+
const withSlash = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
347
|
+
return path.startsWith(withSlash);
|
|
348
|
+
}
|
|
349
|
+
function findEnclosingWorktreeRoot(projectPath) {
|
|
350
|
+
const cfg = loadConfig();
|
|
351
|
+
if (!cfg?.projects) return null;
|
|
352
|
+
let best = null;
|
|
353
|
+
for (const [path, proj] of Object.entries(cfg.projects)) {
|
|
354
|
+
if (!proj?.worktreeRoot) continue;
|
|
355
|
+
if (!isPathPrefix(path, projectPath)) continue;
|
|
356
|
+
if (!best || path.length > best.length) best = path;
|
|
357
|
+
}
|
|
358
|
+
return best;
|
|
359
|
+
}
|
|
360
|
+
function allMetroPorts() {
|
|
361
|
+
const cfg = loadConfig();
|
|
362
|
+
if (!cfg?.projects) return [];
|
|
363
|
+
return Object.values(cfg.projects).map((p) => p.metroPort).filter((p) => typeof p === "number");
|
|
364
|
+
}
|
|
365
|
+
function allConsolePortsAndSerials({ isMounted = isOnMountedVolume } = {}) {
|
|
366
|
+
const cfg = loadConfig();
|
|
367
|
+
const result = {
|
|
368
|
+
androidConsolePorts: [],
|
|
369
|
+
androidPhysicalSerials: []
|
|
370
|
+
};
|
|
371
|
+
if (!cfg) return result;
|
|
372
|
+
for (const [path, proj] of Object.entries(cfg.projects || {})) {
|
|
373
|
+
if (!existsSync(path) && isMounted(path)) continue;
|
|
374
|
+
const android = proj.platforms?.android;
|
|
375
|
+
if (typeof android?.consolePort === "number") result.androidConsolePorts.push(android.consolePort);
|
|
376
|
+
if (android?.serial && !android.avdName) result.androidPhysicalSerials.push(android.serial);
|
|
377
|
+
}
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
380
|
+
//#endregion
|
|
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 };
|