stim 1.0.0 → 1.2.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 (66) hide show
  1. package/README.md +29 -10
  2. package/dist/android-BW8i_YxU.mjs +204 -0
  3. package/dist/android-Bcmev2s2.mjs +2334 -0
  4. package/dist/android-K63_0UfF.mjs +727 -0
  5. package/dist/android-cas-D6HPZ6UQ.mjs +1078 -0
  6. package/dist/android-cas-compiler.mjs +2 -2
  7. package/dist/app-install-NFxUahnH.mjs +1149 -0
  8. package/dist/build-lock-BsVoLDaq.mjs +645 -0
  9. package/dist/build-slots-D0b6sJyo.mjs +152 -0
  10. package/dist/{cache-manifest-4tH64LQ9.mjs → cache-manifest-Cu4_Znw9.mjs} +2 -2
  11. package/dist/cache-manifest.mjs +1 -1
  12. package/dist/cli.mjs +24 -27166
  13. package/dist/collector-run.d.mts +1 -0
  14. package/dist/collector-run.mjs +7 -5
  15. package/dist/command-output-8wHMEde0.mjs +289 -0
  16. package/dist/{config-7kmtuhO2.mjs → config-CMtIaTk9.mjs} +44 -13
  17. package/dist/deps-BLBsTNDS.mjs +435 -0
  18. package/dist/dev-client-RpvXxCXV.mjs +620 -0
  19. package/dist/device-CoCPmazS.mjs +250 -0
  20. package/dist/device-lease-Dpik41RY.mjs +335 -0
  21. package/dist/device-pool-B5pEVa3t.mjs +408 -0
  22. package/dist/device-remote-E81KAdXf.mjs +1209 -0
  23. package/dist/doctor-CqPeMsSm.mjs +1130 -0
  24. package/dist/doctor-D_CX7ib5.mjs +530 -0
  25. package/dist/error-diagnostics-Dw8Gn3yw.mjs +636 -0
  26. package/dist/{exec-bsN9MJXb.mjs → exec-CyylIdq9.mjs} +4 -2
  27. package/dist/gc-B7KkDinF.mjs +1665 -0
  28. package/dist/guide-BS2Xc24o.mjs +4503 -0
  29. package/dist/ios-B7KLgIp5.mjs +2733 -0
  30. package/dist/ios-D1h1Di39.mjs +451 -0
  31. package/dist/ios-device-D3pzTKQE.mjs +81 -0
  32. package/dist/ios-device-xNt0Lg6v.mjs +413 -0
  33. package/dist/logs-DcX9AvRF.mjs +176 -0
  34. package/dist/logs-query-oqn47Ffx.mjs +273 -0
  35. package/dist/metro-COslRK_F.mjs +184 -0
  36. package/dist/metro-store-CDDZHtPl.mjs +92 -0
  37. package/dist/ownership-CGPart6T.mjs +60 -0
  38. package/dist/ownership-CYOyqxUo.mjs +475 -0
  39. package/dist/ownership-claim-7zRxxTfZ.mjs +569 -0
  40. package/dist/{project-Dit1CckW.mjs → project-BBTVABUz.mjs} +12 -3
  41. package/dist/reclaim-C5Jmuahx.mjs +348 -0
  42. package/dist/reload-Z8nKifAk.mjs +332 -0
  43. package/dist/remote-cache-DDmJ1Roq.mjs +994 -0
  44. package/dist/{server-bare-BmTVVmBB.mjs → server-bare-DEk37Mz2.mjs} +8 -2
  45. package/dist/{server-expo-CHJe1r6j.mjs → server-expo-C3ktAwUG.mjs} +6 -5
  46. package/dist/server-expo-Drd7ksHu.mjs +2 -0
  47. package/dist/settings-DwU1_U_q.mjs +708 -0
  48. package/dist/spawn-entry-nCE6m770.mjs +14 -0
  49. package/dist/start-BrY4EuIg.mjs +822 -0
  50. package/dist/{state-2nush3MK.mjs → state-CZ-ER-8d.mjs} +21 -22
  51. package/dist/stats-BCWEVLrM.mjs +307 -0
  52. package/dist/stats-BsF15Bti.mjs +62 -0
  53. package/dist/status-DFI1Hfpg.mjs +393 -0
  54. package/dist/stop-B7DR4awK.mjs +3 -0
  55. package/dist/stop-Btg770vV.mjs +591 -0
  56. package/dist/supervisor-run.d.mts +0 -1
  57. package/dist/supervisor-run.mjs +4 -4
  58. package/dist/workspace-process-lock-BKfdys1q.mjs +47 -0
  59. package/dist/worktree-BPTbI-LN.mjs +1322 -0
  60. package/dist/worktree-C79xx_kM.mjs +543 -0
  61. package/dist/xcode-DeKCribl.mjs +1832 -0
  62. package/package.json +5 -5
  63. package/shim/bundle-response.cjs +5 -0
  64. package/dist/android-jc8MnUyL.mjs +0 -514
  65. package/dist/metro-store-COLl1pOk.mjs +0 -1249
  66. package/dist/server-expo-BZJm--wA.mjs +0 -2
@@ -0,0 +1,636 @@
1
+ import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
+ import { f as getProject, k as withDirLock } from "./config-CMtIaTk9.mjs";
3
+ import { f as readWorkspaceLaunches, p as readWorkspaceState } from "./state-CZ-ER-8d.mjs";
4
+ import { n as deviceConsoleLevel } from "./ios-device-D3pzTKQE.mjs";
5
+ import { i as resolveProjectMetro } from "./metro-COslRK_F.mjs";
6
+ import { n as parseLogcatLine, t as androidClockOffset } from "./android-BW8i_YxU.mjs";
7
+ import { _ as launchErrorPreview } from "./command-output-8wHMEde0.mjs";
8
+ import { a as parseLease, n as fileLeaseIo, t as deviceLeasePath } from "./device-lease-Dpik41RY.mjs";
9
+ import { n as androidHome } from "./android-K63_0UfF.mjs";
10
+ import { c as readLogRecords, t as attachExpoErrorContext } from "./logs-query-oqn47Ffx.mjs";
11
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
12
+ import { createHash, randomUUID } from "node:crypto";
13
+ import { basename, dirname, join, resolve, sep } from "node:path";
14
+ import { homedir } from "node:os";
15
+ //#region src/diagnostic-store.ts
16
+ /** Publishes an immutable diagnostic snapshot atomically; existing evidence wins. */
17
+ function writeDiagnosticOnce(path, content) {
18
+ mkdirSync(dirname(path), { recursive: true });
19
+ withDirLock(`${path}.lock`, () => {
20
+ if (existsSync(path)) return;
21
+ const temporary = `${path}.${randomUUID()}.tmp`;
22
+ try {
23
+ writeFileSync(temporary, content, { flag: "wx" });
24
+ renameSync(temporary, path);
25
+ } finally {
26
+ try {
27
+ unlinkSync(temporary);
28
+ } catch {}
29
+ }
30
+ }, {
31
+ waitMs: 100,
32
+ pollMs: 10
33
+ });
34
+ }
35
+ //#endregion
36
+ //#region src/native-crash.ts
37
+ const IOS_CRASH_REPORT_RETRY = "iOS crash reports can take about a minute or longer to appear. Run `stim logs --errors` again for the native stack.";
38
+ /** Accepts only a report for this app, simulator and launch window. */
39
+ function parseIosCrash(text, target) {
40
+ try {
41
+ const newline = text.indexOf("\n");
42
+ const report = JSON.parse(text.slice(newline + 1));
43
+ const ts = Date.parse(report.captureTime ?? "");
44
+ const simulator = `com.apple.CoreSimulator.SimDevice.${target.deviceId}`;
45
+ if (report.bundleInfo?.CFBundleIdentifier !== target.appId || report.coalitionName !== simulator || !Number.isFinite(ts) || ts < target.since || ts > (target.until ?? Date.now() + 1e3) || !report.incident) return null;
46
+ const stack = ((report.threads?.find((entry) => entry.triggered) ?? report.threads?.[report.faultingThread ?? -1])?.frames ?? []).map((frame) => {
47
+ const image = report.usedImages?.[frame.imageIndex ?? -1];
48
+ return {
49
+ file: frame.sourceFile ?? image?.name ?? "<unknown image>",
50
+ line: frame.sourceLine,
51
+ fn: frame.symbol ? `${frame.symbol}${frame.symbolLocation ? ` + ${frame.symbolLocation}` : ""}` : `+0x${Number(frame.imageOffset ?? 0).toString(16)} [unsymbolicated]`
52
+ };
53
+ });
54
+ const details = [
55
+ report.exception?.type,
56
+ report.exception?.signal,
57
+ report.termination?.namespace,
58
+ report.termination?.indicator
59
+ ].filter(Boolean).join(" / ");
60
+ const reasons = [
61
+ ...report.termination?.reasons ?? [],
62
+ ...report.termination?.details ?? [],
63
+ ...Object.values(report.asi ?? {}).flat()
64
+ ].filter((line) => typeof line === "string");
65
+ return {
66
+ report,
67
+ record: {
68
+ ts,
69
+ src: "device",
70
+ platform: "ios",
71
+ level: "fatal",
72
+ event: "native_crash",
73
+ incident: report.incident,
74
+ appId: target.appId,
75
+ deviceId: target.deviceId,
76
+ pid: report.pid,
77
+ msg: `Native crash: ${details || "process terminated"}${reasons.length ? `\n${reasons.join("\n")}` : ""}`,
78
+ stack,
79
+ rawReport: text
80
+ }
81
+ };
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+ /** Extracts crash-buffer groups only when their own process header names the target app. */
87
+ function parseAndroidCrashes(text, target, clockOffsetMs) {
88
+ const entries = text.split("\n").map((line) => parseLogcatLine(line, { clockOffsetMs })).filter((record) => record !== null && Number(record.ts) >= target.since && Number(record.ts) <= (target.until ?? Infinity));
89
+ const groups = [];
90
+ for (const entry of entries) {
91
+ const last = groups.at(-1);
92
+ if (!last || entry.proc !== last.at(-1)?.proc || /^\s*(?:FATAL EXCEPTION:|\*\*\* \*\*\*)/.test(entry.msg ?? "")) groups.push([entry]);
93
+ else last.push(entry);
94
+ }
95
+ return groups.flatMap((group) => {
96
+ const messages = group.map((record) => record.msg ?? "");
97
+ const process = messages.map((line) => /^\s*(?:Process: (.+?), PID: \d+|pid: \d+, tid: \d+, name: .*?>>> (.*?) <<<)/.exec(line)).find(Boolean);
98
+ const appId = process?.[1] ?? process?.[2];
99
+ if (appId !== target.appId) return [];
100
+ const msg = messages.join("\n");
101
+ if (!/FATAL EXCEPTION:|signal \d+ \(/.test(msg)) return [];
102
+ return [{
103
+ ts: group[0].ts,
104
+ deviceTs: group[0].deviceTs,
105
+ pid: Number(/(?:PID: |pid: )(\d+)/.exec(msg)?.[1]) || void 0,
106
+ src: "device",
107
+ platform: "android",
108
+ level: "fatal",
109
+ event: "native_crash",
110
+ appId,
111
+ deviceId: target.deviceId,
112
+ msg: `Native crash (Android)\n${msg}`,
113
+ rawReport: msg
114
+ }];
115
+ });
116
+ }
117
+ function symbolicateIos(record, report, appPath, deadline) {
118
+ if (!appPath || !existsSync(appPath) || !Array.isArray(record.stack)) return record;
119
+ const thread = report.threads?.find((entry) => entry.triggered) ?? report.threads?.[report.faultingThread ?? -1];
120
+ const stack = [...record.stack];
121
+ const verified = /* @__PURE__ */ new Map();
122
+ for (const [index, frame] of (thread?.frames ?? []).entries()) {
123
+ if (Date.now() >= deadline) break;
124
+ if (frame.sourceFile && frame.sourceLine) continue;
125
+ const imageIndex = frame.imageIndex ?? -1;
126
+ const image = report.usedImages?.[imageIndex];
127
+ if (!image?.path || !image.uuid || !image.arch || !Number.isSafeInteger(image.base) || !Number.isSafeInteger(frame.imageOffset)) continue;
128
+ if (!verified.has(imageIndex)) {
129
+ const marker = `${basename(appPath)}/`;
130
+ const at = image.path.lastIndexOf(marker);
131
+ const relative = at < 0 ? null : image.path.slice(at + marker.length);
132
+ const binary = relative ? resolve(appPath, relative) : null;
133
+ let matched = null;
134
+ if (binary && binary.startsWith(resolve(appPath) + sep) && existsSync(binary)) {
135
+ if (getExecutor().runFileQuiet("xcrun", [
136
+ "dwarfdump",
137
+ "--uuid",
138
+ binary
139
+ ], { timeoutMs: 2e3 })?.toLowerCase().includes(`uuid: ${image.uuid.toLowerCase()} (${image.arch.toLowerCase()})`)) matched = binary;
140
+ }
141
+ verified.set(imageIndex, matched);
142
+ }
143
+ const binary = verified.get(imageIndex);
144
+ if (!binary || Date.now() >= deadline) continue;
145
+ const address = image.base + frame.imageOffset;
146
+ if (!Number.isSafeInteger(address)) continue;
147
+ const output = getExecutor().runFileQuiet("xcrun", [
148
+ "atos",
149
+ "-o",
150
+ binary,
151
+ "-arch",
152
+ image.arch,
153
+ "-l",
154
+ `0x${image.base.toString(16)}`,
155
+ `0x${address.toString(16)}`
156
+ ], { timeoutMs: 2e3 });
157
+ if (output && !/^0x[0-9a-f]+$/i.test(output) && !/error:|cannot|unable/i.test(output)) stack[index] = {
158
+ file: image.name ?? basename(binary),
159
+ fn: output
160
+ };
161
+ }
162
+ return {
163
+ ...record,
164
+ stack
165
+ };
166
+ }
167
+ function symbolicateAndroid(record, root, budget) {
168
+ if (!root || typeof record.msg !== "string" || !/#\d+ pc /.test(record.msg)) return record;
169
+ const frames = [...record.msg.matchAll(/^\s*#\d+ pc ([0-9a-f]+)\s+(\S+)(.*)$/gim)];
170
+ const stack = frames.map(([, address, file, suffix]) => ({
171
+ file: basename(file),
172
+ fn: `${suffix?.trim() || `0x${address}`} [captured native frame]`
173
+ }));
174
+ let deadline = Math.min(Date.now() + 2e3, budget - 1e3);
175
+ const ndks = join(androidHome(), "ndk");
176
+ let tools;
177
+ try {
178
+ for (const version of readdirSync(ndks).toSorted().toReversed()) {
179
+ const prebuilt = join(ndks, version, "toolchains/llvm/prebuilt");
180
+ for (const host of readdirSync(prebuilt)) {
181
+ const bin = join(prebuilt, host, "bin");
182
+ if (existsSync(join(bin, "llvm-symbolizer"))) {
183
+ tools = bin;
184
+ break;
185
+ }
186
+ }
187
+ if (tools) break;
188
+ }
189
+ } catch {}
190
+ const files = [];
191
+ const visit = (directory, depth) => {
192
+ if (depth > 10 || files.length > 500 || Date.now() >= deadline) return;
193
+ try {
194
+ for (const entry of readdirSync(directory, { withFileTypes: true })) if (entry.isDirectory()) visit(join(directory, entry.name), depth + 1);
195
+ else if (entry.isFile() && entry.name.endsWith(".so")) files.push(join(directory, entry.name));
196
+ } catch {}
197
+ };
198
+ if (tools) {
199
+ for (const directory of [
200
+ "android/app/src/main/jniLibs",
201
+ "android/app/build/intermediates/cxx",
202
+ "android/app/.cxx"
203
+ ]) visit(join(root, directory), 0);
204
+ deadline = budget;
205
+ const byId = /* @__PURE__ */ new Map();
206
+ for (const [index, [, address, file, suffix]] of frames.entries()) {
207
+ if (Date.now() >= deadline) break;
208
+ const id = /\(BuildId: ([0-9a-f]+)\)/i.exec(suffix ?? "")?.[1]?.toLowerCase();
209
+ if (!id) continue;
210
+ if (!byId.has(id)) {
211
+ const binary = files.filter((path) => file.endsWith(".apk") || basename(path) === basename(file)).find((path) => {
212
+ if (Date.now() >= deadline) return false;
213
+ const notes = getExecutor().runFileQuiet(join(tools, "llvm-readelf"), ["--notes", path], { timeoutMs: 1e3 });
214
+ return /Build ID: ([0-9a-f]+)/i.exec(notes ?? "")?.[1]?.toLowerCase() === id;
215
+ });
216
+ byId.set(id, binary ?? null);
217
+ }
218
+ const binary = byId.get(id);
219
+ if (!binary) continue;
220
+ const [fn, location] = getExecutor().runFileQuiet(join(tools, "llvm-symbolizer"), [
221
+ "--no-inlines",
222
+ "--demangle",
223
+ `--obj=${binary}`,
224
+ `0x${address}`
225
+ ], { timeoutMs: 1e3 })?.split("\n") ?? [];
226
+ const match = /^(.*):(\d+):(\d+)$/.exec(location ?? "");
227
+ if (fn && fn !== "??" && match && match[1] !== "??" && Number(match[2]) > 0) stack[index] = {
228
+ fn,
229
+ file: match[1],
230
+ line: Number(match[2]),
231
+ column: Number(match[3])
232
+ };
233
+ }
234
+ }
235
+ const summary = record.msg.split("\n").filter((line) => /Native crash|signal \d+|Abort message:|^pid:/.test(line)).join("\n");
236
+ return {
237
+ ...record,
238
+ msg: summary,
239
+ stack,
240
+ symbolicationNote: stack.some((frame) => !frame.line) ? "Native symbols are partial: unresolved frames retain crash-buffer symbols/addresses. Full captured report: stim logs --source device --json." : void 0
241
+ };
242
+ }
243
+ function simulatorConsolePaths(target, prepare = false, remote = false) {
244
+ if (remote) return void 0;
245
+ const container = getExecutor().runFileQuiet("xcrun", [
246
+ "simctl",
247
+ "get_app_container",
248
+ target.deviceId,
249
+ target.appId,
250
+ "data"
251
+ ], { timeoutMs: 2e3 });
252
+ if (!container?.startsWith("/")) return void 0;
253
+ const directory = join(container, "Library/Caches/Stim");
254
+ const key = createHash("sha256").update(JSON.stringify([
255
+ target.deviceId,
256
+ target.appId,
257
+ target.since
258
+ ])).digest("hex");
259
+ const paths = {
260
+ stdout: join(directory, `launch-${key}.stdout`),
261
+ stderr: join(directory, `launch-${key}.stderr`)
262
+ };
263
+ if (prepare) try {
264
+ mkdirSync(directory, { recursive: true });
265
+ for (const path of Object.values(paths)) writeFileSync(path, "", { flag: "wx" });
266
+ } catch {
267
+ return;
268
+ }
269
+ return paths;
270
+ }
271
+ function simulatorFatalConsole(target) {
272
+ const paths = simulatorConsolePaths(target);
273
+ return Object.values(paths ?? {}).flatMap((path) => {
274
+ try {
275
+ if (statSync(path).size > 8388608) return [];
276
+ if (target.until !== void 0 && statSync(path).mtimeMs > target.until) return [];
277
+ const text = readFileSync(path, "utf8");
278
+ const fatal = text.split("\n").filter((line) => deviceConsoleLevel(line) === "fatal");
279
+ if (!fatal.length) return [];
280
+ return [{
281
+ ts: Math.max(target.since + 1, statSync(path).mtimeMs),
282
+ src: "device",
283
+ platform: "ios",
284
+ level: "fatal",
285
+ event: "native_crash",
286
+ appId: target.appId,
287
+ deviceId: target.deviceId,
288
+ incident: basename(path),
289
+ pid: Number(/\[(\d+):\d+\]/.exec(text)?.[1]) || void 0,
290
+ msg: fatal.join("\n"),
291
+ rawReport: text,
292
+ symbolicationNote: `Captured app stderr; the full operating system crash report is not available yet. ${IOS_CRASH_REPORT_RETRY}`
293
+ }];
294
+ } catch {
295
+ return [];
296
+ }
297
+ });
298
+ }
299
+ /** Reads platform crash evidence without starting, stopping or modifying a device. */
300
+ function captureNativeCrashes(target, logsDir) {
301
+ let records = [];
302
+ try {
303
+ if (target.platform === "ios" && !target.physical && process.platform === "darwin") {
304
+ const deadline = Date.now() + 4e3;
305
+ records = simulatorFatalConsole(target);
306
+ const directory = join(homedir(), "Library/Logs/DiagnosticReports");
307
+ const nativeReports = readdirSync(directory).filter((name) => name.endsWith(".ips")).map((name) => ({
308
+ path: join(directory, name),
309
+ time: statSync(join(directory, name)).mtimeMs
310
+ })).filter(({ time }) => time >= target.since).toSorted((a, b) => b.time - a.time).slice(0, 50).flatMap(({ path }) => {
311
+ if (Date.now() >= deadline) return [];
312
+ if (statSync(path).size > 8388608) return [];
313
+ const parsed = parseIosCrash(readFileSync(path, "utf8"), target);
314
+ return parsed ? [symbolicateIos(parsed.record, parsed.report, target.appPath, deadline)] : [];
315
+ });
316
+ if (nativeReports.length) records = nativeReports;
317
+ } else if (target.platform === "android") {
318
+ const offset = androidClockOffset(target.deviceId);
319
+ if (offset === null) return [];
320
+ const text = getExecutor().runFileQuiet("adb", [
321
+ "-s",
322
+ target.deviceId,
323
+ "logcat",
324
+ "-b",
325
+ "crash",
326
+ "-d",
327
+ "-v",
328
+ "time,epoch",
329
+ "-t",
330
+ "2000"
331
+ ], { timeoutMs: 3e3 });
332
+ const deadline = Date.now() + 4e3;
333
+ if (text) records = parseAndroidCrashes(text, target, offset).map((record) => symbolicateAndroid(record, target.root, deadline));
334
+ }
335
+ } catch {}
336
+ for (const record of records) try {
337
+ const key = createHash("sha256").update(JSON.stringify([
338
+ record.deviceId,
339
+ record.incident ?? record.deviceTs,
340
+ record.rawReport
341
+ ])).digest("hex");
342
+ writeDiagnosticOnce(join(logsDir, `native-crash-${key}.ndjson`), `${JSON.stringify(record)}\n`);
343
+ } catch {}
344
+ return records;
345
+ }
346
+ function printNativeCrashReport(target, logsDir, emit, remote) {
347
+ if (remote) return;
348
+ for (const line of launchErrorPreview(captureNativeCrashes(target, logsDir), target.root)) emit(line);
349
+ }
350
+ function captureWorkspaceCrashes(root, logsDir) {
351
+ const attempts = readLogRecords(logsDir).filter((record) => record.event === "launch_attempt");
352
+ const launches = readWorkspaceLaunches(root);
353
+ for (const platform of ["ios", "android"]) try {
354
+ const attempt = attempts.findLast((record) => record.platform === platform);
355
+ if (!attempt || attempt.remote) continue;
356
+ const { deviceId, appId } = attempt;
357
+ const since = attempt.ts;
358
+ const until = Date.now();
359
+ if (typeof deviceId !== "string" || typeof appId !== "string" || typeof since !== "number" || !Number.isFinite(since)) continue;
360
+ if (attempt.physical) {
361
+ const holder = fileLeaseIo.readHolder(root)[platform];
362
+ const lease = parseLease(fileLeaseIo.readLease(deviceLeasePath(platform, deviceId)));
363
+ if (holder?.id !== deviceId || !lease || lease.platform !== platform || lease.id !== deviceId || lease.holder !== root || lease.token !== holder.token || Date.parse(lease.expiresAt) <= until || !lease.grantedAt || !Number.isFinite(Date.parse(lease.grantedAt)) || Date.parse(lease.grantedAt) > since) continue;
364
+ } else {
365
+ const launch = launches[platform];
366
+ if (!launch || launch.deviceId !== deviceId || launch.appId !== appId || Date.parse(launch.launchedAt) !== since) continue;
367
+ const configured = getProject(root)?.platforms?.[platform];
368
+ if (!configured?.owned) continue;
369
+ if (platform === "ios" && configured.deviceUdid !== deviceId) continue;
370
+ if (platform === "android") {
371
+ if (!configured.avdName) continue;
372
+ if (getExecutor().runFileQuiet("adb", [
373
+ "-s",
374
+ deviceId,
375
+ "emu",
376
+ "avd",
377
+ "name"
378
+ ], { timeoutMs: 2e3 })?.split("\n")[0]?.trim() !== configured.avdName) continue;
379
+ }
380
+ }
381
+ captureNativeCrashes({
382
+ root,
383
+ platform,
384
+ deviceId,
385
+ appId,
386
+ since: Number(since),
387
+ until,
388
+ physical: attempt?.physical === true,
389
+ appPath: attempt?.appPath ?? readWorkspaceState(root)?.lastBuild?.appPath
390
+ }, logsDir);
391
+ } catch {}
392
+ }
393
+ //#endregion
394
+ //#region src/error-symbolication.ts
395
+ const LOCATION = /https?:\/\/[^\s)'"\\]+?:\d+:\d+/g;
396
+ function frameFromLocation(location) {
397
+ const match = /^(https?:\/\/.+):(\d+):(\d+)$/.exec(location);
398
+ if (!match) return null;
399
+ const file = match[1];
400
+ if (!/\.bundle(?:[/?]|$)/.test(file)) return null;
401
+ const lineNumber = Number(match[2]);
402
+ const column = Number(match[3]) - 1;
403
+ if (!Number.isSafeInteger(lineNumber) || lineNumber < 1 || !Number.isSafeInteger(column) || column < 0) return null;
404
+ return {
405
+ file,
406
+ lineNumber,
407
+ column,
408
+ methodName: "<unknown>"
409
+ };
410
+ }
411
+ function bundleLocations(record) {
412
+ const found = [...[
413
+ record.msg,
414
+ record.stack,
415
+ record.componentStack
416
+ ].filter((value) => typeof value === "string").join("\n").matchAll(LOCATION)].map(([location]) => location);
417
+ for (const value of [record.stack, record.componentStack]) {
418
+ if (!Array.isArray(value)) continue;
419
+ for (const frame of value) if (frame && typeof frame.file === "string" && typeof frame.line === "number" && typeof frame.column === "number") found.push(`${frame.file}:${frame.line}:${frame.column + 1}`);
420
+ }
421
+ return [...new Set(found)].filter((location) => frameFromLocation(location) !== null);
422
+ }
423
+ /** Resolves captured bundle coordinates against the already-verified workspace Metro. */
424
+ async function symbolicateErrors(records, { port, timeoutMs = 2e3, request = fetch }) {
425
+ const candidates = [...new Set(records.flatMap(bundleLocations))].slice(0, 200);
426
+ if (!candidates.length) return [...records];
427
+ const fallback = (reason) => records.map((record) => bundleLocations(record).length ? {
428
+ ...record,
429
+ symbolicationNote: `JS symbolication unavailable (${reason}); captured coordinates retained.`
430
+ } : record);
431
+ if (!Number.isInteger(port) || !port || port < 1 || port > 65535) return fallback("no verified Metro");
432
+ try {
433
+ const response = await request(`http://127.0.0.1:${port}/symbolicate`, {
434
+ method: "POST",
435
+ headers: { "Content-Type": "application/json" },
436
+ body: JSON.stringify({ stack: candidates.map(frameFromLocation) }),
437
+ signal: AbortSignal.timeout(timeoutMs),
438
+ redirect: "error"
439
+ });
440
+ if (!response.ok) return fallback(`HTTP ${response.status}`);
441
+ const body = await response.json();
442
+ if (!Array.isArray(body.stack) || body.stack.length !== candidates.length) return fallback("invalid Metro response");
443
+ const resolved = /* @__PURE__ */ new Map();
444
+ for (let index = 0; index < candidates.length; index++) {
445
+ const frame = body.stack[index];
446
+ if (!frame || typeof frame !== "object") continue;
447
+ const { file, lineNumber, column } = frame;
448
+ if (typeof file !== "string" || /[\r\n]/.test(file) || /^https?:/.test(file) || !Number.isSafeInteger(lineNumber) || lineNumber < 1 || !Number.isSafeInteger(column) || column < 0) continue;
449
+ resolved.set(candidates[index], `${file}:${lineNumber}:${column + 1}`);
450
+ }
451
+ return records.map((record) => {
452
+ const matches = bundleLocations(record);
453
+ if (!matches.length) return record;
454
+ const replace = (value) => {
455
+ if (typeof value === "string") return value.replace(LOCATION, (location) => resolved.get(location) ?? location);
456
+ if (!Array.isArray(value)) return value;
457
+ return value.map((frame) => {
458
+ if (!frame || typeof frame !== "object") return frame;
459
+ const mapped = resolved.get(`${frame.file}:${frame.line}:${frame.column + 1}`);
460
+ const match = mapped && /^(.*):(\d+):(\d+)$/.exec(mapped);
461
+ return match ? {
462
+ ...frame,
463
+ file: match[1],
464
+ line: Number(match[2]),
465
+ column: Number(match[3]) - 1
466
+ } : frame;
467
+ });
468
+ };
469
+ return {
470
+ ...record,
471
+ ...record.msg !== void 0 ? { msg: replace(record.msg) } : {},
472
+ ...record.stack !== void 0 ? { stack: replace(record.stack) } : {},
473
+ ...record.componentStack !== void 0 ? { componentStack: replace(record.componentStack) } : {},
474
+ ...matches.some((location) => !resolved.has(location)) ? { symbolicationNote: "Some JS frames remain unsymbolicated; captured coordinates retained." } : {}
475
+ };
476
+ });
477
+ } catch {
478
+ return fallback("request failed or timed out");
479
+ }
480
+ }
481
+ //#endregion
482
+ //#region src/error-diagnostics.ts
483
+ function key(record) {
484
+ return createHash("sha256").update(JSON.stringify(record)).digest("hex");
485
+ }
486
+ function errorTitle(record) {
487
+ const line = String(record.msg ?? "").split("\n")[0].trim().replace(/^ERROR\s+/, "");
488
+ return /^(?:\{\s*)?\[?((?:\w*Error|\w*Exception): .+?)\]?$/.exec(line)?.[1] ?? null;
489
+ }
490
+ function identity(record) {
491
+ return JSON.stringify([
492
+ record.src,
493
+ record.ts,
494
+ record.proc,
495
+ record.msg
496
+ ]);
497
+ }
498
+ function attachDeviceErrorContext(timeline, selected) {
499
+ const consumed = /* @__PURE__ */ new Set();
500
+ return selected.map((record) => {
501
+ if (record.src !== "device" || !record.proc) return record;
502
+ if (!errorTitle(record) && !/^\s+at\s+\S/.test(record.msg ?? "")) return record;
503
+ const index = timeline.findIndex((entry) => identity(entry) === identity(record));
504
+ if (index < 0) return record;
505
+ const context = [];
506
+ for (const next of timeline.slice(index + 1)) {
507
+ if (Number(next.ts) - Number(record.ts) > 1e3 || context.length >= 200) break;
508
+ if (next.src !== "device" || next.proc !== record.proc || next.platform !== record.platform) continue;
509
+ if (!/^\s*(?:(?:componentStack|stack):\s*['"]|isComponentError:|[{}],?$|at\s+\S)/.test(next.msg ?? "")) break;
510
+ context.push(String(next.msg));
511
+ consumed.add(identity(next));
512
+ }
513
+ return context.length ? {
514
+ ...record,
515
+ msg: [record.msg, ...context].join("\n")
516
+ } : record;
517
+ }).filter((_record, index) => !consumed.has(identity(selected[index])));
518
+ }
519
+ /** Collapses only cross-source copies with the same error title and a shared stack location. */
520
+ function mergeErrorCopies(records, root) {
521
+ const result = [];
522
+ for (const record of records) {
523
+ if (record.event === "native_crash" && record.pid) {
524
+ const duplicate = result.findIndex((other) => other.event === "native_crash" && other.pid === record.pid && other.deviceId === record.deviceId && other.appId === record.appId && Math.abs(Number(other.ts) - Number(record.ts)) < 5e3);
525
+ if (duplicate >= 0) {
526
+ const prior = result[duplicate];
527
+ const full = Array.isArray(record.stack) ? record : prior;
528
+ const consoleRecord = full === record ? prior : record;
529
+ result[duplicate] = {
530
+ ...full,
531
+ msg: full.msg === consoleRecord.msg ? full.msg : `${consoleRecord.msg}\n${full.msg}`
532
+ };
533
+ continue;
534
+ }
535
+ }
536
+ const title = errorTitle(record);
537
+ const locations = new Set([...JSON.stringify([
538
+ record.msg,
539
+ record.stack,
540
+ record.componentStack
541
+ ]).replaceAll(root ? `${root}/` : "\0", "").matchAll(/[^\s()"\\]+:\d+:\d+/g)].map(([location]) => location));
542
+ const existing = title && locations.size ? result.findIndex((other) => !(Array.isArray(other.mirroredSources) ? other.mirroredSources : [other.src]).includes(record.src) && (!other.platform || !record.platform || other.platform === record.platform) && errorTitle(other) === title && typeof other.ts === "number" && typeof record.ts === "number" && Math.abs(other.ts - record.ts) < 1e3 && [...JSON.stringify([
543
+ other.msg,
544
+ other.stack,
545
+ other.componentStack
546
+ ]).replaceAll(root ? `${root}/` : "\0", "").matchAll(/[^\s()"\\]+:\d+:\d+/g)].some(([location]) => locations.has(location))) : -1;
547
+ if (existing < 0) result.push(record);
548
+ else {
549
+ const prior = result[existing];
550
+ const richer = JSON.stringify(record).length > JSON.stringify(prior).length ? record : prior;
551
+ const other = richer === record ? prior : record;
552
+ const sources = [.../* @__PURE__ */ new Set([...Array.isArray(prior.mirroredSources) ? prior.mirroredSources : [prior.src], record.src])];
553
+ result[existing] = {
554
+ ...richer,
555
+ src: prior.src,
556
+ ts: prior.ts,
557
+ platform: prior.platform ?? record.platform,
558
+ ...richer.stack || other.stack ? { stack: richer.stack ?? other.stack } : {},
559
+ ...richer.componentStack || other.componentStack ? { componentStack: richer.componentStack ?? other.componentStack } : {},
560
+ msg: /\bCall Stack\b/.test(String(other.msg)) && /componentStack/.test(String(richer.msg)) ? `${other.msg}\n${String(richer.msg).replace(/^[^\n]*\n/, "")}` : richer.msg,
561
+ mirroredSources: sources,
562
+ symbolicationNote: [richer.symbolicationNote, `Same error captured by ${sources.join(" + ")}; raw copies retained in logs --json.`].filter(Boolean).join(" ")
563
+ };
564
+ }
565
+ }
566
+ return result;
567
+ }
568
+ /** Keeps symbolicated launch evidence stable when Metro later rebuilds or stops. */
569
+ async function errorDiagnostics(input, { root, logsDir, port, allowRequest = false }) {
570
+ const timeline = readLogRecords(logsDir);
571
+ const records = attachDeviceErrorContext(timeline, attachExpoErrorContext(timeline, input.map((record) => ({
572
+ ...record,
573
+ src: typeof record.src === "string" ? record.src : void 0,
574
+ msg: record.msg == null ? void 0 : String(record.msg),
575
+ level: typeof record.level === "string" ? record.level : void 0,
576
+ ts: typeof record.ts === "number" ? record.ts : void 0
577
+ }))));
578
+ const requestedCount = records.length;
579
+ const related = timeline.filter((candidate) => candidate.src === "device" && records.some((record) => record.src !== "device" && errorTitle(record) && errorTitle(record) === errorTitle(candidate) && Math.abs(Number(record.ts) - Number(candidate.ts)) < 1e3 && (!record.platform || !candidate.platform || record.platform === candidate.platform)) && !records.some((record) => record.src === candidate.src && record.ts === candidate.ts && record.msg === candidate.msg)).slice(-100);
580
+ records.push(...attachDeviceErrorContext(timeline, related));
581
+ const directory = join(logsDir, "error-context");
582
+ const rendered = records.map((record) => {
583
+ try {
584
+ const saved = JSON.parse(readFileSync(join(directory, `${key(record)}.json`), "utf8"));
585
+ return saved.original === key(record) && saved.rendered && typeof saved.rendered === "object" ? saved.rendered : record;
586
+ } catch {
587
+ return record;
588
+ }
589
+ });
590
+ const pending = records.flatMap((record, index) => {
591
+ if (rendered[index] !== record || !bundleLocations(record).length) return [];
592
+ if (timeline.some((event) => ([
593
+ "bundle_build_started",
594
+ "bundle_build_done",
595
+ "bundle_build_failed",
596
+ "server_started",
597
+ "supervisor_started",
598
+ "bundle_response_started",
599
+ "bundle_response_finished",
600
+ "bundle_response_failed"
601
+ ].includes(String(event.event)) || ["expo_stdout", "expo_stderr"].includes(String(event.event)) && /\bBundl(?:ing|ed)\b/.test(event.msg ?? "")) && Number(event.ts) > Number(record.ts) && (!event.platform || !record.platform || event.platform === record.platform))) {
602
+ rendered[index] = {
603
+ ...record,
604
+ symbolicationNote: "Metro rebuilt after this error; captured coordinates retained rather than mapping against a different bundle."
605
+ };
606
+ return [];
607
+ }
608
+ return [{
609
+ record,
610
+ index
611
+ }];
612
+ });
613
+ if (pending.length && allowRequest && port && (await resolveProjectMetro(port, root)).metro) {
614
+ const resolved = await symbolicateErrors(pending.map(({ record }) => record), { port });
615
+ for (let index = 0; index < pending.length; index++) {
616
+ const entry = pending[index];
617
+ const value = resolved[index];
618
+ rendered[entry.index] = value;
619
+ if (bundleLocations(value).length === bundleLocations(entry.record).length) continue;
620
+ try {
621
+ writeDiagnosticOnce(join(directory, `${key(entry.record)}.json`), JSON.stringify({
622
+ original: key(entry.record),
623
+ rendered: value
624
+ }));
625
+ } catch {}
626
+ }
627
+ }
628
+ let merged = mergeErrorCopies(rendered.slice(0, requestedCount), root);
629
+ for (const candidate of rendered.slice(requestedCount)) {
630
+ const combined = mergeErrorCopies([...merged, candidate], root);
631
+ if (combined.length === merged.length) merged = combined;
632
+ }
633
+ return merged;
634
+ }
635
+ //#endregion
636
+ export { printNativeCrashReport as a, captureWorkspaceCrashes as i, IOS_CRASH_REPORT_RETRY as n, simulatorConsolePaths as o, captureNativeCrashes as r, errorDiagnostics as t };
@@ -2,7 +2,7 @@ import { execFileSync, execSync, spawn } from "child_process";
2
2
  //#region src/exec.ts
3
3
  const MAX_BUFFER = 67108864;
4
4
  let active = {
5
- run(cmd, { timeoutMs, cwd } = {}) {
5
+ run(cmd, { timeoutMs, killSignal, cwd } = {}) {
6
6
  const opts = {
7
7
  encoding: "utf-8",
8
8
  stdio: [
@@ -13,10 +13,11 @@ let active = {
13
13
  maxBuffer: MAX_BUFFER
14
14
  };
15
15
  if (timeoutMs) opts.timeout = timeoutMs;
16
+ if (killSignal) opts.killSignal = killSignal;
16
17
  if (cwd) opts.cwd = cwd;
17
18
  return String(execSync(cmd, opts)).trim();
18
19
  },
19
- runFile(file, args = [], { timeoutMs, cwd, env, omitEnv } = {}) {
20
+ runFile(file, args = [], { timeoutMs, killSignal, cwd, env, omitEnv } = {}) {
20
21
  const opts = {
21
22
  encoding: "utf-8",
22
23
  stdio: [
@@ -27,6 +28,7 @@ let active = {
27
28
  maxBuffer: MAX_BUFFER
28
29
  };
29
30
  if (timeoutMs) opts.timeout = timeoutMs;
31
+ if (killSignal) opts.killSignal = killSignal;
30
32
  if (cwd) opts.cwd = cwd;
31
33
  if (env || omitEnv?.length) {
32
34
  const childEnv = {