peachsim 0.1.1469 → 0.1.1481

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 (102) hide show
  1. package/cli/commands/control.ts +68 -42
  2. package/cli/commands/detox.ts +15 -10
  3. package/cli/commands/flow.ts +1 -2
  4. package/cli/commands/inspect/settling.ts +5 -1
  5. package/cli/commands/setup.ts +20 -13
  6. package/cli/commands/test.ts +2 -1
  7. package/cli/dev-build-freshness.ts +14 -14
  8. package/cli/drivers/playwright-sim-host.ts +7 -2
  9. package/cli/drivers/playwright.ts +6 -6
  10. package/cli/outbound-endpoints.ts +80 -6
  11. package/detox/index.ts +27 -14
  12. package/detox/matchers.ts +17 -6
  13. package/dist-lib/agent-daemon-client.cjs +1 -1
  14. package/dist-lib/agent-events.cjs +1 -1
  15. package/dist-lib/attached-projects.cjs +1 -1
  16. package/dist-lib/auth/shared-session.cjs +1 -1
  17. package/dist-lib/backend-origin.cjs +1 -1
  18. package/dist-lib/beta.cjs +1 -1
  19. package/dist-lib/beta.mjs +1 -1
  20. package/dist-lib/bridge-constants.cjs +1 -1
  21. package/dist-lib/bridge-contract-input.cjs +1 -1
  22. package/dist-lib/bridge-contract-input.mjs +1 -1
  23. package/dist-lib/bridge-contract.cjs +1 -1
  24. package/dist-lib/bridge-contract.mjs +1 -1
  25. package/dist-lib/bundle-channel.cjs +54 -15
  26. package/dist-lib/capture-contract.cjs +1 -1
  27. package/dist-lib/capture-contract.mjs +1 -1
  28. package/dist-lib/cloud-contract.cjs +1 -1
  29. package/dist-lib/cloud-contract.mjs +1 -1
  30. package/dist-lib/cloud-test-args.cjs +1 -1
  31. package/dist-lib/cloud-test-args.mjs +1 -1
  32. package/dist-lib/cloud.cjs +1 -1
  33. package/dist-lib/cloud.mjs +1 -1
  34. package/dist-lib/config.cjs +19 -3
  35. package/dist-lib/config.mjs +19 -3
  36. package/dist-lib/detox/index.cjs +125 -52
  37. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  38. package/dist-lib/expo-swift-bundle.cjs +30 -9
  39. package/dist-lib/expo-swift-metadata.cjs +18 -6
  40. package/dist-lib/expo-swift-metadata.mjs +18 -6
  41. package/dist-lib/home-paths.cjs +1 -1
  42. package/dist-lib/host/bridge-host.cjs +621 -390
  43. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  44. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  45. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  46. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  47. package/dist-lib/host/websocket-proxy.cjs +1 -1
  48. package/dist-lib/index.cjs +46 -19
  49. package/dist-lib/jump-to-source-babel.cjs +1 -1
  50. package/dist-lib/jump-to-source-native.cjs +1 -1
  51. package/dist-lib/kotlin-host.cjs +1 -1
  52. package/dist-lib/menu.cjs +1 -1
  53. package/dist-lib/menu.mjs +1 -1
  54. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  55. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  56. package/dist-lib/metro-production-bundle.cjs +24 -6
  57. package/dist-lib/metro-production-bundle.mjs +22 -6
  58. package/dist-lib/metro.cjs +41 -18
  59. package/dist-lib/native-app-identity-project.cjs +1 -1
  60. package/dist-lib/native-app-identity-project.mjs +1 -1
  61. package/dist-lib/native-app-identity.cjs +19 -5
  62. package/dist-lib/native-app-identity.mjs +19 -5
  63. package/dist-lib/native-firebase-metadata.cjs +22 -7
  64. package/dist-lib/native-firebase-metadata.mjs +20 -7
  65. package/dist-lib/plist.cjs +1 -1
  66. package/dist-lib/plist.mjs +1 -1
  67. package/dist-lib/profiles.cjs +1 -1
  68. package/dist-lib/public-brand.cjs +1 -1
  69. package/dist-lib/react-native-host-modules.cjs +3 -1
  70. package/dist-lib/react-native-host-modules.mjs +3 -1
  71. package/dist-lib/scripts/dev-server-scanner.cjs +404 -279
  72. package/dist-lib/sdk.cjs +6 -2
  73. package/dist-lib/sdk.mjs +6 -2
  74. package/dist-lib/skills.cjs +757 -531
  75. package/dist-lib/swift-compile.cjs +7 -2
  76. package/dist-lib/swift-compile.mjs +7 -2
  77. package/dist-lib/swift-package.cjs +1 -1
  78. package/dist-lib/swift-package.mjs +1 -1
  79. package/dist-lib/swift-submit.cjs +1 -1
  80. package/dist-lib/swift-submit.mjs +1 -1
  81. package/dist-lib/swift-toolchain.cjs +1 -1
  82. package/dist-lib/swift.cjs +1 -1
  83. package/dist-lib/vite-swift.cjs +7 -2
  84. package/dist-lib/vite.cjs +1 -1
  85. package/dist-lib/worklet-executable.cjs +27 -7
  86. package/dist-lib/worklet-executable.mjs +27 -7
  87. package/package.json +1 -1
  88. package/scripts/dev-server-scanner.ts +493 -188
  89. package/src/admitted-mac-build.ts +12 -0
  90. package/src/bridge-constants.ts +5 -3
  91. package/src/bundleGlobalFooter.ts +26 -3
  92. package/src/config.ts +3 -2
  93. package/src/configured-bundle-producer.ts +2 -1
  94. package/src/expo-swift-host.ts +33 -7
  95. package/src/host/bridge-host.ts +34 -6
  96. package/src/metro-production-bundle.ts +11 -4
  97. package/src/native-app-identity.ts +3 -2
  98. package/src/native-firebase-metadata.ts +5 -3
  99. package/src/react-native-host-modules.ts +2 -0
  100. package/src/swift-compile.ts +5 -4
  101. package/src/worker-system-chunks.ts +61 -58
  102. package/src/worklet-executable.ts +38 -6
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1469 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1481 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -280,6 +280,7 @@ __export(dev_server_scanner_exports, {
280
280
  __shouldReuseScannerCacheEntry: () => __shouldReuseScannerCacheEntry,
281
281
  discoverListeningPorts: () => discoverListeningPorts,
282
282
  discoverListeningProcesses: () => discoverListeningProcesses,
283
+ pinSharedListenerScan: () => pinSharedListenerScan,
283
284
  probePort: () => probePort,
284
285
  resolveListeningPid: () => resolveListeningPid,
285
286
  resolveProcessCwd: () => resolveProcessCwd,
@@ -292,161 +293,9 @@ var import_promises2 = require("fs/promises");
292
293
  var import_http = __toESM(require("http"), 1);
293
294
  var import_net = __toESM(require("net"), 1);
294
295
  var import_node_fs2 = require("node:fs");
296
+ var import_node_os = require("node:os");
295
297
  var import_node_path3 = require("node:path");
296
298
 
297
- // ../contrast/src/runtime/process-group.ts
298
- var import_node_child_process = require("node:child_process");
299
-
300
- // ../contrast/src/sleep.ts
301
- function sleep(ms) {
302
- return new Promise((resolve2) => setTimeout(resolve2, ms));
303
- }
304
-
305
- // ../contrast/src/runtime/process-group.ts
306
- var GRACEFUL_TIMEOUT_MS = 3e3;
307
- var FORCED_EXIT_TIMEOUT_MS = 1e3;
308
- function signalProcessGroup(child, signal) {
309
- if (!child.pid) return;
310
- try {
311
- process.kill(-child.pid, signal);
312
- } catch {
313
- try {
314
- process.kill(child.pid, signal);
315
- } catch {
316
- }
317
- }
318
- }
319
- function runningPids(pids) {
320
- return pids.filter((pid) => {
321
- try {
322
- process.kill(pid, 0);
323
- return true;
324
- } catch (error) {
325
- return error instanceof Error && "code" in error && error.code === "EPERM";
326
- }
327
- });
328
- }
329
- function groupIsEmpty(pgid) {
330
- try {
331
- process.kill(-pgid, 0);
332
- } catch (error) {
333
- if (!(error instanceof Error && "code" in error && error.code === "EPERM"))
334
- return true;
335
- }
336
- const result = (0, import_node_child_process.spawnSync)("ps", ["-A", "-o", "pgid=,stat="], {
337
- encoding: "utf8",
338
- stdio: ["ignore", "pipe", "pipe"]
339
- });
340
- if (result.error || result.status !== 0) return false;
341
- return !result.stdout.split("\n").some((line) => {
342
- const [group, state] = line.trim().split(/\s+/);
343
- return Number(group) === pgid && state !== void 0 && !state.startsWith("Z");
344
- });
345
- }
346
- async function waitForExit(pids, pgid, timeoutMs) {
347
- const deadline = Date.now() + timeoutMs;
348
- const gone = () => runningPids(pids).length === 0 && groupIsEmpty(pgid);
349
- while (Date.now() < deadline) {
350
- if (gone()) return true;
351
- await sleep(50);
352
- }
353
- return gone();
354
- }
355
- async function terminateProcessGroup(child, signal = "SIGTERM", gracefulTimeoutMs = GRACEFUL_TIMEOUT_MS) {
356
- if (!child.pid) {
357
- try {
358
- child.kill(signal);
359
- } catch {
360
- }
361
- return;
362
- }
363
- signalProcessGroup(child, "SIGSTOP");
364
- const descendants = /* @__PURE__ */ new Set();
365
- let parents = [child.pid];
366
- let inspectionError = null;
367
- while (parents.length > 0) {
368
- const result = (0, import_node_child_process.spawnSync)("pgrep", ["-P", parents.join(",")], {
369
- encoding: "utf8",
370
- stdio: ["ignore", "pipe", "pipe"]
371
- });
372
- if (result.error || result.status !== 0 && result.status !== 1) {
373
- inspectionError = new Error(
374
- `could not inspect descendants of processes ${parents.join(",")}: ${result.error?.message || result.stderr.trim() || `pgrep exited ${result.status}`}`
375
- );
376
- break;
377
- }
378
- const next = [];
379
- for (const line of result.stdout.split("\n")) {
380
- const descendantPid = Number(line.trim());
381
- if (Number.isInteger(descendantPid) && descendantPid > 1 && !descendants.has(descendantPid)) {
382
- descendants.add(descendantPid);
383
- next.push(descendantPid);
384
- try {
385
- process.kill(descendantPid, "SIGSTOP");
386
- } catch {
387
- }
388
- }
389
- }
390
- parents = next;
391
- }
392
- const pids = [child.pid, ...descendants];
393
- const gracefulDeadline = Date.now() + gracefulTimeoutMs;
394
- try {
395
- process.kill(child.pid, signal);
396
- } catch {
397
- }
398
- signalProcessGroup(child, "SIGCONT");
399
- for (const pid of descendants) {
400
- try {
401
- process.kill(pid, "SIGCONT");
402
- } catch {
403
- }
404
- }
405
- let rootExited = child.exitCode !== null || child.signalCode !== null;
406
- if (!rootExited) {
407
- rootExited = await new Promise((resolveExit) => {
408
- const onExit = () => {
409
- clearTimeout(deadlineTimer);
410
- resolveExit(true);
411
- };
412
- const deadlineTimer = setTimeout(
413
- () => {
414
- child.removeListener("exit", onExit);
415
- resolveExit(false);
416
- },
417
- Math.max(0, gracefulDeadline - Date.now())
418
- );
419
- child.once("exit", onExit);
420
- if (child.exitCode !== null || child.signalCode !== null) {
421
- child.removeListener("exit", onExit);
422
- onExit();
423
- }
424
- });
425
- }
426
- if (rootExited) {
427
- signalProcessGroup(child, signal);
428
- for (const pid of descendants) {
429
- try {
430
- process.kill(pid, signal);
431
- } catch {
432
- }
433
- }
434
- }
435
- if (await waitForExit(pids, child.pid, Math.max(0, gracefulDeadline - Date.now()))) {
436
- if (inspectionError) throw inspectionError;
437
- return;
438
- }
439
- for (const pid of pids) {
440
- try {
441
- process.kill(pid, "SIGKILL");
442
- } catch {
443
- }
444
- }
445
- signalProcessGroup(child, "SIGKILL");
446
- await waitForExit(pids, child.pid, FORCED_EXIT_TIMEOUT_MS);
447
- if (inspectionError) throw inspectionError;
448
- }
449
-
450
299
  // src/backend-origin.ts
451
300
  var LOOPBACK_PROBE_HOSTNAMES = ["127.0.0.1", "::1"];
452
301
  function normalizeHostname(hostname) {
@@ -990,52 +839,211 @@ function optionalDemoAppsSync() {
990
839
  }
991
840
 
992
841
  // scripts/dev-server-scanner.ts
993
- function execGuarded(command, timeout, extra = {}) {
994
- const maxBuffer = extra.maxBuffer ?? 1024 * 1024;
995
- return new Promise((resolve2, reject) => {
996
- const child = (0, import_child_process.spawn)("/bin/sh", ["-c", command], {
997
- detached: true,
998
- stdio: ["ignore", "pipe", "ignore"]
999
- });
1000
- let stdout = "";
1001
- let settled = false;
1002
- let killing = false;
1003
- const settle = (err) => {
1004
- if (settled) return;
1005
- settled = true;
1006
- clearTimeout(deadline);
1007
- if (err) reject(err);
1008
- else resolve2({ stdout });
1009
- };
1010
- const killThenSettle = (err) => {
1011
- if (killing) return;
1012
- killing = true;
1013
- clearTimeout(deadline);
1014
- terminateProcessGroup(child, "SIGKILL", 0).finally(() => settle(err));
1015
- };
1016
- const deadline = setTimeout(() => {
1017
- killThenSettle(
1018
- Object.assign(new Error(`\`${command}\` did not exit within ${timeout}ms`), {
1019
- killed: true
1020
- })
1021
- );
1022
- }, timeout);
1023
- child.stdout.setEncoding("utf8");
1024
- child.stdout.on("data", (chunk) => {
1025
- stdout += chunk;
1026
- if (stdout.length > maxBuffer) {
1027
- killThenSettle(new Error(`\`${command}\` exceeded maxBuffer ${maxBuffer}`));
842
+ var LISTENER_LOCK_PATH = (0, import_node_path3.join)((0, import_node_os.tmpdir)(), "peach-listener-scan.lock");
843
+ var LISTENER_RESULT_PATH = (0, import_node_path3.join)((0, import_node_os.tmpdir)(), "peach-listener-scan.result");
844
+ var LISTENER_LOCK_STALE_MS = 25e3;
845
+ var LISTENER_LOCK_WAIT_MS = 8e3;
846
+ function sleep(ms) {
847
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
848
+ }
849
+ function pidIsAlive(pid) {
850
+ if (!Number.isInteger(pid) || pid <= 0) return false;
851
+ try {
852
+ process.kill(pid, 0);
853
+ return true;
854
+ } catch (error) {
855
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
856
+ return code !== "ESRCH";
857
+ }
858
+ }
859
+ function readLockOwner() {
860
+ try {
861
+ const raw = (0, import_node_fs2.readFileSync)(LISTENER_LOCK_PATH, "utf8");
862
+ const mtimeMs = (0, import_node_fs2.statSync)(LISTENER_LOCK_PATH).mtimeMs;
863
+ const pid = Number(raw.split("-")[0]);
864
+ return { raw, pid: Number.isInteger(pid) ? pid : -1, mtimeMs };
865
+ } catch {
866
+ return null;
867
+ }
868
+ }
869
+ function lockIsAbandoned(owner) {
870
+ if (owner.pid <= 0 || !pidIsAlive(owner.pid)) return true;
871
+ return Date.now() - owner.mtimeMs > LISTENER_LOCK_STALE_MS;
872
+ }
873
+ function clearLock(raw) {
874
+ try {
875
+ if ((0, import_node_fs2.readFileSync)(LISTENER_LOCK_PATH, "utf8") !== raw) return;
876
+ (0, import_node_fs2.unlinkSync)(LISTENER_LOCK_PATH);
877
+ } catch {
878
+ }
879
+ }
880
+ function readResult() {
881
+ try {
882
+ const parsed = JSON.parse(
883
+ (0, import_node_fs2.readFileSync)(LISTENER_RESULT_PATH, "utf8")
884
+ );
885
+ if (!parsed || typeof parsed.token !== "string" || typeof parsed.key !== "string")
886
+ return null;
887
+ return parsed;
888
+ } catch {
889
+ return null;
890
+ }
891
+ }
892
+ function publishResult(payload) {
893
+ const tmp = `${LISTENER_RESULT_PATH}.${process.pid}.tmp`;
894
+ (0, import_node_fs2.writeFileSync)(tmp, JSON.stringify(payload));
895
+ (0, import_node_fs2.renameSync)(tmp, LISTENER_RESULT_PATH);
896
+ }
897
+ function throwPublished(payload) {
898
+ throw Object.assign(new Error(payload.message || "listener scan failed"), {
899
+ killed: payload.killed === true
900
+ });
901
+ }
902
+ async function waitForSharedResult(owner, key) {
903
+ const cap = Date.now() + LISTING_TIMEOUT_MS;
904
+ let current = owner;
905
+ while (Date.now() < cap) {
906
+ const published = readResult();
907
+ if (published && published.token === current.raw) {
908
+ if (published.key !== key) return { kind: "retry" };
909
+ if (!published.ok) throwPublished(published);
910
+ return { kind: "value", value: published.value };
911
+ }
912
+ const latest = readLockOwner();
913
+ if (!latest) {
914
+ const again = readResult();
915
+ if (again && again.token === current.raw && again.key === key) {
916
+ if (!again.ok) throwPublished(again);
917
+ return { kind: "value", value: again.value };
1028
918
  }
1029
- });
1030
- child.on("error", (err) => {
1031
- if (!killing) settle(err);
1032
- });
1033
- child.on("close", (code) => {
1034
- if (killing) return;
1035
- settle(code === 0 ? null : new Error(`\`${command}\` exited with ${code}`));
1036
- });
919
+ return { kind: "retry" };
920
+ }
921
+ if (latest.raw !== current.raw) current = latest;
922
+ if (lockIsAbandoned(latest)) {
923
+ clearLock(latest.raw);
924
+ return { kind: "retry" };
925
+ }
926
+ await sleep(25);
927
+ }
928
+ throw Object.assign(new Error("listener scan lock was held past the wait"), {
929
+ killed: true
1037
930
  });
1038
931
  }
932
+ async function withListenerLock(key, body) {
933
+ const token = `${process.pid}-${Date.now()}-${Math.random()}`;
934
+ const deadline = Date.now() + LISTENER_LOCK_WAIT_MS;
935
+ for (; ; ) {
936
+ let fd = null;
937
+ try {
938
+ fd = (0, import_node_fs2.openSync)(LISTENER_LOCK_PATH, "wx");
939
+ } catch (error) {
940
+ const code = error instanceof Error && "code" in error ? error.code : "";
941
+ if (code !== "EEXIST") throw error;
942
+ const owner = readLockOwner();
943
+ if (!owner || lockIsAbandoned(owner)) {
944
+ if (owner) clearLock(owner.raw);
945
+ else {
946
+ try {
947
+ (0, import_node_fs2.unlinkSync)(LISTENER_LOCK_PATH);
948
+ } catch {
949
+ }
950
+ }
951
+ continue;
952
+ }
953
+ if (Date.now() >= deadline) {
954
+ const shared = await waitForSharedResult(owner, key);
955
+ if (shared.kind === "value") return shared.value;
956
+ continue;
957
+ }
958
+ await sleep(25);
959
+ continue;
960
+ }
961
+ if (fd === null) continue;
962
+ try {
963
+ (0, import_node_fs2.writeSync)(fd, token);
964
+ try {
965
+ const value = await body();
966
+ publishResult({ token, key, ok: true, value });
967
+ return value;
968
+ } catch (error) {
969
+ publishResult({
970
+ token,
971
+ key,
972
+ ok: false,
973
+ message: error instanceof Error ? error.message : String(error),
974
+ killed: !!(error && typeof error === "object" && error.killed)
975
+ });
976
+ throw error;
977
+ }
978
+ } finally {
979
+ try {
980
+ (0, import_node_fs2.closeSync)(fd);
981
+ } catch {
982
+ }
983
+ clearLock(token);
984
+ }
985
+ }
986
+ }
987
+ function spawnReaped(file, args, timeoutMs, maxBuffer = 1024 * 1024) {
988
+ return withListenerLock(
989
+ `${file}\0${args.join("\0")}`,
990
+ () => new Promise((resolvePromise, reject) => {
991
+ const child = (0, import_child_process.spawn)(file, args, {
992
+ stdio: ["ignore", "pipe", "pipe"]
993
+ });
994
+ let stdout = "";
995
+ let settled = false;
996
+ let timedOut = false;
997
+ let overflow = false;
998
+ let spawnError = null;
999
+ const timeoutError = () => Object.assign(new Error(`\`${file}\` did not exit within ${timeoutMs}ms`), {
1000
+ killed: true
1001
+ });
1002
+ const finish = (err, code) => {
1003
+ if (settled) return;
1004
+ settled = true;
1005
+ clearTimeout(deadline);
1006
+ clearTimeout(backup);
1007
+ if (err) reject(err);
1008
+ else resolvePromise({ stdout, code });
1009
+ };
1010
+ const killChild = () => {
1011
+ if (child.exitCode !== null || child.signalCode !== null) return;
1012
+ try {
1013
+ child.kill("SIGKILL");
1014
+ } catch {
1015
+ }
1016
+ };
1017
+ let backup;
1018
+ const deadline = setTimeout(() => {
1019
+ timedOut = true;
1020
+ killChild();
1021
+ backup = setTimeout(() => finish(timeoutError(), null), 1e3);
1022
+ }, timeoutMs);
1023
+ child.stdout?.setEncoding("utf8");
1024
+ child.stderr?.on("data", () => {
1025
+ });
1026
+ child.stdout?.on("data", (chunk) => {
1027
+ stdout += chunk;
1028
+ if (stdout.length > maxBuffer) {
1029
+ overflow = true;
1030
+ killChild();
1031
+ }
1032
+ });
1033
+ child.on("error", (err) => {
1034
+ spawnError = err;
1035
+ killChild();
1036
+ });
1037
+ child.on("close", (code) => {
1038
+ if (timedOut) finish(timeoutError(), code);
1039
+ else if (overflow)
1040
+ finish(new Error(`\`${file}\` exceeded maxBuffer ${maxBuffer}`), code);
1041
+ else if (spawnError && !stdout) finish(spawnError, code);
1042
+ else finish(null, code);
1043
+ });
1044
+ })
1045
+ );
1046
+ }
1039
1047
  var TIMEOUT_MS = 250;
1040
1048
  var MANIFEST_TIMEOUT_MS = 1500;
1041
1049
  var TCP_GATE_MS = 120;
@@ -1155,57 +1163,145 @@ async function scanListeningProcesses(excludePorts = []) {
1155
1163
  processes: listing.processes.filter((p) => acceptPort(p.port, excluded))
1156
1164
  };
1157
1165
  }
1166
+ var LISTENER_CACHE_MS = 2e3;
1167
+ var listenerCache = null;
1158
1168
  var inflightListing = null;
1169
+ var listenerHolds = 0;
1170
+ function pinSharedListenerScan() {
1171
+ listenerHolds += 1;
1172
+ let released = false;
1173
+ return () => {
1174
+ if (released) return;
1175
+ released = true;
1176
+ listenerHolds -= 1;
1177
+ if (listenerHolds === 0) listenerCache = null;
1178
+ };
1179
+ }
1180
+ function cachedPidsAlive(scan) {
1181
+ if (scan.state !== "looked") return true;
1182
+ return scan.processes.every((entry) => entry.pid <= 0 || pidIsAlive(entry.pid));
1183
+ }
1184
+ async function cachedPortsStillHeld(scan) {
1185
+ if (scan.state !== "looked") return true;
1186
+ const owned = scan.processes.filter((entry) => entry.pid > 0);
1187
+ if (owned.length === 0) return true;
1188
+ const pids = [...new Set(owned.map((entry) => entry.pid))];
1189
+ try {
1190
+ const { stdout, code } = await spawnReaped(
1191
+ "lsof",
1192
+ ["-nP", "-b", "-w", "-a", "-p", pids.join(","), "-iTCP", "-sTCP:LISTEN", "-Fpcn"],
1193
+ 8e3,
1194
+ 16 * 1024 * 1024
1195
+ );
1196
+ if (code !== 0 && code !== 1) return false;
1197
+ const live = new Set(
1198
+ parseLsofListeners(stdout).map((entry) => `${entry.port}:${entry.pid}`)
1199
+ );
1200
+ return owned.every((entry) => live.has(`${entry.port}:${entry.pid}`));
1201
+ } catch {
1202
+ return false;
1203
+ }
1204
+ }
1159
1205
  function listListenersShared() {
1160
- inflightListing ??= listListeners().finally(() => {
1206
+ inflightListing ??= loadListeners().finally(() => {
1161
1207
  inflightListing = null;
1162
1208
  });
1163
1209
  return inflightListing;
1164
1210
  }
1211
+ async function loadListeners() {
1212
+ const cached2 = listenerCache;
1213
+ if (cached2 && (listenerHolds > 0 || Date.now() - cached2.at < LISTENER_CACHE_MS) && cachedPidsAlive(cached2.scan) && await cachedPortsStillHeld(cached2.scan)) {
1214
+ return cached2.scan;
1215
+ }
1216
+ listenerCache = null;
1217
+ const scan = await listListeners();
1218
+ listenerCache = { at: Date.now(), scan };
1219
+ return scan;
1220
+ }
1221
+ function isSpawnMissing(err) {
1222
+ return !!(err && typeof err === "object" && err.code === "ENOENT");
1223
+ }
1224
+ function parseLsofListeners(stdout) {
1225
+ const seen = /* @__PURE__ */ new Map();
1226
+ let pid = 0;
1227
+ let command = "";
1228
+ for (const line of stdout.split("\n")) {
1229
+ if (line.startsWith("p")) {
1230
+ pid = Number(line.slice(1));
1231
+ command = "";
1232
+ continue;
1233
+ }
1234
+ if (line.startsWith("c")) {
1235
+ command = line.slice(1);
1236
+ continue;
1237
+ }
1238
+ if (!line.startsWith("n") || pid <= 0) continue;
1239
+ if (!/^(node|bun|peach|vite)/.test(command)) continue;
1240
+ const match = line.match(/:(\d+)$/);
1241
+ if (!match) continue;
1242
+ const port = Number(match[1]);
1243
+ if (!seen.has(port)) seen.set(port, pid);
1244
+ }
1245
+ return [...seen.entries()].map(([port, owner]) => ({ port, pid: owner }));
1246
+ }
1247
+ function parseSsListeners(stdout) {
1248
+ const seen = /* @__PURE__ */ new Map();
1249
+ for (const line of stdout.split("\n")) {
1250
+ if (!/"(node|bun|peach|vite)/.test(line)) continue;
1251
+ const portMatch = line.match(/:(\d+)\s/);
1252
+ const pidMatch = line.match(/pid=(\d+)/);
1253
+ if (!portMatch) continue;
1254
+ const port = Number(portMatch[1]);
1255
+ const pid = pidMatch ? Number(pidMatch[1]) : 0;
1256
+ if (!seen.has(port)) seen.set(port, pid);
1257
+ }
1258
+ return [...seen.entries()].map(([port, pid]) => ({ port, pid }));
1259
+ }
1165
1260
  async function listListeners() {
1166
1261
  let timedOut = false;
1167
1262
  try {
1168
- const { stdout } = await execGuarded(
1169
- `lsof -a -c node -c bun -c peach -iTCP -sTCP:LISTEN -P -n 2>/dev/null | grep -E '^(node|bun|peach)'`,
1170
- LISTING_TIMEOUT_MS
1263
+ const { stdout, code } = await spawnReaped(
1264
+ "lsof",
1265
+ [
1266
+ "-nP",
1267
+ "-a",
1268
+ "-c",
1269
+ "node",
1270
+ "-c",
1271
+ "bun",
1272
+ "-c",
1273
+ "peach",
1274
+ "-c",
1275
+ "vite",
1276
+ "-iTCP",
1277
+ "-sTCP:LISTEN",
1278
+ "-Fpcn"
1279
+ ],
1280
+ LISTING_TIMEOUT_MS,
1281
+ 16 * 1024 * 1024
1171
1282
  );
1172
- const seen = /* @__PURE__ */ new Map();
1173
- for (const line of stdout.trim() ? stdout.trim().split("\n") : []) {
1174
- const parts = line.trim().split(/\s+/);
1175
- if (parts.length < 9) continue;
1176
- const pid = Number(parts[1]);
1177
- const m = parts[8].match(/:(\d+)$/);
1178
- if (!m) continue;
1179
- const port = Number(m[1]);
1180
- if (!seen.has(port)) seen.set(port, pid);
1283
+ if (code === 0 || code === 1) {
1284
+ return { state: "looked", processes: parseLsofListeners(stdout) };
1181
1285
  }
1182
- return {
1183
- state: "looked",
1184
- processes: [...seen.entries()].map(([port, pid]) => ({ port, pid }))
1185
- };
1186
1286
  } catch (err) {
1187
1287
  if (wasKilledByTimeout(err)) timedOut = true;
1288
+ else if (!isSpawnMissing(err)) return { state: "could-not-look" };
1188
1289
  }
1189
- try {
1190
- const { stdout } = await execGuarded(
1191
- `ss -tlnp 2>/dev/null | grep -E '"(node|bun|peach)"'`,
1192
- LISTING_TIMEOUT_MS
1193
- );
1194
- const seen = /* @__PURE__ */ new Map();
1195
- for (const line of stdout.trim() ? stdout.trim().split("\n") : []) {
1196
- const portMatch = line.match(/:(\d+)\s/);
1197
- const pidMatch = line.match(/pid=(\d+)/);
1198
- if (!portMatch) continue;
1199
- const port = Number(portMatch[1]);
1200
- const pid = pidMatch ? Number(pidMatch[1]) : 0;
1201
- if (!seen.has(port)) seen.set(port, pid);
1290
+ if (!timedOut) {
1291
+ try {
1292
+ const { stdout, code } = await spawnReaped(
1293
+ "ss",
1294
+ ["-H", "-ltnp"],
1295
+ LISTING_TIMEOUT_MS,
1296
+ 16 * 1024 * 1024
1297
+ );
1298
+ if (code === 0 || code === 1) {
1299
+ return { state: "looked", processes: parseSsListeners(stdout) };
1300
+ }
1301
+ } catch (err) {
1302
+ if (wasKilledByTimeout(err)) timedOut = true;
1303
+ else if (!isSpawnMissing(err)) return { state: "could-not-look" };
1202
1304
  }
1203
- return {
1204
- state: "looked",
1205
- processes: [...seen.entries()].map(([port, pid]) => ({ port, pid }))
1206
- };
1207
- } catch (err) {
1208
- if (wasKilledByTimeout(err)) timedOut = true;
1209
1305
  }
1210
1306
  if (timedOut) return { state: "could-not-look" };
1211
1307
  return { state: "looked", processes: FALLBACK_PORTS };
@@ -1216,61 +1312,86 @@ async function discoverListeningPorts(excludePorts = []) {
1216
1312
  }
1217
1313
  async function resolveListeningPid(port) {
1218
1314
  if (!Number.isInteger(port) || port <= 0) return null;
1219
- if (process.platform === "darwin") {
1220
- try {
1221
- const { stdout } = await execGuarded(
1222
- `lsof -nP -a -iTCP:${port} -sTCP:LISTEN -t 2>/dev/null`,
1223
- 8e3
1224
- );
1225
- const pids = [...new Set(stdout.trim().split(/\s+/).map(Number))];
1226
- return pids.length === 1 && Number.isInteger(pids[0]) && pids[0] > 0 ? pids[0] : null;
1227
- } catch {
1228
- return null;
1229
- }
1230
- }
1231
- try {
1232
- const { stdout } = await execGuarded(`ss -tlnp 2>/dev/null`, 8e3, {
1233
- maxBuffer: 16 * 1024 * 1024
1234
- });
1235
- for (const line of stdout.split("\n")) {
1236
- if (!new RegExp(`:${port}\\s`).test(line)) continue;
1237
- const pid = Number(line.match(/pid=(\d+)/)?.[1]);
1238
- if (pid > 0) return pid;
1239
- }
1240
- } catch {
1241
- }
1242
- return null;
1315
+ const scan = await listListenersShared();
1316
+ if (scan.state !== "looked") return null;
1317
+ const pids = [
1318
+ ...new Set(
1319
+ scan.processes.filter((entry) => entry.port === port && entry.pid > 0).map((entry) => entry.pid)
1320
+ )
1321
+ ];
1322
+ return pids.length === 1 && pids[0] !== void 0 ? pids[0] : null;
1243
1323
  }
1244
1324
  var cwdByPid = /* @__PURE__ */ new Map();
1245
- async function resolveProcessCwd(pid) {
1246
- if (pid <= 0) return null;
1247
- const cached2 = cwdByPid.get(pid);
1248
- if (cached2) return cached2;
1249
- try {
1250
- const cwd = await (0, import_promises2.readlink)(`/proc/${pid}/cwd`);
1251
- if (cwd) {
1252
- cwdByPid.set(pid, cwd);
1253
- return cwd;
1254
- }
1255
- } catch {
1256
- }
1257
- try {
1258
- const { stdout } = await execGuarded(
1259
- `lsof -b -w -p ${pid} -a -d cwd -Fn 2>/dev/null`,
1260
- 8e3
1325
+ var cwdQueue = [];
1326
+ var cwdFlushing = false;
1327
+ function kickCwdFlush() {
1328
+ if (cwdFlushing || cwdQueue.length === 0) return;
1329
+ cwdFlushing = true;
1330
+ void flushCwdQueue().finally(() => {
1331
+ cwdFlushing = false;
1332
+ kickCwdFlush();
1333
+ });
1334
+ }
1335
+ async function flushCwdQueue() {
1336
+ await Promise.resolve();
1337
+ while (cwdQueue.length > 0) {
1338
+ const batch = cwdQueue.splice(0, cwdQueue.length);
1339
+ const pending = [...new Set(batch.map((waiter) => waiter.pid))].filter(
1340
+ (pid) => !cwdByPid.has(pid)
1261
1341
  );
1262
- for (const line of stdout.split("\n")) {
1263
- if (line.startsWith("n") && line.length > 1) {
1264
- const cwd = line.slice(1).trim();
1342
+ const needLsof = [];
1343
+ for (const pid of pending) {
1344
+ try {
1345
+ const cwd = await (0, import_promises2.readlink)(`/proc/${pid}/cwd`);
1265
1346
  if (cwd) {
1266
1347
  cwdByPid.set(pid, cwd);
1267
- return cwd;
1348
+ continue;
1268
1349
  }
1350
+ } catch {
1269
1351
  }
1352
+ needLsof.push(pid);
1270
1353
  }
1271
- } catch {
1354
+ if (needLsof.length > 0) {
1355
+ let looked = false;
1356
+ try {
1357
+ const { stdout, code } = await spawnReaped(
1358
+ "lsof",
1359
+ ["-b", "-w", "-nP", "-a", "-d", "cwd", "-p", needLsof.join(","), "-Fn"],
1360
+ 8e3,
1361
+ 4 * 1024 * 1024
1362
+ );
1363
+ if (code === 0 || code === 1) {
1364
+ looked = true;
1365
+ let current = 0;
1366
+ for (const line of stdout.split("\n")) {
1367
+ if (line.startsWith("p")) {
1368
+ current = Number(line.slice(1));
1369
+ continue;
1370
+ }
1371
+ if (!line.startsWith("n") || line.length <= 1 || !needLsof.includes(current))
1372
+ continue;
1373
+ const cwd = line.slice(1).trim();
1374
+ if (cwd) cwdByPid.set(current, cwd);
1375
+ }
1376
+ }
1377
+ } catch {
1378
+ }
1379
+ if (looked) {
1380
+ for (const pid of needLsof) {
1381
+ if (!cwdByPid.has(pid)) cwdByPid.set(pid, null);
1382
+ }
1383
+ }
1384
+ }
1385
+ for (const waiter of batch) waiter.resolve(cwdByPid.get(waiter.pid) ?? null);
1272
1386
  }
1273
- return null;
1387
+ }
1388
+ function resolveProcessCwd(pid) {
1389
+ if (pid <= 0) return Promise.resolve(null);
1390
+ if (cwdByPid.has(pid)) return Promise.resolve(cwdByPid.get(pid) ?? null);
1391
+ return new Promise((resolveCwd) => {
1392
+ cwdQueue.push({ pid, resolve: resolveCwd });
1393
+ kickCwdFlush();
1394
+ });
1274
1395
  }
1275
1396
  function makeResult(hostname, port, framework, owner) {
1276
1397
  return {
@@ -1678,6 +1799,8 @@ function __resetScannerCache() {
1678
1799
  enrichmentCache.clear();
1679
1800
  enrichmentInFlight.clear();
1680
1801
  portCacheInvalidatedAt.clear();
1802
+ listenerCache = null;
1803
+ cwdByPid.clear();
1681
1804
  __resetAppSettingsBundleCacheForTests();
1682
1805
  }
1683
1806
  async function attachAppSettingsBundle(result, projectRoot) {
@@ -1696,6 +1819,7 @@ async function scanDevServers(opts = {}) {
1696
1819
  const scan = await scanListeningProcesses(opts.excludePorts);
1697
1820
  if (scan.state === "could-not-look") throw new DevServerScanUnavailableError();
1698
1821
  const processes = scan.processes;
1822
+ await Promise.all(processes.map((entry) => resolveProcessCwd(entry.pid)));
1699
1823
  const currentPorts = new Set(processes.map((p) => p.port));
1700
1824
  for (const p of [...portCache.keys()]) {
1701
1825
  if (!currentPorts.has(p)) portCache.delete(p);
@@ -1785,6 +1909,7 @@ async function scanDevServers(opts = {}) {
1785
1909
  __shouldReuseScannerCacheEntry,
1786
1910
  discoverListeningPorts,
1787
1911
  discoverListeningProcesses,
1912
+ pinSharedListenerScan,
1788
1913
  probePort,
1789
1914
  resolveListeningPid,
1790
1915
  resolveProcessCwd,