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,127 @@
1
+ import { runScreenshotsPlan } from '../../src/screenshots/orchestrate'
2
+ import { fetchOrgDeckFrames, type OrgDeckFrame } from '../../src/screenshots/org-deck'
3
+ import { authHeaderValue, resolveCliAuth } from '../auth'
4
+
5
+ interface RunScreenshotsOptions {
6
+ port?: number
7
+ verbose?: boolean
8
+ }
9
+
10
+ export async function runScreenshots(
11
+ args: string[],
12
+ _opts: RunScreenshotsOptions = {},
13
+ ): Promise<number> {
14
+ if (args.includes('--help') || args.includes('-h')) {
15
+ console.log(`
16
+ rnx screenshot appstore — capture and compose app-store screenshot sets
17
+
18
+ usage:
19
+ rnx screenshot appstore --plan <plan.yaml> [options]
20
+
21
+ options:
22
+ --plan <path> screenshot plan yaml
23
+ --sim <id> reuse a live sim for capture
24
+ --app <target> override plan app target (port, url, or shell url)
25
+ --device <model> override plan capture device for this run
26
+ --org <slug> overlay copy/colors from a team's org screenshot deck
27
+ --repo <name> repo for --org (both required to pull the org deck)
28
+ --api-origin <url> contrast origin to fetch the deck from (default: the
29
+ signed-in session origin, else https://contrast.dev)
30
+ --capture-only stop after raw/framed intermediates
31
+ --compose-only skip flow capture, reuse existing raw screenshots
32
+
33
+ examples:
34
+ rnx screenshot appstore --plan app-store.yaml
35
+ rnx screenshot appstore --plan app-store.yaml --sim a9
36
+ rnx screenshot appstore --plan app-store.yaml --org acme --repo mobile
37
+ `)
38
+ return 0
39
+ }
40
+
41
+ const getFlag = (name: string) => args.find((_, i) => args[i - 1] === name)
42
+ const consumedValueFlags = new Set([
43
+ '--plan',
44
+ '--sim',
45
+ '--app',
46
+ '--device',
47
+ '--org',
48
+ '--repo',
49
+ '--api-origin',
50
+ ])
51
+ const positional = args.filter((arg, index) => {
52
+ if (arg.startsWith('-')) return false
53
+ const previous = args[index - 1]
54
+ return !previous || !consumedValueFlags.has(previous)
55
+ })
56
+ const planPath = getFlag('--plan') || positional[0]
57
+ if (!planPath) {
58
+ console.error(' error: screenshots plan path is required (`--plan <path>`).')
59
+ return 1
60
+ }
61
+
62
+ const simId = getFlag('--sim')?.trim() || null
63
+ const appTarget = getFlag('--app')?.trim() || null
64
+ const deviceModel = getFlag('--device')?.trim() || null
65
+ const org = getFlag('--org')?.trim() || null
66
+ const repo = getFlag('--repo')?.trim() || null
67
+ const apiOrigin = getFlag('--api-origin')?.trim() || null
68
+ const captureOnly = args.includes('--capture-only')
69
+ const composeOnly = args.includes('--compose-only')
70
+
71
+ let orgDeckFrames: OrgDeckFrame[] | null = null
72
+ if (org || repo) {
73
+ if (!org || !repo) {
74
+ console.error(' error: --org and --repo must be passed together.')
75
+ return 1
76
+ }
77
+ const auth = resolveCliAuth()
78
+ if (!auth) {
79
+ console.error(
80
+ ' error: --org/--repo needs auth — run `rnx login` or set RNX_API_KEY.',
81
+ )
82
+ return 1
83
+ }
84
+ const origin =
85
+ apiOrigin || (auth.kind === 'session' ? auth.origin : 'https://contrast.dev')
86
+ try {
87
+ orgDeckFrames = await fetchOrgDeckFrames({
88
+ org,
89
+ repo,
90
+ origin,
91
+ authHeader: authHeaderValue(auth),
92
+ })
93
+ } catch (err) {
94
+ console.error(` ${(err as Error).message}`)
95
+ return 1
96
+ }
97
+ if (!orgDeckFrames || orgDeckFrames.length === 0) {
98
+ console.log(
99
+ ` note: no org deck for ${org}/${repo} — composing with the plan's copy/colors.`,
100
+ )
101
+ } else {
102
+ console.log(` org deck: ${orgDeckFrames.length} slide(s) from ${org}/${repo}`)
103
+ }
104
+ }
105
+
106
+ try {
107
+ const result = await runScreenshotsPlan(planPath, {
108
+ simId,
109
+ appTarget,
110
+ deviceModel,
111
+ captureOnly,
112
+ composeOnly,
113
+ orgDeckFrames,
114
+ })
115
+ console.log(` capture manifest: ${result.capture.manifestPath}`)
116
+ if (result.compose) {
117
+ console.log(` compose manifest: ${result.compose.manifestPath}`)
118
+ console.log(` exports: ${result.compose.outputs.length}`)
119
+ } else {
120
+ console.log(' compose skipped')
121
+ }
122
+ return 0
123
+ } catch (err) {
124
+ console.error(` screenshots failed: ${(err as Error).message}`)
125
+ return 1
126
+ }
127
+ }
@@ -0,0 +1,139 @@
1
+ // rnx serve — foreground bridge host
2
+ //
3
+ // hosts the WS bridge + runtime HTTP server on port 7668 (or --port) so
4
+ // CLI commands and electron windows both work against one bridge. sims
5
+ // connect over ws to register themselves; cli clients connect to drive
6
+ // them; electron fetches renderer assets over http from the same port.
7
+ //
8
+ // run in the foreground (ctrl-c to stop). the persistent launchd/systemd
9
+ // daemon installation is handled by `rnx daemon install`.
10
+
11
+ import { DEFAULT_SOOTSIM_BRIDGE_PORT } from '../../src/bridge-constants'
12
+ import {
13
+ ensureRnxHome,
14
+ isDaemonLockfileFresh,
15
+ readDaemonLockfile,
16
+ } from '../../src/home-paths'
17
+ import { SootSimBridgeHost } from '../../src/host/bridge-host'
18
+ import { rnxExit } from '../run-rnx'
19
+ import { resolveDefaultUploadOrigin } from './upload'
20
+
21
+ interface ServeOptions {
22
+ port?: number
23
+ }
24
+
25
+ export async function runServe(args: string[], opts: ServeOptions = {}) {
26
+ if (args.includes('--help') || args.includes('-h')) {
27
+ console.log(`
28
+ rnx serve — run the rnx bridge in the foreground
29
+
30
+ hosts the WS bridge that CLI commands talk to. once running, any rnx
31
+ renderer (browser, electron, headless playwright) that connects to port 7668
32
+ becomes drivable from 'rnx describe', 'rnx do tap', etc.
33
+
34
+ usage:
35
+ rnx serve [options]
36
+
37
+ options:
38
+ --port <n> bridge port (defaults to ${DEFAULT_SOOTSIM_BRIDGE_PORT})
39
+ --quiet suppress per-connection logging
40
+
41
+ examples:
42
+ rnx serve
43
+ rnx serve --port 7668 --quiet
44
+ `)
45
+ rnxExit(0)
46
+ }
47
+
48
+ const portArgIdx = args.indexOf('--port')
49
+ const port =
50
+ portArgIdx >= 0 && args[portArgIdx + 1]
51
+ ? Number(args[portArgIdx + 1])
52
+ : (opts.port ?? DEFAULT_SOOTSIM_BRIDGE_PORT)
53
+ if (Number.isNaN(port)) {
54
+ console.error(` invalid --port value: ${args[portArgIdx + 1]}`)
55
+ rnxExit(1)
56
+ }
57
+ const quiet = args.includes('--quiet') || args.includes('-q')
58
+
59
+ // only one bridge per rnx home. refuse to start a second one
60
+ // regardless of requested port, since both would try to write the same
61
+ // daemon.json and electron/cli clients only read one lockfile.
62
+ const existingLock = readDaemonLockfile()
63
+ if (existingLock && isDaemonLockfileFresh(existingLock)) {
64
+ console.error(
65
+ ` an rnx bridge is already running (pid ${existingLock.pid}, port ${existingLock.bridgePort})`,
66
+ )
67
+ console.error(` stop it with 'rnx daemon stop' first`)
68
+ rnxExit(1)
69
+ }
70
+
71
+ ensureRnxHome()
72
+ // use the explicit upload/auth origin when configured, otherwise production.
73
+ // never probe a local Contrast stack during bridge startup.
74
+ const contrastOrigin = await resolveDefaultUploadOrigin()
75
+ const host = new SootSimBridgeHost({
76
+ port,
77
+ writeLockfile: true,
78
+ contrastOrigin,
79
+ })
80
+ const boundPort = await host.startAsync({ silent: quiet })
81
+
82
+ const started = Date.now()
83
+ const log = (line: string) => {
84
+ if (quiet) return
85
+ process.stdout.write(`${line}\n`)
86
+ }
87
+
88
+ const lastSimIds = new Set<string>()
89
+ const tickInterval = setInterval(() => {
90
+ const sims = host.listSims()
91
+ const currentIds = new Set(sims.map((b) => b.id))
92
+ for (const b of sims) {
93
+ if (!lastSimIds.has(b.id)) {
94
+ const label = b.title || b.url || b.origin || '(unknown)'
95
+ log(` + ${b.id} ${label}`)
96
+ }
97
+ }
98
+ for (const id of lastSimIds) {
99
+ if (!currentIds.has(id)) log(` - ${id}`)
100
+ }
101
+ lastSimIds.clear()
102
+ for (const id of currentIds) lastSimIds.add(id)
103
+ }, 500)
104
+
105
+ log(`rnx bridge listening on ws://localhost:${boundPort} (runtime http on same port)`)
106
+ if (boundPort !== port) {
107
+ log(` (preferred port ${port} was taken — fell back to ${boundPort})`)
108
+ }
109
+ log(` ready for browser, electron, or headless playwright sims to connect`)
110
+ log(` (ctrl-c to stop)`)
111
+
112
+ const shutdown = async (signal: NodeJS.Signals) => {
113
+ clearInterval(tickInterval)
114
+ log(
115
+ `\n ${signal} received — shutting down after ${Math.round((Date.now() - started) / 1000)}s`,
116
+ )
117
+ try {
118
+ await host.close()
119
+ } catch {}
120
+ rnxExit(0)
121
+ }
122
+ process.on('SIGINT', () => shutdown('SIGINT'))
123
+ process.on('SIGTERM', () => shutdown('SIGTERM'))
124
+ // closing the terminal that spawned us sends SIGHUP — treat it like a
125
+ // clean shutdown so the lockfile doesn't linger.
126
+ process.on('SIGHUP', () => shutdown('SIGHUP'))
127
+ // last-ditch lockfile cleanup on any synchronous exit path. the
128
+ // close() call above is async so we can't await it here, but removing
129
+ // the lockfile is cheap + idempotent.
130
+ process.on('exit', () => {
131
+ try {
132
+ host.removeLockfile()
133
+ } catch {}
134
+ })
135
+
136
+ // keep the event loop alive even if ws/http servers all close — we want
137
+ // the process to live until an explicit signal.
138
+ await new Promise<never>(() => {})
139
+ }
@@ -0,0 +1,416 @@
1
+ // rnx setup owns the optional machine and repository choices. rnx itself
2
+ // works without project setup; bare first-run onboarding launches ConnectRN
3
+ // during this flow and leaves the repository choice until the end.
4
+
5
+ import os from 'node:os'
6
+ import path from 'node:path'
7
+ import { DEFAULT_SOOTSIM_BRIDGE_PORT } from '../../src/bridge-constants'
8
+ import { getCliVersion } from '../../src/cli-version'
9
+ import {
10
+ readActiveRuntime,
11
+ readPrivacyPreferences,
12
+ readSimulatorDriverPreference,
13
+ runtimeDir,
14
+ type SimulatorDriverPreference,
15
+ writeOnboardingPreferences,
16
+ writePrivacyPreferences,
17
+ } from '../../src/home-paths'
18
+ import { rnxRuntime } from '../../src/runtime-delivery'
19
+ import { findDesktopCompanion } from '../desktop-companion'
20
+ import { confirm, select } from '../prompt'
21
+ import {
22
+ applyRepositorySetup,
23
+ findRepositoryPackageRoot,
24
+ formatRepositoryOperations,
25
+ planRepositorySetup,
26
+ type RepositorySetupPlan,
27
+ } from '../setup-repository'
28
+ import { trackCliError } from '../telemetry'
29
+ import {
30
+ accent,
31
+ bold,
32
+ muted,
33
+ printBrand,
34
+ printPromptBlock,
35
+ success,
36
+ withSpinner,
37
+ } from '../ui'
38
+ import { runOpenCommand } from './control'
39
+ import { daemonInstall, getDaemonServiceStatus } from './daemon'
40
+ import { isAgentEnv } from './inspect/env'
41
+ import { runInstallDesktop } from './install-desktop'
42
+
43
+ interface SetupOptions {
44
+ appDir: string
45
+ ci: boolean
46
+ dryRun: boolean
47
+ json: boolean
48
+ yes: boolean
49
+ service: boolean | undefined
50
+ repository: boolean | undefined
51
+ productAnalytics: boolean | undefined
52
+ simulatorDriver: SimulatorDriverPreference | undefined
53
+ }
54
+
55
+ interface SetupRunOptions {
56
+ launchConnectRN?: boolean
57
+ }
58
+
59
+ export interface SetupResult {
60
+ runtimeVersion: string
61
+ serviceInstalled: boolean
62
+ repositoryPrepared: boolean
63
+ simulatorDriver: SimulatorDriverPreference
64
+ }
65
+
66
+ export async function runSetup(
67
+ args: string[],
68
+ runOptions: SetupRunOptions = {},
69
+ ): Promise<SetupResult | undefined> {
70
+ if (args.includes('--help') || args.includes('-h')) {
71
+ printHelp()
72
+ return
73
+ }
74
+
75
+ const options = parseSetupOptions(args)
76
+ const launchConnectRN =
77
+ runOptions.launchConnectRN ??
78
+ (!options.ci &&
79
+ !options.yes &&
80
+ !options.json &&
81
+ !options.dryRun &&
82
+ process.stdin.isTTY)
83
+ try {
84
+ printIntro(options)
85
+
86
+ const packageRoot = findRepositoryPackageRoot(options.appDir)
87
+ const serviceStatus = getDaemonServiceStatus()
88
+ const privacy = readPrivacyPreferences()
89
+ const service = await decideService(options, serviceStatus)
90
+ if (options.dryRun && service && !serviceStatus.installed && !options.json) {
91
+ console.log(` ${muted('Would enable')} ${accent('the background service')}`)
92
+ }
93
+ if (!service && !serviceStatus.installed && !options.json) {
94
+ console.log(` ${muted('Enable it later with')} ${accent('rnx daemon install')}`)
95
+ }
96
+ const simulatorDriver = await decideSimulatorDriver(options)
97
+ if (options.dryRun && !options.json) {
98
+ console.log(
99
+ ` ${muted('Would use')} ${accent(simulatorDriver === 'playwright' ? 'Browser' : 'Desktop')}`,
100
+ )
101
+ }
102
+
103
+ const manifest = options.json
104
+ ? await rnxRuntime.fetchManifest()
105
+ : await withSpinner('Checking the rnx runtime', () => rnxRuntime.fetchManifest())
106
+ const runtimeVersion = rnxRuntime.resolveVersion(manifest, {
107
+ channel: 'stable',
108
+ }).version
109
+ const cliVersion = getCliVersion()
110
+ if (cliVersion === '0.0.0') {
111
+ throw new Error('could not resolve the installed rnx CLI version')
112
+ }
113
+
114
+ let runtimeInstalled = false
115
+ if (!options.dryRun) {
116
+ const activeRuntime = readActiveRuntime()
117
+ const install = () =>
118
+ rnxRuntime.install({
119
+ version: runtimeVersion,
120
+ channel: 'stable',
121
+ setActive: true,
122
+ protectVersions: activeRuntime ? [activeRuntime] : [],
123
+ })
124
+ const result = options.json
125
+ ? await install()
126
+ : await withSpinner(`Preparing runtime ${runtimeVersion}`, install)
127
+ runtimeInstalled = result.installed
128
+ }
129
+
130
+ let serviceInstalled = serviceStatus.installed
131
+ if (service && !serviceStatus.installed && !options.dryRun) {
132
+ if (options.json) {
133
+ await daemonInstall({ port: DEFAULT_SOOTSIM_BRIDGE_PORT, force: false })
134
+ } else {
135
+ console.log(`\n ${accent('◆')} ${bold('Enabling the background service')}`)
136
+ await daemonInstall({ port: DEFAULT_SOOTSIM_BRIDGE_PORT, force: false })
137
+ }
138
+ serviceInstalled = true
139
+ }
140
+
141
+ if (simulatorDriver === 'electron' && !options.dryRun && !findDesktopCompanion()) {
142
+ await runInstallDesktop(['--yes'])
143
+ }
144
+
145
+ if (launchConnectRN) {
146
+ console.log(`\n ${accent('◆')} Opening ConnectRN`)
147
+ await runOpenCommand([
148
+ 'ConnectRN',
149
+ '--driver',
150
+ simulatorDriver,
151
+ '--no-describe',
152
+ '--quiet',
153
+ ])
154
+ }
155
+
156
+ const productAnalytics = await decidePrivacy(options, privacy)
157
+ const crashReports = false
158
+
159
+ const repositoryPlan = packageRoot
160
+ ? planRepositorySetup({ appDir: packageRoot, cliVersion })
161
+ : null
162
+ const repository = await decideRepository(options, repositoryPlan)
163
+ if (repository && repositoryPlan) {
164
+ if (!options.json) {
165
+ for (const operation of formatRepositoryOperations(repositoryPlan)) {
166
+ console.log(` ${muted(options.dryRun ? 'Would run' : 'Running')} ${operation}`)
167
+ }
168
+ }
169
+ await applyRepositorySetup(repositoryPlan, {
170
+ dryRun: options.dryRun,
171
+ json: options.json,
172
+ })
173
+ }
174
+
175
+ if (!options.dryRun) {
176
+ writePrivacyPreferences({ productAnalytics, crashReports })
177
+ writeOnboardingPreferences(simulatorDriver)
178
+ }
179
+
180
+ const result: SetupResult = {
181
+ runtimeVersion,
182
+ serviceInstalled,
183
+ repositoryPrepared: repository,
184
+ simulatorDriver,
185
+ }
186
+ if (options.json) {
187
+ console.log(
188
+ JSON.stringify(
189
+ {
190
+ ...result,
191
+ runtimeInstalled,
192
+ dryRun: options.dryRun,
193
+ cache: runtimeCacheMetadata(runtimeVersion),
194
+ repositoryOperations:
195
+ repository && repositoryPlan
196
+ ? formatRepositoryOperations(repositoryPlan)
197
+ : [],
198
+ },
199
+ null,
200
+ 2,
201
+ ),
202
+ )
203
+ } else {
204
+ console.log(
205
+ options.dryRun
206
+ ? `\n ${success('✓')} Dry run complete.`
207
+ : `\n ${success('✓')} ${launchConnectRN ? 'ConnectRN is open. ' : ''}rnx is ready.`,
208
+ )
209
+ }
210
+
211
+ return result
212
+ } catch (error) {
213
+ trackCliError(error)
214
+ throw error
215
+ }
216
+ }
217
+
218
+ function parseSetupOptions(args: string[]): SetupOptions {
219
+ const appIndex = args.indexOf('--app')
220
+ const appArg = appIndex >= 0 ? args[appIndex + 1] : undefined
221
+ if (appIndex >= 0 && (!appArg || appArg.startsWith('-'))) {
222
+ throw new Error('--app requires a directory')
223
+ }
224
+ return {
225
+ appDir: path.resolve(appArg ?? process.cwd()),
226
+ ci: args.includes('--ci') || isAgentEnv(),
227
+ dryRun: args.includes('--dry-run'),
228
+ json: args.includes('--json'),
229
+ yes: args.includes('--yes') || args.includes('-y'),
230
+ service: readBooleanFlag(args, '--service', '--no-service'),
231
+ repository: readBooleanFlag(args, '--repo', '--no-repo'),
232
+ productAnalytics: readOnOffFlag(args, '--analytics'),
233
+ simulatorDriver: readSimulatorDriverFlag(args),
234
+ }
235
+ }
236
+
237
+ function readSimulatorDriverFlag(args: string[]): SimulatorDriverPreference | undefined {
238
+ const desktop = args.includes('--desktop')
239
+ const browser = args.includes('--browser')
240
+ if (desktop && browser) {
241
+ throw new Error('--desktop and --browser cannot be used together')
242
+ }
243
+ if (desktop) return 'electron'
244
+ if (browser) return 'playwright'
245
+ return undefined
246
+ }
247
+
248
+ function readBooleanFlag(
249
+ args: string[],
250
+ positive: string,
251
+ negative: string,
252
+ ): boolean | undefined {
253
+ if (args.includes(positive) && args.includes(negative)) {
254
+ throw new Error(`${positive} and ${negative} cannot be used together`)
255
+ }
256
+ if (args.includes(positive)) return true
257
+ if (args.includes(negative)) return false
258
+ return undefined
259
+ }
260
+
261
+ function readOnOffFlag(args: string[], name: string): boolean | undefined {
262
+ const equals = args.find((arg) => arg.startsWith(`${name}=`))
263
+ const index = args.indexOf(name)
264
+ const raw = equals?.slice(name.length + 1) ?? (index >= 0 ? args[index + 1] : undefined)
265
+ if (raw === undefined) return undefined
266
+ if (raw === 'on' || raw === 'yes' || raw === 'true') return true
267
+ if (raw === 'off' || raw === 'no' || raw === 'false') return false
268
+ throw new Error(`${name} must be on or off`)
269
+ }
270
+
271
+ async function decideService(
272
+ options: SetupOptions,
273
+ status: ReturnType<typeof getDaemonServiceStatus>,
274
+ ): Promise<boolean> {
275
+ if (options.ci) {
276
+ if (options.service === true) throw new Error('--ci cannot install a machine service')
277
+ return false
278
+ }
279
+ if (!status.supported) {
280
+ if (!options.json) {
281
+ printPromptBlock('Background service', [
282
+ 'The persistent service is currently available on macOS and Linux.',
283
+ 'rnx will start the bridge when needed on this platform.',
284
+ ])
285
+ }
286
+ return false
287
+ }
288
+ if (status.installed && options.service === undefined) {
289
+ if (!options.json) console.log(` ${success('✓')} Background service is ready`)
290
+ return true
291
+ }
292
+ if (options.service !== undefined) return options.service
293
+ if (options.yes || options.dryRun) return true
294
+ requireInteractive(options, '--service or --no-service')
295
+ printPromptBlock('Background service', [
296
+ 'Keeps the local bridge and runtime ready between commands.',
297
+ 'Agent inspection, interaction, and test commands start much faster.',
298
+ ])
299
+ return confirm('Enable the background service?', true)
300
+ }
301
+
302
+ async function decideSimulatorDriver(
303
+ options: SetupOptions,
304
+ ): Promise<SimulatorDriverPreference> {
305
+ if (options.simulatorDriver) return options.simulatorDriver
306
+ const current = readSimulatorDriverPreference()
307
+ if (current) return current
308
+ if (options.ci || options.yes || options.dryRun) return 'playwright'
309
+ requireInteractive(options, '--desktop or --browser')
310
+ const desktopDescription =
311
+ process.platform === 'darwin'
312
+ ? 'Desktop downloads a DMG and installs rnx.app.'
313
+ : process.platform === 'win32'
314
+ ? 'Desktop downloads and runs the rnx installer.'
315
+ : 'Desktop downloads the matching rnx AppImage.'
316
+ const desktopChoice =
317
+ process.platform === 'darwin'
318
+ ? 'Desktop app Installs rnx.app from a DMG'
319
+ : process.platform === 'win32'
320
+ ? 'Desktop app Runs the rnx installer'
321
+ : 'Desktop app Installs the matching AppImage'
322
+ printPromptBlock('Simulator window', [
323
+ 'Browser opens immediately in an isolated Chrome window.',
324
+ desktopDescription,
325
+ 'The Desktop app opens in a dedicated native window.',
326
+ ])
327
+ const choice = await select('Where should rnx open?', [
328
+ 'Browser (recommended) No additional download',
329
+ desktopChoice,
330
+ ])
331
+ return choice === 0 ? 'playwright' : 'electron'
332
+ }
333
+
334
+ async function decidePrivacy(
335
+ options: SetupOptions,
336
+ current: ReturnType<typeof readPrivacyPreferences>,
337
+ ): Promise<boolean> {
338
+ if (options.productAnalytics !== undefined) return options.productAnalytics
339
+ if (current.configured) return current.productAnalytics
340
+ if (options.yes || options.ci || !process.stdin.isTTY) return false
341
+ printPromptBlock('Private by default', [
342
+ 'Diagnostics stay off unless you opt in.',
343
+ 'Reports can contain package compatibility or an error class.',
344
+ 'They never contain your code or app data.',
345
+ ])
346
+ return confirm('Share bounded diagnostics to improve rnx?', false)
347
+ }
348
+
349
+ async function decideRepository(
350
+ options: SetupOptions,
351
+ plan: RepositorySetupPlan | null,
352
+ ): Promise<boolean> {
353
+ if (!plan) {
354
+ if (options.repository === true) {
355
+ throw new Error('--repo requires a package.json inside a Git repository')
356
+ }
357
+ return false
358
+ }
359
+ if (plan.operations.length === 0) return true
360
+ if (options.repository !== undefined) return options.repository
361
+ if (options.yes || options.ci || !process.stdin.isTTY) return false
362
+ printPromptBlock('Share rnx with your team', [
363
+ 'rnx already works here with no project setup.',
364
+ 'This only adds an exact rnxsim dev dependency and an rnx script for teammates.',
365
+ ])
366
+ return confirm(`Add rnx to ${path.basename(plan.appDir)}?`, false)
367
+ }
368
+
369
+ function requireInteractive(options: SetupOptions, flags: string): void {
370
+ if (process.stdin.isTTY && !options.ci) return
371
+ throw new Error(`non-interactive setup requires ${flags}, or --yes to accept defaults`)
372
+ }
373
+
374
+ function runtimeCacheMetadata(runtimeVersion: string) {
375
+ const platform = os.platform()
376
+ const arch = os.arch()
377
+ return {
378
+ path: runtimeDir(runtimeVersion),
379
+ key: `rnx-runtime-${platform}-${arch}-${runtimeVersion}`,
380
+ inputs: { platform, arch, runtimeVersion },
381
+ }
382
+ }
383
+
384
+ function printIntro(options: SetupOptions): void {
385
+ if (options.json) return
386
+ printBrand('Run any React Native app in a local simulator. No project setup required.')
387
+ }
388
+
389
+ function printHelp(): void {
390
+ console.log(`
391
+ rnx setup - configure optional local conveniences
392
+
393
+ rnx works without project setup. This command can enable the faster background
394
+ service, choose Browser or Desktop, configure private diagnostics, and optionally
395
+ add an exact rnxsim dependency plus an rnx script for teammates.
396
+
397
+ usage:
398
+ rnx setup
399
+ rnx setup --yes
400
+ rnx setup --ci --yes --json
401
+ rnx setup --dry-run
402
+
403
+ choices:
404
+ --service | --no-service
405
+ --desktop | --browser
406
+ --analytics on|off
407
+ --repo | --no-repo
408
+
409
+ automation:
410
+ --yes accept recommended machine defaults; diagnostics and repo changes stay off
411
+ --ci never prompt or install a background service
412
+ --json emit runtime and cache metadata as JSON
413
+ --dry-run show choices and repository operations without changing anything
414
+ --app DIR inspect DIR for an optional repository installation
415
+ `)
416
+ }