rnxsim 0.1.345 → 0.1.347

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 (174) hide show
  1. package/README.md +9 -3
  2. package/cli/commands/welcome.ts +19 -17
  3. package/cli/outbound-endpoints.ts +1 -1
  4. package/dist-cli/bin.js +5 -5
  5. package/dist-cli/chunks/{agent-52G55XTO.js → agent-VVY2A5YV.js} +2 -2
  6. package/dist-cli/chunks/{agent-wrapper-BOZM43ON.js → agent-wrapper-YBMTZ3KU.js} +2 -2
  7. package/dist-cli/chunks/{app-fonts-A2L6V5EZ.js → app-fonts-EF7FM3VP.js} +2 -2
  8. package/dist-cli/chunks/{assert-CJKIP4LN.js → assert-J7N6WGMS.js} +2 -2
  9. package/dist-cli/chunks/{auth-VHHN3TOB.js → auth-YGX3WVEL.js} +2 -2
  10. package/dist-cli/chunks/{beta-EBDIGV3E.js → beta-JXYFAOHO.js} +2 -2
  11. package/dist-cli/chunks/{camera-5F4KKCQ3.js → camera-WIVF3UQ3.js} +2 -2
  12. package/dist-cli/chunks/{chunk-PLB6UVVX.js → chunk-23WNLQUB.js} +2 -2
  13. package/dist-cli/chunks/{chunk-KZFJNDH3.js → chunk-24Q4J4U6.js} +1 -1
  14. package/dist-cli/chunks/chunk-2E5KDIGN.js +14 -0
  15. package/dist-cli/chunks/{chunk-XJBFJGNT.js → chunk-2PAG4FBP.js} +2 -2
  16. package/dist-cli/chunks/{chunk-7LGINKM6.js → chunk-2UZ6QQ6S.js} +2 -2
  17. package/dist-cli/chunks/{chunk-7V4KPZ7O.js → chunk-2W4ME6YI.js} +2 -2
  18. package/dist-cli/chunks/{chunk-5GXRRUOA.js → chunk-3HBHHU5F.js} +3 -3
  19. package/dist-cli/chunks/{chunk-E2RGSCHT.js → chunk-3SPBSEVL.js} +1 -1
  20. package/dist-cli/chunks/{chunk-QNBBKRCA.js → chunk-4KK5IRA6.js} +2 -2
  21. package/dist-cli/chunks/{chunk-4OKV7T5D.js → chunk-4KOWLH77.js} +2 -2
  22. package/dist-cli/chunks/{chunk-DPP2M4AJ.js → chunk-4RI6KXVH.js} +1 -1
  23. package/dist-cli/chunks/{chunk-DHM3JDCK.js → chunk-6CGQWXXT.js} +2 -2
  24. package/dist-cli/chunks/{chunk-ZMTZ5G5H.js → chunk-7C6XE6AT.js} +2 -2
  25. package/dist-cli/chunks/{chunk-7YMQRA6L.js → chunk-7D56NCJ3.js} +2 -2
  26. package/dist-cli/chunks/{chunk-4ZILDTZX.js → chunk-ABBCVE5V.js} +2 -2
  27. package/dist-cli/chunks/{chunk-JC33TOAF.js → chunk-ANANQDHB.js} +1 -1
  28. package/dist-cli/chunks/{chunk-BFTDL2GX.js → chunk-BA45ZZKU.js} +2 -2
  29. package/dist-cli/chunks/{chunk-RB4VFUNM.js → chunk-BAXXUHKV.js} +2 -2
  30. package/dist-cli/chunks/{chunk-LAQGSSTT.js → chunk-DC4MQAR3.js} +1 -1
  31. package/dist-cli/chunks/{chunk-KA4PJNTE.js → chunk-DTNRKFSE.js} +2 -2
  32. package/dist-cli/chunks/{chunk-A2Q76LJV.js → chunk-EEZEVA2C.js} +2 -2
  33. package/dist-cli/chunks/{chunk-7UIIAHVU.js → chunk-EIIZZ2XK.js} +2 -2
  34. package/dist-cli/chunks/{chunk-TEU5NGUA.js → chunk-FQ3OHHCY.js} +2 -2
  35. package/dist-cli/chunks/{chunk-BCQV63BW.js → chunk-HWAGFGV7.js} +3 -3
  36. package/dist-cli/chunks/{chunk-XXZLK4BY.js → chunk-HY7XOD5E.js} +1 -1
  37. package/dist-cli/chunks/{chunk-T7SBFG5I.js → chunk-IQWQ6KI7.js} +2 -2
  38. package/dist-cli/chunks/{chunk-TMRD5MHL.js → chunk-IS2LT6MN.js} +2 -2
  39. package/dist-cli/chunks/{chunk-NUPRHZQW.js → chunk-JG2TIZ7E.js} +2 -2
  40. package/dist-cli/chunks/{chunk-YVSUFVVB.js → chunk-JKRVWCTC.js} +1 -1
  41. package/dist-cli/chunks/{chunk-3DBTS4MC.js → chunk-JP4FCK5J.js} +6 -6
  42. package/dist-cli/chunks/{chunk-RTDHX6CW.js → chunk-JQTBZERT.js} +2 -2
  43. package/dist-cli/chunks/{chunk-OPHT4DUO.js → chunk-KYTBJ2QV.js} +1 -1
  44. package/dist-cli/chunks/{chunk-63ASUHOV.js → chunk-LRRQI4TP.js} +2 -2
  45. package/dist-cli/chunks/{chunk-RT3DXTDP.js → chunk-LWE2HRS4.js} +2 -2
  46. package/dist-cli/chunks/{chunk-U5YHXQHH.js → chunk-NJ2JVAGV.js} +2 -2
  47. package/dist-cli/chunks/{chunk-4BIPA7MD.js → chunk-O7WPHIYQ.js} +1 -1
  48. package/dist-cli/chunks/{chunk-WPPGU5IK.js → chunk-OG3KGU4V.js} +2 -2
  49. package/dist-cli/chunks/{chunk-KH777UAO.js → chunk-PCU2CQCI.js} +3 -3
  50. package/dist-cli/chunks/{chunk-KRZPFXVN.js → chunk-QJYUHYQS.js} +1 -1
  51. package/dist-cli/chunks/{chunk-HK7YKH6N.js → chunk-QK2U5IW3.js} +2 -2
  52. package/dist-cli/chunks/{chunk-3N7M5HMJ.js → chunk-SMLCC5EP.js} +1 -1
  53. package/dist-cli/chunks/{chunk-QNRWPAOA.js → chunk-SNBSRIKO.js} +2 -2
  54. package/dist-cli/chunks/{chunk-R7ZNRGQN.js → chunk-SYQZTZTR.js} +1 -1
  55. package/dist-cli/chunks/{chunk-7GO2DSYQ.js → chunk-TAYE2YII.js} +1 -1
  56. package/dist-cli/chunks/{chunk-EPLVIDPC.js → chunk-U6CJ3EHB.js} +1 -1
  57. package/dist-cli/chunks/{chunk-GKGXULJK.js → chunk-UM7IG4DE.js} +2 -2
  58. package/dist-cli/chunks/{chunk-VLBN3KRC.js → chunk-VENH5KLL.js} +1 -1
  59. package/dist-cli/chunks/{chunk-P2CGGMU2.js → chunk-W4MASUQ7.js} +2 -2
  60. package/dist-cli/chunks/{chunk-WU72KL6Z.js → chunk-WIYBEKBM.js} +2 -2
  61. package/dist-cli/chunks/{chunk-LRM2XCEQ.js → chunk-WNZOJDBL.js} +1 -1
  62. package/dist-cli/chunks/{chunk-UVOEHIGP.js → chunk-XB7DLZNQ.js} +1 -1
  63. package/dist-cli/chunks/{chunk-PLUWN2V5.js → chunk-XGUCAKZM.js} +1 -1
  64. package/dist-cli/chunks/{chunk-DGTV3KOW.js → chunk-Z6K6TD7X.js} +1 -1
  65. package/dist-cli/chunks/{chunk-BHKSCNDC.js → chunk-ZPCQFQEN.js} +1 -1
  66. package/dist-cli/chunks/{chunk-HXE344V3.js → chunk-ZY44NCRQ.js} +1 -1
  67. package/dist-cli/chunks/{cleanup-MAVZJXVW.js → cleanup-ZIDQQLEN.js} +2 -2
  68. package/dist-cli/chunks/cli-version-BEJJXF35.js +4 -0
  69. package/dist-cli/chunks/{compat-7AURLGCS.js → compat-5PFPTBBK.js} +2 -2
  70. package/dist-cli/chunks/{config-TN3Y4J5N.js → config-CG73FOTM.js} +2 -2
  71. package/dist-cli/chunks/{control-EV2ELU5R.js → control-7IYIDOOI.js} +2 -2
  72. package/dist-cli/chunks/daemon-G6VR7LJI.js +4 -0
  73. package/dist-cli/chunks/{debug-JICITUB6.js → debug-6AQQXXE3.js} +3 -3
  74. package/dist-cli/chunks/{desktop-4F5HGLLX.js → desktop-HJE5B4XO.js} +3 -3
  75. package/dist-cli/chunks/{detox-ZC7NHLJI.js → detox-KUZ7EYOP.js} +2 -2
  76. package/dist-cli/chunks/{device-V57GHRXX.js → device-O6WI4PQT.js} +2 -2
  77. package/dist-cli/chunks/{diagnose-FF4IWYXC.js → diagnose-FTCJGH2Q.js} +2 -2
  78. package/dist-cli/chunks/{disk-cleanup-T277L6ZO.js → disk-cleanup-YAXG52EZ.js} +2 -2
  79. package/dist-cli/chunks/drivers-6QOL2SON.js +4 -0
  80. package/dist-cli/chunks/{film-G7ZAPWB7.js → film-KTLKRCJR.js} +3 -3
  81. package/dist-cli/chunks/flow-6VQN4VNW.js +4 -0
  82. package/dist-cli/chunks/help-22OURA2E.js +4 -0
  83. package/dist-cli/chunks/{hidden-runtime-alias-VSJNNSNI.js → hidden-runtime-alias-W375XBOM.js} +2 -2
  84. package/dist-cli/chunks/{home-paths-75HP3HKA.js → home-paths-CIGRWLDB.js} +2 -2
  85. package/dist-cli/chunks/inspect-36A2IEJL.js +4 -0
  86. package/dist-cli/chunks/install-desktop-KOW3XXUF.js +4 -0
  87. package/dist-cli/chunks/{login-ZSIHGHUK.js → login-AJWLXLPQ.js} +2 -2
  88. package/dist-cli/chunks/{logout-Q4DXMI2C.js → logout-CEJLZIOI.js} +2 -2
  89. package/dist-cli/chunks/{maestro-LE5LCRBL.js → maestro-YF4M6M6L.js} +3 -3
  90. package/dist-cli/chunks/{maestro-generate-D3ZIJCRL.js → maestro-generate-FJWDLMEB.js} +3 -3
  91. package/dist-cli/chunks/{mode-BWX6MDPJ.js → mode-6BJ523BY.js} +2 -2
  92. package/dist-cli/chunks/{optional-demo-registry-6CVASL3S.js → optional-demo-registry-5CKCN2TP.js} +2 -2
  93. package/dist-cli/chunks/{perf-NXQZISN6.js → perf-WE6QTFJU.js} +2 -2
  94. package/dist-cli/chunks/{permissions-5PD67RRO.js → permissions-OZZZZFPB.js} +2 -2
  95. package/dist-cli/chunks/privacy-3BERNBUM.js +4 -0
  96. package/dist-cli/chunks/{record-YJF4KXMR.js → record-QHQS2524.js} +3 -3
  97. package/dist-cli/chunks/{report-issue-XOL5FQBG.js → report-issue-SEMVVJUH.js} +2 -2
  98. package/dist-cli/chunks/reset-A5AA5ML6.js +4 -0
  99. package/dist-cli/chunks/runtime-R26QWA2M.js +4 -0
  100. package/dist-cli/chunks/{screenshot-command-VRPJXWFS.js → screenshot-command-WW33MAB3.js} +3 -3
  101. package/dist-cli/chunks/{screenshot-layers-QP4WA2F5.js → screenshot-layers-2GJTEQ4T.js} +2 -2
  102. package/dist-cli/chunks/{screenshots-capture-O2IBAVSP.js → screenshots-capture-J5HKONCC.js} +2 -2
  103. package/dist-cli/chunks/{serve-WC6RZNJL.js → serve-FEBNDKUD.js} +2 -2
  104. package/dist-cli/chunks/{setup-A6BIG3KL.js → setup-NGZWS373.js} +2 -2
  105. package/dist-cli/chunks/{skills-A7YC6RL5.js → skills-CFCZFEME.js} +2 -2
  106. package/dist-cli/chunks/{state-IJI5YROJ.js → state-XWRKK7FW.js} +2 -2
  107. package/dist-cli/chunks/{storage-Z2DBO2PQ.js → storage-C5GNK4XB.js} +2 -2
  108. package/dist-cli/chunks/store-L3A4CLND.js +4 -0
  109. package/dist-cli/chunks/telemetry-O67J22QU.js +4 -0
  110. package/dist-cli/chunks/{timeline-N7V2M6QA.js → timeline-7RA7ZILQ.js} +2 -2
  111. package/dist-cli/chunks/{upgrade-P2NH3BOR.js → upgrade-ZW6IJ5LX.js} +2 -2
  112. package/dist-cli/chunks/upload-UKUN6HCD.js +4 -0
  113. package/dist-cli/chunks/{version-OUHCSCUS.js → version-N3E4OIMM.js} +2 -2
  114. package/dist-cli/chunks/{web-6NRTF3UV.js → web-UC2SFEBA.js} +2 -2
  115. package/dist-cli/chunks/welcome-DAZPCMES.js +5 -0
  116. package/dist-cli/chunks/{what-happened-FUEL3BEF.js → what-happened-VUNKID5W.js} +2 -2
  117. package/dist-lib/agent-daemon-client.cjs +1 -1
  118. package/dist-lib/agent-events.cjs +1 -1
  119. package/dist-lib/agent-identity.cjs +1 -1
  120. package/dist-lib/agent-sessions.cjs +1 -1
  121. package/dist-lib/attached-projects.cjs +1 -1
  122. package/dist-lib/auth/shared-session.cjs +1 -1
  123. package/dist-lib/backend-origin.cjs +13 -1
  124. package/dist-lib/beta.cjs +1 -1
  125. package/dist-lib/beta.mjs +1 -1
  126. package/dist-lib/bridge-constants.cjs +1 -1
  127. package/dist-lib/bridge-contract.cjs +1 -1
  128. package/dist-lib/cli-constants.cjs +1 -1
  129. package/dist-lib/config.cjs +1 -1
  130. package/dist-lib/detox/index.cjs +1 -1
  131. package/dist-lib/dev-bundle-resolution.cjs +26 -10
  132. package/dist-lib/home-paths.cjs +1 -1
  133. package/dist-lib/host/bridge-host.cjs +101 -58
  134. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  135. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  136. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  137. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  138. package/dist-lib/host/websocket-proxy.cjs +1 -1
  139. package/dist-lib/index.cjs +1 -1
  140. package/dist-lib/jump-to-source-babel.cjs +1 -1
  141. package/dist-lib/menu.cjs +1 -1
  142. package/dist-lib/menu.mjs +1 -1
  143. package/dist-lib/metro-production-bundle.cjs +1 -1
  144. package/dist-lib/metro-production-bundle.mjs +1 -1
  145. package/dist-lib/metro.cjs +1 -1
  146. package/dist-lib/profiles.cjs +1 -1
  147. package/dist-lib/public-brand.cjs +1 -1
  148. package/dist-lib/react-native-host-modules.cjs +1 -1
  149. package/dist-lib/react-native-host-modules.mjs +1 -1
  150. package/dist-lib/render-mode.cjs +1 -1
  151. package/dist-lib/scripts/dev-server-scanner.cjs +106 -59
  152. package/dist-lib/sdk.cjs +1 -1
  153. package/dist-lib/sdk.mjs +1 -1
  154. package/dist-lib/skills.cjs +127 -72
  155. package/dist-lib/vite.cjs +1 -1
  156. package/package.json +1 -1
  157. package/scripts/dev-server-scanner.ts +101 -47
  158. package/src/backend-origin.ts +15 -1
  159. package/src/dev-bundle-resolution.ts +29 -12
  160. package/dist-cli/chunks/chunk-TOF5TA5X.js +0 -14
  161. package/dist-cli/chunks/cli-version-2B4KRNML.js +0 -4
  162. package/dist-cli/chunks/daemon-KKDG5MYR.js +0 -4
  163. package/dist-cli/chunks/drivers-LCX7S76X.js +0 -4
  164. package/dist-cli/chunks/flow-ONZGGFGD.js +0 -4
  165. package/dist-cli/chunks/help-DROVDXJM.js +0 -4
  166. package/dist-cli/chunks/inspect-MDA7ESGQ.js +0 -4
  167. package/dist-cli/chunks/install-desktop-EIVZHIFA.js +0 -4
  168. package/dist-cli/chunks/privacy-H5KNRNIZ.js +0 -4
  169. package/dist-cli/chunks/reset-5LZN5P5B.js +0 -4
  170. package/dist-cli/chunks/runtime-U36VWBH3.js +0 -4
  171. package/dist-cli/chunks/store-BYTCKNIC.js +0 -4
  172. package/dist-cli/chunks/telemetry-JLHFMD7N.js +0 -4
  173. package/dist-cli/chunks/upload-Z2WX5DCD.js +0 -4
  174. package/dist-cli/chunks/welcome-6RS7PEJW.js +0 -5
@@ -22,6 +22,11 @@ import { promisify } from 'util'
22
22
  import type { AppSettingsSpecifier } from '@sootsim/globals'
23
23
 
24
24
  const execP = promisify(exec)
25
+ import {
26
+ buildLoopbackProbeBaseUrl,
27
+ LOOPBACK_PROBE_HOSTNAMES,
28
+ normalizeHostname,
29
+ } from '../src/backend-origin.ts'
25
30
  import { applySootSimConfigToUrl } from '../src/config.ts'
26
31
  import {
27
32
  alignLoopbackBundleUrlWithProbe,
@@ -74,30 +79,42 @@ interface HttpResult {
74
79
  statusCode: number
75
80
  body: string
76
81
  contentType?: string
82
+ hostname: string
77
83
  }
78
84
 
79
85
  // cheap TCP-connect gate. lsof already says the socket is LISTEN, but the
80
86
  // process can be closing, the fd can be a different protocol, or a zombie
81
87
  // can linger — none of those cases are worth firing 5 HTTP probes at.
82
- function tcpPing(port: number, timeout = TCP_GATE_MS): Promise<boolean> {
83
- return new Promise((resolve) => {
84
- const sock = new net.Socket()
85
- let settled = false
86
- const done = (ok: boolean) => {
87
- if (settled) return
88
- settled = true
89
- sock.destroy()
90
- resolve(ok)
91
- }
92
- sock.setTimeout(timeout)
93
- sock.once('connect', () => done(true))
94
- sock.once('timeout', () => done(false))
95
- sock.once('error', () => done(false))
96
- sock.connect(port, 'localhost')
97
- })
88
+ async function tcpPing(port: number, timeout = TCP_GATE_MS): Promise<string | null> {
89
+ try {
90
+ return await Promise.any(
91
+ LOOPBACK_PROBE_HOSTNAMES.map(
92
+ (hostname) =>
93
+ new Promise<string>((resolve, reject) => {
94
+ const sock = new net.Socket()
95
+ let settled = false
96
+ const done = (reachable: boolean) => {
97
+ if (settled) return
98
+ settled = true
99
+ sock.destroy()
100
+ if (reachable) resolve(hostname)
101
+ else reject(new Error(`unreachable loopback host ${hostname}`))
102
+ }
103
+ sock.setTimeout(timeout)
104
+ sock.once('connect', () => done(true))
105
+ sock.once('timeout', () => done(false))
106
+ sock.once('error', () => done(false))
107
+ sock.connect(port, hostname)
108
+ }),
109
+ ),
110
+ )
111
+ } catch {
112
+ return null
113
+ }
98
114
  }
99
115
 
100
116
  function httpGet(
117
+ hostname: string,
101
118
  port: number,
102
119
  path: string,
103
120
  method: 'GET' | 'HEAD' = 'GET',
@@ -106,7 +123,7 @@ function httpGet(
106
123
  ): Promise<HttpResult | null> {
107
124
  return new Promise((resolve) => {
108
125
  const req = http.request(
109
- { hostname: 'localhost', port, path, method, timeout, headers },
126
+ { hostname, port, path, method, timeout, headers },
110
127
  (res) => {
111
128
  let body = ''
112
129
  res.on('data', (c: Buffer) => (body += c.toString()))
@@ -117,7 +134,12 @@ function httpGet(
117
134
  return undefined
118
135
  })()
119
136
  res.on('end', () =>
120
- resolve({ statusCode: res.statusCode || 0, body, contentType }),
137
+ resolve({
138
+ statusCode: res.statusCode || 0,
139
+ body,
140
+ contentType,
141
+ hostname,
142
+ }),
121
143
  )
122
144
  },
123
145
  )
@@ -342,6 +364,7 @@ export async function resolveProcessCwd(pid: number): Promise<string | null> {
342
364
  // ── per-port probe orchestration ────────────────────────────────────────────
343
365
 
344
366
  function makeResult(
367
+ hostname: string,
345
368
  port: number,
346
369
  framework: DiscoveredServer['framework'],
347
370
  owner: Pick<DiscoveredServer, 'cwd' | 'pid'>,
@@ -351,10 +374,10 @@ function makeResult(
351
374
  framework,
352
375
  bundleUrl: withRuntimeConfig(
353
376
  port,
354
- `http://localhost:${port}/index.bundle?platform=ios&dev=true&hot=true&minify=false`,
377
+ `${buildLoopbackProbeBaseUrl('http:', hostname, port)}/index.bundle?platform=ios&dev=true&hot=true&minify=false`,
355
378
  owner.cwd,
356
379
  ),
357
- hmrUrl: `ws://localhost:${port}/hot`,
380
+ hmrUrl: `${buildLoopbackProbeBaseUrl('ws:', hostname, port)}/hot`,
358
381
  lastSeen: Date.now(),
359
382
  ...owner,
360
383
  }
@@ -408,7 +431,10 @@ function applyManifest(
408
431
  const rawLaunchUrl = manifest?.launchAsset?.url
409
432
  const launchUrl =
410
433
  typeof rawLaunchUrl === 'string'
411
- ? alignLoopbackBundleUrlWithProbe(rawLaunchUrl, `http://localhost:${result.port}`)
434
+ ? alignLoopbackBundleUrlWithProbe(
435
+ rawLaunchUrl,
436
+ buildLoopbackProbeBaseUrl('http:', manifestRes.hostname, result.port),
437
+ )
412
438
  : rawLaunchUrl
413
439
  if (launchUrl && !result.patched && !isDirectOneBundleUrl(result.bundleUrl)) {
414
440
  result.bundleUrl = withRuntimeConfig(result.port, launchUrl, result.cwd)
@@ -423,14 +449,14 @@ function applyManifest(
423
449
  } else {
424
450
  const cleanPath = rawIconUrl.replace(/^\.\//, '')
425
451
  result.iconUrl = buildIconProxyUrl(
426
- `http://localhost:${result.port}/assets/${cleanPath}`,
452
+ `${buildLoopbackProbeBaseUrl('http:', manifestRes.hostname, result.port)}/assets/${cleanPath}`,
427
453
  )
428
454
  }
429
455
  } else {
430
456
  // no proxy requested — use the raw URL
431
457
  result.iconUrl = rawIconUrl.startsWith('http')
432
458
  ? rawIconUrl
433
- : `http://localhost:${result.port}/assets/${rawIconUrl.replace(/^\.\//, '')}`
459
+ : `${buildLoopbackProbeBaseUrl('http:', manifestRes.hostname, result.port)}/assets/${rawIconUrl.replace(/^\.\//, '')}`
434
460
  }
435
461
  }
436
462
  } catch {}
@@ -441,7 +467,11 @@ export function __applyManifestForTests(
441
467
  result: DiscoveredServer,
442
468
  manifestBody: string,
443
469
  ): DiscoveredServer {
444
- return applyManifest(result, { statusCode: 200, body: manifestBody })
470
+ return applyManifest(result, {
471
+ statusCode: 200,
472
+ body: manifestBody,
473
+ hostname: new URL(result.bundleUrl).hostname,
474
+ })
445
475
  }
446
476
 
447
477
  // ── name/icon enrichment ────────────────────────────────────────────────────
@@ -490,30 +520,42 @@ function enrichWithManifest(
490
520
 
491
521
  if (!enrichmentInFlight.has(result.port)) {
492
522
  enrichmentInFlight.add(result.port)
493
- const port = result.port
494
- void fetchManifestEnrichment(port, buildIconProxyUrl).finally(() =>
495
- enrichmentInFlight.delete(port),
523
+ void fetchManifestEnrichment(result, buildIconProxyUrl).finally(() =>
524
+ enrichmentInFlight.delete(result.port),
496
525
  )
497
526
  }
498
527
  return result
499
528
  }
500
529
 
501
530
  async function fetchManifestEnrichment(
502
- port: number,
531
+ result: DiscoveredServer,
503
532
  buildIconProxyUrl?: (externalUrl: string) => string,
504
533
  ): Promise<void> {
505
- const manifestRes = await httpGet(port, '/', 'GET', ENRICH_TIMEOUT_MS, {
506
- 'expo-platform': 'ios',
507
- })
534
+ const hostname = normalizeHostname(new URL(result.bundleUrl).hostname)
535
+ const manifestRes = await httpGet(
536
+ hostname,
537
+ result.port,
538
+ '/',
539
+ 'GET',
540
+ ENRICH_TIMEOUT_MS,
541
+ {
542
+ 'expo-platform': 'ios',
543
+ },
544
+ )
508
545
  if (!manifestRes || manifestRes.statusCode !== 200) return
509
546
  // reuse applyManifest's exact field extraction against a throwaway result.
510
547
  const parsed = applyManifest(
511
- { port, framework: 'one', bundleUrl: '', lastSeen: 0 },
548
+ {
549
+ port: result.port,
550
+ framework: 'one',
551
+ bundleUrl: `${buildLoopbackProbeBaseUrl('http:', hostname, result.port)}/index.bundle`,
552
+ lastSeen: 0,
553
+ },
512
554
  manifestRes,
513
555
  buildIconProxyUrl,
514
556
  )
515
557
  if (!parsed.projectName && !parsed.iconUrl) return
516
- enrichmentCache.set(port, {
558
+ enrichmentCache.set(result.port, {
517
559
  projectName: parsed.projectName,
518
560
  iconUrl: parsed.iconUrl,
519
561
  iconPath: parsed.iconPath,
@@ -521,7 +563,7 @@ async function fetchManifestEnrichment(
521
563
  })
522
564
  // the nameless result cached as a strong entry and won't re-probe on its own;
523
565
  // drop it so the next scan re-runs probePort and publishes the enriched name.
524
- portCache.delete(port)
566
+ portCache.delete(result.port)
525
567
  }
526
568
 
527
569
  // ports confirmed to be running a non-sootsim dev server — safe to skip the
@@ -569,7 +611,8 @@ export async function probePort(
569
611
  // too, or a cold CLI process can build the known app's bundle url before its
570
612
  // runtime config is available.
571
613
  await loadOptionalDemoApps()
572
- if (!(await tcpPing(port))) return null
614
+ const probeHostname = await tcpPing(port)
615
+ if (!probeHostname) return null
573
616
 
574
617
  const suppliedPid = listeningProcess?.pid
575
618
  const ownerPid =
@@ -584,12 +627,18 @@ export async function probePort(
584
627
 
585
628
  // wave 1 — safe parallel probes. none of these mutate metro state.
586
629
  const [sootsimRes, statusRes, manifestRes, expoRes] = await Promise.all([
587
- knownNonPatched.has(port) ? Promise.resolve(null) : httpGet(port, '/__soot/'),
588
- httpGet(port, '/status'),
630
+ knownNonPatched.has(port)
631
+ ? Promise.resolve(null)
632
+ : httpGet(probeHostname, port, '/__soot/'),
633
+ httpGet(probeHostname, port, '/status'),
589
634
  knownOne.has(port)
590
635
  ? Promise.resolve(null)
591
- : httpGet(port, '/', 'GET', MANIFEST_TIMEOUT_MS, { 'expo-platform': 'ios' }),
592
- knownNonExpo.has(port) ? Promise.resolve(null) : httpGet(port, '/_expo/status'),
636
+ : httpGet(probeHostname, port, '/', 'GET', MANIFEST_TIMEOUT_MS, {
637
+ 'expo-platform': 'ios',
638
+ }),
639
+ knownNonExpo.has(port)
640
+ ? Promise.resolve(null)
641
+ : httpGet(probeHostname, port, '/_expo/status'),
593
642
  ])
594
643
 
595
644
  // remember negative /_expo/status responses so we don't keep probing this
@@ -627,7 +676,7 @@ export async function probePort(
627
676
  knownNonPatched.add(port)
628
677
  const launchUrl =
629
678
  manifestLaunchUrl ||
630
- `http://localhost:${port}/index.bundle?platform=ios&dev=true&hot=true&minify=false`
679
+ `${buildLoopbackProbeBaseUrl('http:', probeHostname, port)}/index.bundle?platform=ios&dev=true&hot=true&minify=false`
631
680
  // call the framework "one" only when launchAsset clearly points at
632
681
  // a One metro entry; otherwise treat it as plain expo so downstream
633
682
  // bundle handling uses the right path semantics.
@@ -641,7 +690,7 @@ export async function probePort(
641
690
  port,
642
691
  framework,
643
692
  bundleUrl: withRuntimeConfig(port, launchUrl, owner.cwd),
644
- hmrUrl: `ws://localhost:${port}/hot`,
693
+ hmrUrl: `${buildLoopbackProbeBaseUrl('ws:', probeHostname, port)}/hot`,
645
694
  lastSeen: Date.now(),
646
695
  ...owner,
647
696
  },
@@ -661,7 +710,12 @@ export async function probePort(
661
710
  knownNonPatched.add(port)
662
711
  return enrichWithManifest(
663
712
  applyManifest(
664
- makeResult(port, expoRes && expoRes.statusCode === 200 ? 'expo' : 'metro', owner),
713
+ makeResult(
714
+ probeHostname,
715
+ port,
716
+ expoRes && expoRes.statusCode === 200 ? 'expo' : 'metro',
717
+ owner,
718
+ ),
665
719
  manifestRes,
666
720
  buildIconProxyUrl,
667
721
  ),
@@ -684,10 +738,10 @@ export async function probePort(
684
738
  framework: 'one',
685
739
  bundleUrl: withRuntimeConfig(
686
740
  port,
687
- `http://localhost:${port}/__soot/bundle.js`,
741
+ `${buildLoopbackProbeBaseUrl('http:', probeHostname, port)}/__soot/bundle.js`,
688
742
  owner.cwd,
689
743
  ),
690
- hmrUrl: `ws://localhost:${port}/hot`,
744
+ hmrUrl: `${buildLoopbackProbeBaseUrl('ws:', probeHostname, port)}/hot`,
691
745
  lastSeen: Date.now(),
692
746
  patched: true,
693
747
  ...owner,
@@ -726,7 +780,7 @@ export async function probePort(
726
780
  return null
727
781
  }
728
782
 
729
- const oneRes = await httpGet(port, onePath, 'HEAD')
783
+ const oneRes = await httpGet(probeHostname, port, onePath, 'HEAD')
730
784
  if (
731
785
  oneRes &&
732
786
  oneRes.statusCode > 0 &&
@@ -742,10 +796,10 @@ export async function probePort(
742
796
  framework: 'one',
743
797
  bundleUrl: withRuntimeConfig(
744
798
  port,
745
- `http://localhost:${port}${onePath}&minify=false`,
799
+ `${buildLoopbackProbeBaseUrl('http:', probeHostname, port)}${onePath}&minify=false`,
746
800
  owner.cwd,
747
801
  ),
748
- hmrUrl: `ws://localhost:${port}/hot`,
802
+ hmrUrl: `${buildLoopbackProbeBaseUrl('ws:', probeHostname, port)}/hot`,
749
803
  lastSeen: Date.now(),
750
804
  ...owner,
751
805
  },
@@ -1,9 +1,23 @@
1
1
  const DEFAULT_CONTRAST_BACKEND_ORIGIN = 'https://contrast.dev'
2
2
 
3
- function normalizeHostname(hostname: string): string {
3
+ export const LOOPBACK_PROBE_HOSTNAMES = ['::1', '127.0.0.1'] as const
4
+
5
+ export function normalizeHostname(hostname: string): string {
4
6
  return hostname.replace(/^\[|\]$/g, '').toLowerCase()
5
7
  }
6
8
 
9
+ export function buildLoopbackProbeBaseUrl(
10
+ protocol: 'http:' | 'ws:',
11
+ hostname: string,
12
+ port: number,
13
+ ): string {
14
+ const normalizedHostname = normalizeHostname(hostname)
15
+ const urlHost = normalizedHostname.includes(':')
16
+ ? `[${normalizedHostname}]`
17
+ : normalizedHostname
18
+ return `${protocol}//${urlHost}:${port}`
19
+ }
20
+
7
21
  export function isLoopbackHost(hostname: string): boolean {
8
22
  const normalized = normalizeHostname(hostname)
9
23
  return (
@@ -1,4 +1,8 @@
1
- import { isLoopbackHost } from './backend-origin'
1
+ import {
2
+ buildLoopbackProbeBaseUrl,
3
+ isLoopbackHost,
4
+ LOOPBACK_PROBE_HOSTNAMES,
5
+ } from './backend-origin'
2
6
  import {
3
7
  alignLoopbackBundleUrlWithProbe,
4
8
  METRO_BUNDLE_PATH,
@@ -127,21 +131,12 @@ async function resolvePortViaServerScan(port: number): Promise<ResolvedDevBundle
127
131
  }
128
132
  }
129
133
 
130
- async function probeBaseUrlBundle(
134
+ async function probeBaseUrlBundleDirect(
131
135
  baseUrl: string,
132
136
  port: number,
133
137
  ): Promise<ResolvedDevBundle | null> {
134
138
  const normalizedBaseUrl = baseUrl.replace(/\/+$/, '')
135
139
 
136
- // browser/worker loopback targets resolve through the server-side scanner so
137
- // they match the dev-server list exactly — see resolvePortViaServerScan.
138
- try {
139
- if (isLoopbackHost(new URL(normalizedBaseUrl).hostname)) {
140
- const viaScan = await resolvePortViaServerScan(port)
141
- if (viaScan) return viaScan
142
- }
143
- } catch {}
144
-
145
140
  try {
146
141
  const manifestRes = await fetchDevProbe(`${normalizedBaseUrl}/`, {
147
142
  headers: { 'expo-platform': 'ios' },
@@ -198,8 +193,30 @@ async function probeBaseUrlBundle(
198
193
  return null
199
194
  }
200
195
 
196
+ async function probeBaseUrlBundle(
197
+ baseUrl: string,
198
+ port: number,
199
+ ): Promise<ResolvedDevBundle | null> {
200
+ // browser/worker loopback targets resolve through the server-side scanner so
201
+ // they match the dev-server list exactly — see resolvePortViaServerScan.
202
+ try {
203
+ if (isLoopbackHost(new URL(baseUrl).hostname)) {
204
+ const viaScan = await resolvePortViaServerScan(port)
205
+ if (viaScan) return viaScan
206
+ }
207
+ } catch {}
208
+ return probeBaseUrlBundleDirect(baseUrl, port)
209
+ }
210
+
201
211
  export async function probePortBundle(port: number): Promise<ResolvedDevBundle | null> {
202
- return probeBaseUrlBundle(buildBaseUrl('http:', 'localhost', port), port)
212
+ const viaScan = await resolvePortViaServerScan(port)
213
+ if (viaScan) return viaScan
214
+ const candidates = await Promise.all(
215
+ LOOPBACK_PROBE_HOSTNAMES.map((hostname) =>
216
+ probeBaseUrlBundleDirect(buildLoopbackProbeBaseUrl('http:', hostname, port), port),
217
+ ),
218
+ )
219
+ return candidates.find((candidate) => candidate !== null) ?? null
203
220
  }
204
221
 
205
222
  export async function resolveConnectionInput(input: string): Promise<ResolvedDevBundle> {