peachsim 0.1.527

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 (324) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +640 -0
  3. package/cli/app-config.ts +82 -0
  4. package/cli/app-fonts.ts +388 -0
  5. package/cli/app-project.ts +59 -0
  6. package/cli/app-splash.ts +171 -0
  7. package/cli/app-state-reset.ts +26 -0
  8. package/cli/auth.ts +184 -0
  9. package/cli/bin.ts +59 -0
  10. package/cli/bridge-diagnostics.ts +226 -0
  11. package/cli/bridge-flow-runner.ts +2806 -0
  12. package/cli/browser-backend.ts +44 -0
  13. package/cli/browser-evals.ts +96 -0
  14. package/cli/cli-update.ts +261 -0
  15. package/cli/cloud-client.ts +1530 -0
  16. package/cli/cloud-dispatch.ts +206 -0
  17. package/cli/cloud-session.ts +227 -0
  18. package/cli/command-contract.ts +221 -0
  19. package/cli/command-registry.ts +304 -0
  20. package/cli/commands/agent-wrapper.ts +1048 -0
  21. package/cli/commands/agent.ts +439 -0
  22. package/cli/commands/app-fonts.ts +99 -0
  23. package/cli/commands/assert.ts +542 -0
  24. package/cli/commands/auth.ts +71 -0
  25. package/cli/commands/box/bundle-plane.ts +71 -0
  26. package/cli/commands/box/checkout-plane.ts +256 -0
  27. package/cli/commands/box/plane-share.ts +208 -0
  28. package/cli/commands/box/rnx-command.ts +60 -0
  29. package/cli/commands/box/search-command.ts +157 -0
  30. package/cli/commands/box.ts +925 -0
  31. package/cli/commands/camera.ts +266 -0
  32. package/cli/commands/cleanup.ts +169 -0
  33. package/cli/commands/compat.ts +176 -0
  34. package/cli/commands/config.ts +34 -0
  35. package/cli/commands/control.ts +2154 -0
  36. package/cli/commands/cpu-profile.ts +941 -0
  37. package/cli/commands/daemon-mac-app.ts +165 -0
  38. package/cli/commands/daemon.ts +800 -0
  39. package/cli/commands/debug.ts +751 -0
  40. package/cli/commands/desktop.ts +39 -0
  41. package/cli/commands/detect.ts +201 -0
  42. package/cli/commands/detox.ts +481 -0
  43. package/cli/commands/device.ts +134 -0
  44. package/cli/commands/diagnose.ts +589 -0
  45. package/cli/commands/do-chain.ts +408 -0
  46. package/cli/commands/electron.ts +94 -0
  47. package/cli/commands/film.ts +379 -0
  48. package/cli/commands/flow.ts +1240 -0
  49. package/cli/commands/inspect/actions.ts +516 -0
  50. package/cli/commands/inspect/core.ts +2086 -0
  51. package/cli/commands/inspect/count.ts +17 -0
  52. package/cli/commands/inspect/describe.ts +119 -0
  53. package/cli/commands/inspect/env.ts +25 -0
  54. package/cli/commands/inspect/find.ts +169 -0
  55. package/cli/commands/inspect/get-layout.ts +53 -0
  56. package/cli/commands/inspect/keyboard.ts +54 -0
  57. package/cli/commands/inspect/list.ts +175 -0
  58. package/cli/commands/inspect/memory.ts +215 -0
  59. package/cli/commands/inspect/redaction.ts +39 -0
  60. package/cli/commands/inspect/resolve-target.ts +77 -0
  61. package/cli/commands/inspect/screens.ts +78 -0
  62. package/cli/commands/inspect/settle.ts +32 -0
  63. package/cli/commands/inspect/settling.ts +197 -0
  64. package/cli/commands/inspect/shared.ts +259 -0
  65. package/cli/commands/inspect/sleep.ts +15 -0
  66. package/cli/commands/inspect/tree.ts +23 -0
  67. package/cli/commands/inspect/url.ts +17 -0
  68. package/cli/commands/inspect/wait-event.ts +211 -0
  69. package/cli/commands/inspect/wait-idle.ts +25 -0
  70. package/cli/commands/inspect/wait-ready.ts +87 -0
  71. package/cli/commands/inspect/wait-selector.ts +59 -0
  72. package/cli/commands/inspect/wait.ts +32 -0
  73. package/cli/commands/inspect.ts +4738 -0
  74. package/cli/commands/install-cli.ts +275 -0
  75. package/cli/commands/install-desktop.ts +385 -0
  76. package/cli/commands/login.ts +357 -0
  77. package/cli/commands/logout.ts +24 -0
  78. package/cli/commands/maestro-generate.ts +375 -0
  79. package/cli/commands/maestro.ts +536 -0
  80. package/cli/commands/mode.ts +58 -0
  81. package/cli/commands/no-bridge-hint.ts +80 -0
  82. package/cli/commands/perf.ts +67 -0
  83. package/cli/commands/permissions.ts +203 -0
  84. package/cli/commands/platform.ts +410 -0
  85. package/cli/commands/preview.ts +103 -0
  86. package/cli/commands/profile.ts +106 -0
  87. package/cli/commands/react.ts +353 -0
  88. package/cli/commands/record.ts +1435 -0
  89. package/cli/commands/report-issue.ts +305 -0
  90. package/cli/commands/reset.ts +89 -0
  91. package/cli/commands/runtime.ts +348 -0
  92. package/cli/commands/screenshot-command.ts +106 -0
  93. package/cli/commands/screenshot-layers.ts +144 -0
  94. package/cli/commands/screenshot-mode.ts +38 -0
  95. package/cli/commands/screenshot.ts +492 -0
  96. package/cli/commands/screenshots-capture.ts +608 -0
  97. package/cli/commands/screenshots.ts +127 -0
  98. package/cli/commands/serve.ts +139 -0
  99. package/cli/commands/setup.ts +416 -0
  100. package/cli/commands/shell-boolean-mode.ts +82 -0
  101. package/cli/commands/skills.ts +468 -0
  102. package/cli/commands/slides.ts +357 -0
  103. package/cli/commands/state.ts +94 -0
  104. package/cli/commands/storage.ts +58 -0
  105. package/cli/commands/telemetry.ts +55 -0
  106. package/cli/commands/test.ts +535 -0
  107. package/cli/commands/three-mode.ts +771 -0
  108. package/cli/commands/timeline.ts +121 -0
  109. package/cli/commands/upgrade.ts +167 -0
  110. package/cli/commands/upload.ts +1203 -0
  111. package/cli/commands/version.ts +63 -0
  112. package/cli/commands/welcome.ts +17 -0
  113. package/cli/commands/what-happened.ts +328 -0
  114. package/cli/current-sim.ts +217 -0
  115. package/cli/desktop-companion.ts +315 -0
  116. package/cli/drivers/electron.ts +70 -0
  117. package/cli/drivers/index.ts +20 -0
  118. package/cli/drivers/playwright-provisioning.ts +184 -0
  119. package/cli/drivers/playwright-sim-host.ts +537 -0
  120. package/cli/drivers/playwright.ts +209 -0
  121. package/cli/drivers/registry.ts +65 -0
  122. package/cli/drivers/types.ts +102 -0
  123. package/cli/flow-export.ts +55 -0
  124. package/cli/flow-file.ts +142 -0
  125. package/cli/flow-live-status.ts +126 -0
  126. package/cli/flow-session.ts +140 -0
  127. package/cli/help-core.ts +143 -0
  128. package/cli/help.ts +44 -0
  129. package/cli/hidden-runtime-alias.ts +19 -0
  130. package/cli/hints.ts +214 -0
  131. package/cli/inspect-notice-state.ts +114 -0
  132. package/cli/internal-child.ts +176 -0
  133. package/cli/maestro-js.ts +323 -0
  134. package/cli/main.ts +704 -0
  135. package/cli/open-url.ts +8 -0
  136. package/cli/outbound-endpoints.ts +830 -0
  137. package/cli/parent-pid.ts +204 -0
  138. package/cli/parse-args.ts +189 -0
  139. package/cli/privacy.ts +53 -0
  140. package/cli/prompt.ts +56 -0
  141. package/cli/recording-access.ts +108 -0
  142. package/cli/registry.ts +2 -0
  143. package/cli/resolve-assets.ts +63 -0
  144. package/cli/rnx-in-process.ts +98 -0
  145. package/cli/run-registry.ts +226 -0
  146. package/cli/run-rnx-core.ts +148 -0
  147. package/cli/run-rnx.ts +137 -0
  148. package/cli/runtime-notes.ts +66 -0
  149. package/cli/runtime-summary.ts +25 -0
  150. package/cli/secure-file.ts +31 -0
  151. package/cli/self-invocation.ts +58 -0
  152. package/cli/send-to-box.ts +109 -0
  153. package/cli/setup-repository.ts +191 -0
  154. package/cli/shell-init.ts +98 -0
  155. package/cli/standalone.ts +4 -0
  156. package/cli/telemetry.ts +84 -0
  157. package/cli/ui.ts +68 -0
  158. package/cli/ws-bridge.ts +1020 -0
  159. package/detox/attempt-events.cjs +23 -0
  160. package/detox/colors.ts +54 -0
  161. package/detox/config-loader.ts +135 -0
  162. package/detox/element-types.ts +48 -0
  163. package/detox/expectations.ts +534 -0
  164. package/detox/gestures.ts +501 -0
  165. package/detox/index.ts +2694 -0
  166. package/detox/jest-preset.cjs +29 -0
  167. package/detox/jest-setup-after-env.cjs +22 -0
  168. package/detox/matchers.ts +30 -0
  169. package/detox/motion-change.cjs +109 -0
  170. package/detox/motion-change.d.cts +35 -0
  171. package/detox/navigation-lifecycle.ts +290 -0
  172. package/detox/navigation.ts +43 -0
  173. package/detox/proof-frame.cjs +114 -0
  174. package/detox/record-block-execution.cjs +78 -0
  175. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  176. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  177. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  178. package/detox/screenshots/theme-blue-toggle.png +0 -0
  179. package/detox/screenshots/theme-blue.png +0 -0
  180. package/detox/screenshots/theme-red-toggle.png +0 -0
  181. package/detox/screenshots/theme-red.png +0 -0
  182. package/dist-lib/agent-daemon-client.cjs +502 -0
  183. package/dist-lib/agent-events.cjs +48 -0
  184. package/dist-lib/agent-identity.cjs +53 -0
  185. package/dist-lib/agent-sessions.cjs +759 -0
  186. package/dist-lib/attached-projects.cjs +526 -0
  187. package/dist-lib/auth/shared-session.cjs +208 -0
  188. package/dist-lib/backend-origin.cjs +88 -0
  189. package/dist-lib/beta.cjs +41 -0
  190. package/dist-lib/beta.mjs +13 -0
  191. package/dist-lib/bridge-constants.cjs +161 -0
  192. package/dist-lib/bridge-contract-input.cjs +451 -0
  193. package/dist-lib/bridge-contract-input.mjs +414 -0
  194. package/dist-lib/bridge-contract.cjs +130 -0
  195. package/dist-lib/bridge-contract.mjs +94 -0
  196. package/dist-lib/capture-contract.cjs +220 -0
  197. package/dist-lib/capture-contract.mjs +190 -0
  198. package/dist-lib/cli-constants.cjs +32 -0
  199. package/dist-lib/cloud-contract.cjs +194 -0
  200. package/dist-lib/cloud-contract.mjs +145 -0
  201. package/dist-lib/cloud.cjs +670 -0
  202. package/dist-lib/cloud.mjs +639 -0
  203. package/dist-lib/config.cjs +96 -0
  204. package/dist-lib/detox/index.cjs +3789 -0
  205. package/dist-lib/detox/jest-preset.cjs +29 -0
  206. package/dist-lib/dev-bundle-resolution.cjs +290 -0
  207. package/dist-lib/home-paths.cjs +638 -0
  208. package/dist-lib/host/bridge-host.cjs +10903 -0
  209. package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
  210. package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
  211. package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
  212. package/dist-lib/host/replacement-module-handler.cjs +469 -0
  213. package/dist-lib/host/websocket-proxy.cjs +298 -0
  214. package/dist-lib/index.cjs +4268 -0
  215. package/dist-lib/jump-to-source-babel.cjs +93 -0
  216. package/dist-lib/jump-to-source-native.cjs +112 -0
  217. package/dist-lib/menu.cjs +646 -0
  218. package/dist-lib/menu.mjs +604 -0
  219. package/dist-lib/metro-fingerprint-registry.cjs +186 -0
  220. package/dist-lib/metro-fingerprint-registry.mjs +156 -0
  221. package/dist-lib/metro-production-bundle.cjs +329 -0
  222. package/dist-lib/metro-production-bundle.mjs +295 -0
  223. package/dist-lib/metro.cjs +904 -0
  224. package/dist-lib/profiles.cjs +246 -0
  225. package/dist-lib/public-brand.cjs +39 -0
  226. package/dist-lib/react-native-host-modules.cjs +51 -0
  227. package/dist-lib/react-native-host-modules.mjs +25 -0
  228. package/dist-lib/render-mode.cjs +54 -0
  229. package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
  230. package/dist-lib/sdk.cjs +2433 -0
  231. package/dist-lib/sdk.mjs +2336 -0
  232. package/dist-lib/skills.cjs +39132 -0
  233. package/dist-lib/swift-submit.cjs +92 -0
  234. package/dist-lib/swift-submit.mjs +65 -0
  235. package/dist-lib/swift.cjs +1646 -0
  236. package/dist-lib/vite.cjs +1591 -0
  237. package/package.json +290 -0
  238. package/scripts/dev-server-scanner.ts +1291 -0
  239. package/scripts/optional-demo-registry.ts +113 -0
  240. package/skills/contrast/SKILL.md +165 -0
  241. package/skills/rnx-debug/SKILL.md +433 -0
  242. package/skills/rnx-perf/SKILL.md +399 -0
  243. package/skills/rnx-setup/SKILL.md +172 -0
  244. package/skills/rnx-test/SKILL.md +321 -0
  245. package/skills/rnx-visual/SKILL.md +126 -0
  246. package/src/agent-daemon-client.ts +410 -0
  247. package/src/agent-events.ts +71 -0
  248. package/src/agent-identity.ts +23 -0
  249. package/src/agent-prompt.ts +71 -0
  250. package/src/agent-sessions.ts +580 -0
  251. package/src/app-url.ts +39 -0
  252. package/src/attached-projects.ts +539 -0
  253. package/src/auth/shared-session.ts +235 -0
  254. package/src/backend-origin.ts +70 -0
  255. package/src/beta.ts +19 -0
  256. package/src/bridge-constants.ts +175 -0
  257. package/src/bridge-contract-input.ts +502 -0
  258. package/src/bridge-contract.ts +558 -0
  259. package/src/browser-cache.ts +16 -0
  260. package/src/capture-contract.ts +587 -0
  261. package/src/cli-constants.ts +1 -0
  262. package/src/cli-version.ts +40 -0
  263. package/src/cloud-contract.ts +274 -0
  264. package/src/cloud.ts +762 -0
  265. package/src/codex-client.ts +234 -0
  266. package/src/config.ts +132 -0
  267. package/src/connect.ts +101 -0
  268. package/src/dev-bundle-resolution.ts +346 -0
  269. package/src/dev-server-open.ts +198 -0
  270. package/src/disk-cleanup.ts +540 -0
  271. package/src/flow-timeout-scale.ts +15 -0
  272. package/src/home-paths.ts +806 -0
  273. package/src/host/agent-host.ts +666 -0
  274. package/src/host/bridge-host.ts +2788 -0
  275. package/src/host/fetch-proxy-handler.ts +449 -0
  276. package/src/host/fetch-proxy-overrides.ts +51 -0
  277. package/src/host/open-url.ts +89 -0
  278. package/src/host/plane-host.ts +203 -0
  279. package/src/host/replacement-module-handler.ts +158 -0
  280. package/src/host/websocket-proxy.ts +315 -0
  281. package/src/index.ts +71 -0
  282. package/src/jump-to-source-babel.ts +135 -0
  283. package/src/jump-to-source-native.ts +102 -0
  284. package/src/menu.ts +777 -0
  285. package/src/metro-fingerprint-registry.ts +302 -0
  286. package/src/metro-plugin.ts +341 -0
  287. package/src/metro-production-bundle.ts +471 -0
  288. package/src/native-dev-bundle-url.ts +118 -0
  289. package/src/native-seam-manifest.ts +341 -0
  290. package/src/playwright-host-log.ts +16 -0
  291. package/src/plist.ts +27 -0
  292. package/src/poll-until.ts +123 -0
  293. package/src/profiles.ts +179 -0
  294. package/src/public-brand.ts +9 -0
  295. package/src/react-native-host-modules.ts +26 -0
  296. package/src/render-mode.ts +27 -0
  297. package/src/rolldown-plugin-swift.ts +240 -0
  298. package/src/runtime-assets.ts +233 -0
  299. package/src/runtime-delivery.ts +54 -0
  300. package/src/screenshots/compose.ts +422 -0
  301. package/src/screenshots/frame-compose.ts +514 -0
  302. package/src/screenshots/orchestrate.ts +256 -0
  303. package/src/screenshots/org-deck.ts +91 -0
  304. package/src/screenshots/registry.ts +58 -0
  305. package/src/screenshots/schema.ts +364 -0
  306. package/src/sdk.ts +15 -0
  307. package/src/settings-bundle.ts +418 -0
  308. package/src/shell-bridge-identity.ts +15 -0
  309. package/src/sim-client.ts +697 -0
  310. package/src/skills/builtin/a11y-review.ts +132 -0
  311. package/src/skills/builtin/compat-check.ts +71 -0
  312. package/src/skills/builtin/maestro-test.ts +121 -0
  313. package/src/skills/builtin/perf-profile.ts +90 -0
  314. package/src/skills/builtin/screenshot-all.ts +52 -0
  315. package/src/skills/builtin/visual-diff.ts +100 -0
  316. package/src/skills/registry.ts +107 -0
  317. package/src/skills/types.ts +41 -0
  318. package/src/swift-submit.ts +185 -0
  319. package/src/swift.ts +7 -0
  320. package/src/team-machine-prompt.ts +73 -0
  321. package/src/vite-plugin-one.ts +264 -0
  322. package/src/vite-plugin-swift.ts +620 -0
  323. package/src/vite-plugin.ts +1946 -0
  324. package/src/worklets-babel.ts +250 -0
@@ -0,0 +1,346 @@
1
+ import {
2
+ buildLoopbackProbeBaseUrl,
3
+ isLoopbackHost,
4
+ LOOPBACK_PROBE_HOSTNAMES,
5
+ } from './backend-origin'
6
+ import {
7
+ alignLoopbackBundleUrlWithProbe,
8
+ METRO_BUNDLE_PATH,
9
+ normalizeNativeDevBundleUrl,
10
+ } from './native-dev-bundle-url'
11
+
12
+ export const DEFAULT_TEST_DRIVE_PORT = 8081
13
+
14
+ export interface ResolvedDevBundle {
15
+ bundleUrl: string
16
+ port: number
17
+ framework: string
18
+ projectName?: string
19
+ }
20
+
21
+ function buildUnresolvedTargetError(targetLabel: string): Error {
22
+ return new Error(
23
+ `could not resolve a native bundle for ${targetLabel}. pass an explicit bundle URL or open Connect and choose the app there.`,
24
+ )
25
+ }
26
+
27
+ export function inferManifestFramework(
28
+ launchUrl: string | undefined,
29
+ sdkVersion: unknown,
30
+ ): string {
31
+ if (launchUrl?.includes('/one/metro-entry.bundle')) return 'one'
32
+ if (typeof sdkVersion === 'string' && sdkVersion) return 'expo'
33
+ return 'unknown'
34
+ }
35
+
36
+ function buildBaseUrl(protocol: string, host: string, port: number) {
37
+ return `${protocol}//${host}:${port}`
38
+ }
39
+
40
+ function shouldProxyLoopbackProbe(targetUrl: string) {
41
+ if (typeof window === 'undefined') return false
42
+ try {
43
+ const parsed = new URL(targetUrl)
44
+ return (
45
+ isLoopbackHost(parsed.hostname) &&
46
+ isLoopbackHost(window.location.hostname) &&
47
+ parsed.origin !== window.location.origin
48
+ )
49
+ } catch {
50
+ return false
51
+ }
52
+ }
53
+
54
+ async function fetchDevProbe(targetUrl: string, init?: RequestInit): Promise<Response> {
55
+ const requestInit: RequestInit = {
56
+ ...init,
57
+ cache: init?.cache ?? 'no-store',
58
+ }
59
+ if (!shouldProxyLoopbackProbe(targetUrl)) {
60
+ return fetch(targetUrl, requestInit)
61
+ }
62
+ return fetch(`/__fetch-proxy?url=${encodeURIComponent(targetUrl)}`, requestInit)
63
+ }
64
+
65
+ function getDefaultPortForProtocol(protocol: string) {
66
+ return protocol === 'https:' ? 443 : 80
67
+ }
68
+
69
+ function isBaseServerUrl(url: URL) {
70
+ const path = url.pathname || '/'
71
+ return (path === '/' || path === '') && !url.search && !url.hash
72
+ }
73
+
74
+ // resolve a loopback dev-server port through the SAME server-side scanner the
75
+ // dev-server list uses (`/__server-scan`), so an explicit port (`/rn/<port>`, a
76
+ // typed port) gets the same manifest path and query the list resolves. loopback
77
+ // hosts stay aligned to the address that answered discovery.
78
+ //
79
+ // why this exists: in a browser/worker the direct manifest probe below is
80
+ // cross-origin (shell on :5173, dev server on :<port>), and a dev server
81
+ // answers a cross-origin `GET /` with its SSR HTML, not the expo manifest. so
82
+ // the manifest branch silently fails (`.json()` throws on HTML) and the probe
83
+ // falls back to a generic `/index.bundle` — a DIFFERENT metro entry point than
84
+ // the manifest's `metro-entry.bundle`, which makes metro build the same app
85
+ // twice. the scanner runs server-side (same-origin node fetch, sends the
86
+ // expo-platform header) and reads `launchAsset.url` correctly.
87
+ //
88
+ // env-agnostic by design: it just attempts `/__server-scan` and lets the result
89
+ // decide. in the tenant worker (which has no `location`) the patched fetch
90
+ // resolves the relative path to the shell origin and returns the scan json. in a
91
+ // node/bun CLI a relative-URL fetch with no base throws → null → the direct probe
92
+ // runs (correct there, since node can read the cross-origin manifest). in prod
93
+ // the path SPA-falls-back to HTML → the json content-type guard returns null →
94
+ // direct probe. so there is exactly one resolver per environment, no env sniff.
95
+ async function resolvePortViaServerScan(port: number): Promise<ResolvedDevBundle | null> {
96
+ try {
97
+ const res = await fetch('/__server-scan', { cache: 'no-store' })
98
+ if (!res.ok) return null
99
+ if (!(res.headers.get('content-type') || '').toLowerCase().includes('json'))
100
+ return null
101
+ const servers: unknown = await res.json()
102
+ if (!Array.isArray(servers)) return null
103
+ const match = servers.find(
104
+ (
105
+ s,
106
+ ): s is {
107
+ port: number
108
+ bundleUrl: string
109
+ framework?: string
110
+ projectName?: string
111
+ bundleUrlProvisional?: boolean
112
+ } =>
113
+ !!s &&
114
+ typeof s === 'object' &&
115
+ (s as { port?: unknown }).port === port &&
116
+ typeof (s as { bundleUrl?: unknown }).bundleUrl === 'string',
117
+ )
118
+ if (!match) return null
119
+ // the scanner marks a bundle URL provisional when its own fast manifest
120
+ // probe got no answer and it fell back to the generic `/index.bundle`. that
121
+ // is enough to LIST the server and not enough to open it, so fall through
122
+ // to the direct probe below, which asks the manifest again with a budget
123
+ // that fits a real dev server. same rule the CLI applies in
124
+ // `resolveConnectionInputForCli`; a provisional URL must never reach a sim.
125
+ if (match.bundleUrlProvisional) return null
126
+ return {
127
+ // normalize exactly like the manifest-probe branch below — the scan
128
+ // returns metro's manifest launchAsset url (lazy=true for code-split
129
+ // one/vxrn apps), but sootsim loads ONE self-contained bundle into the tenant
130
+ // worker, so a deferred React.lazy chunk throws "Requiring unknown module
131
+ // <id>". normalizeNativeDevBundleUrl forces lazy=false + strips bytecode.
132
+ bundleUrl: normalizeNativeDevBundleUrl(match.bundleUrl),
133
+ port,
134
+ framework: typeof match.framework === 'string' ? match.framework : 'unknown',
135
+ projectName: typeof match.projectName === 'string' ? match.projectName : undefined,
136
+ }
137
+ } catch {
138
+ return null
139
+ }
140
+ }
141
+
142
+ // how a dev server answered `GET /`, which is the one request that tells the
143
+ // two kinds of dev server apart. an Expo or One server answers with its
144
+ // manifest. a vanilla Metro server answers with Metro's own HTML landing page
145
+ // and has no manifest to serve. NO answer is neither: a One server that is
146
+ // still starting leaves `/` hanging until its manifest middleware is up, while
147
+ // `/status` already reports packager-status:running. reading that silence as
148
+ // "this server has no manifest" opens the app on `<base>/index.bundle` — a
149
+ // second entry point, with its own module graph, that the app's manifest never
150
+ // named — so Metro builds the whole app twice and the connection is labelled
151
+ // `metro` instead of its real framework.
152
+ type ManifestProbe =
153
+ | { state: 'resolved'; bundle: ResolvedDevBundle }
154
+ | { state: 'no-manifest' }
155
+ | { state: 'no-answer' }
156
+
157
+ // a `/` that has not answered in this long is a server still coming up, not a
158
+ // server telling us what it is. without it an unresponsive dev server hangs
159
+ // the caller for as long as it feels like.
160
+ const MANIFEST_PROBE_TIMEOUT_MS = 5_000
161
+ // how long to keep asking a confirmed-running packager for its manifest.
162
+ const MANIFEST_BOOT_BUDGET_MS = 30_000
163
+ const MANIFEST_BOOT_RETRY_MS = 1_000
164
+
165
+ async function probeExpoManifest(
166
+ normalizedBaseUrl: string,
167
+ port: number,
168
+ ): Promise<ManifestProbe> {
169
+ let body: string
170
+ // the deadline means STOP WAITING, not KILL THE REQUEST. `AbortSignal.timeout`
171
+ // does the second: it tears down a dev server that is still assembling its
172
+ // manifest, which fires "Cannot pipe to a closed or destroyed stream" into
173
+ // One's manifest middleware and can exit the user's dev server. so nothing
174
+ // here aborts. we race the probe against the budget, answer `no-answer` on
175
+ // time, and let the request run itself out in the background, discarded.
176
+ const pending = fetchDevProbe(`${normalizedBaseUrl}/`, {
177
+ headers: { 'expo-platform': 'ios' },
178
+ }).then(
179
+ async (res) => (res.ok ? { ok: true as const, body: await res.text() } : null),
180
+ () => undefined,
181
+ )
182
+ let probeDeadline: ReturnType<typeof setTimeout> | undefined
183
+ const answered = await Promise.race([
184
+ pending,
185
+ new Promise<'timeout'>((resolve) => {
186
+ probeDeadline = setTimeout(() => resolve('timeout'), MANIFEST_PROBE_TIMEOUT_MS)
187
+ }),
188
+ ])
189
+ // the race is settled, so drop the deadline rather than leaving it pending.
190
+ clearTimeout(probeDeadline)
191
+ if (answered === 'timeout' || answered === undefined) return { state: 'no-answer' }
192
+ if (answered === null) return { state: 'no-manifest' }
193
+ body = answered.body
194
+ if (!body.trim()) return { state: 'no-answer' }
195
+
196
+ let manifest: any
197
+ try {
198
+ manifest = JSON.parse(body)
199
+ } catch {
200
+ // a page rather than a manifest — Metro's landing page, or an app's own
201
+ // SSR html. that is a real answer: this server serves no manifest.
202
+ return { state: 'no-manifest' }
203
+ }
204
+
205
+ const client = manifest?.extra?.expoClient || manifest?.extra || {}
206
+ const rawLaunchUrl =
207
+ typeof manifest?.launchAsset?.url === 'string' ? manifest.launchAsset.url : undefined
208
+ const launchUrl = rawLaunchUrl
209
+ ? alignLoopbackBundleUrlWithProbe(rawLaunchUrl, normalizedBaseUrl)
210
+ : undefined
211
+ if (!launchUrl && !client.name) return { state: 'no-manifest' }
212
+
213
+ return {
214
+ state: 'resolved',
215
+ bundle: {
216
+ bundleUrl: normalizeNativeDevBundleUrl(
217
+ launchUrl || `${normalizedBaseUrl}${METRO_BUNDLE_PATH}`,
218
+ ),
219
+ port,
220
+ framework: inferManifestFramework(launchUrl, client.sdkVersion),
221
+ projectName: client.name,
222
+ },
223
+ }
224
+ }
225
+
226
+ async function isPackagerRunning(normalizedBaseUrl: string): Promise<boolean> {
227
+ try {
228
+ const res = await fetchDevProbe(`${normalizedBaseUrl}/status`)
229
+ if (!res.ok) return false
230
+ return (await res.text()).includes('packager-status:running')
231
+ } catch {
232
+ return false
233
+ }
234
+ }
235
+
236
+ async function probeBaseUrlBundleDirect(
237
+ baseUrl: string,
238
+ port: number,
239
+ ): Promise<ResolvedDevBundle | null> {
240
+ const normalizedBaseUrl = baseUrl.replace(/\/+$/, '')
241
+
242
+ let manifest = await probeExpoManifest(normalizedBaseUrl, port)
243
+ let packagerConfirmed = false
244
+
245
+ // nothing answered `/`. that is either a closed address — the other loopback
246
+ // family, a dead port — or a packager whose manifest middleware has not come
247
+ // up yet. `/status` separates them, and only the second is worth waiting on.
248
+ if (manifest.state === 'no-answer') {
249
+ packagerConfirmed = await isPackagerRunning(normalizedBaseUrl)
250
+ if (!packagerConfirmed) return null
251
+ const deadline = Date.now() + MANIFEST_BOOT_BUDGET_MS
252
+ while (manifest.state === 'no-answer' && Date.now() < deadline) {
253
+ await new Promise((resolve) => setTimeout(resolve, MANIFEST_BOOT_RETRY_MS))
254
+ manifest = await probeExpoManifest(normalizedBaseUrl, port)
255
+ }
256
+ }
257
+
258
+ if (manifest.state === 'resolved') return manifest.bundle
259
+ // a server that never says what it is gets no guessed entry point.
260
+ if (manifest.state === 'no-answer') return null
261
+
262
+ // vanilla Metro (or any non-Expo dev server) — packager-status:running on
263
+ // /status is the canonical signature. we previously also did `HEAD
264
+ // /node_modules/one/metro-entry.bundle` as a framework discriminator, but
265
+ // Metro lazy-resolves modules at request time, so HEAD on *any* `.bundle`
266
+ // path can return 200 even when the underlying file doesn't exist. that
267
+ // caused false-positive One framework detection on non-One apps that had
268
+ // `node_modules/one` anywhere on disk (or that returned 200 HEAD + 404
269
+ // GET for missing bundles), surfacing as
270
+ // "failed to fetch bundle (404|502): .../node_modules/one/metro-entry.bundle…"
271
+ // the Expo manifest probe above already returns the correct launchAsset.url
272
+ // for One apps (which serve the Expo manifest format via vxrn), so we
273
+ // drop the bespoke One probe entirely.
274
+ if (!packagerConfirmed && !(await isPackagerRunning(normalizedBaseUrl))) return null
275
+ return {
276
+ bundleUrl: `${normalizedBaseUrl}${METRO_BUNDLE_PATH}`,
277
+ port,
278
+ framework: 'metro',
279
+ }
280
+ }
281
+
282
+ async function probeBaseUrlBundle(
283
+ baseUrl: string,
284
+ port: number,
285
+ ): Promise<ResolvedDevBundle | null> {
286
+ // browser/worker loopback targets resolve through the server-side scanner so
287
+ // they match the dev-server list exactly — see resolvePortViaServerScan.
288
+ try {
289
+ if (isLoopbackHost(new URL(baseUrl).hostname)) {
290
+ const viaScan = await resolvePortViaServerScan(port)
291
+ if (viaScan) return viaScan
292
+ }
293
+ } catch {}
294
+ return probeBaseUrlBundleDirect(baseUrl, port)
295
+ }
296
+
297
+ export async function probePortBundle(port: number): Promise<ResolvedDevBundle | null> {
298
+ const viaScan = await resolvePortViaServerScan(port)
299
+ if (viaScan) return viaScan
300
+ const candidates = await Promise.all(
301
+ LOOPBACK_PROBE_HOSTNAMES.map((hostname) =>
302
+ probeBaseUrlBundleDirect(buildLoopbackProbeBaseUrl('http:', hostname, port), port),
303
+ ),
304
+ )
305
+ return candidates.find((candidate) => candidate !== null) ?? null
306
+ }
307
+
308
+ export async function resolveConnectionInput(input: string): Promise<ResolvedDevBundle> {
309
+ const trimmed = input.trim()
310
+
311
+ if (/^\d+$/.test(trimmed)) {
312
+ const port = parseInt(trimmed, 10)
313
+ const resolved = await probePortBundle(port)
314
+ if (resolved) return resolved
315
+ throw buildUnresolvedTargetError(`localhost:${port}`)
316
+ }
317
+
318
+ const bundleUrl = trimmed.startsWith('http') ? trimmed : `http://${trimmed}`
319
+ let parsed: URL
320
+
321
+ try {
322
+ parsed = new URL(bundleUrl)
323
+ } catch {
324
+ throw new Error(
325
+ `could not parse "${input}". pass a dev-server port, a dev-server base URL, or a full bundle URL.`,
326
+ )
327
+ }
328
+
329
+ const protocol = parsed.protocol || 'http:'
330
+ const port = parsed.port
331
+ ? parseInt(parsed.port, 10)
332
+ : getDefaultPortForProtocol(protocol)
333
+ const baseUrl = buildBaseUrl(protocol, parsed.hostname, port)
334
+
335
+ // a bare base URL (just an origin, no path) is unambiguously a dev server
336
+ // to probe — regardless of host. probe its live manifest rather than
337
+ // guessing that the origin itself is the bundle URL. loopback vs. remote
338
+ // only affects how the probe fetch is routed (see shouldProxyLoopbackProbe).
339
+ if (isBaseServerUrl(parsed)) {
340
+ const resolved = await probeBaseUrlBundle(baseUrl, port)
341
+ if (resolved) return resolved
342
+ throw buildUnresolvedTargetError(baseUrl)
343
+ }
344
+
345
+ return { bundleUrl: parsed.toString(), port, framework: 'unknown' }
346
+ }
@@ -0,0 +1,198 @@
1
+ import { spawn, type ChildProcess } from 'node:child_process'
2
+ import { createHash } from 'node:crypto'
3
+ import fs from 'node:fs'
4
+ import http from 'node:http'
5
+ import path from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ export type RnxDesktopOpenOption = boolean | { appName: string }
9
+
10
+ const scheduledMetroOpens = new Set<string>()
11
+
12
+ export function resolveRnxDesktopAppName(
13
+ open: RnxDesktopOpenOption | undefined,
14
+ projectRoot: string,
15
+ ): string | null {
16
+ if (!open) return null
17
+ if (typeof open === 'object') {
18
+ const appName = open.appName.trim()
19
+ if (!appName) throw new Error('rnx: open.appName must not be empty')
20
+ return appName
21
+ }
22
+
23
+ const packageJsonPath = path.join(projectRoot, 'package.json')
24
+ const packageJson: unknown = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
25
+ const packageName =
26
+ packageJson && typeof packageJson === 'object'
27
+ ? Reflect.get(packageJson, 'name')
28
+ : undefined
29
+ if (typeof packageName !== 'string' || !packageName.trim()) {
30
+ throw new Error(`rnx: ${packageJsonPath} must declare a package name`)
31
+ }
32
+ return packageName.trim()
33
+ }
34
+
35
+ export function tagRnxBundleForPlugin(
36
+ bundleUrl: string,
37
+ replacementOrigin: string,
38
+ replacementToken: string,
39
+ projectRoot: string,
40
+ appName?: string | null,
41
+ ): string {
42
+ const absolute = /^[a-z][a-z\d+.-]*:\/\//i.test(bundleUrl)
43
+ const url = new URL(bundleUrl, absolute ? undefined : 'http://rnx.local')
44
+ if (appName) {
45
+ url.searchParams.set('rnxAppName', appName)
46
+ url.searchParams.set(
47
+ 'rnxProjectId',
48
+ createHash('sha256')
49
+ .update(fs.realpathSync(projectRoot))
50
+ .digest('hex')
51
+ .slice(0, 32),
52
+ )
53
+ }
54
+ url.searchParams.set('rnxReplacementOrigin', replacementOrigin)
55
+ url.searchParams.set('rnxReplacementToken', replacementToken)
56
+ return absolute ? url.toString() : `${url.pathname}${url.search}${url.hash}`
57
+ }
58
+
59
+ export function resolveRnxDesktopLaunchTarget(port: number, bundleUrl: string): string {
60
+ return new URL(bundleUrl, `http://localhost:${port}`).toString()
61
+ }
62
+
63
+ export function buildRnxDesktopOpenArgs(bundleUrl: string): string[] {
64
+ return ['open', bundleUrl, '--new', '--driver', 'electron', '--no-describe']
65
+ }
66
+
67
+ export function observeRnxDesktopLaunch(child: ChildProcess, appName: string): void {
68
+ let stderr = ''
69
+ let spawnFailed = false
70
+ child.stderr?.setEncoding('utf8')
71
+ child.stderr?.on('data', (chunk: string) => {
72
+ stderr = `${stderr}${chunk}`.slice(-16_384)
73
+ })
74
+ child.once('error', (error) => {
75
+ spawnFailed = true
76
+ console.error(`[rnx] could not open ${appName}: ${error.message}`)
77
+ })
78
+ child.once('close', (code, signal) => {
79
+ if (spawnFailed) return
80
+ if (code === 0) {
81
+ console.log(`[rnx] opened ${appName} in the desktop simulator`)
82
+ return
83
+ }
84
+ const detail = stderr.trim() || (signal ? `signal ${signal}` : `exit ${code}`)
85
+ console.error(`[rnx] could not open ${appName}: ${detail}`)
86
+ })
87
+ }
88
+
89
+ export function launchRnxDesktop(bundleUrl: string, appName: string): void {
90
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
91
+ const binPath = path.join(packageRoot, 'dist-cli', 'bin.js')
92
+ console.log(`[rnx] opening ${appName} in the desktop simulator`)
93
+ const child = spawn(
94
+ process.execPath,
95
+ [binPath, ...buildRnxDesktopOpenArgs(bundleUrl)],
96
+ {
97
+ stdio: ['ignore', 'ignore', 'pipe'],
98
+ windowsHide: true,
99
+ },
100
+ )
101
+ observeRnxDesktopLaunch(child, appName)
102
+ }
103
+
104
+ interface MetroDesktopOpenOptions {
105
+ port: number
106
+ bundleUrl: string
107
+ appName: string
108
+ projectRoot: string
109
+ serverId: string
110
+ intervalMs?: number
111
+ timeoutMs?: number
112
+ launch?: (bundleUrl: string, appName: string) => void
113
+ }
114
+
115
+ export function openRnxDesktopWhenMetroIsReady({
116
+ port,
117
+ bundleUrl,
118
+ appName,
119
+ projectRoot,
120
+ serverId,
121
+ intervalMs = 100,
122
+ timeoutMs = 30_000,
123
+ launch = launchRnxDesktop,
124
+ }: MetroDesktopOpenOptions): Promise<boolean> {
125
+ const key = `${projectRoot}\0${port}`
126
+ if (scheduledMetroOpens.has(key)) return Promise.resolve(false)
127
+ scheduledMetroOpens.add(key)
128
+ const deadline = Date.now() + timeoutMs
129
+
130
+ return new Promise((resolve) => {
131
+ let finished = false
132
+ let retryTimer: ReturnType<typeof setTimeout> | undefined
133
+ const finish = (opened: boolean) => {
134
+ if (finished) return
135
+ finished = true
136
+ clearTimeout(retryTimer)
137
+ scheduledMetroOpens.delete(key)
138
+ resolve(opened)
139
+ }
140
+ const retry = () => {
141
+ if (finished) return
142
+ if (Date.now() >= deadline) {
143
+ console.error(
144
+ `[rnx] could not open ${appName}: Metro did not become ready on port ${port}`,
145
+ )
146
+ finish(false)
147
+ return
148
+ }
149
+ retryTimer = setTimeout(check, intervalMs)
150
+ }
151
+ const check = () => {
152
+ const request = http.get(
153
+ `http://127.0.0.1:${port}/__server-scan`,
154
+ { headers: { Accept: 'application/json' } },
155
+ (response) => {
156
+ let body = ''
157
+ response.setEncoding('utf8')
158
+ response.on('data', (chunk: string) => {
159
+ body = `${body}${chunk}`.slice(0, 65_536)
160
+ })
161
+ response.once('end', () => {
162
+ try {
163
+ const scan: unknown = JSON.parse(body)
164
+ const matches =
165
+ response.statusCode === 200 &&
166
+ Array.isArray(scan) &&
167
+ scan.some((entry: unknown) => {
168
+ if (!entry || typeof entry !== 'object') return false
169
+ const scannedPort = Reflect.get(entry, 'port')
170
+ const projectName = Reflect.get(entry, 'projectName')
171
+ const scannedServerId = Reflect.get(entry, 'rnxServerId')
172
+ const scannedBundle = Reflect.get(entry, 'bundleUrl')
173
+ if (
174
+ scannedPort !== port ||
175
+ projectName !== appName ||
176
+ scannedServerId !== serverId ||
177
+ typeof scannedBundle !== 'string'
178
+ ) {
179
+ return false
180
+ }
181
+ return new URL(scannedBundle).searchParams.get('rnxAppName') === appName
182
+ })
183
+ if (matches) {
184
+ launch(bundleUrl, appName)
185
+ finish(true)
186
+ return
187
+ }
188
+ } catch {}
189
+ retry()
190
+ })
191
+ },
192
+ )
193
+ request.setTimeout(Math.min(1_000, timeoutMs), () => request.destroy())
194
+ request.once('error', retry)
195
+ }
196
+ check()
197
+ })
198
+ }