rnxsim 0.1.343 → 0.1.345

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 (209) hide show
  1. package/README.md +59 -31
  2. package/cli/app-fonts.ts +8 -28
  3. package/cli/app-project.ts +12 -184
  4. package/cli/app-splash.ts +3 -17
  5. package/cli/bin.ts +15 -28
  6. package/cli/commands/control.ts +20 -30
  7. package/cli/commands/daemon.ts +19 -44
  8. package/cli/commands/flow.ts +0 -27
  9. package/cli/commands/install-desktop.ts +2 -16
  10. package/cli/commands/login.ts +3 -43
  11. package/cli/commands/logout.ts +0 -7
  12. package/cli/commands/serve.ts +2 -32
  13. package/cli/commands/setup.ts +20 -194
  14. package/cli/commands/telemetry.ts +7 -5
  15. package/cli/commands/upload.ts +1 -48
  16. package/cli/commands/welcome.ts +64 -0
  17. package/cli/outbound-endpoints.ts +586 -0
  18. package/cli/privacy.ts +53 -0
  19. package/cli/runtime-notes.ts +2 -2
  20. package/cli/telemetry.ts +46 -149
  21. package/dist-cli/bin.js +6 -6
  22. package/dist-cli/chunks/{agent-RFD32ACU.js → agent-52G55XTO.js} +2 -2
  23. package/dist-cli/chunks/{agent-wrapper-2YLW2HQK.js → agent-wrapper-BOZM43ON.js} +2 -2
  24. package/dist-cli/chunks/{app-fonts-6OATDF3Y.js → app-fonts-A2L6V5EZ.js} +2 -2
  25. package/dist-cli/chunks/{assert-PSQICFOY.js → assert-CJKIP4LN.js} +2 -2
  26. package/dist-cli/chunks/{auth-XWLFKZT7.js → auth-VHHN3TOB.js} +2 -2
  27. package/dist-cli/chunks/{beta-I6WTVM6C.js → beta-EBDIGV3E.js} +2 -2
  28. package/dist-cli/chunks/{camera-45HKTLWG.js → camera-5F4KKCQ3.js} +2 -2
  29. package/dist-cli/chunks/chunk-3DBTS4MC.js +15 -0
  30. package/dist-cli/chunks/chunk-3N7M5HMJ.js +6 -0
  31. package/dist-cli/chunks/{chunk-RWYA2USN.js → chunk-4BIPA7MD.js} +1 -1
  32. package/dist-cli/chunks/chunk-4OKV7T5D.js +4 -0
  33. package/dist-cli/chunks/{chunk-T4T6DDKM.js → chunk-4ZILDTZX.js} +2 -2
  34. package/dist-cli/chunks/{chunk-R7X74I3I.js → chunk-5GXRRUOA.js} +26 -26
  35. package/dist-cli/chunks/chunk-63ASUHOV.js +4 -0
  36. package/dist-cli/chunks/{chunk-DASP2EKF.js → chunk-7GO2DSYQ.js} +1 -1
  37. package/dist-cli/chunks/{chunk-HJXIBZ35.js → chunk-7LGINKM6.js} +2 -2
  38. package/dist-cli/chunks/{chunk-2IPMH24A.js → chunk-7UIIAHVU.js} +2 -2
  39. package/dist-cli/chunks/{chunk-ES5BHPTS.js → chunk-7V4KPZ7O.js} +2 -2
  40. package/dist-cli/chunks/{chunk-FCHRGEHA.js → chunk-7YMQRA6L.js} +2 -2
  41. package/dist-cli/chunks/{chunk-DC7TS573.js → chunk-A2Q76LJV.js} +2 -2
  42. package/dist-cli/chunks/{chunk-SAUCA7SN.js → chunk-BCQV63BW.js} +45 -45
  43. package/dist-cli/chunks/{chunk-3FYJ7U3C.js → chunk-BFTDL2GX.js} +17 -17
  44. package/dist-cli/chunks/chunk-BHKSCNDC.js +4 -0
  45. package/dist-cli/chunks/{chunk-QPTJNLPT.js → chunk-DGTV3KOW.js} +1 -1
  46. package/dist-cli/chunks/chunk-DHM3JDCK.js +4 -0
  47. package/dist-cli/chunks/{chunk-TQRVLK64.js → chunk-DPP2M4AJ.js} +1 -1
  48. package/dist-cli/chunks/chunk-E2RGSCHT.js +4 -0
  49. package/dist-cli/chunks/{chunk-TQ5OPG33.js → chunk-EPLVIDPC.js} +1 -1
  50. package/dist-cli/chunks/{chunk-P3IDK7FR.js → chunk-GKGXULJK.js} +2 -2
  51. package/dist-cli/chunks/{chunk-34KDMQ6U.js → chunk-HK7YKH6N.js} +2 -2
  52. package/dist-cli/chunks/{chunk-BJUNXCPO.js → chunk-HXE344V3.js} +1 -1
  53. package/dist-cli/chunks/{chunk-HJHFC5ZZ.js → chunk-JC33TOAF.js} +1 -1
  54. package/dist-cli/chunks/{chunk-G3B2RZ47.js → chunk-KA4PJNTE.js} +2 -2
  55. package/dist-cli/chunks/{chunk-VC4TDPJQ.js → chunk-KH777UAO.js} +3 -3
  56. package/dist-cli/chunks/{chunk-4PFMDBLB.js → chunk-KRZPFXVN.js} +1 -1
  57. package/dist-cli/chunks/{chunk-6SLM6URN.js → chunk-KZFJNDH3.js} +1 -1
  58. package/dist-cli/chunks/{chunk-KLMWLQAO.js → chunk-LAQGSSTT.js} +1 -1
  59. package/dist-cli/chunks/chunk-LRM2XCEQ.js +11 -0
  60. package/dist-cli/chunks/{chunk-YKKVDKLE.js → chunk-NUPRHZQW.js} +2 -2
  61. package/dist-cli/chunks/{chunk-AKV2DGKM.js → chunk-OPHT4DUO.js} +1 -1
  62. package/dist-cli/chunks/{chunk-KON6XG4Y.js → chunk-P2CGGMU2.js} +2 -2
  63. package/dist-cli/chunks/{chunk-5FB4EA33.js → chunk-PLB6UVVX.js} +2 -2
  64. package/dist-cli/chunks/{chunk-FRRHCLG2.js → chunk-PLUWN2V5.js} +1 -1
  65. package/dist-cli/chunks/{chunk-WDA4QR6U.js → chunk-QNBBKRCA.js} +2 -2
  66. package/dist-cli/chunks/{chunk-G7S6T5OX.js → chunk-QNRWPAOA.js} +2 -2
  67. package/dist-cli/chunks/{chunk-2EVBZ37G.js → chunk-R7ZNRGQN.js} +1 -1
  68. package/dist-cli/chunks/{chunk-TMJGPUWJ.js → chunk-RB4VFUNM.js} +3 -3
  69. package/dist-cli/chunks/chunk-RT3DXTDP.js +25 -0
  70. package/dist-cli/chunks/{chunk-OYLIWHDB.js → chunk-RTDHX6CW.js} +2 -2
  71. package/dist-cli/chunks/{chunk-YLFDA4WW.js → chunk-T7SBFG5I.js} +2 -2
  72. package/dist-cli/chunks/{chunk-W7EXE22C.js → chunk-TEU5NGUA.js} +2 -2
  73. package/dist-cli/chunks/chunk-TMRD5MHL.js +99 -0
  74. package/dist-cli/chunks/{chunk-SU7RE3PV.js → chunk-TOF5TA5X.js} +11 -11
  75. package/dist-cli/chunks/{chunk-6IVEGYWV.js → chunk-U5YHXQHH.js} +2 -2
  76. package/dist-cli/chunks/{chunk-BA3HRFJ5.js → chunk-UVOEHIGP.js} +1 -1
  77. package/dist-cli/chunks/{chunk-SW4DV2TY.js → chunk-VLBN3KRC.js} +1 -1
  78. package/dist-cli/chunks/{chunk-7WKV6EKX.js → chunk-WPPGU5IK.js} +2 -2
  79. package/dist-cli/chunks/{chunk-JOE3J46E.js → chunk-WU72KL6Z.js} +2 -2
  80. package/dist-cli/chunks/chunk-XJBFJGNT.js +4 -0
  81. package/dist-cli/chunks/{chunk-3CMKNWRL.js → chunk-XXZLK4BY.js} +1 -1
  82. package/dist-cli/chunks/{chunk-BKMPV6N4.js → chunk-YVSUFVVB.js} +1 -1
  83. package/dist-cli/chunks/{chunk-RO3WVQPB.js → chunk-ZMTZ5G5H.js} +2 -2
  84. package/dist-cli/chunks/{cleanup-OU4VKFC3.js → cleanup-MAVZJXVW.js} +2 -2
  85. package/dist-cli/chunks/cli-version-2B4KRNML.js +4 -0
  86. package/dist-cli/chunks/{compat-GAEKP67T.js → compat-7AURLGCS.js} +2 -2
  87. package/dist-cli/chunks/config-TN3Y4J5N.js +13 -0
  88. package/dist-cli/chunks/{control-VQJURMFD.js → control-EV2ELU5R.js} +2 -2
  89. package/dist-cli/chunks/daemon-KKDG5MYR.js +4 -0
  90. package/dist-cli/chunks/{debug-F5N2LO4T.js → debug-JICITUB6.js} +3 -3
  91. package/dist-cli/chunks/{desktop-I4ILT4BS.js → desktop-4F5HGLLX.js} +3 -3
  92. package/dist-cli/chunks/{detox-2HMPYSP4.js → detox-ZC7NHLJI.js} +2 -2
  93. package/dist-cli/chunks/{device-5LWLOB5R.js → device-V57GHRXX.js} +2 -2
  94. package/dist-cli/chunks/{diagnose-5S4IQPAP.js → diagnose-FF4IWYXC.js} +2 -2
  95. package/dist-cli/chunks/{disk-cleanup-CJSZVJQX.js → disk-cleanup-T277L6ZO.js} +2 -2
  96. package/dist-cli/chunks/drivers-LCX7S76X.js +4 -0
  97. package/dist-cli/chunks/{film-HCPU2TDB.js → film-G7ZAPWB7.js} +3 -3
  98. package/dist-cli/chunks/flow-ONZGGFGD.js +4 -0
  99. package/dist-cli/chunks/help-DROVDXJM.js +4 -0
  100. package/dist-cli/chunks/{hidden-runtime-alias-YYUEZQPR.js → hidden-runtime-alias-VSJNNSNI.js} +2 -2
  101. package/dist-cli/chunks/{home-paths-3FXISM5I.js → home-paths-75HP3HKA.js} +2 -2
  102. package/dist-cli/chunks/inspect-MDA7ESGQ.js +4 -0
  103. package/dist-cli/chunks/install-desktop-EIVZHIFA.js +4 -0
  104. package/dist-cli/chunks/login-ZSIHGHUK.js +26 -0
  105. package/dist-cli/chunks/logout-Q4DXMI2C.js +4 -0
  106. package/dist-cli/chunks/{maestro-F5KK4RC4.js → maestro-LE5LCRBL.js} +3 -3
  107. package/dist-cli/chunks/{maestro-generate-6SUU3Y5I.js → maestro-generate-D3ZIJCRL.js} +3 -3
  108. package/dist-cli/chunks/{mode-CSWGOJLI.js → mode-BWX6MDPJ.js} +2 -2
  109. package/dist-cli/chunks/{optional-demo-registry-Y6VJ4SVV.js → optional-demo-registry-6CVASL3S.js} +2 -2
  110. package/dist-cli/chunks/{perf-NRWWAJL4.js → perf-NXQZISN6.js} +2 -2
  111. package/dist-cli/chunks/{permissions-OD62A3DJ.js → permissions-5PD67RRO.js} +2 -2
  112. package/dist-cli/chunks/privacy-H5KNRNIZ.js +4 -0
  113. package/dist-cli/chunks/{record-JZT2GM3G.js → record-YJF4KXMR.js} +3 -3
  114. package/dist-cli/chunks/{report-issue-ZWSNHOPB.js → report-issue-XOL5FQBG.js} +2 -2
  115. package/dist-cli/chunks/reset-5LZN5P5B.js +4 -0
  116. package/dist-cli/chunks/runtime-U36VWBH3.js +4 -0
  117. package/dist-cli/chunks/{screenshot-command-ZGJR7ICY.js → screenshot-command-VRPJXWFS.js} +3 -3
  118. package/dist-cli/chunks/{screenshot-layers-5AQKOJZK.js → screenshot-layers-QP4WA2F5.js} +2 -2
  119. package/dist-cli/chunks/{screenshots-capture-CPTWK5Y3.js → screenshots-capture-O2IBAVSP.js} +2 -2
  120. package/dist-cli/chunks/serve-WC6RZNJL.js +118 -0
  121. package/dist-cli/chunks/setup-A6BIG3KL.js +30 -0
  122. package/dist-cli/chunks/{skills-NVWRBMEO.js → skills-A7YC6RL5.js} +2 -2
  123. package/dist-cli/chunks/{state-QGLTKN6K.js → state-IJI5YROJ.js} +2 -2
  124. package/dist-cli/chunks/{storage-OZMEZ4AV.js → storage-Z2DBO2PQ.js} +4 -4
  125. package/dist-cli/chunks/store-BYTCKNIC.js +4 -0
  126. package/dist-cli/chunks/telemetry-JLHFMD7N.js +4 -0
  127. package/dist-cli/chunks/{timeline-EXLDREY3.js → timeline-N7V2M6QA.js} +2 -2
  128. package/dist-cli/chunks/{upgrade-4VWXQO6I.js → upgrade-P2NH3BOR.js} +2 -2
  129. package/dist-cli/chunks/upload-Z2WX5DCD.js +4 -0
  130. package/dist-cli/chunks/{version-WZM6XCD2.js → version-OUHCSCUS.js} +2 -2
  131. package/dist-cli/chunks/{web-ZYPEUVWL.js → web-6NRTF3UV.js} +2 -2
  132. package/dist-cli/chunks/welcome-6RS7PEJW.js +5 -0
  133. package/dist-cli/chunks/{what-happened-KF2HLWKW.js → what-happened-FUEL3BEF.js} +2 -2
  134. package/dist-lib/agent-daemon-client.cjs +1 -1
  135. package/dist-lib/agent-events.cjs +1 -1
  136. package/dist-lib/agent-identity.cjs +1 -1
  137. package/dist-lib/agent-sessions.cjs +1 -1
  138. package/dist-lib/attached-projects.cjs +1 -1
  139. package/dist-lib/auth/shared-session.cjs +1 -1
  140. package/dist-lib/backend-origin.cjs +1 -1
  141. package/dist-lib/beta.cjs +1 -1
  142. package/dist-lib/beta.mjs +1 -1
  143. package/dist-lib/bridge-constants.cjs +1 -1
  144. package/dist-lib/bridge-contract.cjs +1 -1
  145. package/dist-lib/cli-constants.cjs +1 -1
  146. package/dist-lib/config.cjs +1 -1
  147. package/dist-lib/detox/index.cjs +1 -1
  148. package/dist-lib/dev-bundle-resolution.cjs +18 -4
  149. package/dist-lib/home-paths.cjs +145 -11
  150. package/dist-lib/host/bridge-host.cjs +236 -31
  151. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  152. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  153. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  154. package/dist-lib/host/replacement-module-handler.cjs +71 -4
  155. package/dist-lib/host/websocket-proxy.cjs +1 -1
  156. package/dist-lib/index.cjs +337 -37
  157. package/dist-lib/jump-to-source-babel.cjs +1 -1
  158. package/dist-lib/menu.cjs +1 -1
  159. package/dist-lib/menu.mjs +1 -1
  160. package/dist-lib/metro-production-bundle.cjs +1 -1
  161. package/dist-lib/metro-production-bundle.mjs +1 -1
  162. package/dist-lib/metro.cjs +484 -14
  163. package/dist-lib/profiles.cjs +1 -1
  164. package/dist-lib/public-brand.cjs +1 -1
  165. package/dist-lib/react-native-host-modules.cjs +1 -1
  166. package/dist-lib/react-native-host-modules.mjs +1 -1
  167. package/dist-lib/render-mode.cjs +1 -1
  168. package/dist-lib/scripts/dev-server-scanner.cjs +25 -2
  169. package/dist-lib/sdk.cjs +1 -1
  170. package/dist-lib/sdk.mjs +1 -1
  171. package/dist-lib/skills.cjs +384 -385
  172. package/dist-lib/vite.cjs +193 -37
  173. package/package.json +2 -1
  174. package/scripts/dev-server-scanner.ts +12 -5
  175. package/skills/rnx-test/SKILL.md +4 -0
  176. package/src/dev-bundle-resolution.ts +13 -6
  177. package/src/dev-server-open.ts +197 -0
  178. package/src/home-paths.ts +168 -15
  179. package/src/host/bridge-host.ts +2 -2
  180. package/src/host/replacement-module-handler.ts +68 -2
  181. package/src/metro-plugin.ts +83 -5
  182. package/src/native-dev-bundle-url.ts +22 -0
  183. package/src/vite-plugin-one.ts +42 -28
  184. package/dist-cli/chunks/chunk-44LRQEU3.js +0 -4
  185. package/dist-cli/chunks/chunk-4RKTGL63.js +0 -9
  186. package/dist-cli/chunks/chunk-5SCIDXJD.js +0 -25
  187. package/dist-cli/chunks/chunk-CUVY4A2A.js +0 -4
  188. package/dist-cli/chunks/chunk-DPJGB7MP.js +0 -101
  189. package/dist-cli/chunks/chunk-HD6P3W5R.js +0 -4
  190. package/dist-cli/chunks/chunk-HFNESSHG.js +0 -15
  191. package/dist-cli/chunks/chunk-I7HHBFSD.js +0 -4
  192. package/dist-cli/chunks/chunk-LV2G34IX.js +0 -4
  193. package/dist-cli/chunks/cli-version-REQOM22G.js +0 -4
  194. package/dist-cli/chunks/config-QTFXJKD6.js +0 -13
  195. package/dist-cli/chunks/daemon-ETA375WE.js +0 -4
  196. package/dist-cli/chunks/drivers-R4YB2XAF.js +0 -4
  197. package/dist-cli/chunks/flow-3VQL6XZT.js +0 -4
  198. package/dist-cli/chunks/help-2RYCKZ2G.js +0 -4
  199. package/dist-cli/chunks/inspect-XDSX5D6D.js +0 -4
  200. package/dist-cli/chunks/install-desktop-OKVRLL35.js +0 -4
  201. package/dist-cli/chunks/login-4VCBPAY2.js +0 -28
  202. package/dist-cli/chunks/logout-WSL5JS5R.js +0 -4
  203. package/dist-cli/chunks/reset-3C52YQCV.js +0 -4
  204. package/dist-cli/chunks/runtime-4V2RBUOR.js +0 -4
  205. package/dist-cli/chunks/serve-ERAZQGCJ.js +0 -118
  206. package/dist-cli/chunks/setup-UG6PLYIL.js +0 -32
  207. package/dist-cli/chunks/store-VPBQKKWA.js +0 -4
  208. package/dist-cli/chunks/telemetry-FRW7OCYZ.js +0 -4
  209. package/dist-cli/chunks/upload-C6676NST.js +0 -4
package/src/home-paths.ts CHANGED
@@ -233,8 +233,8 @@ export function readPrivacyPreferences(): PrivacyPreferences {
233
233
  const productAnalytics = settings?.productAnalytics
234
234
  const crashReports = settings?.crashReports
235
235
  return {
236
- productAnalytics: typeof productAnalytics === 'boolean' ? productAnalytics : true,
237
- crashReports: typeof crashReports === 'boolean' ? crashReports : true,
236
+ productAnalytics: typeof productAnalytics === 'boolean' ? productAnalytics : false,
237
+ crashReports: typeof crashReports === 'boolean' ? crashReports : false,
238
238
  configured:
239
239
  typeof productAnalytics === 'boolean' && typeof crashReports === 'boolean',
240
240
  }
@@ -401,6 +401,13 @@ export interface DevBridgeLockfile {
401
401
  }
402
402
 
403
403
  const DAEMON_LOCKFILE_MAX_BYTES = 16 * 1024
404
+ const DAEMON_CLAIM_STALE_MS = 30_000
405
+
406
+ interface DaemonClaim {
407
+ pid: number
408
+ startedAt: number
409
+ createdAt: number
410
+ }
404
411
 
405
412
  export function readDaemonLockfile(): DaemonLockfile | null {
406
413
  try {
@@ -549,9 +556,28 @@ export function readLiveRuntimeVersions(): string[] {
549
556
 
550
557
  export function writeDaemonLockfile(data: DaemonLockfile): void {
551
558
  ensureSootsimHome()
552
- const tmp = `${daemonLockfilePath()}.tmp`
559
+ const lockfilePath = daemonLockfilePath()
560
+ const existing = readDaemonLockfile()
561
+ if (!existing) {
562
+ if (createDaemonLockfileExclusive(data)) return
563
+ throw new Error('another rnx daemon claimed the lockfile before this write')
564
+ }
565
+ if (!sameDaemonOwner(existing, data)) {
566
+ throw new Error('refusing to overwrite another rnx daemon lockfile')
567
+ }
568
+ const tmp = `${lockfilePath}.${data.pid}.${randomUUID()}.tmp`
553
569
  fs.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
554
- fs.renameSync(tmp, daemonLockfilePath())
570
+ try {
571
+ const current = readDaemonLockfile()
572
+ if (!current || !sameDaemonOwner(current, data)) {
573
+ throw new Error('rnx daemon lock ownership changed during heartbeat')
574
+ }
575
+ fs.renameSync(tmp, lockfilePath)
576
+ } finally {
577
+ try {
578
+ fs.unlinkSync(tmp)
579
+ } catch {}
580
+ }
555
581
  }
556
582
 
557
583
  export function writeDevBridgeLockfile(data: DevBridgeLockfile): void {
@@ -562,23 +588,150 @@ export function writeDevBridgeLockfile(data: DevBridgeLockfile): void {
562
588
  fs.renameSync(tmp, lockfilePath)
563
589
  }
564
590
 
565
- /** try to claim the lockfile atomically on daemon boot. returns true if
566
- * we now own it. returns false when another fresh daemon beat us to the
567
- * punch — callers should refuse to start. a stale lockfile (dead pid or
568
- * old heartbeat) is overwritten. race-safe: the final fs.renameSync is
569
- * atomic on POSIX and on NTFS. */
591
+ function sameDaemonOwner(
592
+ left: Pick<DaemonLockfile, 'pid' | 'startedAt'>,
593
+ right: Pick<DaemonLockfile, 'pid' | 'startedAt'>,
594
+ ): boolean {
595
+ return left.pid === right.pid && left.startedAt === right.startedAt
596
+ }
597
+
598
+ function isAlreadyExists(error: unknown): boolean {
599
+ return (
600
+ error instanceof Error && 'code' in error && Reflect.get(error, 'code') === 'EEXIST'
601
+ )
602
+ }
603
+
604
+ function createDaemonLockfileExclusive(data: DaemonLockfile): boolean {
605
+ const lockfilePath = daemonLockfilePath()
606
+ const tmp = `${lockfilePath}.${data.pid}.${randomUUID()}.claim`
607
+ fs.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
608
+ try {
609
+ fs.linkSync(tmp, lockfilePath)
610
+ return true
611
+ } catch (error) {
612
+ if (isAlreadyExists(error)) return false
613
+ throw error
614
+ } finally {
615
+ try {
616
+ fs.unlinkSync(tmp)
617
+ } catch {}
618
+ }
619
+ }
620
+
621
+ function daemonClaimPath(): string {
622
+ return `${daemonLockfilePath()}.claim`
623
+ }
624
+
625
+ function parseDaemonClaim(raw: string): DaemonClaim | null {
626
+ try {
627
+ const value: unknown = JSON.parse(raw)
628
+ if (
629
+ value !== null &&
630
+ typeof value === 'object' &&
631
+ typeof Reflect.get(value, 'pid') === 'number' &&
632
+ typeof Reflect.get(value, 'startedAt') === 'number' &&
633
+ typeof Reflect.get(value, 'createdAt') === 'number'
634
+ ) {
635
+ return {
636
+ pid: Reflect.get(value, 'pid'),
637
+ startedAt: Reflect.get(value, 'startedAt'),
638
+ createdAt: Reflect.get(value, 'createdAt'),
639
+ }
640
+ }
641
+ } catch {}
642
+ return null
643
+ }
644
+
645
+ function readDaemonClaim(): DaemonClaim | null {
646
+ try {
647
+ return parseDaemonClaim(fs.readFileSync(daemonClaimPath(), 'utf8'))
648
+ } catch {
649
+ return null
650
+ }
651
+ }
652
+
653
+ function isDaemonClaimFresh(claim: DaemonClaim, now = Date.now()): boolean {
654
+ if (now - claim.createdAt > DAEMON_CLAIM_STALE_MS) return false
655
+ try {
656
+ process.kill(claim.pid, 0)
657
+ return true
658
+ } catch {
659
+ return false
660
+ }
661
+ }
662
+
663
+ function acquireDaemonClaim(data: DaemonLockfile): boolean {
664
+ const claim: DaemonClaim = {
665
+ pid: data.pid,
666
+ startedAt: data.startedAt,
667
+ createdAt: Date.now(),
668
+ }
669
+ const serialized = `${JSON.stringify(claim)}\n`
670
+ for (let attempt = 0; attempt < 8; attempt++) {
671
+ try {
672
+ fs.writeFileSync(daemonClaimPath(), serialized, { encoding: 'utf8', flag: 'wx' })
673
+ return true
674
+ } catch (error) {
675
+ if (!isAlreadyExists(error)) throw error
676
+ }
677
+ const existing = readDaemonClaim()
678
+ if (existing && sameDaemonOwner(existing, claim)) return true
679
+ if (existing && isDaemonClaimFresh(existing)) return false
680
+ let before: string
681
+ try {
682
+ before = fs.readFileSync(daemonClaimPath(), 'utf8')
683
+ const latest = parseDaemonClaim(before)
684
+ if (latest && isDaemonClaimFresh(latest)) return false
685
+ if (fs.readFileSync(daemonClaimPath(), 'utf8') !== before) continue
686
+ fs.unlinkSync(daemonClaimPath())
687
+ } catch {}
688
+ }
689
+ return false
690
+ }
691
+
692
+ function releaseDaemonClaim(owner: Pick<DaemonLockfile, 'pid' | 'startedAt'>): void {
693
+ try {
694
+ const claim = readDaemonClaim()
695
+ if (!claim || !sameDaemonOwner(claim, owner)) return
696
+ fs.unlinkSync(daemonClaimPath())
697
+ } catch {}
698
+ }
699
+
700
+ /** try to claim the lockfile atomically on daemon boot. a short-lived,
701
+ * process-owned election file serializes concurrent fallback-port binds;
702
+ * the complete lock snapshot is then installed with an exclusive hard link. */
570
703
  export function claimDaemonLockfile(data: DaemonLockfile): boolean {
571
704
  ensureSootsimHome()
572
- const existing = readDaemonLockfile()
573
- if (existing && isDaemonLockfileFresh(existing) && existing.pid !== data.pid) {
574
- return false
705
+ if (!acquireDaemonClaim(data)) return false
706
+ try {
707
+ const existing = readDaemonLockfile()
708
+ if (existing && sameDaemonOwner(existing, data)) {
709
+ writeDaemonLockfile(data)
710
+ return true
711
+ }
712
+ if (existing && isDaemonLockfileFresh(existing)) return false
713
+ if (existing) {
714
+ const latest = readDaemonLockfile()
715
+ if (!latest || !sameDaemonOwner(latest, existing)) return false
716
+ if (isDaemonLockfileFresh(latest)) return false
717
+ try {
718
+ fs.unlinkSync(daemonLockfilePath())
719
+ } catch {
720
+ return false
721
+ }
722
+ }
723
+ return createDaemonLockfileExclusive(data)
724
+ } finally {
725
+ releaseDaemonClaim(data)
575
726
  }
576
- writeDaemonLockfile(data)
577
- return true
578
727
  }
579
728
 
580
- export function removeDaemonLockfile(): void {
729
+ export function removeDaemonLockfile(
730
+ owner: Pick<DaemonLockfile, 'pid' | 'startedAt'>,
731
+ ): void {
581
732
  try {
733
+ const lockfile = readDaemonLockfile()
734
+ if (!lockfile || !sameDaemonOwner(lockfile, owner)) return
582
735
  fs.unlinkSync(daemonLockfilePath())
583
736
  } catch {}
584
737
  }
@@ -1525,7 +1525,7 @@ export class SootSimBridgeHost {
1525
1525
  removeLockfile() {
1526
1526
  if (!this.shouldWriteLockfile) return
1527
1527
  try {
1528
- removeDaemonLockfile()
1528
+ removeDaemonLockfile({ pid: process.pid, startedAt: this.startedAt })
1529
1529
  } catch {}
1530
1530
  }
1531
1531
 
@@ -2229,7 +2229,7 @@ export class SootSimBridgeHost {
2229
2229
  }
2230
2230
  if (this.shouldWriteLockfile) {
2231
2231
  try {
2232
- removeDaemonLockfile()
2232
+ removeDaemonLockfile({ pid: process.pid, startedAt: this.startedAt })
2233
2233
  } catch {}
2234
2234
  }
2235
2235
  if (this.shouldWriteDevLockfile) {
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
  import { build } from 'esbuild'
4
+ import { isLoopbackHost } from '../backend-origin.ts'
4
5
  import type { IncomingMessage, ServerResponse } from 'node:http'
5
6
 
6
7
  export const REPLACEMENT_MODULE_ROUTE = '/__sootsim-replacement-module'
@@ -8,6 +9,9 @@ export const REPLACEMENT_RUNTIME_PREFIX = '/__sootsim-replacement-runtime'
8
9
 
9
10
  export interface ReplacementModuleHandlerOptions {
10
11
  reactBridgeUrl?: string
12
+ projectRoot?: string
13
+ requireLoopbackOrigin?: boolean
14
+ requestToken?: string
11
15
  }
12
16
 
13
17
  function getReplacementLoader(filePath: string): 'js' | 'jsx' | 'ts' | 'tsx' | null {
@@ -113,9 +117,19 @@ export const Appearance = rn.Appearance
113
117
  }
114
118
 
115
119
  function sendJavaScript(req: IncomingMessage, res: ServerResponse, source: string): void {
120
+ const origin = req.headers.origin
121
+ let allowedOrigin: string | undefined
122
+ if (origin) {
123
+ try {
124
+ if (isLoopbackHost(new URL(origin).hostname)) allowedOrigin = origin
125
+ } catch {}
126
+ }
116
127
  res.writeHead(200, {
117
128
  'Content-Type': 'application/javascript; charset=utf-8',
118
129
  'Cache-Control': 'no-cache',
130
+ ...(allowedOrigin
131
+ ? { 'Access-Control-Allow-Origin': allowedOrigin, Vary: 'Origin' }
132
+ : {}),
119
133
  })
120
134
  res.end(req.method === 'HEAD' ? undefined : source)
121
135
  }
@@ -146,6 +160,24 @@ export async function handleReplacementModuleRequest(
146
160
  return false
147
161
  }
148
162
 
163
+ if (options.requireLoopbackOrigin) {
164
+ const origin = req.headers.origin
165
+ const remoteAddress = req.socket.remoteAddress?.replace(/^::ffff:/, '')
166
+ let allowed = Boolean(remoteAddress && isLoopbackHost(remoteAddress))
167
+ if (allowed && origin) {
168
+ try {
169
+ allowed = isLoopbackHost(new URL(origin).hostname)
170
+ } catch {
171
+ allowed = false
172
+ }
173
+ }
174
+ if (!allowed) {
175
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' })
176
+ res.end('replacement modules are available only to loopback origins')
177
+ return true
178
+ }
179
+ }
180
+
149
181
  if (url.pathname === `${REPLACEMENT_RUNTIME_PREFIX}/react`) {
150
182
  sendJavaScript(req, res, reactRuntimeSource(options.reactBridgeUrl))
151
183
  return true
@@ -156,6 +188,12 @@ export async function handleReplacementModuleRequest(
156
188
  }
157
189
  if (url.pathname !== REPLACEMENT_MODULE_ROUTE) return false
158
190
 
191
+ if (options.requestToken && url.searchParams.get('token') !== options.requestToken) {
192
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' })
193
+ res.end('replacement module request token is invalid')
194
+ return true
195
+ }
196
+
159
197
  const targetPath = url.searchParams.get('path')
160
198
  if (!targetPath) {
161
199
  res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' })
@@ -170,8 +208,27 @@ export async function handleReplacementModuleRequest(
170
208
  return true
171
209
  }
172
210
 
211
+ if (!path.isAbsolute(targetPath)) {
212
+ res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' })
213
+ res.end('replacement file path must be absolute')
214
+ return true
215
+ }
216
+
217
+ let resolvedProjectRoot: string | undefined
218
+ if (options.projectRoot) {
219
+ const lexicalProjectRoot = path.resolve(options.projectRoot)
220
+ resolvedProjectRoot = await fs.realpath(options.projectRoot)
221
+ const lexicalTarget = path.relative(lexicalProjectRoot, targetPath)
222
+ if (lexicalTarget.startsWith('..') || path.isAbsolute(lexicalTarget)) {
223
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' })
224
+ res.end('replacement file must be inside the project root')
225
+ return true
226
+ }
227
+ }
228
+
229
+ let resolvedTargetPath: string
173
230
  try {
174
- await fs.access(targetPath)
231
+ resolvedTargetPath = await fs.realpath(targetPath)
175
232
  } catch (error) {
176
233
  const message = error instanceof Error ? error.message : String(error)
177
234
  res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' })
@@ -179,9 +236,18 @@ export async function handleReplacementModuleRequest(
179
236
  return true
180
237
  }
181
238
 
239
+ if (resolvedProjectRoot) {
240
+ const relativeTarget = path.relative(resolvedProjectRoot, resolvedTargetPath)
241
+ if (relativeTarget.startsWith('..') || path.isAbsolute(relativeTarget)) {
242
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' })
243
+ res.end('replacement file must be inside the project root')
244
+ return true
245
+ }
246
+ }
247
+
182
248
  try {
183
249
  const result = await build({
184
- entryPoints: [targetPath],
250
+ entryPoints: [resolvedTargetPath],
185
251
  bundle: true,
186
252
  write: false,
187
253
  format: 'esm',
@@ -10,7 +10,19 @@
10
10
 
11
11
  import fs from 'fs'
12
12
  import { createRequire } from 'module'
13
+ import { randomUUID } from 'node:crypto'
13
14
  import path from 'path'
15
+ import {
16
+ openRnxDesktopWhenMetroIsReady,
17
+ resolveRnxDesktopAppName,
18
+ resolveRnxDesktopLaunchTarget,
19
+ tagRnxBundleForPlugin,
20
+ type RnxDesktopOpenOption,
21
+ } from './dev-server-open'
22
+ import {
23
+ handleReplacementModuleRequest,
24
+ isReplacementModuleRequestUrl,
25
+ } from './host/replacement-module-handler'
14
26
  import {
15
27
  RNXSIM_METRO_PRODUCTION_BUNDLE_OPTION,
16
28
  createRNXMetroModulePathsFooter,
@@ -79,6 +91,7 @@ type MetroSerializer = (
79
91
  export interface RNXMetroOptions {
80
92
  bundleUrl?: string
81
93
  devServer?: boolean
94
+ open?: RnxDesktopOpenOption
82
95
  productionBundles?: boolean
83
96
  }
84
97
 
@@ -97,6 +110,7 @@ type MetroConfigWithServer = {
97
110
  }
98
111
  server?: {
99
112
  enhanceMiddleware?: (middleware: unknown, server: unknown) => unknown
113
+ port?: number
100
114
  [key: string]: unknown
101
115
  }
102
116
  }
@@ -210,15 +224,25 @@ export function withRNX<T extends Record<string, unknown>>(
210
224
  config: T,
211
225
  options: RNXMetroOptions = {},
212
226
  ): T {
213
- if (!options.devServer && !options.productionBundles) return config
227
+ if (!options.devServer && !options.open && !options.productionBundles) return config
214
228
 
215
- const bundleUrl =
229
+ const cfgProjectRoot = (config as MetroConfigWithServer).projectRoot || process.cwd()
230
+ const appName = resolveRnxDesktopAppName(options.open, cfgProjectRoot)
231
+ const configuredBundleUrl =
216
232
  options.bundleUrl || '/index.bundle?platform=ios&dev=true&hot=true&minify=false'
217
233
 
218
234
  const cfgServer = (config as MetroConfigWithServer).server
235
+ const port = resolveMetroPort(cfgServer)
236
+ const metroServerId = randomUUID()
237
+ const bundleUrl = tagRnxBundleForPlugin(
238
+ configuredBundleUrl,
239
+ `http://localhost:${port}`,
240
+ metroServerId,
241
+ cfgProjectRoot,
242
+ appName,
243
+ )
219
244
  const cfgResolver = (config as MetroConfigWithServer).resolver
220
245
  const cfgSerializer = (config as MetroConfigWithServer).serializer
221
- const cfgProjectRoot = (config as MetroConfigWithServer).projectRoot || process.cwd()
222
246
  const existingEnhance = cfgServer?.enhanceMiddleware
223
247
  const existingResolveRequest = cfgResolver?.resolveRequest
224
248
  const existingSerializer = cfgSerializer?.customSerializer
@@ -299,9 +323,40 @@ export function withRNX<T extends Record<string, unknown>>(
299
323
  const server = connect()
300
324
 
301
325
  server.use((req: any, res: any, next: any) => {
326
+ if (isReplacementModuleRequestUrl(req.url)) {
327
+ void handleReplacementModuleRequest(req, res, {
328
+ projectRoot: cfgProjectRoot,
329
+ requestToken: metroServerId,
330
+ requireLoopbackOrigin: true,
331
+ }).then((handled) => {
332
+ if (!handled) next()
333
+ })
334
+ return
335
+ }
336
+
302
337
  const url = req.url || ''
303
338
  const pathname = url.split('?')[0]
304
339
 
340
+ if (pathname === '/__server-scan') {
341
+ const absoluteBundleUrl = bundleUrl.startsWith('http')
342
+ ? bundleUrl
343
+ : `http://localhost:${port}${bundleUrl}`
344
+ res.setHeader('content-type', 'application/json')
345
+ res.end(
346
+ JSON.stringify([
347
+ {
348
+ port,
349
+ framework: 'metro',
350
+ rnxServerId: metroServerId,
351
+ bundleUrl: absoluteBundleUrl,
352
+ ...(appName ? { projectName: appName } : {}),
353
+ lastSeen: Date.now(),
354
+ },
355
+ ]),
356
+ )
357
+ return
358
+ }
359
+
305
360
  const runtimeRoot = resolveRuntimeRootForRequestHost(req.headers?.host)
306
361
 
307
362
  if (!runtimeRoot) {
@@ -350,7 +405,7 @@ export function withRNX<T extends Record<string, unknown>>(
350
405
  let html = fs.readFileSync(htmlPath, 'utf8')
351
406
  html = html.replace(
352
407
  '</head>',
353
- `<script>history.replaceState(null,'','${prefix}/?bundle=${encodeURIComponent(bundleUrl)}')</script></head>`,
408
+ `<script>(function(){var u=new URL(location.href);if(!u.searchParams.has('bundle'))u.searchParams.set('bundle',${JSON.stringify(bundleUrl)});history.replaceState(null,'','${prefix}/'+u.search)})()</script></head>`,
354
409
  )
355
410
  res.setHeader('content-type', 'text/html')
356
411
  res.end(html)
@@ -368,11 +423,34 @@ export function withRNX<T extends Record<string, unknown>>(
368
423
  },
369
424
  }
370
425
 
426
+ if (appName && !process.env.CI && !process.env.SOOTSIM_NO_OPEN) {
427
+ void openRnxDesktopWhenMetroIsReady({
428
+ port,
429
+ bundleUrl: resolveRnxDesktopLaunchTarget(port, bundleUrl),
430
+ appName,
431
+ projectRoot: cfgProjectRoot,
432
+ serverId: metroServerId,
433
+ })
434
+ }
435
+
371
436
  return {
372
437
  ...config,
373
438
  ...productionConfig,
374
- ...(options.devServer ? { server: serverConfig } : {}),
439
+ ...(options.devServer || options.open ? { server: serverConfig } : {}),
375
440
  } as T
376
441
  }
377
442
 
443
+ function resolveMetroPort(server: MetroConfigWithServer['server']): number {
444
+ const inlinePort = process.argv.find((arg) => arg.startsWith('--port='))?.slice(7)
445
+ const portIndex = process.argv.indexOf('--port')
446
+ const splitPort = portIndex >= 0 ? process.argv[portIndex + 1] : undefined
447
+ const rawPort =
448
+ inlinePort || splitPort || process.env.RCT_METRO_PORT || server?.port || 8081
449
+ const port = Number(rawPort)
450
+ if (!Number.isInteger(port) || port <= 0) {
451
+ throw new Error(`rnx: Metro port must be a positive integer, got ${rawPort}`)
452
+ }
453
+ return port
454
+ }
455
+
378
456
  export default withRNX
@@ -1,3 +1,5 @@
1
+ import { isLoopbackHost } from './backend-origin'
2
+
1
3
  // canonical entry path for a plain Metro packager that serves no Expo
2
4
  // manifest. only used when `/status` confirms `packager-status:running` and
3
5
  // the server reports nothing else — never as a guess for a manifest-capable
@@ -13,6 +15,26 @@ function isNativeDevBundlePath(pathname: string): boolean {
13
15
  return pathname.endsWith('.bundle')
14
16
  }
15
17
 
18
+ // expo can advertise an IPv4 loopback launch URL even when its --localhost
19
+ // listener resolved to IPv6 only. keep the manifest's path and query, but bind
20
+ // its loopback host to the base URL that answered the discovery request.
21
+ export function alignLoopbackBundleUrlWithProbe(
22
+ bundleUrl: string,
23
+ probeBaseUrl: string,
24
+ ): string {
25
+ try {
26
+ const bundle = new URL(bundleUrl)
27
+ const probe = new URL(probeBaseUrl)
28
+ if (!isLoopbackHost(bundle.hostname) || !isLoopbackHost(probe.hostname)) {
29
+ return bundleUrl
30
+ }
31
+ bundle.hostname = probe.hostname
32
+ return bundle.toString()
33
+ } catch {
34
+ return bundleUrl
35
+ }
36
+ }
37
+
16
38
  // opt-in (contrastbot preview-recording context only): serve a production
17
39
  // minified bundle instead of the default dev=true unminified one. a
18
40
  // preview run just records the app — it never needs fast-refresh or dev
@@ -6,11 +6,17 @@
6
6
  // import { rnxPlugin } from 'rnxsim/vite'
7
7
  // export default { plugins: [one(), rnxPlugin()] }
8
8
 
9
- import { spawn } from 'child_process'
10
9
  import fs from 'fs'
10
+ import { randomUUID } from 'node:crypto'
11
11
  import path from 'path'
12
- import { fileURLToPath } from 'url'
13
12
  import { DEFAULT_SOOTSIM_BRIDGE_PORT } from './bridge-constants'
13
+ import {
14
+ launchRnxDesktop,
15
+ resolveRnxDesktopAppName,
16
+ resolveRnxDesktopLaunchTarget,
17
+ tagRnxBundleForPlugin,
18
+ type RnxDesktopOpenOption,
19
+ } from './dev-server-open'
14
20
  import { isDaemonLockfileFresh, readDaemonLockfile } from './home-paths'
15
21
  import { handleReplacementModuleRequest } from './host/replacement-module-handler'
16
22
  import {
@@ -31,7 +37,7 @@ export interface RnxPluginOptions {
31
37
  enabled?: boolean
32
38
  // launch the sootsim simulator loading this server's bundle once the dev
33
39
  // server is listening. skipped when CI or SOOTSIM_NO_OPEN is set.
34
- open?: boolean
40
+ open?: RnxDesktopOpenOption
35
41
  }
36
42
 
37
43
  export function rnxPlugin(options: RnxPluginOptions = {}): Plugin {
@@ -45,6 +51,21 @@ export function rnxPlugin(options: RnxPluginOptions = {}): Plugin {
45
51
  const bundleUrl =
46
52
  options.bundleUrl ||
47
53
  '/node_modules/one/metro-entry.bundle?platform=ios&dev=true&minify=false'
54
+ const appName = resolveRnxDesktopAppName(options.open, server.config.root)
55
+ const replacementToken = randomUUID()
56
+ const configuredPort = server.config.server.port || 8081
57
+ const listeningPort = (): number => {
58
+ const address = server.httpServer?.address()
59
+ return address && typeof address === 'object' ? address.port : configuredPort
60
+ }
61
+ const bundleForPort = (port: number): string =>
62
+ tagRnxBundleForPlugin(
63
+ bundleUrl,
64
+ `http://localhost:${port}`,
65
+ replacementToken,
66
+ server.config.root,
67
+ appName,
68
+ )
48
69
 
49
70
  // build the shell HTML, injecting the bundle URL (so main.tsx loads this
50
71
  // app's bundle) AND window.__sootsimBridgePort. the bridge-port injection
@@ -70,7 +91,7 @@ export function rnxPlugin(options: RnxPluginOptions = {}): Plugin {
70
91
  // `open` timed out after 60s.
71
92
  const inject =
72
93
  `<script>(function(){var u=new URL(location.href);` +
73
- `if(!u.searchParams.has('bundle'))u.searchParams.set('bundle',${JSON.stringify(bundleUrl)});` +
94
+ `if(!u.searchParams.has('bundle'))u.searchParams.set('bundle',${JSON.stringify(bundleForPort(listeningPort()))});` +
74
95
  `window.__sootsimBridgePort=${bridgePort};` +
75
96
  `history.replaceState(null,'','${prefix}/'+u.search)})()</script>`
76
97
  return html.replace('</head>', inject + '</head>')
@@ -107,10 +128,11 @@ export function rnxPlugin(options: RnxPluginOptions = {}): Plugin {
107
128
  // internals. shape matches DiscoveredServer enough for the resolver.
108
129
  server.middlewares.use((req, res, next) => {
109
130
  if ((req.url || '').split('?')[0] !== '/__server-scan') return next()
110
- const scanPort = server.config.server.port || 8081
111
- const absoluteBundleUrl = bundleUrl.startsWith('http')
112
- ? bundleUrl
113
- : `http://localhost:${scanPort}${bundleUrl}`
131
+ const scanPort = listeningPort()
132
+ const pluginBundleUrl = bundleForPort(scanPort)
133
+ const absoluteBundleUrl = pluginBundleUrl.startsWith('http')
134
+ ? pluginBundleUrl
135
+ : `http://localhost:${scanPort}${pluginBundleUrl}`
114
136
  res.setHeader('content-type', 'application/json')
115
137
  res.end(
116
138
  JSON.stringify([
@@ -118,6 +140,7 @@ export function rnxPlugin(options: RnxPluginOptions = {}): Plugin {
118
140
  port: scanPort,
119
141
  framework: 'one',
120
142
  bundleUrl: absoluteBundleUrl,
143
+ ...(appName ? { projectName: appName } : {}),
121
144
  lastSeen: Date.now(),
122
145
  },
123
146
  ]),
@@ -125,7 +148,11 @@ export function rnxPlugin(options: RnxPluginOptions = {}): Plugin {
125
148
  })
126
149
 
127
150
  server.middlewares.use((req, res, next) => {
128
- void handleReplacementModuleRequest(req, res).then((handled) => {
151
+ void handleReplacementModuleRequest(req, res, {
152
+ projectRoot: server.config.root,
153
+ requestToken: replacementToken,
154
+ requireLoopbackOrigin: true,
155
+ }).then((handled) => {
129
156
  if (!handled) next()
130
157
  })
131
158
  })
@@ -189,30 +216,17 @@ export function rnxPlugin(options: RnxPluginOptions = {}): Plugin {
189
216
  next()
190
217
  })
191
218
 
192
- const port = server.config.server.port || 8081
219
+ const port = configuredPort
193
220
  const sootsimUrl = `http://localhost:${port}${prefix}/`
194
221
  console.log(`[rnx] serving at ${sootsimUrl}`)
195
222
 
196
- if (options.open && !process.env.CI && !process.env.SOOTSIM_NO_OPEN) {
223
+ if (appName && !process.env.CI && !process.env.SOOTSIM_NO_OPEN) {
197
224
  server.httpServer?.once('listening', () => {
198
- // this package's own cli (package.json bin: dist-cli/bin.js),
199
- // resolved from this file so it works from both the workspace
200
- // source export and the published dist-lib build.
201
- const packageRoot = path.resolve(
202
- path.dirname(fileURLToPath(import.meta.url)),
203
- '..',
225
+ const openPort = listeningPort()
226
+ launchRnxDesktop(
227
+ resolveRnxDesktopLaunchTarget(openPort, bundleForPort(openPort)),
228
+ appName,
204
229
  )
205
- const binPath = path.join(packageRoot, 'dist-cli', 'bin.js')
206
- const openPort = server.config.server.port || port
207
- console.log(`[rnx] opening simulator for :${openPort}`)
208
- // invoke the cli script through the current node binary rather
209
- // than the bin shim so the same spawn works on windows, macos,
210
- // and linux without a shell.
211
- spawn(process.execPath, [binPath, 'open', String(openPort)], {
212
- detached: true,
213
- stdio: 'ignore',
214
- windowsHide: true,
215
- }).unref()
216
230
  })
217
231
  }
218
232
  },
@@ -1,4 +0,0 @@
1
- /*! rnx v0.1.343 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
- import { createRequire as __sootsimCreateRequire } from 'node:module'
3
- var require = __sootsimCreateRequire(import.meta.url)
4
- import{a as c}from"./chunk-W7EXE22C.js";import{J as u,l}from"./chunk-4RKTGL63.js";function m(t){return t.replace(/^\[|\]$/g,"").toLowerCase()}function R(t){let n=m(t);return n==="localhost"||n.endsWith(".localhost")||n==="0.0.0.0"||n==="::1"||/^127(?:\.\d{1,3}){3}$/.test(n)}import o from"fs";import e from"path";import{fileURLToPath as d}from"url";var s;function g(){if(s!==void 0)return s;let t=e.dirname(d(import.meta.url));for(let n=0;n<12&&t!==e.dirname(t);n++){if(o.existsSync(e.join(t,"packages/sootsim-engine/package.json"))&&o.existsSync(e.join(t,"public/sootsim")))return s=t,t;t=e.dirname(t)}return s=null,null}function p(){if(process.env.SOOTSIM_HOME&&u())return null;let t=g();if(!t)return null;let n=e.join(t,"public/sootsim");return o.existsSync(e.join(n,"index.html"))&&o.existsSync(e.join(n,"engine/index.html"))&&o.existsSync(e.join(n,"engine-tenant/worker.js"))?n:null}function x(){let t=p();if(t)return t;let n=u();if(!n)return null;let i=l(n);return o.existsSync(e.join(i,"index.html"))?i:null}var a=".runtime.localhost";function H(t){let n=t.trim();if(!c(n))throw new Error(`invalid rnx runtime version: ${t}`);let r=Buffer.from(n,"utf8").toString("hex").match(/.{1,60}/g);if(!r)throw new Error(`invalid rnx runtime version: ${t}`);return`${r.join(".")}${a}`}function f(t){return t.toLowerCase().endsWith(a)}function h(t){if(!f(t))return null;let n=t.slice(0,-a.length).split(".");if(n.length===0||n.some(r=>!/^[0-9a-f]{1,60}$/i.test(r)))return null;let i=n.join("");if(i.length%2!==0)return null;try{let r=Buffer.from(i,"hex").toString("utf8");return c(r)?r:null}catch{return null}}function v(t){let n=l(t);return o.existsSync(e.join(n,"index.html"))?n:null}function T(t){if(!f(t))return x();let n=h(t);return n?v(n):null}export{R as a,p as b,H as c,f as d,h as e,T as f};