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,492 @@
1
+ // rnx screenshot — capture the canvas as a PNG
2
+ //
3
+ // prefers the WS bridge when one is reachable so the screenshot comes from
4
+ // the user's current sim (and can be cropped to a logical
5
+ // rect via --area / --id / --text). falls back to a fresh headless
6
+ // chromium when nothing is running, so CI / one-shot capture still works
7
+ // without a daemon.
8
+
9
+ import { mkdirSync, writeFileSync } from 'fs'
10
+ import { dirname, resolve } from 'path'
11
+ import { devices, type DeviceModel } from 'sootsim-engine/settings'
12
+ import { launchReapedChrome } from '../../../../scripts/lib/reap-browser'
13
+ import { DEFAULT_SOOTSIM_BRIDGE_PORT } from '../../src/bridge-constants'
14
+ import { composeFramedScreenshot } from '../../src/screenshots/frame-compose'
15
+ import { buildResolveRectEval, type SampleRect } from '../browser-evals'
16
+ import { createCloudBridgeForParsed } from '../cloud-client'
17
+ import { rnxExit } from '../run-rnx'
18
+ import {
19
+ callInBridge,
20
+ checkSimHealth,
21
+ createBridge,
22
+ createBridgeFromParsed,
23
+ parseBridgeCliArgs,
24
+ } from '../ws-bridge'
25
+ import { inspectWaitReady, waitReadyReason } from './inspect/core'
26
+
27
+ interface ScreenshotOptions {
28
+ port?: number
29
+ verbose?: boolean
30
+ }
31
+
32
+ export async function runScreenshot(rawArgs: string[], opts: ScreenshotOptions) {
33
+ // resolve common aliases up front so `-o`, `--out`, `--testid`, etc. land
34
+ // in the same code path as their canonical spellings. before this, agents
35
+ // typing `-o foo.png` got "unknown flag" and lost the value.
36
+ const args = normalizeAliasFlags(rawArgs)
37
+ if (args.includes('--help') || args.includes('-h')) {
38
+ console.log(`
39
+ rnx screenshot — capture the canvas as a PNG
40
+
41
+ usage:
42
+ rnx screenshot [output] [options]
43
+ rnx screenshot [options] --output <path>
44
+
45
+ options:
46
+ --output <path> output file path (default: /tmp/rnx-inspect.png)
47
+ first positional arg works too: \`screenshot /tmp/x.png\`
48
+ --with-frame wrap the captured screen in a tight device frame
49
+ --no-frame capture the raw screen bitmap (default)
50
+ --no-shell tenant-only capture (no status bar, keyboard, toasts,
51
+ notification center, or other shell overlays). use for
52
+ clean app screenshots when a shell surface is in the way.
53
+ --shell-only capture only the shell chrome, no tenant content
54
+ --allow-loading capture after caller-owned readiness checks even when
55
+ visible app copy matches a generic loading phrase
56
+ --area x,y,w,h crop to a logical rnx rect
57
+ --id <testID> crop to a node's bounding box
58
+ --text <text> crop to a node found by text content
59
+
60
+ examples:
61
+ rnx screenshot
62
+ rnx screenshot --no-shell --output app.png
63
+ rnx screenshot --with-frame --output framed.png
64
+ rnx screenshot --area 0,200,393,400 --output hero.png
65
+ rnx screenshot --id loginButton --output button.png
66
+ `)
67
+ rnxExit(0)
68
+ }
69
+
70
+ const unknown = findUnknownFlags(args)
71
+ if (unknown.length > 0) {
72
+ for (const { flag, suggestion } of unknown) {
73
+ console.error(
74
+ suggestion
75
+ ? ` unknown flag: ${flag} (did you mean ${suggestion}?)`
76
+ : ` unknown flag: ${flag}`,
77
+ )
78
+ }
79
+ console.error(' run `rnx screenshot --help` for the list of supported flags')
80
+ rnxExit(1)
81
+ }
82
+
83
+ const allowLoading = args.includes('--allow-loading')
84
+ const withFrame = resolveFrameMode(args)
85
+ const layers = resolveLayers(args)
86
+
87
+ if (
88
+ withFrame &&
89
+ args.some((arg) => arg === '--area' || arg === '--id' || arg === '--text')
90
+ ) {
91
+ console.error(' --with-frame only supports full-screen capture for now')
92
+ console.error(' remove --area / --id / --text, or capture raw and compose later')
93
+ rnxExit(1)
94
+ }
95
+
96
+ // share the standard bridge arg parser so --sim / --port / saved sims
97
+ // behave identically to every other bridge command.
98
+ const parsed = parseBridgeCliArgs(args, {
99
+ port: opts.port,
100
+ stripBooleanFlags: [
101
+ '--with-frame',
102
+ '--no-frame',
103
+ '--no-shell',
104
+ '--shell-only',
105
+ '--allow-loading',
106
+ ],
107
+ stripValueFlags: ['--output', '--area', '--id', '--text'],
108
+ })
109
+ const outputArg = resolveOutputPath(args)
110
+ const cloudBridge = createCloudBridgeForParsed(parsed)
111
+ if (cloudBridge) {
112
+ try {
113
+ const dataUrl: unknown = await cloudBridge.send({ type: 'screenshot' })
114
+ const pngPrefix = 'data:image/png;base64,'
115
+ if (typeof dataUrl !== 'string' || !dataUrl.startsWith(pngPrefix)) {
116
+ throw new Error('remote simulator screenshot returned invalid image data')
117
+ }
118
+ const outputPath = resolve(process.cwd(), outputArg || '/tmp/rnx-inspect.png')
119
+ mkdirSync(dirname(outputPath), { recursive: true })
120
+ const png = Buffer.from(dataUrl.slice(pngPrefix.length), 'base64')
121
+ if (png.length === 0)
122
+ throw new Error('remote simulator screenshot returned no image data')
123
+ writeFileSync(outputPath, png)
124
+ console.log(` saved: ${outputPath}`)
125
+ } catch (error) {
126
+ console.error(
127
+ ` screenshot failed: ${error instanceof Error ? error.message : String(error)}`,
128
+ )
129
+ rnxExit(1)
130
+ } finally {
131
+ cloudBridge.close()
132
+ }
133
+ return
134
+ }
135
+ const rect = await parseRect(args)
136
+ const usingBridge = await probeBridge(parsed.wsPort)
137
+
138
+ if (usingBridge) {
139
+ // captureViaBridge throws plain Errors for expected conditions (unknown
140
+ // --id / --text, missing device model). surface them as a one-line
141
+ // failure instead of an uncaught internal stack trace.
142
+ try {
143
+ await captureViaBridge(parsed, outputArg, rect, withFrame, layers, allowLoading)
144
+ } catch (err) {
145
+ console.error(
146
+ ` screenshot failed: ${err instanceof Error ? err.message : String(err)}`,
147
+ )
148
+ rnxExit(1)
149
+ }
150
+ return
151
+ }
152
+
153
+ if (withFrame) {
154
+ console.error(' --with-frame requires a running rnx bridge')
155
+ console.error(' open the app in a live sim first, then rerun the command')
156
+ rnxExit(1)
157
+ }
158
+
159
+ if (rect) {
160
+ console.error(' --area / --id / --text require a running rnx bridge')
161
+ console.error(
162
+ ' start one with `rnx serve`, `rnx desktop`, or your project dev server',
163
+ )
164
+ rnxExit(1)
165
+ }
166
+
167
+ await runPlaywrightScreenshot(args, opts)
168
+ }
169
+
170
+ async function probeBridge(port: number): Promise<boolean> {
171
+ const bridge = createBridge(port, { commandTimeoutMs: 1000 })
172
+ try {
173
+ await bridge.listSims()
174
+ return true
175
+ } catch {
176
+ return false
177
+ } finally {
178
+ bridge.close()
179
+ }
180
+ }
181
+
182
+ async function parseRect(args: string[]): Promise<SampleRect | null> {
183
+ const areaArg = args.find((_, i) => args[i - 1] === '--area')
184
+ if (areaArg) {
185
+ const parts = areaArg.split(',').map((p) => Number(p.trim()))
186
+ if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) {
187
+ throw new Error(`--area expects x,y,w,h (got "${areaArg}")`)
188
+ }
189
+ const [x, y, w, h] = parts
190
+ return { x, y, w, h }
191
+ }
192
+ const idArg = args.find((_, i) => args[i - 1] === '--id')
193
+ const textArg = args.find((_, i) => args[i - 1] === '--text')
194
+ if (idArg || textArg) {
195
+ // defer to the bridge-side resolver in captureViaBridge since the node
196
+ // must be looked up at capture time.
197
+ return { __matcher: { id: idArg, text: textArg } } as unknown as SampleRect
198
+ }
199
+ return null
200
+ }
201
+
202
+ async function captureViaBridge(
203
+ parsed: ReturnType<typeof parseBridgeCliArgs>,
204
+ outputArg: string | undefined,
205
+ rect: SampleRect | null,
206
+ withFrame: boolean,
207
+ layers: 'full' | 'tenant' | 'shell' | undefined,
208
+ allowLoading: boolean,
209
+ ) {
210
+ // 45s, not 10s: a first-paint capture on a headless CI runner with software
211
+ // GL (SwiftShader) is bounded but slow. forceRenderAll caps at ~6s (two
212
+ // 3s-bounded passes) and the WebGL→2D drawImage readback + PNG encode of a
213
+ // full ~3.2M-px device frame adds several seconds more under build
214
+ // contention (metro serving a 24MB bundle, asset capture running alongside).
215
+ // every step is bounded/synchronous, so the capture always completes — but
216
+ // the prior 10s ceiling fired mid-capture, so every branch build landed with
217
+ // a null screenshotKey (no org-grid thumbnail). runtime proof: 3pc Contrast
218
+ // build 27864867412 logged `screenshot failed: command timed out after 10s`.
219
+ // a generous ceiling costs nothing on fast hardware — bridge.send resolves
220
+ // the instant the frame is ready (~1.5s), never waiting out the timeout.
221
+ const bridge = createBridgeFromParsed({ ...parsed, commandTimeoutMs: 45000 })
222
+ try {
223
+ // a hidden tab paints to a throttled buffer — the canvas is whatever was
224
+ // last drawn before rAF stopped pumping. unlike interaction warnings, this
225
+ // affects screenshot fidelity directly, so call it out explicitly so the
226
+ // user doesn't accept a stale frame as ground truth.
227
+ const health = await checkSimHealth(bridge)
228
+ if (health.hidden) {
229
+ process.stderr.write(
230
+ ' ⚠ screenshot will capture the LAST PAINTED canvas — likely stale.\n' +
231
+ ' navigation/`shell` commands sent before this screenshot may not\n' +
232
+ ' have repainted yet. focus the tab manually or `rnx --sim <id>`\n' +
233
+ ' to target a visible sim before capturing.\n\n',
234
+ )
235
+ }
236
+ // re-assert readiness on THIS bridge connection right before capturing.
237
+ // the readiness gate (`rnx wait ready`) and this capture run as
238
+ // separate CLI processes; between them the engine can re-enter its
239
+ // boot/connect overlay — a late reload fires `sootsim:externalAppReloadStart`
240
+ // and clears `__sootsimExternalAppReady` — so a capture taken now can grab
241
+ // the "opening app" loading card instead of the rendered app. that race is
242
+ // why branch-build thumbnails landed showing the boot spinner over a faint
243
+ // login screen. gate on the same probe `wait ready` uses, on this very
244
+ // connection, so the capture only ever fires on real painted content; a
245
+ // genuinely-stuck app yields no image (run.sh treats that as no thumbnail,
246
+ // which beats a boot-card thumbnail). already-ready apps clear in <1s.
247
+ if (!allowLoading) {
248
+ const readyMaxMs = Number(process.env.CONTRAST_SCREENSHOT_READY_MAX_MS) || 90_000
249
+ const readiness = await inspectWaitReady(bridge, readyMaxMs)
250
+ // "don't baseline this" is not "don't capture this". a boot/loading card
251
+ // is transient and capturing it produces a thumbnail that lies. a crashed
252
+ // app is the opposite: the red box IS the evidence a boot-failure report
253
+ // needs, and it is the only screen that app will ever paint. capture it,
254
+ // and say plainly what it is so nobody blesses it as a baseline.
255
+ if (!readiness.ready && readiness.externalError) {
256
+ process.stderr.write(
257
+ ` ⚠ capturing an ERROR screen. the app failed to load: ${readiness.externalError}\n` +
258
+ ' this is failure evidence, not app content. never use it as a baseline.\n\n',
259
+ )
260
+ } else if (!readiness.ready) {
261
+ throw new Error(
262
+ `app not ready to capture after ${Math.round(readiness.elapsedMs / 1000)}s ` +
263
+ `(${waitReadyReason(readiness)})` +
264
+ '; refusing to capture the boot/loading card. retry once the app paints.',
265
+ )
266
+ }
267
+ }
268
+
269
+ let resolvedRect: SampleRect | null = rect
270
+ const matcher = (rect as { __matcher?: { id?: string; text?: string } })?.__matcher
271
+ if (matcher) {
272
+ const node = await bridge.send({
273
+ type: 'evaluate',
274
+ code: buildResolveRectEval(matcher),
275
+ })
276
+ if (!node) {
277
+ throw new Error(
278
+ matcher.id
279
+ ? `no node with id "${matcher.id}"`
280
+ : `no node matching text "${matcher.text}"`,
281
+ )
282
+ }
283
+ resolvedRect = node as SampleRect
284
+ }
285
+
286
+ const cropRect = resolvedRect
287
+ ? { x: resolvedRect.x, y: resolvedRect.y, w: resolvedRect.w, h: resolvedRect.h }
288
+ : undefined
289
+ const request: {
290
+ type: 'screenshot'
291
+ layers?: typeof layers
292
+ crop?: typeof cropRect
293
+ } = {
294
+ type: 'screenshot',
295
+ }
296
+ if (layers) request.layers = layers
297
+ if (cropRect) request.crop = cropRect
298
+ const dataUrl: string = await bridge.send(request)
299
+ const pngPrefix = 'data:image/png;base64,'
300
+ // never write a 0-byte "saved" file from an empty or malformed payload —
301
+ // the sim may be alive but not painting (check `rnx get errors`)
302
+ if (typeof dataUrl !== 'string' || dataUrl.length <= pngPrefix.length) {
303
+ throw new Error(
304
+ 'screenshot capture returned no image data — the sim is connected but not painting; check `rnx get errors`',
305
+ )
306
+ }
307
+ const base64 = dataUrl.replace(/^data:image\/png;base64,/, '')
308
+ if (cropRect) {
309
+ console.log(
310
+ ` area: x=${cropRect.x} y=${cropRect.y} w=${cropRect.w} h=${cropRect.h}`,
311
+ )
312
+ }
313
+ if (layers) console.log(` layers: ${layers}`)
314
+
315
+ const outputPath = resolve(process.cwd(), outputArg || '/tmp/rnx-inspect.png')
316
+ mkdirSync(dirname(outputPath), { recursive: true })
317
+ const rawBuffer = Buffer.from(base64, 'base64')
318
+ if (withFrame) {
319
+ const model = await readCurrentDeviceModel(bridge)
320
+ if (!model) {
321
+ throw new Error('could not read current device model from the target sim')
322
+ }
323
+ const framed = await composeFramedScreenshot(rawBuffer, model)
324
+ writeFileSync(outputPath, framed)
325
+ console.log(` frame: ${model}`)
326
+ } else {
327
+ writeFileSync(outputPath, rawBuffer)
328
+ }
329
+ console.log(` saved: ${outputPath}`)
330
+ } finally {
331
+ bridge.close()
332
+ }
333
+ }
334
+
335
+ async function readCurrentDeviceModel(
336
+ bridge: ReturnType<typeof createBridgeFromParsed>,
337
+ ): Promise<DeviceModel | null> {
338
+ const settings = (await callInBridge<Record<string, unknown> | null>(
339
+ bridge,
340
+ 'SootSim.bridges.settings.get',
341
+ )) ?? { deviceModel: null }
342
+ const model = typeof settings.deviceModel === 'string' ? settings.deviceModel : null
343
+ if (!model || !(model in devices)) return null
344
+ return model as DeviceModel
345
+ }
346
+
347
+ function resolveFrameMode(args: string[]): boolean {
348
+ let withFrame = false
349
+ for (const arg of args) {
350
+ if (arg === '--with-frame') withFrame = true
351
+ if (arg === '--no-frame') withFrame = false
352
+ }
353
+ return withFrame
354
+ }
355
+
356
+ function resolveLayers(args: string[]): 'full' | 'tenant' | 'shell' | undefined {
357
+ if (args.includes('--shell-only')) return 'shell'
358
+ if (args.includes('--no-shell')) return 'tenant'
359
+ return undefined
360
+ }
361
+
362
+ // known flags for `rnx screenshot`. booleans take no value, value flags
363
+ // consume the next arg. kept local so this validation can't drift from the
364
+ // flags the command actually reads.
365
+ const SCREENSHOT_BOOLEAN_FLAGS = new Set([
366
+ '--help',
367
+ '-h',
368
+ '--verbose',
369
+ '-v',
370
+ '--with-frame',
371
+ '--no-frame',
372
+ '--no-shell',
373
+ '--shell-only',
374
+ '--allow-loading',
375
+ ])
376
+ const SCREENSHOT_VALUE_FLAGS = new Set([
377
+ '--output',
378
+ '--area',
379
+ '--id',
380
+ '--text',
381
+ '--url',
382
+ '--port',
383
+ '--timeout',
384
+ '--sim',
385
+ ])
386
+ // common typos agents reach for, mapped to the real flag.
387
+ const SCREENSHOT_FLAG_SUGGESTIONS: Record<string, string> = {
388
+ '--rect': '--area',
389
+ '--crop': '--area',
390
+ '--region': '--area',
391
+ '--bounds': '--area',
392
+ '--box': '--area',
393
+ '--xywh': '--area',
394
+ '--out': '--output',
395
+ '-o': '--output',
396
+ '--file': '--output',
397
+ '--path': '--output',
398
+ '--testid': '--id',
399
+ '--test-id': '--id',
400
+ '--test_id': '--id',
401
+ }
402
+
403
+ // preprocess args: normalize common aliases to their canonical flag so the
404
+ // rest of the parser only has to think about one spelling. handled here
405
+ // rather than in findUnknownFlags so the value-consuming code paths see
406
+ // the canonical flag too — otherwise `-o /tmp/x.png` would print a
407
+ // suggestion *and* lose the value.
408
+ function normalizeAliasFlags(args: string[]): string[] {
409
+ const out: string[] = []
410
+ const valueAliases: Record<string, string> = {
411
+ '-o': '--output',
412
+ '--out': '--output',
413
+ '--file': '--output',
414
+ '--path': '--output',
415
+ '--testid': '--id',
416
+ '--test-id': '--id',
417
+ '--test_id': '--id',
418
+ '--rect': '--area',
419
+ '--crop': '--area',
420
+ '--region': '--area',
421
+ '--bounds': '--area',
422
+ '--box': '--area',
423
+ '--xywh': '--area',
424
+ }
425
+ for (const arg of args) {
426
+ const replacement = valueAliases[arg]
427
+ out.push(replacement ?? arg)
428
+ }
429
+ return out
430
+ }
431
+
432
+ function findUnknownFlags(args: string[]): Array<{ flag: string; suggestion?: string }> {
433
+ const unknown: Array<{ flag: string; suggestion?: string }> = []
434
+ for (let i = 0; i < args.length; i++) {
435
+ const arg = args[i]
436
+ if (!arg.startsWith('-')) continue
437
+ if (SCREENSHOT_VALUE_FLAGS.has(arg)) {
438
+ i++
439
+ continue
440
+ }
441
+ if (SCREENSHOT_BOOLEAN_FLAGS.has(arg)) continue
442
+ unknown.push({ flag: arg, suggestion: SCREENSHOT_FLAG_SUGGESTIONS[arg] })
443
+ }
444
+ return unknown
445
+ }
446
+
447
+ // resolve output path: prefer --output <path>, fall back to the first
448
+ // positional arg so `rnx screenshot /tmp/foo.png` works the way every
449
+ // other screenshot tool does.
450
+ function resolveOutputPath(args: string[]): string | undefined {
451
+ const explicit = args.find((_, i) => args[i - 1] === '--output')
452
+ if (explicit) return explicit
453
+ for (let i = 0; i < args.length; i++) {
454
+ const arg = args[i]
455
+ if (arg.startsWith('-')) {
456
+ if (SCREENSHOT_VALUE_FLAGS.has(arg)) i++
457
+ continue
458
+ }
459
+ return arg
460
+ }
461
+ return undefined
462
+ }
463
+
464
+ // capture a page in headless chromium when no bridge is running.
465
+ async function runPlaywrightScreenshot(args: string[], opts: ScreenshotOptions) {
466
+ const output = resolveOutputPath(args) || 'rnx-screenshot.png'
467
+ const url =
468
+ args.find((_, i) => args[i - 1] === '--url') ||
469
+ `http://localhost:${opts.port || 5173}`
470
+
471
+ const { chromium } = await import('playwright')
472
+ const browser = await launchReapedChrome((options) => chromium.launch(options), {
473
+ headless: true,
474
+ })
475
+
476
+ try {
477
+ const page = await browser.newPage({ viewport: { width: 500, height: 900 } })
478
+ await page.goto(url, { waitUntil: 'networkidle' })
479
+ await page.waitForTimeout(2000)
480
+
481
+ const outputPath = resolve(process.cwd(), output)
482
+ mkdirSync(dirname(outputPath), { recursive: true })
483
+ await page.screenshot({ path: outputPath })
484
+ console.log(` saved: ${outputPath}`)
485
+ await page.close()
486
+ } catch (err: any) {
487
+ console.error(` screenshot failed: ${err.message}`)
488
+ process.exitCode = 1
489
+ } finally {
490
+ await browser.close()
491
+ }
492
+ }