rnxsim 0.1.370 → 0.1.372

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 (207) hide show
  1. package/cli/bin.ts +2 -2
  2. package/cli/commands/box/checkout-plane.ts +65 -14
  3. package/cli/commands/box/cloud-box.ts +294 -0
  4. package/cli/commands/box/plane-share.ts +208 -0
  5. package/cli/commands/box/rnx-command.ts +2 -2
  6. package/cli/commands/box/search-command.ts +157 -0
  7. package/cli/commands/box.ts +269 -13
  8. package/cli/commands/control.ts +6 -1
  9. package/cli/outbound-endpoints.ts +37 -1
  10. package/dist-cli/bin.js +5 -5
  11. package/dist-cli/chunks/{agent-3R4424LW.js → agent-RTAKJ5UM.js} +2 -2
  12. package/dist-cli/chunks/{agent-wrapper-4PPWZWSY.js → agent-wrapper-JWRDI26D.js} +2 -2
  13. package/dist-cli/chunks/{app-fonts-KYXB6CXQ.js → app-fonts-RKO3N4PB.js} +2 -2
  14. package/dist-cli/chunks/{assert-7EZOWGYQ.js → assert-Y5LXPWC6.js} +2 -2
  15. package/dist-cli/chunks/{auth-L7M36H6C.js → auth-4SPTRWTM.js} +2 -2
  16. package/dist-cli/chunks/{beta-FKAF6YWJ.js → beta-KYS22QPY.js} +2 -2
  17. package/dist-cli/chunks/{box-5Y5VBMHV.js → box-7CK72ZPF.js} +617 -596
  18. package/dist-cli/chunks/{camera-XHF2RPFG.js → camera-RUL426MQ.js} +2 -2
  19. package/dist-cli/chunks/{chunk-EQ3OE2AV.js → chunk-2MA3Q44D.js} +1 -1
  20. package/dist-cli/chunks/{chunk-DHYTRCIP.js → chunk-2OA2WE7G.js} +2 -2
  21. package/dist-cli/chunks/{chunk-DMHSD6YJ.js → chunk-3C6LTIDV.js} +2 -2
  22. package/dist-cli/chunks/{chunk-NR2V2ONN.js → chunk-4UMODU6C.js} +2 -2
  23. package/dist-cli/chunks/{chunk-Z2VBSNFI.js → chunk-5Q6ZEZFF.js} +1 -1
  24. package/dist-cli/chunks/{chunk-LUOHRBJG.js → chunk-7FYOZQBM.js} +1 -1
  25. package/dist-cli/chunks/{chunk-Q4MRJ6PO.js → chunk-BRRWRBW7.js} +2 -2
  26. package/dist-cli/chunks/chunk-CEQXZ3GA.js +11 -0
  27. package/dist-cli/chunks/{chunk-DUXHYPBP.js → chunk-CZ2FOZVC.js} +2 -2
  28. package/dist-cli/chunks/{chunk-CMVRTZ5G.js → chunk-D5AT7UT5.js} +2 -2
  29. package/dist-cli/chunks/{chunk-D2GZASRV.js → chunk-DQOQQFMP.js} +1 -1
  30. package/dist-cli/chunks/{chunk-3UXXYJVD.js → chunk-EKSSAC4L.js} +1 -1
  31. package/dist-cli/chunks/{chunk-5BR33NAZ.js → chunk-EY5OHL2M.js} +1 -1
  32. package/dist-cli/chunks/{chunk-FK4JSTZ3.js → chunk-FOW2LPM7.js} +2 -2
  33. package/dist-cli/chunks/{chunk-WFQHT4QD.js → chunk-G5G7PQKO.js} +2 -2
  34. package/dist-cli/chunks/{chunk-WPV4H5S4.js → chunk-GPYG2L7R.js} +2 -2
  35. package/dist-cli/chunks/{chunk-3SOPYG7S.js → chunk-H2SKPUSI.js} +2 -2
  36. package/dist-cli/chunks/{chunk-6A6L3VJR.js → chunk-H567W7JT.js} +2 -2
  37. package/dist-cli/chunks/{chunk-PAUEZO6F.js → chunk-HDGZAVH3.js} +2 -2
  38. package/dist-cli/chunks/{chunk-AALKIAUZ.js → chunk-I37LRPZ3.js} +1 -1
  39. package/dist-cli/chunks/{chunk-JZLDHPB7.js → chunk-I7LRVN5M.js} +2 -2
  40. package/dist-cli/chunks/{chunk-3BG6UCQY.js → chunk-IX25FFF3.js} +11 -11
  41. package/dist-cli/chunks/{chunk-72S5FCBY.js → chunk-J2TJRPDB.js} +2 -2
  42. package/dist-cli/chunks/{chunk-RRX52O37.js → chunk-JPRHJMUJ.js} +1 -1
  43. package/dist-cli/chunks/{chunk-BMIJZNKD.js → chunk-K4TGUDY4.js} +2 -2
  44. package/dist-cli/chunks/{chunk-7C7TJYZ3.js → chunk-KSE5XGYU.js} +2 -2
  45. package/dist-cli/chunks/{chunk-OJZHECGH.js → chunk-KVVNCIJA.js} +1 -1
  46. package/dist-cli/chunks/{chunk-LXPHL6FO.js → chunk-KWLUOLYQ.js} +2 -2
  47. package/dist-cli/chunks/{chunk-C7BA5CCM.js → chunk-KXAE64HF.js} +2 -2
  48. package/dist-cli/chunks/{chunk-EWSWOWXK.js → chunk-L5BEWXQZ.js} +2 -2
  49. package/dist-cli/chunks/{chunk-D24EOJ4C.js → chunk-LAQKTHRM.js} +2 -2
  50. package/dist-cli/chunks/{chunk-IDLFVRPN.js → chunk-LWLHVF6Y.js} +1 -1
  51. package/dist-cli/chunks/{chunk-EOIBLKZO.js → chunk-MKHW3JNL.js} +2 -2
  52. package/dist-cli/chunks/{chunk-64JAEJ6N.js → chunk-MSM2E74F.js} +1 -1
  53. package/dist-cli/chunks/{chunk-DXV37CJN.js → chunk-NBEOA72C.js} +1 -1
  54. package/dist-cli/chunks/{chunk-H5X5FJ63.js → chunk-NLMDMNO6.js} +2 -2
  55. package/dist-cli/chunks/{chunk-6W4HIWC7.js → chunk-NT43X5LI.js} +2 -2
  56. package/dist-cli/chunks/{chunk-PNOENVMJ.js → chunk-NWPYM54M.js} +1 -1
  57. package/dist-cli/chunks/{chunk-O6QLKSAM.js → chunk-OLHHB6LD.js} +2 -2
  58. package/dist-cli/chunks/{chunk-MDCY532E.js → chunk-P6K6XFWE.js} +2 -2
  59. package/dist-cli/chunks/{chunk-LIMWG54N.js → chunk-PETP6LVI.js} +1 -1
  60. package/dist-cli/chunks/{chunk-YIZGNT7A.js → chunk-PKY76S57.js} +1 -1
  61. package/dist-cli/chunks/chunk-POR7R3TP.js +4 -0
  62. package/dist-cli/chunks/chunk-PUH6RQBJ.js +7 -0
  63. package/dist-cli/chunks/{chunk-NJMU5BN6.js → chunk-Q5WPPOFV.js} +1 -1
  64. package/dist-cli/chunks/{chunk-HQQTS2CB.js → chunk-Q7JSU5DI.js} +3 -3
  65. package/dist-cli/chunks/{chunk-U63T6LIQ.js → chunk-QA2PTSOC.js} +1 -1
  66. package/dist-cli/chunks/{chunk-2FOGXVOK.js → chunk-R7HMFLXI.js} +2 -2
  67. package/dist-cli/chunks/{chunk-FA75F34Z.js → chunk-RXKZJAGY.js} +1 -1
  68. package/dist-cli/chunks/{chunk-5RKRVS5A.js → chunk-SC7PGULZ.js} +2 -2
  69. package/dist-cli/chunks/{chunk-UFVAAWGZ.js → chunk-SLTEYNAE.js} +1 -1
  70. package/dist-cli/chunks/{chunk-6GPS7U4X.js → chunk-SSQCKJQZ.js} +2 -2
  71. package/dist-cli/chunks/{chunk-ARMXC3C3.js → chunk-SZX6ARJX.js} +3 -3
  72. package/dist-cli/chunks/{chunk-6S5LJV2O.js → chunk-U3XYGLXG.js} +2 -2
  73. package/dist-cli/chunks/chunk-UA5WQCBU.js +7 -0
  74. package/dist-cli/chunks/{chunk-VVQCZ42C.js → chunk-UVKX6FQ4.js} +1 -1
  75. package/dist-cli/chunks/{chunk-Z3DENGXW.js → chunk-VQQXUXXF.js} +2 -2
  76. package/dist-cli/chunks/{chunk-I52NT6VG.js → chunk-WR7LRCYW.js} +1 -1
  77. package/dist-cli/chunks/{chunk-HEKHLQ3B.js → chunk-XA4YU3A4.js} +3 -3
  78. package/dist-cli/chunks/{chunk-GRO74J2X.js → chunk-YEGWC2OY.js} +1 -1
  79. package/dist-cli/chunks/{chunk-AVJBT5LF.js → chunk-YWTBN7SD.js} +1 -1
  80. package/dist-cli/chunks/{chunk-PIZXOXBL.js → chunk-ZRQKEKHX.js} +2 -2
  81. package/dist-cli/chunks/{chunk-X6WQ4YWE.js → chunk-ZTZTRD5R.js} +1 -1
  82. package/dist-cli/chunks/{cleanup-MXMMJ7JW.js → cleanup-7ATBNZHC.js} +2 -2
  83. package/dist-cli/chunks/cli-version-D2IDQO7X.js +4 -0
  84. package/dist-cli/chunks/{compat-E6KE5KDI.js → compat-4ZC6ZMMA.js} +2 -2
  85. package/dist-cli/chunks/{config-QSLLVZDS.js → config-EOBODQ32.js} +2 -2
  86. package/dist-cli/chunks/{connect-QE4VU6GB.js → connect-ZQ72CZ7O.js} +2 -2
  87. package/dist-cli/chunks/control-FKPKLOE2.js +4 -0
  88. package/dist-cli/chunks/daemon-WFHWS3JU.js +4 -0
  89. package/dist-cli/chunks/{debug-5FG3MBJS.js → debug-7Z325GNU.js} +3 -3
  90. package/dist-cli/chunks/{desktop-5EZX7NX5.js → desktop-CR7ALX7H.js} +3 -3
  91. package/dist-cli/chunks/{detox-WJGQP5TM.js → detox-RZ37736M.js} +2 -2
  92. package/dist-cli/chunks/{device-D7LKCAMS.js → device-TA3SQP6K.js} +2 -2
  93. package/dist-cli/chunks/{diagnose-A3W6ZTGA.js → diagnose-JFKFKR3S.js} +2 -2
  94. package/dist-cli/chunks/disk-cleanup-HZM7VM4S.js +4 -0
  95. package/dist-cli/chunks/{do-chain-RUJNRPPF.js → do-chain-MTT65YNX.js} +2 -2
  96. package/dist-cli/chunks/drivers-BXEXWRVJ.js +4 -0
  97. package/dist-cli/chunks/{env-U67SQVTV.js → env-SFI24PLS.js} +2 -2
  98. package/dist-cli/chunks/{film-XC6UAQ3V.js → film-GPHSJLIH.js} +3 -3
  99. package/dist-cli/chunks/flow-FBCU3TZN.js +4 -0
  100. package/dist-cli/chunks/help-6MLROWYO.js +4 -0
  101. package/dist-cli/chunks/{hidden-runtime-alias-7S2TNZXG.js → hidden-runtime-alias-7Z6PYH53.js} +2 -2
  102. package/dist-cli/chunks/{home-paths-LMO5LB4F.js → home-paths-MEGVA6ET.js} +2 -2
  103. package/dist-cli/chunks/inspect-EWT5SXBD.js +4 -0
  104. package/dist-cli/chunks/install-desktop-IFBUH7MH.js +4 -0
  105. package/dist-cli/chunks/{login-JIU42S4C.js → login-EHUZYRAM.js} +2 -2
  106. package/dist-cli/chunks/{logout-JTAWQXZO.js → logout-WPGT62N6.js} +2 -2
  107. package/dist-cli/chunks/{maestro-TYBQYOGB.js → maestro-4HVDLCTI.js} +3 -3
  108. package/dist-cli/chunks/{maestro-generate-YLAGWVKE.js → maestro-generate-VBS2EZMM.js} +3 -3
  109. package/dist-cli/chunks/{mode-DV24ZRZ6.js → mode-MGTS4QTF.js} +2 -2
  110. package/dist-cli/chunks/{optional-demo-registry-JZH66VIM.js → optional-demo-registry-F3FQT5WT.js} +2 -2
  111. package/dist-cli/chunks/{perf-LXXJ4EAX.js → perf-47R3OEI7.js} +2 -2
  112. package/dist-cli/chunks/{permissions-KAOVZUPW.js → permissions-ZOLFZWR2.js} +2 -2
  113. package/dist-cli/chunks/{platform-KW35446W.js → platform-C4E26ZBZ.js} +3 -3
  114. package/dist-cli/chunks/privacy-WXGGG4ZI.js +4 -0
  115. package/dist-cli/chunks/{record-EK3ETFHZ.js → record-OUXWCWUF.js} +3 -3
  116. package/dist-cli/chunks/{report-issue-NW5PCAZF.js → report-issue-LJJXONXQ.js} +2 -2
  117. package/dist-cli/chunks/reset-ZZYLPETS.js +4 -0
  118. package/dist-cli/chunks/resolve-target-MVZZR2JW.js +4 -0
  119. package/dist-cli/chunks/runtime-DEC5AF3M.js +4 -0
  120. package/dist-cli/chunks/{screenshot-command-TQSPTFT2.js → screenshot-command-CUHDOS5D.js} +3 -3
  121. package/dist-cli/chunks/{screenshot-layers-IZP3L3AQ.js → screenshot-layers-DVWQVKT6.js} +2 -2
  122. package/dist-cli/chunks/{screenshots-capture-K7LJOVAE.js → screenshots-capture-GWIUJ3A6.js} +2 -2
  123. package/dist-cli/chunks/{serve-MMX4WXXG.js → serve-TX5OKGJM.js} +19 -19
  124. package/dist-cli/chunks/setup-X7LLGW67.js +4 -0
  125. package/dist-cli/chunks/{sim-client-KLAYIFOC.js → sim-client-W4WZOZ3P.js} +2 -2
  126. package/dist-cli/chunks/{skills-N4D3POIS.js → skills-TDFQAZ6A.js} +2 -2
  127. package/dist-cli/chunks/{state-ES6TFQZF.js → state-P2Z25OLQ.js} +2 -2
  128. package/dist-cli/chunks/{storage-52DR5V2C.js → storage-DWEOFXEB.js} +2 -2
  129. package/dist-cli/chunks/store-TKBLMYCV.js +4 -0
  130. package/dist-cli/chunks/telemetry-E3CBKJMD.js +4 -0
  131. package/dist-cli/chunks/{timeline-NRVNB5JR.js → timeline-55XNA4GX.js} +2 -2
  132. package/dist-cli/chunks/{upgrade-LNED5YQ2.js → upgrade-3XRCVDVC.js} +2 -2
  133. package/dist-cli/chunks/upload-QB77GSMQ.js +4 -0
  134. package/dist-cli/chunks/{version-VEF2DYGG.js → version-WTYGLXBJ.js} +2 -2
  135. package/dist-cli/chunks/{web-CGDE6S73.js → web-ZWRY3VHM.js} +2 -2
  136. package/dist-cli/chunks/welcome-7ATM5EGX.js +5 -0
  137. package/dist-cli/chunks/{what-happened-FSIDWL32.js → what-happened-Q3IGIH4K.js} +2 -2
  138. package/dist-cli/chunks/{ws-bridge-QXFQLRE7.js → ws-bridge-KCPZSBBX.js} +2 -2
  139. package/dist-lib/agent-daemon-client.cjs +1 -1
  140. package/dist-lib/agent-events.cjs +1 -1
  141. package/dist-lib/agent-identity.cjs +1 -1
  142. package/dist-lib/agent-sessions.cjs +1 -1
  143. package/dist-lib/attached-projects.cjs +1 -1
  144. package/dist-lib/auth/shared-session.cjs +1 -1
  145. package/dist-lib/backend-origin.cjs +1 -1
  146. package/dist-lib/beta.cjs +1 -1
  147. package/dist-lib/beta.mjs +1 -1
  148. package/dist-lib/bridge-constants.cjs +1 -1
  149. package/dist-lib/bridge-contract-input.cjs +1 -1
  150. package/dist-lib/bridge-contract-input.mjs +1 -1
  151. package/dist-lib/bridge-contract.cjs +1 -1
  152. package/dist-lib/bridge-contract.mjs +1 -1
  153. package/dist-lib/cli-constants.cjs +1 -1
  154. package/dist-lib/config.cjs +1 -1
  155. package/dist-lib/detox/index.cjs +1 -1
  156. package/dist-lib/dev-bundle-resolution.cjs +69 -34
  157. package/dist-lib/home-paths.cjs +1 -1
  158. package/dist-lib/host/bridge-host.cjs +430 -106
  159. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  160. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  161. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  162. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  163. package/dist-lib/host/websocket-proxy.cjs +1 -1
  164. package/dist-lib/index.cjs +1 -1
  165. package/dist-lib/jump-to-source-babel.cjs +1 -1
  166. package/dist-lib/menu.cjs +1 -1
  167. package/dist-lib/menu.mjs +1 -1
  168. package/dist-lib/metro-production-bundle.cjs +1 -1
  169. package/dist-lib/metro-production-bundle.mjs +1 -1
  170. package/dist-lib/metro.cjs +1 -1
  171. package/dist-lib/profiles.cjs +1 -1
  172. package/dist-lib/public-brand.cjs +1 -1
  173. package/dist-lib/react-native-host-modules.cjs +1 -1
  174. package/dist-lib/react-native-host-modules.mjs +1 -1
  175. package/dist-lib/render-mode.cjs +1 -1
  176. package/dist-lib/scripts/dev-server-scanner.cjs +149 -93
  177. package/dist-lib/sdk.cjs +1 -1
  178. package/dist-lib/sdk.mjs +1 -1
  179. package/dist-lib/skills.cjs +679 -307
  180. package/dist-lib/vite.cjs +1 -1
  181. package/package.json +1 -1
  182. package/scripts/dev-server-scanner.ts +275 -92
  183. package/src/dev-bundle-resolution.ts +123 -40
  184. package/src/disk-cleanup.ts +44 -0
  185. package/src/host/bridge-host.ts +11 -0
  186. package/src/host/plane-host.ts +203 -0
  187. package/dist-cli/chunks/chunk-GJRLPJQL.js +0 -6
  188. package/dist-cli/chunks/chunk-NTC46LSA.js +0 -4
  189. package/dist-cli/chunks/chunk-OTFTMGAL.js +0 -7
  190. package/dist-cli/chunks/cli-version-MLS55UTY.js +0 -4
  191. package/dist-cli/chunks/control-XD5F6K5T.js +0 -4
  192. package/dist-cli/chunks/daemon-UN5SE2JO.js +0 -4
  193. package/dist-cli/chunks/disk-cleanup-BDRHX5ZG.js +0 -4
  194. package/dist-cli/chunks/drivers-3NJ5RL6X.js +0 -4
  195. package/dist-cli/chunks/flow-FHZZD3P6.js +0 -4
  196. package/dist-cli/chunks/help-BYYDUSXF.js +0 -4
  197. package/dist-cli/chunks/inspect-RQCTWDCC.js +0 -4
  198. package/dist-cli/chunks/install-desktop-GOWXCQUE.js +0 -4
  199. package/dist-cli/chunks/privacy-MIFE72CG.js +0 -4
  200. package/dist-cli/chunks/reset-RZ4O5AFJ.js +0 -4
  201. package/dist-cli/chunks/resolve-target-7R5BNYMM.js +0 -4
  202. package/dist-cli/chunks/runtime-MZHLSRS6.js +0 -4
  203. package/dist-cli/chunks/setup-YFP3KEQZ.js +0 -4
  204. package/dist-cli/chunks/store-HOL45WCI.js +0 -4
  205. package/dist-cli/chunks/telemetry-BV6OTYPF.js +0 -4
  206. package/dist-cli/chunks/upload-RXKHFIBU.js +0 -4
  207. package/dist-cli/chunks/welcome-FT2AMUWW.js +0 -5
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.370 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.372 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -114,7 +114,6 @@ var import_promises2 = require("fs/promises");
114
114
  var import_http = __toESM(require("http"), 1);
115
115
  var import_net = __toESM(require("net"), 1);
116
116
  var import_node_path3 = require("node:path");
117
- var import_util = require("util");
118
117
 
119
118
  // src/backend-origin.ts
120
119
  var LOOPBACK_PROBE_HOSTNAMES = ["::1", "127.0.0.1"];
@@ -301,9 +300,9 @@ function safeChildPlistName(value) {
301
300
  name: file.toLowerCase().endsWith(".plist") ? file : `${file}.plist`
302
301
  };
303
302
  }
304
- async function readableSettingsBundle(path10) {
303
+ async function readableSettingsBundle(path12) {
305
304
  try {
306
- const root = await (0, import_promises.stat)((0, import_node_path.join)(path10, "Root.plist"));
305
+ const root = await (0, import_promises.stat)((0, import_node_path.join)(path12, "Root.plist"));
307
306
  return root.isFile() && root.size <= MAX_PLIST_BYTES;
308
307
  } catch {
309
308
  return false;
@@ -348,11 +347,11 @@ async function settingsBundleFingerprint(bundlePath) {
348
347
  return null;
349
348
  }
350
349
  }
351
- async function parsePlistRecord(path10) {
350
+ async function parsePlistRecord(path12) {
352
351
  try {
353
- const metadata = await (0, import_promises.stat)(path10);
352
+ const metadata = await (0, import_promises.stat)(path12);
354
353
  if (!metadata.isFile() || metadata.size > MAX_PLIST_BYTES) return null;
355
- const source = await (0, import_promises.readFile)(path10, "utf8");
354
+ const source = await (0, import_promises.readFile)(path12, "utf8");
356
355
  const parsed = parsePlistSource(source);
357
356
  return isRecord2(parsed) ? parsed : null;
358
357
  } catch {
@@ -538,10 +537,30 @@ async function readAppSettingsBundle(projectRoot) {
538
537
 
539
538
  // scripts/dev-server-scanner.ts
540
539
  init_optional_demo_registry();
541
- var execP = (0, import_util.promisify)(import_child_process.exec);
540
+ var EXEC_EXIT_GRACE_MS = 2e3;
541
+ function execGuarded(command, timeout, extra = {}) {
542
+ let child;
543
+ const ran = new Promise((resolve2, reject) => {
544
+ child = (0, import_child_process.exec)(command, { encoding: "utf8", timeout, ...extra }, (err, stdout) => {
545
+ if (err) reject(err);
546
+ else resolve2({ stdout });
547
+ });
548
+ });
549
+ const deadline = new Promise((_resolve, reject) => {
550
+ const timer = setTimeout(() => {
551
+ child?.kill("SIGKILL");
552
+ const err = new Error(`\`${command}\` did not exit within ${timeout}ms`);
553
+ Object.assign(err, { killed: true });
554
+ reject(err);
555
+ }, timeout + EXEC_EXIT_GRACE_MS);
556
+ timer.unref?.();
557
+ });
558
+ return Promise.race([ran, deadline]);
559
+ }
542
560
  var TIMEOUT_MS = 250;
543
561
  var MANIFEST_TIMEOUT_MS = 1500;
544
562
  var TCP_GATE_MS = 120;
563
+ var BODY_TIMEOUT_MS = 3e4;
545
564
  async function tcpPing(port, timeout = TCP_GATE_MS) {
546
565
  try {
547
566
  return await Promise.any(
@@ -568,36 +587,46 @@ async function tcpPing(port, timeout = TCP_GATE_MS) {
568
587
  return null;
569
588
  }
570
589
  }
571
- function httpGet(hostname, port, path10, method = "GET", timeout = TIMEOUT_MS, headers = {}) {
590
+ function httpGet(hostname, port, path12, method = "GET", timeout = TIMEOUT_MS, headers = {}) {
572
591
  return new Promise((resolve2) => {
573
- const req = import_http.default.request(
574
- { hostname, port, path: path10, method, timeout, headers },
575
- (res) => {
576
- let body = "";
577
- res.on("data", (c) => body += c.toString());
578
- const contentType = (() => {
579
- const raw = res.headers["content-type"];
580
- if (typeof raw === "string") return raw;
581
- if (Array.isArray(raw)) return raw[0];
582
- return void 0;
583
- })();
584
- res.on(
585
- "end",
586
- () => resolve2({
587
- statusCode: res.statusCode || 0,
588
- body,
589
- contentType,
590
- hostname
591
- })
592
- );
593
- }
594
- );
595
- req.on("error", () => resolve2(null));
596
- req.setTimeout(timeout, () => {
597
- req.destroy();
598
- resolve2(null);
592
+ let settled = false;
593
+ const settle = (value) => {
594
+ if (settled) return;
595
+ settled = true;
596
+ resolve2(value);
597
+ };
598
+ const req = import_http.default.request({ hostname, port, path: path12, method, headers }, (res) => {
599
+ let body = "";
600
+ res.on("data", (c) => body += c.toString());
601
+ const contentType = (() => {
602
+ const raw = res.headers["content-type"];
603
+ if (typeof raw === "string") return raw;
604
+ if (Array.isArray(raw)) return raw[0];
605
+ return void 0;
606
+ })();
607
+ res.on(
608
+ "end",
609
+ () => settle({
610
+ statusCode: res.statusCode || 0,
611
+ body,
612
+ contentType,
613
+ hostname
614
+ })
615
+ );
599
616
  });
617
+ req.on("error", () => settle(null));
600
618
  req.end();
619
+ const giveUp = setTimeout(() => {
620
+ settle(null);
621
+ req.socket?.unref();
622
+ }, timeout);
623
+ const abandon = setTimeout(() => req.destroy(), BODY_TIMEOUT_MS);
624
+ abandon.unref?.();
625
+ req.on("close", () => {
626
+ clearTimeout(giveUp);
627
+ clearTimeout(abandon);
628
+ setImmediate(() => settle(null));
629
+ });
601
630
  });
602
631
  }
603
632
  var FALLBACK_PORTS = [
@@ -611,61 +640,83 @@ var FALLBACK_PORTS = [
611
640
  3001,
612
641
  19e3
613
642
  ].map((port) => ({ port, pid: 0 }));
643
+ var SELF_PORT_RANGE_START = 5170;
644
+ var SELF_PORT_RANGE_END = 5200;
645
+ function isOwnShellPort(port) {
646
+ const offset = Number(process.env.PORT_OFFSET) || 0;
647
+ if (port >= SELF_PORT_RANGE_START && port <= SELF_PORT_RANGE_END) return true;
648
+ return offset > 0 && port >= SELF_PORT_RANGE_START + offset && port <= SELF_PORT_RANGE_END + offset;
649
+ }
614
650
  function acceptPort(port, excluded) {
615
651
  if (port <= 0 || port >= 2e4) return false;
616
652
  if (excluded.has(port)) return false;
617
- if (port >= 5170 && port <= 5200) return false;
653
+ if (isOwnShellPort(port)) return false;
618
654
  return true;
619
655
  }
620
- async function discoverListeningProcesses(excludePorts = []) {
656
+ var DevServerScanUnavailableError = class extends Error {
657
+ constructor() {
658
+ super("could not enumerate listening processes (lsof/ss unavailable or timed out)");
659
+ this.name = "DevServerScanUnavailableError";
660
+ }
661
+ };
662
+ var LISTING_TIMEOUT_MS = 1e4;
663
+ function wasKilledByTimeout(err) {
664
+ return !!(err && typeof err === "object" && err.killed);
665
+ }
666
+ async function scanListeningProcesses(excludePorts = []) {
621
667
  const excluded = new Set(excludePorts);
668
+ let timedOut = false;
622
669
  try {
623
- const { stdout } = await execP(
670
+ const { stdout } = await execGuarded(
624
671
  `lsof -iTCP -sTCP:LISTEN -P -n 2>/dev/null | grep -E '^(node|bun)'`,
625
- { encoding: "utf8", timeout: 2e3 }
672
+ LISTING_TIMEOUT_MS
626
673
  );
627
- if (stdout.trim()) {
628
- const seen = /* @__PURE__ */ new Map();
629
- for (const line of stdout.trim().split("\n")) {
630
- const parts = line.trim().split(/\s+/);
631
- if (parts.length < 9) continue;
632
- const pid = Number(parts[1]);
633
- const addr = parts[8];
634
- const m = addr.match(/:(\d+)$/);
635
- if (!m) continue;
636
- const port = Number(m[1]);
637
- if (!acceptPort(port, excluded)) continue;
638
- if (!seen.has(port)) seen.set(port, pid);
639
- }
640
- if (seen.size > 0) {
641
- return [...seen.entries()].map(([port, pid]) => ({ port, pid }));
642
- }
674
+ const seen = /* @__PURE__ */ new Map();
675
+ for (const line of stdout.trim() ? stdout.trim().split("\n") : []) {
676
+ const parts = line.trim().split(/\s+/);
677
+ if (parts.length < 9) continue;
678
+ const pid = Number(parts[1]);
679
+ const addr = parts[8];
680
+ const m = addr.match(/:(\d+)$/);
681
+ if (!m) continue;
682
+ const port = Number(m[1]);
683
+ if (!acceptPort(port, excluded)) continue;
684
+ if (!seen.has(port)) seen.set(port, pid);
643
685
  }
644
- } catch {
686
+ return {
687
+ state: "looked",
688
+ processes: [...seen.entries()].map(([port, pid]) => ({ port, pid }))
689
+ };
690
+ } catch (err) {
691
+ if (wasKilledByTimeout(err)) timedOut = true;
645
692
  }
646
693
  try {
647
- const { stdout } = await execP(`ss -tlnp 2>/dev/null | grep -E '"(node|bun)"'`, {
648
- encoding: "utf8",
649
- timeout: 2e3
650
- });
651
- if (stdout.trim()) {
652
- const seen = /* @__PURE__ */ new Map();
653
- for (const line of stdout.trim().split("\n")) {
654
- const portMatch = line.match(/:(\d+)\s/);
655
- const pidMatch = line.match(/pid=(\d+)/);
656
- if (!portMatch) continue;
657
- const port = Number(portMatch[1]);
658
- const pid = pidMatch ? Number(pidMatch[1]) : 0;
659
- if (!acceptPort(port, excluded)) continue;
660
- if (!seen.has(port)) seen.set(port, pid);
661
- }
662
- if (seen.size > 0) {
663
- return [...seen.entries()].map(([port, pid]) => ({ port, pid }));
664
- }
694
+ const { stdout } = await execGuarded(
695
+ `ss -tlnp 2>/dev/null | grep -E '"(node|bun)"'`,
696
+ LISTING_TIMEOUT_MS
697
+ );
698
+ const seen = /* @__PURE__ */ new Map();
699
+ for (const line of stdout.trim() ? stdout.trim().split("\n") : []) {
700
+ const portMatch = line.match(/:(\d+)\s/);
701
+ const pidMatch = line.match(/pid=(\d+)/);
702
+ if (!portMatch) continue;
703
+ const port = Number(portMatch[1]);
704
+ const pid = pidMatch ? Number(pidMatch[1]) : 0;
705
+ if (!acceptPort(port, excluded)) continue;
706
+ if (!seen.has(port)) seen.set(port, pid);
665
707
  }
666
- } catch {
708
+ return {
709
+ state: "looked",
710
+ processes: [...seen.entries()].map(([port, pid]) => ({ port, pid }))
711
+ };
712
+ } catch (err) {
713
+ if (wasKilledByTimeout(err)) timedOut = true;
667
714
  }
668
- return FALLBACK_PORTS.filter((p) => acceptPort(p.port, excluded));
715
+ if (timedOut) return { state: "could-not-look" };
716
+ return {
717
+ state: "looked",
718
+ processes: FALLBACK_PORTS.filter((p) => acceptPort(p.port, excluded))
719
+ };
669
720
  }
670
721
  function pidFromNetstat(stdout, port) {
671
722
  for (const line of stdout.split("\n")) {
@@ -685,9 +736,7 @@ function pidFromNetstat(stdout, port) {
685
736
  async function resolveListeningPid(port) {
686
737
  if (!Number.isInteger(port) || port <= 0) return null;
687
738
  try {
688
- const { stdout } = await execP("netstat -anv -p tcp 2>/dev/null", {
689
- encoding: "utf8",
690
- timeout: 8e3,
739
+ const { stdout } = await execGuarded("netstat -anv -p tcp 2>/dev/null", 8e3, {
691
740
  maxBuffer: 16 * 1024 * 1024
692
741
  });
693
742
  const pid = pidFromNetstat(stdout, port);
@@ -695,9 +744,7 @@ async function resolveListeningPid(port) {
695
744
  } catch {
696
745
  }
697
746
  try {
698
- const { stdout } = await execP(`ss -tlnp 2>/dev/null`, {
699
- encoding: "utf8",
700
- timeout: 8e3,
747
+ const { stdout } = await execGuarded(`ss -tlnp 2>/dev/null`, 8e3, {
701
748
  maxBuffer: 16 * 1024 * 1024
702
749
  });
703
750
  for (const line of stdout.split("\n")) {
@@ -723,10 +770,10 @@ async function resolveProcessCwd(pid) {
723
770
  } catch {
724
771
  }
725
772
  try {
726
- const { stdout } = await execP(`lsof -b -w -p ${pid} -a -d cwd -Fn 2>/dev/null`, {
727
- encoding: "utf8",
728
- timeout: 8e3
729
- });
773
+ const { stdout } = await execGuarded(
774
+ `lsof -b -w -p ${pid} -a -d cwd -Fn 2>/dev/null`,
775
+ 8e3
776
+ );
730
777
  for (const line of stdout.split("\n")) {
731
778
  if (line.startsWith("n") && line.length > 1) {
732
779
  const cwd = line.slice(1).trim();
@@ -894,7 +941,8 @@ async function probePort(port, buildIconProxyUrl, listeningProcess) {
894
941
  const manifestParsed = manifestRes ? safeParseManifest(manifestRes.body) : null;
895
942
  const manifestLaunchUrl = typeof manifestParsed?.launchAsset?.url === "string" ? manifestParsed.launchAsset.url : null;
896
943
  const manifestClient = manifestParsed?.extra?.expoClient || manifestParsed?.extra || {};
897
- if (manifestParsed && (manifestLaunchUrl || typeof manifestClient.name === "string")) {
944
+ const manifestState = !manifestRes ? "no-answer" : manifestParsed && (manifestLaunchUrl || typeof manifestClient.name === "string") ? "resolved" : "no-manifest";
945
+ if (manifestState === "resolved") {
898
946
  knownNonPatched.add(port);
899
947
  const launchUrl2 = manifestLaunchUrl || `${buildLoopbackProbeBaseUrl("http:", probeHostname, port)}/index.bundle?platform=ios&dev=true&hot=true&minify=false`;
900
948
  const framework = launchUrl2.includes(
@@ -915,17 +963,15 @@ async function probePort(port, buildIconProxyUrl, listeningProcess) {
915
963
  }
916
964
  if (statusRes && statusRes.body.includes("packager-status:running")) {
917
965
  knownNonPatched.add(port);
966
+ const result = makeResult(
967
+ probeHostname,
968
+ port,
969
+ expoRes && expoRes.statusCode === 200 ? "expo" : "metro",
970
+ owner
971
+ );
972
+ if (manifestState === "no-answer") result.bundleUrlProvisional = true;
918
973
  return enrichWithManifest(
919
- applyManifest(
920
- makeResult(
921
- probeHostname,
922
- port,
923
- expoRes && expoRes.statusCode === 200 ? "expo" : "metro",
924
- owner
925
- ),
926
- manifestRes,
927
- buildIconProxyUrl
928
- ),
974
+ applyManifest(result, manifestRes, buildIconProxyUrl),
929
975
  buildIconProxyUrl
930
976
  );
931
977
  }
@@ -1021,7 +1067,9 @@ async function attachAppSettingsBundle(result, projectRoot) {
1021
1067
  }
1022
1068
  async function scanDevServers(opts = {}) {
1023
1069
  await loadOptionalDemoApps();
1024
- const processes = await discoverListeningProcesses(opts.excludePorts);
1070
+ const scan = await scanListeningProcesses(opts.excludePorts);
1071
+ if (scan.state === "could-not-look") throw new DevServerScanUnavailableError();
1072
+ const processes = scan.processes;
1025
1073
  const currentPorts = new Set(processes.map((p) => p.port));
1026
1074
  for (const p of [...portCache.keys()]) {
1027
1075
  if (!currentPorts.has(p)) portCache.delete(p);
@@ -3708,13 +3756,283 @@ async function openUrl(url, options = {}) {
3708
3756
  await launchUrl(url, options);
3709
3757
  }
3710
3758
 
3759
+ // src/host/plane-host.ts
3760
+ var import_node_fs10 = __toESM(require("node:fs"), 1);
3761
+ var import_node_path11 = __toESM(require("node:path"), 1);
3762
+ var import_plane = require("@contrast/harness/plane");
3763
+
3764
+ // cli/commands/box/checkout-plane.ts
3765
+ var import_node_child_process4 = require("node:child_process");
3766
+ var import_node_fs9 = __toESM(require("node:fs"), 1);
3767
+ var import_node_path10 = __toESM(require("node:path"), 1);
3768
+ var CheckoutRequiresGitError = class extends Error {
3769
+ };
3770
+ var BINARY_SNIFF_BYTES = 8192;
3771
+ var LIST_FILES_TIMEOUT_MS = 1e4;
3772
+ function looksBinary(filePath, size) {
3773
+ if (size === 0) return false;
3774
+ let fd;
3775
+ try {
3776
+ fd = import_node_fs9.default.openSync(filePath, "r");
3777
+ const buffer = Buffer.alloc(Math.min(BINARY_SNIFF_BYTES, size));
3778
+ const read = import_node_fs9.default.readSync(fd, buffer, 0, buffer.length, 0);
3779
+ return buffer.subarray(0, read).includes(0);
3780
+ } catch {
3781
+ return false;
3782
+ } finally {
3783
+ if (fd !== void 0) import_node_fs9.default.closeSync(fd);
3784
+ }
3785
+ }
3786
+ var CheckoutFilePlane = class {
3787
+ root;
3788
+ entries = /* @__PURE__ */ new Map();
3789
+ sniffed = /* @__PURE__ */ new Map();
3790
+ // the deadline above, as a number a caller can set. what it changes is how
3791
+ // long a wedged git is waited on, never whether it is waited on, so proving
3792
+ // the kill does not have to cost the production deadline.
3793
+ listTimeoutMs;
3794
+ constructor(root, options = {}) {
3795
+ this.root = import_node_path10.default.resolve(root);
3796
+ this.listTimeoutMs = options.listTimeoutMs ?? LIST_FILES_TIMEOUT_MS;
3797
+ }
3798
+ index() {
3799
+ return this.entries;
3800
+ }
3801
+ async refresh() {
3802
+ let stdout;
3803
+ try {
3804
+ stdout = (0, import_node_child_process4.execFileSync)("git", ["ls-files", "-co", "--exclude-standard", "-z"], {
3805
+ cwd: this.root,
3806
+ encoding: "utf8",
3807
+ maxBuffer: 64 * 1024 * 1024,
3808
+ timeout: this.listTimeoutMs,
3809
+ // stating the environment is not redundant here. bun 1.3.14 resolves
3810
+ // the binary for the synchronous form against the PATH it captured at
3811
+ // startup and ignores a later change to `process.env.PATH`, so without
3812
+ // this it runs a different git than every other call in the process.
3813
+ env: process.env
3814
+ });
3815
+ } catch (error) {
3816
+ const message = error instanceof Error ? error.message : String(error);
3817
+ const killed = error instanceof Error && "signal" in error && error.signal === "SIGTERM";
3818
+ throw new CheckoutRequiresGitError(
3819
+ killed ? `git did not list the project's files in ${this.root} within ${this.listTimeoutMs / 1e3}s, so the box has no file set to serve. the checkout may be on a stalled filesystem, or this machine may be too loaded to start a subprocess.` : `could not list the project's files with git in ${this.root}: ${message}`
3820
+ );
3821
+ }
3822
+ if (stdout.length === 0) {
3823
+ throw new CheckoutRequiresGitError(
3824
+ `git listed no files in ${this.root}. either git tracks nothing there and nothing untracked is visible to it, or this machine could not read the child process's output.`
3825
+ );
3826
+ }
3827
+ const next = /* @__PURE__ */ new Map();
3828
+ const nextSniffed = /* @__PURE__ */ new Map();
3829
+ for (const relative of stdout.split("\0")) {
3830
+ if (!relative) continue;
3831
+ let stat2;
3832
+ try {
3833
+ stat2 = import_node_fs9.default.statSync(import_node_path10.default.join(this.root, relative));
3834
+ } catch {
3835
+ continue;
3836
+ }
3837
+ if (!stat2.isFile()) continue;
3838
+ const seen = this.sniffed.get(relative);
3839
+ const binary = seen && seen.size === stat2.size && seen.mtimeMs === stat2.mtimeMs ? seen.binary : looksBinary(import_node_path10.default.join(this.root, relative), stat2.size);
3840
+ nextSniffed.set(relative, { size: stat2.size, mtimeMs: stat2.mtimeMs, binary });
3841
+ next.set(relative, {
3842
+ size: stat2.size,
3843
+ // a checkout carries no write policy of its own; if the user can write
3844
+ // the file, so can the shell
3845
+ readOnly: false,
3846
+ binary
3847
+ });
3848
+ }
3849
+ this.entries = next;
3850
+ this.sniffed = nextSniffed;
3851
+ }
3852
+ absolute(planePath) {
3853
+ const resolved = import_node_path10.default.resolve(this.root, planePath);
3854
+ if (resolved !== this.root && !resolved.startsWith(`${this.root}${import_node_path10.default.sep}`)) {
3855
+ throw new Error(`path escapes the project: ${planePath}`);
3856
+ }
3857
+ return resolved;
3858
+ }
3859
+ async read(planePath) {
3860
+ return import_node_fs9.default.promises.readFile(this.absolute(planePath), "utf8");
3861
+ }
3862
+ async readBytes(planePath) {
3863
+ return import_node_fs9.default.promises.readFile(this.absolute(planePath));
3864
+ }
3865
+ async write(planePath, content) {
3866
+ const absolute = this.absolute(planePath);
3867
+ await import_node_fs9.default.promises.mkdir(import_node_path10.default.dirname(absolute), { recursive: true });
3868
+ await import_node_fs9.default.promises.writeFile(absolute, content);
3869
+ this.entries.set(planePath, {
3870
+ size: content.byteLength,
3871
+ readOnly: false,
3872
+ // the bytes are in hand, so the same NUL test costs nothing here
3873
+ binary: content.subarray(0, BINARY_SNIFF_BYTES).includes(0)
3874
+ });
3875
+ }
3876
+ async delete(planePath) {
3877
+ await import_node_fs9.default.promises.rm(this.absolute(planePath), { force: true });
3878
+ this.entries.delete(planePath);
3879
+ }
3880
+ };
3881
+
3882
+ // src/host/plane-host.ts
3883
+ var WS_OPEN2 = 1;
3884
+ var PlaneNotRegisteredError = class extends Error {
3885
+ };
3886
+ var PlaneHost = class {
3887
+ // roots a box authorized, each tied to the socket that authorized it
3888
+ registered = /* @__PURE__ */ new Map();
3889
+ open = /* @__PURE__ */ new Map();
3890
+ byRoot = /* @__PURE__ */ new Map();
3891
+ nextPlaneId = 1;
3892
+ /** handle a plane:* message. returns true iff it was one. */
3893
+ async handleMessage(ws, msg) {
3894
+ if (typeof msg !== "object" || msg === null) return false;
3895
+ const record = { ...msg };
3896
+ const type = record.type;
3897
+ if (typeof type !== "string" || !type.startsWith("plane:")) return false;
3898
+ const id = record.id;
3899
+ try {
3900
+ this.respond(ws, id, await this.dispatch(ws, type, record));
3901
+ } catch (err) {
3902
+ const code = err instanceof PlaneNotRegisteredError ? "plane-not-registered" : void 0;
3903
+ this.respondError(ws, id, err instanceof Error ? err.message : String(err), code);
3904
+ }
3905
+ return true;
3906
+ }
3907
+ unregisterSocket(ws) {
3908
+ for (const [root, sockets] of [...this.registered]) {
3909
+ if (!sockets.delete(ws)) continue;
3910
+ if (sockets.size > 0) continue;
3911
+ this.registered.delete(root);
3912
+ const planeId = this.byRoot.get(root);
3913
+ if (planeId) {
3914
+ this.open.delete(planeId);
3915
+ this.byRoot.delete(root);
3916
+ }
3917
+ }
3918
+ for (const [planeId, entry] of [...this.open]) {
3919
+ entry.sockets.delete(ws);
3920
+ if (entry.sockets.size === 0 && !this.registered.has(entry.root)) {
3921
+ this.open.delete(planeId);
3922
+ this.byRoot.delete(entry.root);
3923
+ }
3924
+ }
3925
+ }
3926
+ /** roots currently reachable, for the daemon's status output. */
3927
+ registeredRoots() {
3928
+ return [...this.registered.keys()];
3929
+ }
3930
+ async dispatch(ws, type, msg) {
3931
+ switch (type) {
3932
+ case "plane:register": {
3933
+ const root = this.realRoot(String(msg.root ?? ""));
3934
+ const sockets = this.registered.get(root) ?? /* @__PURE__ */ new Set();
3935
+ sockets.add(ws);
3936
+ this.registered.set(root, sockets);
3937
+ return { root, holders: sockets.size };
3938
+ }
3939
+ // READ ONLY, and that is the whole point of it existing beside the
3940
+ // `holders` field on plane:register.
3941
+ //
3942
+ // registration is the AUTHORIZATION step: it is what permits any page on
3943
+ // this machine to open that root over a loopback socket that CORS does
3944
+ // not protect. so a caller asking "is anyone else in this checkout"
3945
+ // cannot be made to authorize the root in order to find out, even
3946
+ // briefly. it also means the count is the honest one: no box reads 0 and
3947
+ // a single box reads 1, where asking through register would have made
3948
+ // every answer one higher and forced a threshold that counts the asker.
3949
+ case "plane:status": {
3950
+ const root = this.realRoot(String(msg.root ?? ""));
3951
+ const sockets = this.registered.get(root);
3952
+ return { root, holders: sockets?.size ?? 0, registered: Boolean(sockets) };
3953
+ }
3954
+ case "plane:open": {
3955
+ const root = this.realRoot(String(msg.root ?? ""));
3956
+ if (!this.registered.has(root)) {
3957
+ throw new PlaneNotRegisteredError(
3958
+ `no box has registered ${root}; start one with \`rnx box\` there`
3959
+ );
3960
+ }
3961
+ const existingId = this.byRoot.get(root);
3962
+ const existing = existingId ? this.open.get(existingId) : void 0;
3963
+ if (existingId && existing) {
3964
+ existing.sockets.add(ws);
3965
+ return { planeId: existingId, version: existing.version };
3966
+ }
3967
+ const plane = new CheckoutFilePlane(root);
3968
+ await plane.refresh();
3969
+ const planeId = `plane-${this.nextPlaneId++}`;
3970
+ const entry = {
3971
+ root,
3972
+ plane,
3973
+ version: (0, import_plane.indexVersion)(plane.index()),
3974
+ sockets: /* @__PURE__ */ new Set([ws])
3975
+ };
3976
+ this.open.set(planeId, entry);
3977
+ this.byRoot.set(root, planeId);
3978
+ return { planeId, version: entry.version };
3979
+ }
3980
+ case "plane:close": {
3981
+ const entry = this.open.get(String(msg.planeId ?? ""));
3982
+ if (entry) entry.sockets.delete(ws);
3983
+ return { ok: true };
3984
+ }
3985
+ default: {
3986
+ const entry = this.entry(msg);
3987
+ const handled = await (0, import_plane.dispatchPlaneCommand)(entry.plane, type, msg);
3988
+ if (!handled) throw new Error(`unknown plane command: ${type}`);
3989
+ if (handled.version) entry.version = handled.version;
3990
+ return handled.result;
3991
+ }
3992
+ }
3993
+ }
3994
+ entry(msg) {
3995
+ const entry = this.open.get(String(msg.planeId ?? ""));
3996
+ if (!entry) throw new Error(`unknown planeId: ${String(msg.planeId)}`);
3997
+ return entry;
3998
+ }
3999
+ // resolve symlinks and relative segments before comparing, so a registered
4000
+ // root and an opened one cannot disagree about the same directory.
4001
+ realRoot(root) {
4002
+ if (!root) throw new Error("root is required");
4003
+ const resolved = import_node_path11.default.resolve(root);
4004
+ let real;
4005
+ try {
4006
+ real = import_node_fs10.default.realpathSync(resolved);
4007
+ } catch {
4008
+ throw new Error(`no such directory: ${resolved}`);
4009
+ }
4010
+ if (!import_node_fs10.default.statSync(real).isDirectory()) throw new Error(`not a directory: ${real}`);
4011
+ return real;
4012
+ }
4013
+ respond(ws, id, result) {
4014
+ if (ws.readyState !== WS_OPEN2) return;
4015
+ try {
4016
+ ws.send(JSON.stringify({ id, result }));
4017
+ } catch {
4018
+ }
4019
+ }
4020
+ respondError(ws, id, error, code) {
4021
+ if (ws.readyState !== WS_OPEN2) return;
4022
+ try {
4023
+ ws.send(JSON.stringify({ id, error, ...code ? { code } : {} }));
4024
+ } catch {
4025
+ }
4026
+ }
4027
+ };
4028
+
3711
4029
  // src/host/replacement-module-handler.ts
3712
4030
  var import_promises5 = __toESM(require("node:fs/promises"), 1);
3713
- var import_node_path10 = __toESM(require("node:path"), 1);
4031
+ var import_node_path12 = __toESM(require("node:path"), 1);
3714
4032
  var import_esbuild = require("esbuild");
3715
4033
  var REPLACEMENT_MODULE_ROUTE = "/__sootsim-replacement-module";
3716
4034
  function getReplacementLoader(filePath) {
3717
- const ext = import_node_path10.default.extname(filePath).toLowerCase();
4035
+ const ext = import_node_path12.default.extname(filePath).toLowerCase();
3718
4036
  switch (ext) {
3719
4037
  case ".js":
3720
4038
  return "js";
@@ -3796,7 +4114,7 @@ async function handleReplacementModuleRequest(req, res, options = {}) {
3796
4114
  res.end("replacement files must be .js, .jsx, .ts, or .tsx");
3797
4115
  return true;
3798
4116
  }
3799
- if (!import_node_path10.default.isAbsolute(targetPath)) {
4117
+ if (!import_node_path12.default.isAbsolute(targetPath)) {
3800
4118
  res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
3801
4119
  res.end("replacement file path must be absolute");
3802
4120
  return true;
@@ -3804,10 +4122,10 @@ async function handleReplacementModuleRequest(req, res, options = {}) {
3804
4122
  const runtimePackages = new Set(url.searchParams.getAll("runtimePackage"));
3805
4123
  let resolvedProjectRoot;
3806
4124
  if (options.projectRoot) {
3807
- const lexicalProjectRoot = import_node_path10.default.resolve(options.projectRoot);
4125
+ const lexicalProjectRoot = import_node_path12.default.resolve(options.projectRoot);
3808
4126
  resolvedProjectRoot = await import_promises5.default.realpath(options.projectRoot);
3809
- const lexicalTarget = import_node_path10.default.relative(lexicalProjectRoot, targetPath);
3810
- if (lexicalTarget.startsWith("..") || import_node_path10.default.isAbsolute(lexicalTarget)) {
4127
+ const lexicalTarget = import_node_path12.default.relative(lexicalProjectRoot, targetPath);
4128
+ if (lexicalTarget.startsWith("..") || import_node_path12.default.isAbsolute(lexicalTarget)) {
3811
4129
  res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
3812
4130
  res.end("replacement file must be inside the project root");
3813
4131
  return true;
@@ -3823,8 +4141,8 @@ async function handleReplacementModuleRequest(req, res, options = {}) {
3823
4141
  return true;
3824
4142
  }
3825
4143
  if (resolvedProjectRoot) {
3826
- const relativeTarget = import_node_path10.default.relative(resolvedProjectRoot, resolvedTargetPath);
3827
- if (relativeTarget.startsWith("..") || import_node_path10.default.isAbsolute(relativeTarget)) {
4144
+ const relativeTarget = import_node_path12.default.relative(resolvedProjectRoot, resolvedTargetPath);
4145
+ if (relativeTarget.startsWith("..") || import_node_path12.default.isAbsolute(relativeTarget)) {
3828
4146
  res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
3829
4147
  res.end("replacement file must be inside the project root");
3830
4148
  return true;
@@ -4183,6 +4501,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
4183
4501
  nextCliFallbackId = 1;
4184
4502
  cliIdleTimer = null;
4185
4503
  agentHost;
4504
+ planeHost = new PlaneHost();
4186
4505
  static CLI_IDLE_TIMEOUT_MS = 6e4;
4187
4506
  static CLI_LEASE_TTL_MS = 6e5;
4188
4507
  static USER_ACTIVE_LEASE_TTL_MS = 8e3;
@@ -4414,6 +4733,10 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
4414
4733
  void this.agentHost.handleMessage(ws, msg);
4415
4734
  return;
4416
4735
  }
4736
+ if (typeof msg.type === "string" && msg.type.startsWith("plane:")) {
4737
+ void this.planeHost.handleMessage(ws, msg);
4738
+ return;
4739
+ }
4417
4740
  if (msg.type === "runtime:list") {
4418
4741
  const versions = sootsimRuntime.listInstalled();
4419
4742
  const active = this.getActiveRuntime();
@@ -4736,6 +5059,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
4736
5059
  });
4737
5060
  ws.on("close", () => {
4738
5061
  this.agentHost.unregisterSocket(ws);
5062
+ this.planeHost.unregisterSocket(ws);
4739
5063
  if (role === "sim" && sim) {
4740
5064
  this.rememberDisconnectedSim(sim);
4741
5065
  if (this.primarySimId === sim.id) {