stim 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +23 -155
  2. package/dist/{android-BRgBYzU0.mjs → android-BPGizezh.mjs} +112 -135
  3. package/dist/{android-DeYdjjBP.mjs → android-DEgo4oCo.mjs} +2 -2
  4. package/dist/{android-DWssqHQ8.mjs → android-_y4Ohi5z.mjs} +54 -15
  5. package/dist/android-cas-compiler.mjs +2 -2
  6. package/dist/{android-cas-SWIjzeuL.mjs → android-cas-eiExNLf7.mjs} +21 -14
  7. package/dist/app-install-Uf3a83Tv.mjs +819 -0
  8. package/dist/{build-lock-CgtsB6TV.mjs → build-lock-DPXPLgTc.mjs} +120 -24
  9. package/dist/build-progress-CVqkfPC6.mjs +188 -0
  10. package/dist/{cache-manifest-CYJi8-Hx.mjs → cache-manifest-Dg7OnMrp.mjs} +4 -4
  11. package/dist/cache-manifest.mjs +1 -1
  12. package/dist/cli.mjs +22 -19
  13. package/dist/collector-run.d.mts +2 -0
  14. package/dist/collector-run.mjs +10 -9
  15. package/dist/{command-output-B3ycpsYv.mjs → command-output-gByVmIjy.mjs} +11 -2
  16. package/dist/{config-B7iyKWkL.mjs → config-Cp9lM3wX.mjs} +187 -57
  17. package/dist/{dependency-state-D0M7oKA7.mjs → dependency-state-Bq5DcLGn.mjs} +2 -2
  18. package/dist/{deps-oCXVNEqQ.mjs → deps-Cip5vUm1.mjs} +8 -5
  19. package/dist/{detached-entry-Bpo6ZY_I.mjs → detached-entry-KEu0_AXL.mjs} +1 -1
  20. package/dist/{device-BUoZyMoR.mjs → dev-client-HyqISM0h.mjs} +1178 -635
  21. package/dist/{device-RdujjHLB.mjs → device-D4U-xLty.mjs} +13 -20
  22. package/dist/{device-lease-Ffz_M5t0.mjs → device-lease-Crs-HkJY.mjs} +16 -9
  23. package/dist/{device-pool-BxRwdufO.mjs → device-pool-CnTAz8X1.mjs} +7 -7
  24. package/dist/{device-remote-ByN8P6CB.mjs → device-remote-AQ5rQPqr.mjs} +482 -35
  25. package/dist/{doctor-BMt156qe.mjs → doctor-B_Ei3bZu.mjs} +95 -107
  26. package/dist/{error-diagnostics-D1Kaf-eT.mjs → error-diagnostics-Vfz83GCF.mjs} +22 -16
  27. package/dist/exec-jIAlgilk.mjs +132 -0
  28. package/dist/{gc-v2YcoJvm.mjs → gc-Ckw8RQHi.mjs} +763 -126
  29. package/dist/{guide-BTAlD1_y.mjs → guide-BiAbhs1J.mjs} +891 -501
  30. package/dist/{guide-status-Craq9z0t.mjs → guide-status-BAMVU0ZA.mjs} +5 -3
  31. package/dist/{ios-BcCiI4TS.mjs → ios-BL2NFckr.mjs} +43 -20
  32. package/dist/{ios-BP04pY5f.mjs → ios-BzUJWk1K.mjs} +141 -175
  33. package/dist/{ios-device-CauFJiYx.mjs → ios-device-D972i9wf.mjs} +149 -52
  34. package/dist/{ios-device-CFXA46ez.mjs → ios-device-DPsSm0v_.mjs} +3 -2
  35. package/dist/launch-verify-Q_OtW1Rf.mjs +365 -0
  36. package/dist/{logs-CakdBTtm.mjs → logs-CI9NQSK7.mjs} +18 -13
  37. package/dist/{logs-query-BGL-Pl12.mjs → logs-query-Cqma3WIu.mjs} +36 -28
  38. package/dist/{metro-C7bcJ0uo.mjs → metro-DrLHaMn2.mjs} +78 -8
  39. package/dist/{named-ports-DzMOlmaX.mjs → named-ports-CmLQSIRj.mjs} +4 -4
  40. package/dist/{ndjson-BZwI1NBv.mjs → ndjson-9uFNRwfe.mjs} +36 -3
  41. package/dist/{ownership-Cp52QAK1.mjs → ownership-D15Op8vN.mjs} +3 -3
  42. package/dist/{ownership-wolE-0Do.mjs → ownership-DCQPuWEc.mjs} +26 -7
  43. package/dist/{ports-BIbJaKNW.mjs → ports-BNKoWVWZ.mjs} +3 -3
  44. package/dist/{process-output-Dl5uKE03.mjs → process-output-Fpl8PYi8.mjs} +1 -1
  45. package/dist/{project-DDZWf_16.mjs → project-DS112DYB.mjs} +8 -3
  46. package/dist/{reload-BdypPbjM.mjs → reload-ZNJUo1pl.mjs} +12 -13
  47. package/dist/{server-bare-BQkyxa_b.mjs → server-bare-DWkkcIxV.mjs} +2 -2
  48. package/dist/{server-expo-BVAA1OqJ.mjs → server-expo-B_lctBR3.mjs} +8 -7
  49. package/dist/server-expo-CeILEu86.mjs +2 -0
  50. package/dist/settings-BKNh5rl4.mjs +376 -0
  51. package/dist/{settings-BXXorVM2.mjs → settings-D030k0t4.mjs} +464 -152
  52. package/dist/settings.schema.json +989 -0
  53. package/dist/{teardown-BJqEzPo_.mjs → sim-pool-KaIsp2Ft.mjs} +14 -419
  54. package/dist/simslim-B_LsGLuJ.mjs +259 -0
  55. package/dist/{slot-launch-ZUUe0TQP.mjs → slot-launch-DU9A-gYw.mjs} +4 -4
  56. package/dist/spawn-claims-Dn3C_N6w.mjs +49 -0
  57. package/dist/{start-CmxtOKjU.mjs → start-DYkkKF0s.mjs} +27 -31
  58. package/dist/{state-DZobzxLW.mjs → state-NcJzMEBF.mjs} +5 -3
  59. package/dist/{stats-CLg5RuDS.mjs → stats-B63-JVAY.mjs} +4 -4
  60. package/dist/{stats-BYcZ6n1h.mjs → stats-DG9e66Rz.mjs} +66 -5
  61. package/dist/status-S6ehL4Ui.mjs +932 -0
  62. package/dist/{stop-C8JW6kKp.mjs → stop-BO28HGqF.mjs} +101 -25
  63. package/dist/{stop-B1kOpYei.mjs → stop-T1R-cZRz.mjs} +2 -2
  64. package/dist/supervisor-run.mjs +52 -34
  65. package/dist/{workspace-process-lock-BYfZz_S-.mjs → workspace-process-lock-DWaiRD0e.mjs} +14 -9
  66. package/dist/{workspace-state-BT5E3SA2.mjs → workspace-state-DF2gCFhx.mjs} +52 -29
  67. package/dist/worktree-DKhSF0UX.mjs +2 -0
  68. package/dist/{worktree-CFLA-6MX.mjs → worktree-LGB-UwgG.mjs} +603 -59
  69. package/dist/{worktree-FG_ZKKdb.mjs → worktree-R_cUlE7s.mjs} +62 -41
  70. package/dist/{xcode-B6YaWeg0.mjs → xcode-BZqyxh3e.mjs} +216 -58
  71. package/package.json +5 -6
  72. package/skill/SKILL.md +2 -0
  73. package/dist/app-install-BDbsSgCl.mjs +0 -1175
  74. package/dist/build-slots-C-oAHRT1.mjs +0 -151
  75. package/dist/dev-client-BmT-ioFI.mjs +0 -797
  76. package/dist/exec-B29XNuhP.mjs +0 -77
  77. package/dist/reclaim-KJ-0uWEF.mjs +0 -361
  78. package/dist/server-expo-YDDG3eP7.mjs +0 -2
  79. package/dist/status-DdkLki47.mjs +0 -426
  80. package/dist/{metro-store-CDDZHtPl.mjs → metro-store-DzeWkfqW.mjs} +1 -1
@@ -0,0 +1,365 @@
1
+ import { t as getExecutor } from "./exec-jIAlgilk.mjs";
2
+ import { o as readNdjsonGenerations } from "./ndjson-9uFNRwfe.mjs";
3
+ import { t as DEV_MENU_LAUNCH_ARGS } from "./ios-device-DPsSm0v_.mjs";
4
+ import { s as isAppLaunchError } from "./command-output-gByVmIjy.mjs";
5
+ import { d as deviceShellArg, n as ANDROID_DISABLE_AUTO_LAUNCH_EXTRA, o as androidAppProcess } from "./app-install-Uf3a83Tv.mjs";
6
+ import { join } from "node:path";
7
+ //#region src/engine/app-readiness.ts
8
+ const APP_READINESS_TIMEOUT_MS = 3e4;
9
+ function appReadinessSignal(record, platform) {
10
+ if (record.src !== "device" || !platform || record.platform !== platform) return null;
11
+ if (record.level !== "info" && record.level !== "debug") return null;
12
+ if (typeof record.msg !== "string") return null;
13
+ const text = record.msg.trim();
14
+ const message = /^(?:'[^']*'|"[^"]*")$/.test(text) ? text.slice(1, -1) : text;
15
+ return /^\[stim:readiness\] (pending|ready)$/.exec(message)?.[1] ?? null;
16
+ }
17
+ //#endregion
18
+ //#region src/engine/launch-verify.ts
19
+ const RELEASE_VERIFY_WAIT_MS = 3e3;
20
+ async function verifyReleaseLaunch({ pid, waitMs = RELEASE_VERIFY_WAIT_MS, alive = isProcessAlive, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) }) {
21
+ const startedAt = now();
22
+ if (!pid) return {
23
+ verified: false,
24
+ reason: "no-pid",
25
+ waitedMs: 0
26
+ };
27
+ await sleep(Math.max(0, waitMs));
28
+ const waitedMs = now() - startedAt;
29
+ return alive(pid) ? {
30
+ verified: true,
31
+ waitedMs
32
+ } : {
33
+ verified: false,
34
+ reason: "exited",
35
+ waitedMs
36
+ };
37
+ }
38
+ async function verifyAndroidReleaseLaunch({ serial, packageName, waitMs = RELEASE_VERIFY_WAIT_MS, exec = null, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) }) {
39
+ const e = exec || getExecutor();
40
+ const startedAt = now();
41
+ await sleep(Math.max(0, waitMs));
42
+ const pid = androidAppProcess(serial, packageName, { exec: e });
43
+ const waitedMs = now() - startedAt;
44
+ if (pid === void 0) return {
45
+ verified: false,
46
+ reason: "probe-failed",
47
+ waitedMs
48
+ };
49
+ return pid === null ? {
50
+ verified: false,
51
+ reason: "exited",
52
+ waitedMs,
53
+ pid: null
54
+ } : {
55
+ verified: true,
56
+ waitedMs,
57
+ pid
58
+ };
59
+ }
60
+ function isProcessAlive(pid) {
61
+ try {
62
+ process.kill(pid, 0);
63
+ return true;
64
+ } catch (err) {
65
+ return err?.code === "EPERM";
66
+ }
67
+ }
68
+ const LAUNCH_UNVERIFIED = "unverified";
69
+ const LAUNCH_BUNDLING = "bundling";
70
+ const LAUNCH_FATAL = "fatal";
71
+ const VERIFY_TIMEOUT_MS = 2e4;
72
+ const STABILITY_WINDOW_MS = 3e3;
73
+ const VERIFY_POLL_MS = 500;
74
+ const BUNDLE_EVENTS = /* @__PURE__ */ new Set([
75
+ "bundle_build_started",
76
+ "bundle_build_done",
77
+ "bundle_build_failed",
78
+ "bundling_error",
79
+ "transformer_error",
80
+ "bundle_response_started",
81
+ "bundle_response_finished",
82
+ "bundle_response_failed"
83
+ ]);
84
+ function isBundleProof(record, since = 0, platform = null) {
85
+ if (!record || typeof record !== "object") return false;
86
+ const rec = record;
87
+ const ts = Number(rec.ts);
88
+ if (!Number.isFinite(ts) || ts < Number(since || 0)) return false;
89
+ if (platform && recordPlatform(rec) !== platform) return false;
90
+ if (typeof rec.event === "string" && BUNDLE_EVENTS.has(rec.event)) return true;
91
+ if (rec.src === "metro" && typeof rec.msg === "string" && expoBundleLine(rec.msg)) return true;
92
+ return false;
93
+ }
94
+ const BUNDLE_URL_PATH = /\.bundle\b|\/_expo\/|expo-development-client/i;
95
+ function isBundleRequestProof(record, since = 0, port = null, platform = null) {
96
+ if (!record || typeof record !== "object" || !port) return false;
97
+ const rec = record;
98
+ const ts = Number(rec.ts);
99
+ if (!Number.isFinite(ts) || ts < Number(since || 0)) return false;
100
+ if (typeof rec.msg !== "string") return false;
101
+ if (rec.level === "error" || rec.level === "fatal") return false;
102
+ const sourcePlatform = recordPlatform(rec);
103
+ if (platform && sourcePlatform && sourcePlatform !== platform) return false;
104
+ const digits = String(port).replace(/[^0-9]/g, "");
105
+ if (!digits) return false;
106
+ if (!new RegExp(`:${digits}\\b`).test(rec.msg)) return false;
107
+ return BUNDLE_URL_PATH.test(rec.msg);
108
+ }
109
+ function expoBundleLine(msg) {
110
+ return /\bBundl(?:ing|ed)\b/.test(msg);
111
+ }
112
+ function nativeLaunchFailure({ readCrashes, since, platform, processAlive, mode, waitedMs }) {
113
+ if (!readCrashes) return null;
114
+ const crashes = readCrashes().filter((record) => record.event === "native_crash" && after(record, since) && recordCouldBelongToPlatform(record, platform));
115
+ return crashes.length ? {
116
+ verified: false,
117
+ fatal: true,
118
+ errors: crashes,
119
+ processAlive: processAlive?.() ?? null,
120
+ mode,
121
+ waitedMs
122
+ } : null;
123
+ }
124
+ async function verifyLaunch({ slot, logsDir, since, metroPort = null, platform = null, mode = null, requireBundleResponse, timeoutMs = VERIFY_TIMEOUT_MS, stabilityMs = STABILITY_WINDOW_MS, pollMs = VERIFY_POLL_MS, readRecords = null, readDeviceRecords = null, readClientRecords = null, readNativeCrashes = null, processAlive = null, onReadinessPending = null, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) } = {}) {
125
+ const read = readRecords || (() => readMetroRecords(logsDir));
126
+ const readDevice = readDeviceRecords || (() => readNdjson(logsDir, "device.ndjson"));
127
+ const readClient = readClientRecords || (() => readNdjson(logsDir, "client.ndjson"));
128
+ const startedAt = now();
129
+ const bundleDeadline = startedAt + Math.max(0, timeoutMs);
130
+ let proof = null;
131
+ let activity = null;
132
+ let stabilityDeadline = null;
133
+ let pendingAt = null;
134
+ let deliveryId = null;
135
+ let runtimeLoadingAt = null;
136
+ let runtimeStartedAt = null;
137
+ let nextCrashCheck = startedAt + 1e3;
138
+ while (true) {
139
+ const metroRecords = read().filter((record) => after(record, since) && (slot === void 0 || record.slot === slot));
140
+ const bundleRecords = metroRecords.filter((record) => !requireBundleResponse || String(record.event).startsWith("bundle_response_"));
141
+ const deviceRecords = readDevice().filter((record) => after(record, since) && (slot === void 0 || (record.slot ?? "default") === slot));
142
+ const clientRecords = readClient().filter((record) => after(record, since) && (slot === void 0 || record.slot === slot));
143
+ if (now() >= nextCrashCheck) {
144
+ nextCrashCheck = now() + 2e3;
145
+ const crash = nativeLaunchFailure({
146
+ readCrashes: readNativeCrashes,
147
+ since,
148
+ platform,
149
+ processAlive,
150
+ mode,
151
+ waitedMs: now() - startedAt
152
+ });
153
+ if (crash) return crash;
154
+ }
155
+ if (deliveryId === null) {
156
+ const request = bundleRecords.find((record) => record.event === "bundle_response_started" && typeof record.requestId === "string" && isBundleProof(record, since, platform));
157
+ if (request) {
158
+ deliveryId = request.requestId;
159
+ proof = null;
160
+ stabilityDeadline = null;
161
+ }
162
+ }
163
+ for (const record of bundleRecords) {
164
+ if (isBundleProof(record, since, platform)) activity = record;
165
+ const completed = deliveryId === null ? isBundleReadyProof(record, since, platform) : record.event === "bundle_response_finished" && record.requestId === deliveryId && isBundleProof(record, since, platform);
166
+ if (!proof && completed) {
167
+ proof = record;
168
+ stabilityDeadline = Number(record.ts) + Math.max(0, stabilityMs);
169
+ }
170
+ }
171
+ const signals = deviceRecords.filter((record) => Number(record.ts) <= now()).toSorted((a, b) => Number(a.ts) - Number(b.ts));
172
+ if (platform === "android" && runtimeLoadingAt === null) {
173
+ const loading = signals.find((record) => record.src === "device" && record.platform === "android" && /^(?:unknown:)?BridgelessReact\(\d+\)$/.test(String(record.proc)) && /^ReactHost\{\d+\}\.getOrCreateReactInstanceTask\(\): Loading JS Bundle$/.test(String(record.msg)) && (stabilityDeadline === null || Number(record.ts) <= stabilityDeadline));
174
+ if (loading) runtimeLoadingAt = Number(loading.ts);
175
+ }
176
+ if (runtimeLoadingAt !== null && runtimeStartedAt === null) {
177
+ const executing = signals.find((record) => record.src === "device" && record.platform === "android" && Number(record.ts) >= runtimeLoadingAt && (/^ReactNativeJS\(\d+\)$/.test(String(record.proc)) || appReadinessSignal(record, platform) !== null));
178
+ if (executing) runtimeStartedAt = Number(executing.ts);
179
+ }
180
+ const runtimeWaiting = runtimeLoadingAt !== null && runtimeStartedAt === null;
181
+ const completedAt = proof ? Math.max(Number(proof.ts), runtimeStartedAt ?? 0) : null;
182
+ if (completedAt !== null) stabilityDeadline = completedAt + Math.max(0, stabilityMs);
183
+ const readinessDeadline = completedAt !== null ? completedAt + APP_READINESS_TIMEOUT_MS : bundleDeadline;
184
+ if (pendingAt === null) {
185
+ const pending = signals.find((record) => appReadinessSignal(record, platform) === "pending" && (runtimeWaiting || stabilityDeadline === null || Number(record.ts) <= stabilityDeadline));
186
+ if (pending) {
187
+ pendingAt = Number(pending.ts);
188
+ onReadinessPending?.();
189
+ }
190
+ }
191
+ const ready = pendingAt !== null && signals.some((record) => Number(record.ts) >= pendingAt && Number(record.ts) <= readinessDeadline && appReadinessSignal(record, platform) === "ready");
192
+ const bundleErrors = bundleRecords.filter((record) => isFatalLaunchError(record, platform) || deliveryId !== null && record.event === "bundle_response_failed" && record.requestId === deliveryId && isBundleProof(record, since, platform));
193
+ if (bundleErrors.length) {
194
+ const alive = processAlive ? processAlive() : null;
195
+ const fatalRecord = bundleErrors[bundleErrors.length - 1];
196
+ const fatalAt = Number(fatalRecord?.ts ?? since ?? 0);
197
+ return {
198
+ verified: false,
199
+ fatal: true,
200
+ errors: metroRecords.filter((record) => after(record, fatalAt) && recordCouldBelongToPlatform(record, platform) && isLaunchError(record, platform)),
201
+ processAlive: alive,
202
+ record: fatalRecord,
203
+ mode,
204
+ waitedMs: now() - startedAt,
205
+ ...pendingAt !== null ? { readiness: "error" } : {}
206
+ };
207
+ }
208
+ if (proof && stabilityDeadline !== null && (pendingAt !== null || now() >= stabilityDeadline)) {
209
+ const errorSince = pendingAt === null ? Number(proof.ts ?? since ?? 0) : Number(since ?? 0);
210
+ const errors = [
211
+ ...metroRecords.filter((record) => after(record, errorSince) && recordCouldBelongToPlatform(record, platform)),
212
+ ...deviceRecords.filter((record) => after(record, errorSince) && recordCouldBelongToPlatform(record, platform)),
213
+ ...clientRecords.filter((record) => after(record, errorSince) && recordCouldBelongToPlatform(record, platform))
214
+ ].filter((record) => isLaunchError(record, platform));
215
+ const alive = processAlive ? processAlive() : null;
216
+ const waitedMs = now() - startedAt;
217
+ if (alive === false) return {
218
+ verified: false,
219
+ fatal: true,
220
+ errors,
221
+ processAlive: alive,
222
+ record: proof,
223
+ mode,
224
+ waitedMs,
225
+ ...pendingAt !== null ? { readiness: "error" } : {}
226
+ };
227
+ const actionableErrors = errors.filter((record) => !isIosConnectionRefusal(record, platform));
228
+ const appErrors = actionableErrors.some(isAppLaunchError);
229
+ if (!runtimeWaiting && pendingAt === null || appErrors || ready || now() >= readinessDeadline) return {
230
+ verified: true,
231
+ record: proof,
232
+ errors: actionableErrors,
233
+ processAlive: alive,
234
+ mode,
235
+ waitedMs,
236
+ ...pendingAt !== null ? { readiness: appErrors ? "error" : ready ? "ready" : "timed-out" } : {}
237
+ };
238
+ }
239
+ if ((!proof || runtimeWaiting) && now() >= bundleDeadline) {
240
+ const requested = findBundleRequest(deviceRecords, since, metroPort, platform) ?? activity;
241
+ const waitedMs = now() - startedAt;
242
+ if (requested) return {
243
+ verified: false,
244
+ timedOut: true,
245
+ requested: true,
246
+ record: requested,
247
+ mode,
248
+ waitedMs
249
+ };
250
+ return {
251
+ verified: false,
252
+ timedOut: true,
253
+ mode,
254
+ waitedMs
255
+ };
256
+ }
257
+ await sleep(Math.min(pollMs, Math.max(0, (runtimeWaiting ? bundleDeadline : proof && pendingAt !== null ? readinessDeadline : stabilityDeadline ?? bundleDeadline) - now())));
258
+ }
259
+ }
260
+ function after(record, since) {
261
+ const ts = Number(record.ts);
262
+ return Number.isFinite(ts) && ts >= Number(since ?? 0);
263
+ }
264
+ function isLaunchError(record, platform) {
265
+ return record.level === "error" || record.level === "fatal" || isFatalLaunchError(record, platform);
266
+ }
267
+ const TCP_REFUSAL = /^TCP Conn 0x[0-9a-f]+ Failed : error 0:61 \[61\]$/i;
268
+ function isIosConnectionRefusal(record, platform) {
269
+ return platform === "ios" && record.src === "device" && record.level === "error" && typeof record.msg === "string" && TCP_REFUSAL.test(record.msg);
270
+ }
271
+ function isFatalLaunchError(record, platform) {
272
+ if (platform && recordPlatform(record) !== platform) return false;
273
+ return record.event === "bundle_build_failed" || record.event === "bundling_error" || record.event === "transformer_error" || (record.event === "expo_stdout" || record.event === "expo_stderr") && typeof record.msg === "string" && /\bBundling failed\b/.test(record.msg);
274
+ }
275
+ function isBundleReadyProof(record, since = 0, platform = null) {
276
+ if (!isBundleProof(record, since, platform)) return false;
277
+ const rec = record;
278
+ if (rec.event === "bundle_build_done") return true;
279
+ return rec.src === "metro" && typeof rec.msg === "string" && /\bBundled\b/.test(rec.msg);
280
+ }
281
+ function findBundleRequest(records, since, port, platform) {
282
+ for (const record of records) if (isBundleRequestProof(record, since ?? 0, port, platform)) return record;
283
+ return null;
284
+ }
285
+ function recordCouldBelongToPlatform(record, platform) {
286
+ if (!platform) return true;
287
+ const sourcePlatform = recordPlatform(record);
288
+ return sourcePlatform === null || sourcePlatform === platform;
289
+ }
290
+ function recordPlatform(record) {
291
+ if (record.platform === "ios" || record.platform === "android") return record.platform;
292
+ if (typeof record.msg !== "string") return null;
293
+ const match = record.msg.match(/\b(iOS|Android)\s+Bundl(?:ed|ing)\b/i);
294
+ if (!match) return null;
295
+ return match[1].toLowerCase() === "ios" ? "ios" : "android";
296
+ }
297
+ function readMetroRecords(logsDir) {
298
+ return readNdjson(logsDir, "metro.ndjson");
299
+ }
300
+ function readCollectorRecords(logsDir) {
301
+ return readNdjson(logsDir, "device.ndjson");
302
+ }
303
+ function readNdjson(logsDir, name) {
304
+ return logsDir ? readNdjsonGenerations(join(logsDir, name)) : [];
305
+ }
306
+ function unverifiedLaunchLines({ platform, metroPort, waitedMs = VERIFY_TIMEOUT_MS, bundleId = null, udid = null, serial = null, devClientUrl: url = null, mode = null, remote = false, physical = false, devClient = false, lanOrigin = null, metroOrigin = null, localNetworkPending = false, component = null }) {
307
+ const seconds = Math.round(Number(waitedMs || 0) / 1e3);
308
+ const localNetwork = localNetworkPending && platform === "ios" && physical;
309
+ const lines = [`The app was started, but nothing fetched a bundle from this workspace's Metro (port ${metroPort}) within ${seconds}s.`];
310
+ const origin = metroOrigin || `http://localhost:${metroPort}`;
311
+ const picker = `If expo-dev-launcher's DEVELOPMENT SERVERS picker is showing, tap the entry for ${origin} -- NOT another workspace's, which would load a different project's bundle onto this device.`;
312
+ let step = 0;
313
+ const push = (text) => lines.push(` ${++step}. ${text}`);
314
+ lines.push(localNetwork ? `THE PHONE'S LOCAL NETWORK PERMISSION IS NOT GRANTED: the LAN connections this launch made failed with NSURLErrorDomain -1009 and the path reason "unsatisfied (Local network prohibited)", which is what iOS returns while the "would like to find and connect to devices on your local network" prompt is unanswered OR was answered Don't Allow earlier -- the log reads the same either way. Nothing the app sends reaches ${lanOrigin || origin} until it is granted. Do this, in order:` : "The app is launched; what is unproven is that it is talking to THIS dev server. Do this, in order:");
315
+ if (remote) {
316
+ push(`Check that ${origin} is reachable FROM THE DEVICE's network, not just from this machine. That is the usual cause: a tunnel that stopped, or one this machine can reach and the device cannot.`);
317
+ push("If an \"Open in <app>?\" alert or the expo-dev-launcher picker is showing on the remote device, confirm it with your device tool (`agent-device snapshot -i`, then `agent-device press`).");
318
+ lines.push(`Then check \`stim logs --source metro\`${mode ? ` (${mode})` : ""} for a bundle request.`);
319
+ return lines;
320
+ }
321
+ if (platform === "ios" && physical) {
322
+ const target = lanOrigin || origin;
323
+ const relaunch = `xcrun devicectl device process launch --device ${udid} --terminate-existing ` + (url ? `--payload-url '${url}' ` : "") + bundleId + (url ? ` -- ${DEV_MENU_LAUNCH_ARGS.join(" ")}` : "");
324
+ if (localNetwork) {
325
+ push(`See the prompt: agent-device alert get --platform ios --udid ${udid}. It reads the alert without opening anything, so it works while the app sits behind it.`);
326
+ push(`Tap Allow: agent-device alert accept --platform ios --udid ${udid}. A second \`alert get\` then finds none.`);
327
+ push("If the FIRST `alert get` already finds no alert, this permission was denied on an earlier run -- a Don't Allow persists across upgrade installs. Turn the app on by hand under Settings > Privacy & Security > Local Network; there is no API for that switch.");
328
+ if (devClient) {
329
+ push(`The app does NOT retry after the grant -- it stays on "Failed to load app ... The Internet connection appears to be offline." with a Reload button. Press it: agent-device snapshot -i --platform ios --udid ${udid}, then agent-device press 'label="Reload"' --platform ios --udid ${udid}. This launch carried -EXDevMenuShowsAtLaunch 0, so the Expo dev menu is not over the app; if the app was started another way and the menu is on screen, agent-device press 'label="Close"' --platform ios --udid ${udid} dismisses it.`);
330
+ push(`Without agent-device, relaunching also recovers: ${relaunch}. It costs the device log -- it replaces the process the collector follows, so \`stim logs --source device\` stops for the rest of this run. Pressing Reload keeps the collector alive.`);
331
+ push("By hand: tap Allow on the phone, then tap Reload on the app.");
332
+ } else {
333
+ push(`The app does NOT retry after the grant. A bare app is expected to show React Native's "Could not connect to development server" RedBox -- NOT VERIFIED ON HARDWARE, so read the screen rather than trusting that: agent-device snapshot -i --platform ios --udid ${udid}, then press its Reload button by the ref or label that snapshot reports.`);
334
+ push(`Cleanest for a bare app: relaunch, which re-reads ip.txt: ${relaunch}. It costs the device log -- it replaces the process the collector follows, so \`stim logs --source device\` stops for the rest of this run.`);
335
+ push("By hand: tap Allow on the phone, then tap Reload on the RedBox.");
336
+ }
337
+ lines.push("`agent-device metro reload` does NOT recover either screen: it only reaches an app already connected to Metro's websocket, and this app never connected.");
338
+ lines.push(`Then check \`stim logs --source metro\`${mode ? ` (${mode})` : ""} for a bundle request.`);
339
+ return lines;
340
+ }
341
+ push(`Tap Allow on the phone's "would like to find and connect to devices on your local network" prompt if it is showing, or turn the app on under Settings > Privacy & Security > Local Network. iOS gates every LAN connection behind that permission, it cannot be PRE-granted from this machine -- but once the prompt is up a device tool can accept it (agent-device alert get, then agent-device alert accept, both --platform ios --udid ${udid}) -- and until it is granted nothing the app sends reaches ${target}.`);
342
+ push("Check the phone is on the same network as this Mac -- the same Wi-Fi SSID, not cellular, not a VPN -- and that the network does not isolate clients from each other.");
343
+ push("Check macOS is not blocking inbound connections: System Settings > Network > Firewall, or `/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate`. The gate on this machine passes either way, because macOS routes a host connection to its own address over loopback.");
344
+ push(`If this Mac has several network interfaces, ${target} may not be the one the phone shares: set ios.lanHost in .stim.json to the address it can reach.`);
345
+ if (devClient) {
346
+ push(picker);
347
+ if (url && udid) push(`Retry the deep link: xcrun devicectl device process launch --device ${udid} --terminate-existing --payload-url '${url}' ${bundleId} -- ${DEV_MENU_LAUNCH_ARGS.join(" ")}`);
348
+ } else lines.push("AND READ THIS: a Debug device build with no dev client carries the JS bundle baked in when the artifact was built, so an unreachable Metro is silent. The app on screen is not broken -- it is running THAT bundle, which on a cache hit is another workspace's JS, not this workspace's.");
349
+ lines.push(`Then check \`stim logs --source metro\`${mode ? ` (${mode})` : ""} for a bundle request.`);
350
+ return lines;
351
+ }
352
+ if (platform === "ios") {
353
+ if (devClient) push(picker);
354
+ if (url && udid) push(`Retry the deep link: xcrun simctl openurl ${udid} '${url}'`);
355
+ else if (udid && bundleId) push(`Re-launch: xcrun simctl launch --terminate-running-process ${udid} ${bundleId}`);
356
+ } else {
357
+ if (url && serial) push(`Re-send the dev-client deep link -- this is the command that points the app at THIS workspace's Metro: adb -s ${serial} shell am start -a android.intent.action.VIEW -d ${deviceShellArg(deviceShellArg(url))} --ez ${ANDROID_DISABLE_AUTO_LAUNCH_EXTRA} true`);
358
+ if (devClient) push(picker);
359
+ if (serial && bundleId) push(`If the app is stuck, restart its process: ${component ? `adb -s ${serial} shell am force-stop ${bundleId} && adb -s ${serial} shell am start -n ${component}` : url ? `adb -s ${serial} shell am force-stop ${bundleId} && adb -s ${serial} shell am start -a android.intent.action.VIEW -d ${deviceShellArg(deviceShellArg(url))} --ez ${ANDROID_DISABLE_AUTO_LAUNCH_EXTRA} true` : `adb -s ${serial} shell am force-stop ${bundleId} && adb -s ${serial} shell monkey -p ${bundleId} 1`}`);
360
+ }
361
+ lines.push(`Then check \`stim logs --source metro\`${mode ? ` (${mode})` : ""} for a bundle request.`);
362
+ return lines;
363
+ }
364
+ //#endregion
365
+ export { VERIFY_TIMEOUT_MS as a, readMetroRecords as c, verifyLaunch as d, verifyReleaseLaunch as f, RELEASE_VERIFY_WAIT_MS as i, unverifiedLaunchLines as l, LAUNCH_FATAL as n, isBundleProof as o, APP_READINESS_TIMEOUT_MS as p, LAUNCH_UNVERIFIED as r, readCollectorRecords as s, LAUNCH_BUNDLING as t, verifyAndroidReleaseLaunch as u };
@@ -1,10 +1,10 @@
1
- import { B as validateDeviceSlot, g as loadConfig, h as isPathPrefix } from "./config-B7iyKWkL.mjs";
2
- import { n as SOURCES, t as LEVELS } from "./ndjson-BZwI1NBv.mjs";
3
- import { _ as workspaceLogsDir, r as readWorkspaceState } from "./workspace-state-BT5E3SA2.mjs";
4
- import { a as findProjectRoot } from "./project-DDZWf_16.mjs";
5
- import { _ as launchErrorPreview } from "./command-output-B3ycpsYv.mjs";
6
- import { i as captureWorkspaceCrashes, t as errorDiagnostics } from "./error-diagnostics-D1Kaf-eT.mjs";
7
- import { a as followLogs, c as queryLogs, i as fileSizes, n as buildCriteria, o as logFiles, r as compileGrep, s as parseSince } from "./logs-query-BGL-Pl12.mjs";
1
+ import { G as parseDeviceSlotOption, g as loadConfig, h as isPathPrefix } from "./config-Cp9lM3wX.mjs";
2
+ import { n as SOURCES, t as LEVELS } from "./ndjson-9uFNRwfe.mjs";
3
+ import { b as workspaceLogsDir, r as readWorkspaceState } from "./workspace-state-DF2gCFhx.mjs";
4
+ import { o as findProjectRoot } from "./project-DS112DYB.mjs";
5
+ import { f as refuseNoProject, v as launchErrorPreview } from "./command-output-gByVmIjy.mjs";
6
+ import { i as captureWorkspaceCrashes, t as errorDiagnostics } from "./error-diagnostics-Vfz83GCF.mjs";
7
+ import { a as fileSizes, c as parseSince, i as compileGrep, l as queryLogs, o as followLogs, r as buildCriteria, s as logFiles, u as readLogRecords } from "./logs-query-Cqma3WIu.mjs";
8
8
  import { realpathSync } from "node:fs";
9
9
  import { relative, sep } from "node:path";
10
10
  import chalk from "chalk";
@@ -93,11 +93,11 @@ const LEVEL_COLOURS = {
93
93
  fatal: chalk.bgRed
94
94
  };
95
95
  function logsCommand(program) {
96
- program.command("logs").description("Query this workspace's merged NDJSON log timeline (bundler, client, device, build). Prints and exits; an existing timeline with nothing matching is a successful, empty result. Use --follow to stream.").option("--slot <name>", "Only records attributed to this device slot", validateDeviceSlot).option("--source <s...>", "Only these sources: metro, client, device, build, or all").option("--level <l>", `Minimum level: ${LEVELS.join(", ")}`).option("--since <d>", "Only records newer than this, e.g. 30s, 5m, 2h").option("--grep <re>", "Only records whose message matches this regular expression").option("--tail <n>", "Only the last n matching records").option("--errors", "Errors and fatals since the last marker, from metro, client and build, plus confirmed native app-crash reports. Add --source device or --source all for general device errors.").option("--follow", "Keep streaming new records until interrupted").option("--json", "Emit the raw records, one per line (valid NDJSON; zero matches is zero bytes, exit 0)").action(async (opts) => {
96
+ program.command("logs").description("Query this workspace's merged NDJSON log timeline (bundler, client, device, build). Prints and exits; an existing timeline with nothing matching is a successful, empty result. Use --follow to stream.").option("--slot <name>", "Only records attributed to this device slot", parseDeviceSlotOption).option("--source <s...>", "Only these sources: metro, client, device, build, or all").option("--level <l>", `Minimum level: ${LEVELS.join(", ")}`).option("--since <d>", "Only records newer than this, e.g. 30s, 5m, 2h").option("--grep <re>", "Only records whose message matches this regular expression").option("--tail <n>", "Only the last n matching records").option("--errors", "Errors and fatals since the last marker, from metro, client and build, plus confirmed native app-crash reports. Add --source device or --source all for general device errors.").option("--follow", "Keep streaming new records until interrupted").option("--json", "Emit the raw records, one per line (valid NDJSON; zero matches is zero bytes, exit 0)").action(async (opts) => {
97
97
  const root = findProjectRoot(process.cwd());
98
98
  if (!root) {
99
- console.error(chalk.red("Not in a React Native project (no package.json found)."));
100
- process.exit(1);
99
+ refuseNoProject({ json: false });
100
+ return;
101
101
  }
102
102
  const dir = workspaceLogsDir(root);
103
103
  let sources;
@@ -150,15 +150,20 @@ function logsCommand(program) {
150
150
  };
151
151
  console.log(formatRecord(rendered, { paint: record?.level ? LEVEL_COLOURS[record.level] : void 0 }));
152
152
  };
153
+ const captured = captureWorkspaceCrashes(root, dir);
153
154
  const offsets = opts.follow ? fileSizes(dir) : null;
154
- captureWorkspaceCrashes(root, dir);
155
- const rawRecords = queryLogs(query);
155
+ const timeline = opts.follow ? readLogRecords(dir) : captured;
156
+ const rawRecords = queryLogs({
157
+ ...query,
158
+ records: timeline
159
+ });
156
160
  const supervisorPort = readWorkspaceState(root)?.supervisor?.port;
157
161
  const records = opts.json ? rawRecords : await errorDiagnostics(rawRecords, {
158
162
  root,
159
163
  logsDir: dir,
160
164
  port: typeof supervisorPort === "number" ? supervisorPort : null,
161
- allowRequest: true
165
+ allowRequest: true,
166
+ timeline
162
167
  });
163
168
  const errorCount = records.filter((record) => record.errorContext !== true).length;
164
169
  let seenErrors = 0;
@@ -1,6 +1,7 @@
1
- import { a as parseNdjsonLine, i as levelRank, o as parseNdjsonText } from "./ndjson-BZwI1NBv.mjs";
2
- import { closeSync, openSync, readFileSync, readSync, readdirSync, statSync } from "fs";
1
+ import { a as parseNdjsonLine, i as levelRank, o as readNdjsonGenerations } from "./ndjson-9uFNRwfe.mjs";
2
+ import { closeSync, openSync, readSync, readdirSync, statSync } from "fs";
3
3
  import { join } from "path";
4
+ import { rotatedLogPath } from "@stim-cli/core";
4
5
  import { StringDecoder } from "string_decoder";
5
6
  //#region src/diagnostics/logs-query.ts
6
7
  const SINCE_UNITS = {
@@ -111,17 +112,9 @@ function fileSizes(dir) {
111
112
  } catch {}
112
113
  return sizes;
113
114
  }
114
- function readLogRecords(dir) {
115
+ function readLogRecords(dir, include = () => true) {
115
116
  const all = [];
116
- for (const name of logFiles(dir)) {
117
- let text;
118
- try {
119
- text = readFileSync(join(dir, name), "utf-8");
120
- } catch {
121
- continue;
122
- }
123
- for (const record of parseNdjsonText(text)) all.push(record);
124
- }
117
+ for (const name of logFiles(dir).filter(include)) for (const record of readNdjsonGenerations(join(dir, name))) all.push(record);
125
118
  return sortByTs(all);
126
119
  }
127
120
  function isExpoErrorContext(record, event) {
@@ -172,8 +165,8 @@ function launchMarkersBySlot(records) {
172
165
  }
173
166
  return markers;
174
167
  }
175
- function queryLogs({ slot, dir, sources, minLevel, since, grep, tail, errorsOnly, errorContext, now } = {}) {
176
- const all = readLogRecords(dir).filter((record) => slot === void 0 || (record.slot ?? "default") === slot);
168
+ function queryLogs({ slot, dir, records, sources, minLevel, since, grep, tail, errorsOnly, errorContext, now } = {}) {
169
+ const all = (records ?? readLogRecords(dir)).filter((record) => slot === void 0 || (record.slot ?? "default") === slot);
177
170
  if (all.length === 0) return [];
178
171
  const { launchTs, bundleTs } = errorsOnly ? markerWindow(all) : {
179
172
  launchTs: null,
@@ -241,27 +234,31 @@ function followLogs({ dir, onRecord, criteria = {}, intervalMs = 500, offsets =
241
234
  });
242
235
  decoders.set(name, new StringDecoder("utf8"));
243
236
  }
237
+ function emit(records) {
238
+ for (const record of records) if (recordMatches(record, criteria)) onRecord(record);
239
+ }
240
+ function drainRotated(name, entry) {
241
+ const path = rotatedLogPath(join(dir, name));
242
+ let size;
243
+ try {
244
+ size = statSync(path).size;
245
+ } catch {
246
+ return;
247
+ }
248
+ const decoder = decoders.get(name) ?? new StringDecoder("utf8");
249
+ emit(advanceTail(entry, readChunk(path, decoder, entry.offset, size) + decoder.end(), size).records);
250
+ }
244
251
  function pollFile(name) {
245
252
  const path = join(dir, name);
246
253
  const size = statSync(path).size;
247
254
  const entry = state.get(name);
255
+ if (entry && size < entry.offset) drainRotated(name, entry);
248
256
  const { start, prev } = tailRead(entry, size);
249
257
  if (prev !== entry) decoders.set(name, new StringDecoder("utf8"));
250
258
  if (!decoders.has(name)) decoders.set(name, new StringDecoder("utf8"));
251
- let chunk = "";
252
- if (size > start) {
253
- const fd = openSync(path, "r");
254
- try {
255
- const buf = Buffer.allocUnsafe(size - start);
256
- const read = readSync(fd, buf, 0, size - start, start);
257
- chunk = decoders.get(name).write(buf.subarray(0, read));
258
- } finally {
259
- closeSync(fd);
260
- }
261
- }
262
- const next = advanceTail(prev, chunk, size);
259
+ const next = advanceTail(prev, readChunk(path, decoders.get(name), start, size), size);
263
260
  state.set(name, next.state);
264
- for (const record of next.records) if (recordMatches(record, criteria)) onRecord(record);
261
+ emit(next.records);
265
262
  }
266
263
  function poll() {
267
264
  for (const name of logFiles(dir)) try {
@@ -273,6 +270,17 @@ function followLogs({ dir, onRecord, criteria = {}, intervalMs = 500, offsets =
273
270
  clearInterval(timer);
274
271
  };
275
272
  }
273
+ function readChunk(path, decoder, start, size) {
274
+ if (size <= start) return "";
275
+ const fd = openSync(path, "r");
276
+ try {
277
+ const buf = Buffer.allocUnsafe(size - start);
278
+ const read = readSync(fd, buf, 0, size - start, start);
279
+ return decoder.write(buf.subarray(0, read));
280
+ } finally {
281
+ closeSync(fd);
282
+ }
283
+ }
276
284
  function tsOf(record) {
277
285
  return typeof record?.ts === "number" && Number.isFinite(record.ts) ? record.ts : null;
278
286
  }
@@ -287,4 +295,4 @@ function sortByTs(records) {
287
295
  });
288
296
  }
289
297
  //#endregion
290
- export { followLogs as a, queryLogs as c, fileSizes as i, readLogRecords as l, buildCriteria as n, logFiles as o, compileGrep as r, parseSince as s, attachExpoErrorContext as t };
298
+ export { fileSizes as a, parseSince as c, recordMatches as d, sortByTs as f, compileGrep as i, queryLogs as l, attachExpoErrorContext as n, followLogs as o, buildCriteria as r, logFiles as s, ERROR_SOURCES as t, readLogRecords as u };
@@ -1,6 +1,6 @@
1
- import { t as getExecutor } from "./exec-B29XNuhP.mjs";
2
- import { A as isOnMountedVolume, g as loadConfig, j as listMountedVolumes, n as allMetroPorts, r as claimMetroPort, v as removeProject, w as process_identity_exports } from "./config-B7iyKWkL.mjs";
3
- import { r as readWorkspaceState } from "./workspace-state-BT5E3SA2.mjs";
1
+ import { D as process_identity_exports, I as isOnMountedVolume, L as listMountedVolumes, g as loadConfig, n as allMetroPorts, r as claimMetroPort, y as releaseMetroPort } from "./config-Cp9lM3wX.mjs";
2
+ import { t as getExecutor } from "./exec-jIAlgilk.mjs";
3
+ import { r as readWorkspaceState } from "./workspace-state-DF2gCFhx.mjs";
4
4
  import { existsSync, readlinkSync, realpathSync } from "fs";
5
5
  import { sep } from "path";
6
6
  import { request } from "http";
@@ -75,7 +75,7 @@ async function findReclaimablePort(excludeProjectPath, probe = isMetroRunning, {
75
75
  async function allocatePort(projectPath, probe = isMetroRunning, isFree = isPortFree) {
76
76
  const reclaim = await findReclaimablePort(projectPath, probe);
77
77
  if (reclaim && await isFree(reclaim.port)) {
78
- removeProject(reclaim.ownerPath);
78
+ releaseMetroPort(reclaim.ownerPath, reclaim.port);
79
79
  return reclaim.port;
80
80
  }
81
81
  return computeNextPort(isFree);
@@ -139,6 +139,76 @@ function parseLsofCwd(out) {
139
139
  const nLine = lines.slice(idx + 1).find((l) => l.startsWith("n"));
140
140
  return nLine ? nLine.slice(1) : null;
141
141
  }
142
+ function lsofOutput(args) {
143
+ return getExecutor().runFileAsync("lsof", args).catch((error) => typeof error?.stdout === "string" ? error.stdout : null);
144
+ }
145
+ function parseLsofListeners(out) {
146
+ const byPort = /* @__PURE__ */ new Map();
147
+ let pid = null;
148
+ for (const line of String(out ?? "").split("\n")) if (line.startsWith("p")) pid = parseInt(line.slice(1), 10);
149
+ else if (line.startsWith("n") && pid !== null && Number.isFinite(pid)) {
150
+ const port = addressPort(line.slice(1));
151
+ const pids = byPort.get(port) ?? [];
152
+ if (!pids.includes(pid)) pids.push(pid);
153
+ byPort.set(port, pids);
154
+ }
155
+ return byPort;
156
+ }
157
+ /**
158
+ * `listeningPids` for several ports with one lsof call. A port lsof could not be asked about is
159
+ * missing from the map.
160
+ */
161
+ async function listeningPidsByPort(ports) {
162
+ const result = /* @__PURE__ */ new Map();
163
+ if (ports.length === 0) return result;
164
+ const out = await lsofOutput([
165
+ "-nP",
166
+ `-iTCP:${ports.join(",")}`,
167
+ "-sTCP:LISTEN",
168
+ "-Fpn"
169
+ ]);
170
+ if (out === null) return result;
171
+ const found = parseLsofListeners(out);
172
+ for (const port of ports) result.set(port, found.get(port) ?? []);
173
+ return result;
174
+ }
175
+ function parseLsofCwds(out) {
176
+ const cwds = /* @__PURE__ */ new Map();
177
+ let pid = null;
178
+ let inCwd = false;
179
+ for (const line of String(out ?? "").split("\n")) if (line.startsWith("p")) {
180
+ pid = parseInt(line.slice(1), 10);
181
+ inCwd = false;
182
+ } else if (line.startsWith("f")) inCwd = line === "fcwd";
183
+ else if (inCwd && line.startsWith("n") && pid !== null && !cwds.has(pid)) cwds.set(pid, line.slice(1));
184
+ return cwds;
185
+ }
186
+ /** `processCwd` for several pids with at most one lsof call. A pid lsof could not be asked about is missing from the map. */
187
+ async function processCwds(pids) {
188
+ const cwds = /* @__PURE__ */ new Map();
189
+ const viaLsof = [];
190
+ for (const pid of new Set(pids)) {
191
+ let cwd = null;
192
+ if (process.platform === "linux") try {
193
+ cwd = readlinkSync(`/proc/${pid}/cwd`);
194
+ } catch {}
195
+ if (cwd) cwds.set(pid, cwd);
196
+ else viaLsof.push(pid);
197
+ }
198
+ if (viaLsof.length === 0) return cwds;
199
+ const out = await lsofOutput([
200
+ "-a",
201
+ "-p",
202
+ viaLsof.join(","),
203
+ "-d",
204
+ "cwd",
205
+ "-Fn"
206
+ ]);
207
+ if (out === null) return cwds;
208
+ const found = parseLsofCwds(out);
209
+ for (const pid of viaLsof) cwds.set(pid, found.get(pid) ?? null);
210
+ return cwds;
211
+ }
142
212
  function processCwd(pid) {
143
213
  if (process.platform === "linux") try {
144
214
  return readlinkSync(`/proc/${pid}/cwd`);
@@ -166,15 +236,15 @@ function ownedSupervisor(projectPath, port) {
166
236
  if (supervisor?.port !== port || (0, process_identity_exports.inspectProcessIdentity)(supervisor) !== "same") return null;
167
237
  return supervisor;
168
238
  }
169
- async function resolveProjectMetro(port, projectPath, { probe = isMetroRunning, cwdOf = processCwd } = {}) {
170
- const pid = listeningPids(port)[0];
239
+ async function resolveProjectMetro(port, projectPath, { probe = isMetroRunning, pidsOf = listeningPids, cwdOf = processCwd } = {}) {
240
+ const pid = (await pidsOf(port))[0];
171
241
  if (pid === void 0) return { missing: true };
172
242
  if (!await probe(port)) return {
173
243
  notOurs: `pid ${pid} on port ${port} does not answer Metro's /status`,
174
244
  kind: NOT_OURS_UNRESPONSIVE,
175
245
  pid
176
246
  };
177
- const cwd = cwdOf(pid);
247
+ const cwd = await cwdOf(pid);
178
248
  if (!cwd) {
179
249
  const owner = ownedSupervisor(projectPath, port);
180
250
  if (owner && (pid === owner.pid || pid === owner.serverPid)) return { metro: {
@@ -230,4 +300,4 @@ function killMetroTree(leader, processToken) {
230
300
  }
231
301
  }
232
302
  //#endregion
233
- export { resolveProjectMetro as a, isPortFree as c, pidExists as i, reserveMetroPort as l, killMetroTree as n, signalProcessTree as o, listeningPids as r, isMetroRunning as s, NOT_OURS_FOREIGN_CWD as t };
303
+ export { pidExists as a, resolveProjectMetro as c, isPortFree as d, reserveMetroPort as f, listeningPidsByPort as i, signalProcessTree as l, killMetroTree as n, processCwd as o, listeningPids as r, processCwds as s, NOT_OURS_FOREIGN_CWD as t, isMetroRunning as u };