rnxsim 0.1.313 → 0.1.314

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 (297) hide show
  1. package/LICENSE +11 -10
  2. package/README.md +5 -0
  3. package/cli/app-config.ts +65 -0
  4. package/cli/app-fonts.ts +408 -0
  5. package/cli/app-project.ts +231 -0
  6. package/cli/app-splash.ts +185 -0
  7. package/cli/app-state-reset.ts +24 -0
  8. package/cli/auth.ts +155 -0
  9. package/cli/bin.ts +594 -0
  10. package/cli/bridge-diagnostics.ts +226 -0
  11. package/cli/bridge-flow-runner.ts +2830 -0
  12. package/cli/browser-evals.ts +96 -0
  13. package/cli/commands/agent-wrapper.ts +986 -0
  14. package/cli/commands/agent.ts +423 -0
  15. package/cli/commands/app-fonts.ts +98 -0
  16. package/cli/commands/assert.ts +541 -0
  17. package/cli/commands/auth.ts +59 -0
  18. package/cli/commands/camera.ts +266 -0
  19. package/cli/commands/cleanup.ts +169 -0
  20. package/cli/commands/compat.ts +87 -0
  21. package/cli/commands/config.ts +32 -0
  22. package/cli/commands/control.ts +2142 -0
  23. package/cli/commands/cpu-profile.ts +269 -0
  24. package/cli/commands/daemon-mac-app.ts +169 -0
  25. package/cli/commands/daemon.ts +874 -0
  26. package/cli/commands/debug.ts +719 -0
  27. package/cli/commands/desktop.ts +39 -0
  28. package/cli/commands/detect.ts +197 -0
  29. package/cli/commands/detox.ts +385 -0
  30. package/cli/commands/device.ts +133 -0
  31. package/cli/commands/diagnose.ts +589 -0
  32. package/cli/commands/electron.ts +95 -0
  33. package/cli/commands/film.ts +379 -0
  34. package/cli/commands/flow.ts +1124 -0
  35. package/cli/commands/inspect/actions.ts +622 -0
  36. package/cli/commands/inspect/core.ts +2405 -0
  37. package/cli/commands/inspect/count.ts +17 -0
  38. package/cli/commands/inspect/describe.ts +192 -0
  39. package/cli/commands/inspect/env.ts +23 -0
  40. package/cli/commands/inspect/find.ts +171 -0
  41. package/cli/commands/inspect/get-layout.ts +39 -0
  42. package/cli/commands/inspect/keyboard.ts +52 -0
  43. package/cli/commands/inspect/list.ts +58 -0
  44. package/cli/commands/inspect/memory.ts +215 -0
  45. package/cli/commands/inspect/redaction.ts +39 -0
  46. package/cli/commands/inspect/resolve-target.ts +82 -0
  47. package/cli/commands/inspect/screens.ts +78 -0
  48. package/cli/commands/inspect/settle.ts +22 -0
  49. package/cli/commands/inspect/settling.ts +158 -0
  50. package/cli/commands/inspect/shared.ts +353 -0
  51. package/cli/commands/inspect/sleep.ts +14 -0
  52. package/cli/commands/inspect/tree.ts +32 -0
  53. package/cli/commands/inspect/url.ts +17 -0
  54. package/cli/commands/inspect/wait-event.ts +210 -0
  55. package/cli/commands/inspect/wait-idle.ts +24 -0
  56. package/cli/commands/inspect/wait-ready.ts +74 -0
  57. package/cli/commands/inspect/wait-selector.ts +54 -0
  58. package/cli/commands/inspect/wait.ts +31 -0
  59. package/cli/commands/inspect.ts +4519 -0
  60. package/cli/commands/install-desktop.ts +351 -0
  61. package/cli/commands/login.ts +331 -0
  62. package/cli/commands/logout.ts +31 -0
  63. package/cli/commands/maestro-generate.ts +361 -0
  64. package/cli/commands/maestro.ts +453 -0
  65. package/cli/commands/mode.ts +57 -0
  66. package/cli/commands/no-bridge-hint.ts +80 -0
  67. package/cli/commands/perf.ts +66 -0
  68. package/cli/commands/permissions.ts +203 -0
  69. package/cli/commands/profile.ts +108 -0
  70. package/cli/commands/react.ts +353 -0
  71. package/cli/commands/record.ts +1434 -0
  72. package/cli/commands/report-issue.ts +305 -0
  73. package/cli/commands/reset.ts +85 -0
  74. package/cli/commands/runtime.ts +351 -0
  75. package/cli/commands/screenshot-command.ts +106 -0
  76. package/cli/commands/screenshot-layers.ts +143 -0
  77. package/cli/commands/screenshot-mode.ts +37 -0
  78. package/cli/commands/screenshot.ts +488 -0
  79. package/cli/commands/screenshots-capture.ts +607 -0
  80. package/cli/commands/screenshots.ts +127 -0
  81. package/cli/commands/serve.ts +168 -0
  82. package/cli/commands/setup.ts +545 -0
  83. package/cli/commands/shell-boolean-mode.ts +81 -0
  84. package/cli/commands/skills.ts +467 -0
  85. package/cli/commands/slides.ts +361 -0
  86. package/cli/commands/state.ts +87 -0
  87. package/cli/commands/storage.ts +58 -0
  88. package/cli/commands/telemetry.ts +54 -0
  89. package/cli/commands/three-mode.ts +763 -0
  90. package/cli/commands/timeline.ts +122 -0
  91. package/cli/commands/upgrade.ts +208 -0
  92. package/cli/commands/upload.ts +1225 -0
  93. package/cli/commands/version.ts +54 -0
  94. package/cli/commands/what-happened.ts +327 -0
  95. package/cli/current-sim.ts +204 -0
  96. package/cli/desktop-companion.ts +300 -0
  97. package/cli/drivers/electron.ts +70 -0
  98. package/cli/drivers/index.ts +20 -0
  99. package/cli/drivers/playwright-provisioning.ts +180 -0
  100. package/cli/drivers/playwright.ts +698 -0
  101. package/cli/drivers/registry.ts +65 -0
  102. package/cli/drivers/types.ts +102 -0
  103. package/cli/flow-file.ts +142 -0
  104. package/cli/flow-live-status.ts +120 -0
  105. package/cli/flow-session.ts +187 -0
  106. package/cli/help.ts +80 -0
  107. package/cli/hidden-runtime-alias.ts +19 -0
  108. package/cli/hints.ts +216 -0
  109. package/cli/inspect-notice-state.ts +114 -0
  110. package/cli/maestro-js.ts +334 -0
  111. package/cli/open-url.ts +8 -0
  112. package/cli/parent-pid.ts +204 -0
  113. package/cli/parse-args.ts +211 -0
  114. package/cli/prompt.ts +51 -0
  115. package/cli/recording-access.ts +107 -0
  116. package/cli/registry.ts +1 -0
  117. package/cli/resolve-assets.ts +63 -0
  118. package/cli/run-registry.ts +226 -0
  119. package/cli/runtime-notes.ts +66 -0
  120. package/cli/runtime-summary.ts +25 -0
  121. package/cli/setup-repository.ts +187 -0
  122. package/cli/telemetry.ts +187 -0
  123. package/cli/ws-bridge.ts +798 -0
  124. package/dist-cli/bin.js +5 -5
  125. package/dist-cli/chunks/{agent-XZ2KTPCU.js → agent-7YBDCYMA.js} +2 -2
  126. package/dist-cli/chunks/{agent-wrapper-JJYYW2WH.js → agent-wrapper-2GHFBHCR.js} +2 -2
  127. package/dist-cli/chunks/{app-fonts-IXRNQG6B.js → app-fonts-RSNVPQSU.js} +2 -2
  128. package/dist-cli/chunks/{assert-54T5SK5F.js → assert-XCMX3XJX.js} +2 -2
  129. package/dist-cli/chunks/{auth-FI5UDI45.js → auth-B442HRAX.js} +2 -2
  130. package/dist-cli/chunks/{beta-JV6UKADW.js → beta-XJ55JK3M.js} +2 -2
  131. package/dist-cli/chunks/camera-UGYSSLIK.js +33 -0
  132. package/dist-cli/chunks/{chunk-3NV2NCNX.js → chunk-277AEQZX.js} +2 -2
  133. package/dist-cli/chunks/{chunk-2YR5BGA5.js → chunk-2JNSK774.js} +2 -2
  134. package/dist-cli/chunks/{chunk-RSZWCKNT.js → chunk-32WOTSTR.js} +3 -3
  135. package/dist-cli/chunks/{chunk-WEXDAC74.js → chunk-3S753SNQ.js} +2 -2
  136. package/dist-cli/chunks/{chunk-IJO63TDP.js → chunk-46ZOLOYA.js} +2 -2
  137. package/dist-cli/chunks/{chunk-WUSWBCWA.js → chunk-5L7ELDQL.js} +8 -9
  138. package/dist-cli/chunks/{chunk-WF3T4SVI.js → chunk-7OOPFSQS.js} +2 -2
  139. package/dist-cli/chunks/{chunk-TZFFR3SD.js → chunk-7SV3RPRW.js} +2 -2
  140. package/dist-cli/chunks/{chunk-WWZIXIRD.js → chunk-7ZC35MOU.js} +1 -1
  141. package/dist-cli/chunks/chunk-AMG5E6CC.js +9 -0
  142. package/dist-cli/chunks/{chunk-WINYQ44O.js → chunk-APWNH3A4.js} +1 -1
  143. package/dist-cli/chunks/chunk-B57XUKY3.js +4 -0
  144. package/dist-cli/chunks/{chunk-YDGQTMQL.js → chunk-C2NL26TD.js} +1 -1
  145. package/dist-cli/chunks/{chunk-NMF2ZMZQ.js → chunk-D2FNUWAB.js} +4 -4
  146. package/dist-cli/chunks/{chunk-YIFT42WN.js → chunk-E5T4XSJ3.js} +2 -2
  147. package/dist-cli/chunks/{chunk-46EUUFJ5.js → chunk-EAC34EQS.js} +1 -1
  148. package/dist-cli/chunks/{chunk-OVFJFXUD.js → chunk-EG32ML36.js} +2 -2
  149. package/dist-cli/chunks/{chunk-5YJCOWCH.js → chunk-FXUAC6D5.js} +1 -1
  150. package/dist-cli/chunks/chunk-GQSL4USA.js +6 -0
  151. package/dist-cli/chunks/{chunk-VFCMSYZK.js → chunk-HAXW27SS.js} +2 -2
  152. package/dist-cli/chunks/{chunk-2D2UPBBR.js → chunk-IZAHPAN6.js} +1 -1
  153. package/dist-cli/chunks/{chunk-7GN3LVWB.js → chunk-J62KM5TB.js} +2 -2
  154. package/dist-cli/chunks/{chunk-BTWORNNG.js → chunk-JEMCD5E4.js} +1 -1
  155. package/dist-cli/chunks/{chunk-DCEMHR2Y.js → chunk-JZS3Q37N.js} +2 -2
  156. package/dist-cli/chunks/{chunk-D4FFVGI5.js → chunk-KR4JON7D.js} +1 -1
  157. package/dist-cli/chunks/chunk-KWCYKQIQ.js +4 -0
  158. package/dist-cli/chunks/{chunk-VNQEB4L7.js → chunk-MCWPL644.js} +2 -2
  159. package/dist-cli/chunks/chunk-MJYK3N2I.js +4 -0
  160. package/dist-cli/chunks/{chunk-5TEF3ET3.js → chunk-O6TRIZNS.js} +2 -2
  161. package/dist-cli/chunks/{chunk-QKDWYITG.js → chunk-P7XL2E73.js} +3 -3
  162. package/dist-cli/chunks/{chunk-BBULZ7CG.js → chunk-PFQTUKQ4.js} +62 -87
  163. package/dist-cli/chunks/{chunk-GGRX24GF.js → chunk-PG5RZCTN.js} +2 -2
  164. package/dist-cli/chunks/{chunk-W6K4EFPH.js → chunk-QGRI2Z4M.js} +2 -2
  165. package/dist-cli/chunks/{chunk-ZMJD5GEC.js → chunk-QLXXE7GE.js} +1 -1
  166. package/dist-cli/chunks/{chunk-VZXWHRUZ.js → chunk-QOJJJWJE.js} +89 -133
  167. package/dist-cli/chunks/{chunk-OZSSI4WN.js → chunk-RWZY5427.js} +2 -2
  168. package/dist-cli/chunks/{chunk-UC6U3MML.js → chunk-RZKU2K3J.js} +2 -2
  169. package/dist-cli/chunks/{chunk-IJ5CAZZC.js → chunk-SBV4IK4H.js} +1 -1
  170. package/dist-cli/chunks/{chunk-DZS6WPUI.js → chunk-SGMVFFMK.js} +1 -1
  171. package/dist-cli/chunks/{chunk-OHAZNXLK.js → chunk-TAX4UT2N.js} +1 -1
  172. package/dist-cli/chunks/{chunk-5DHC6KHQ.js → chunk-TUOFAWXT.js} +1 -1
  173. package/dist-cli/chunks/{chunk-GASE6UBA.js → chunk-UHZLOHGP.js} +1 -1
  174. package/dist-cli/chunks/{chunk-F5ZRSS3C.js → chunk-VC7V76U3.js} +1 -1
  175. package/dist-cli/chunks/{chunk-RTN5C5RL.js → chunk-VQVMLW4U.js} +1 -1
  176. package/dist-cli/chunks/{chunk-WMIIKMGK.js → chunk-VUKKYPZN.js} +2 -2
  177. package/dist-cli/chunks/{chunk-5TPRP5QT.js → chunk-WGGRJDRE.js} +1 -1
  178. package/dist-cli/chunks/chunk-X6H76EKP.js +15 -0
  179. package/dist-cli/chunks/chunk-XLZ5FNRT.js +27 -0
  180. package/dist-cli/chunks/{chunk-HI5TFJWN.js → chunk-XULEACM4.js} +2 -2
  181. package/dist-cli/chunks/{chunk-MJRLLB4R.js → chunk-YFSDM7AX.js} +4 -4
  182. package/dist-cli/chunks/chunk-YWI3UEVX.js +5 -0
  183. package/dist-cli/chunks/{chunk-XEVZYVIW.js → chunk-ZBSJO4NB.js} +10 -9
  184. package/dist-cli/chunks/{cleanup-P27PA6JI.js → cleanup-EYLGCA6Z.js} +2 -2
  185. package/dist-cli/chunks/cli-version-LL2UGIHE.js +4 -0
  186. package/dist-cli/chunks/{compat-ZD65FED3.js → compat-TLJYHB4E.js} +2 -2
  187. package/dist-cli/chunks/{config-XMJRNM2A.js → config-4JWUOEXK.js} +2 -2
  188. package/dist-cli/chunks/{control-KMIQT3QP.js → control-HPAOYF4N.js} +2 -2
  189. package/dist-cli/chunks/daemon-OYLASXLE.js +4 -0
  190. package/dist-cli/chunks/{debug-PT4HOP7N.js → debug-4BKXF6KI.js} +5 -5
  191. package/dist-cli/chunks/{desktop-S3FG72AK.js → desktop-ZQ6ZD2S6.js} +3 -3
  192. package/dist-cli/chunks/{detox-B3D4IFCN.js → detox-WNPASSS3.js} +2 -2
  193. package/dist-cli/chunks/{device-XBNDSB2R.js → device-BXLXVG3V.js} +2 -2
  194. package/dist-cli/chunks/{diagnose-HMQXJE5N.js → diagnose-3QX7W5RW.js} +2 -2
  195. package/dist-cli/chunks/{disk-cleanup-BLCZ5BSZ.js → disk-cleanup-KSWKI7WB.js} +2 -2
  196. package/dist-cli/chunks/drivers-EPIEFF7P.js +4 -0
  197. package/dist-cli/chunks/{film-BJGTBYZB.js → film-GDMR33VO.js} +3 -3
  198. package/dist-cli/chunks/flow-NKMPBYCJ.js +4 -0
  199. package/dist-cli/chunks/help-OGCBHOKA.js +4 -0
  200. package/dist-cli/chunks/{hidden-runtime-alias-ANOYADHM.js → hidden-runtime-alias-S2GTDX3T.js} +2 -2
  201. package/dist-cli/chunks/home-paths-QRCDLTTV.js +4 -0
  202. package/dist-cli/chunks/inspect-FUYMOZPY.js +4 -0
  203. package/dist-cli/chunks/install-desktop-FR6YKW7Y.js +4 -0
  204. package/dist-cli/chunks/{login-FJ737MWG.js → login-IJAPUZHI.js} +4 -4
  205. package/dist-cli/chunks/{logout-ZCNMMHMY.js → logout-QMXDFU2X.js} +2 -2
  206. package/dist-cli/chunks/{maestro-SZTNKLDF.js → maestro-ZXU3YCVX.js} +3 -3
  207. package/dist-cli/chunks/{maestro-generate-DCFAIZ4H.js → maestro-generate-PYB5QY7K.js} +3 -3
  208. package/dist-cli/chunks/{mode-GRMQCRXR.js → mode-WGUCL5FZ.js} +2 -2
  209. package/dist-cli/chunks/{optional-demo-registry-W36EWFFB.js → optional-demo-registry-WH2O6H36.js} +2 -2
  210. package/dist-cli/chunks/{perf-QYBAAUZG.js → perf-TOD3UFAH.js} +2 -2
  211. package/dist-cli/chunks/{permissions-3QCQ6VF4.js → permissions-I5BRJGTB.js} +2 -2
  212. package/dist-cli/chunks/{record-QPPC2S4E.js → record-ZYL2FYSK.js} +3 -3
  213. package/dist-cli/chunks/{report-issue-7NMFP4HK.js → report-issue-TAI6DYZO.js} +2 -2
  214. package/dist-cli/chunks/reset-7YGYKQPQ.js +4 -0
  215. package/dist-cli/chunks/runtime-B4JO6QRI.js +4 -0
  216. package/dist-cli/chunks/{screenshot-command-67AECJFB.js → screenshot-command-7ANLODZY.js} +7 -7
  217. package/dist-cli/chunks/{screenshot-layers-ASWBYPJL.js → screenshot-layers-A7FYXSVU.js} +3 -3
  218. package/dist-cli/chunks/{screenshots-capture-PXA3HFQK.js → screenshots-capture-WT2ZY6CB.js} +2 -2
  219. package/dist-cli/chunks/serve-TG5WKA4V.js +44 -0
  220. package/dist-cli/chunks/{setup-7DWPMRSB.js → setup-PCWLD22V.js} +2 -2
  221. package/dist-cli/chunks/{skills-S3Y22TUA.js → skills-27ZHWCQS.js} +2 -2
  222. package/dist-cli/chunks/state-ZQVY46ZO.js +14 -0
  223. package/dist-cli/chunks/{storage-XUIMJWAJ.js → storage-OYC57C4X.js} +6 -6
  224. package/dist-cli/chunks/store-32HSPZHI.js +4 -0
  225. package/dist-cli/chunks/telemetry-PBJR7XHR.js +4 -0
  226. package/dist-cli/chunks/{timeline-TMPLQPSP.js → timeline-RAJJFQT6.js} +2 -2
  227. package/dist-cli/chunks/{upgrade-7HDSIM7K.js → upgrade-OEFNZRIP.js} +2 -2
  228. package/dist-cli/chunks/upload-LQJITLKK.js +4 -0
  229. package/dist-cli/chunks/version-NIXTY6PL.js +6 -0
  230. package/dist-cli/chunks/{web-DG3WBYD3.js → web-XBUTBVGR.js} +2 -2
  231. package/dist-cli/chunks/{what-happened-XFVUTZR7.js → what-happened-YNHRFUDX.js} +3 -3
  232. package/dist-lib/agent-daemon-client.cjs +1 -1
  233. package/dist-lib/agent-events.cjs +1 -1
  234. package/dist-lib/agent-identity.cjs +1 -1
  235. package/dist-lib/agent-sessions.cjs +1 -1
  236. package/dist-lib/attached-projects.cjs +1 -1
  237. package/dist-lib/auth/shared-session.cjs +1 -1
  238. package/dist-lib/backend-origin.cjs +1 -1
  239. package/dist-lib/beta.cjs +1 -1
  240. package/dist-lib/beta.mjs +1 -1
  241. package/dist-lib/bridge-constants.cjs +1 -1
  242. package/dist-lib/bridge-contract.cjs +20 -0
  243. package/dist-lib/cli-constants.cjs +1 -1
  244. package/dist-lib/config.cjs +1 -1
  245. package/dist-lib/detox/index.cjs +1 -1
  246. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  247. package/dist-lib/home-paths.cjs +67 -28
  248. package/dist-lib/host/bridge-host.cjs +140 -12
  249. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  250. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  251. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  252. package/dist-lib/host/websocket-proxy.cjs +1 -1
  253. package/dist-lib/index.cjs +2815 -40
  254. package/dist-lib/jump-to-source-babel.cjs +1 -1
  255. package/dist-lib/menu.cjs +1 -1
  256. package/dist-lib/menu.mjs +1 -1
  257. package/dist-lib/metro.cjs +1 -1
  258. package/dist-lib/profiles.cjs +1 -1
  259. package/dist-lib/public-brand.cjs +1 -1
  260. package/dist-lib/render-mode.cjs +1 -1
  261. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  262. package/dist-lib/sdk.cjs +2549 -2061
  263. package/dist-lib/sdk.mjs +2543 -2061
  264. package/dist-lib/skills.cjs +480 -280
  265. package/dist-lib/vite.cjs +1 -1
  266. package/package.json +8 -2
  267. package/src/bridge-constants.ts +3 -4
  268. package/src/bridge-contract.ts +251 -0
  269. package/src/connect.ts +83 -0
  270. package/src/disk-cleanup.ts +30 -0
  271. package/src/home-paths.ts +81 -38
  272. package/src/host/bridge-host.ts +134 -6
  273. package/src/index.ts +27 -1
  274. package/src/sdk.ts +8 -0
  275. package/src/sim-client.ts +660 -0
  276. package/dist-cli/chunks/camera-VL73YIKP.js +0 -22
  277. package/dist-cli/chunks/chunk-4NPPOV2N.js +0 -5
  278. package/dist-cli/chunks/chunk-FSUYIVJ6.js +0 -9
  279. package/dist-cli/chunks/chunk-G2WW6L2C.js +0 -23
  280. package/dist-cli/chunks/chunk-KTHV3RUS.js +0 -26
  281. package/dist-cli/chunks/chunk-LF2ZVT7O.js +0 -6
  282. package/dist-cli/chunks/chunk-NFK7T35W.js +0 -4
  283. package/dist-cli/chunks/chunk-TIVZIMMW.js +0 -4
  284. package/dist-cli/chunks/cli-version-WWLPBDQ7.js +0 -4
  285. package/dist-cli/chunks/daemon-G2ME7NLB.js +0 -4
  286. package/dist-cli/chunks/drivers-LDECZGP2.js +0 -4
  287. package/dist-cli/chunks/flow-UEQNVTU7.js +0 -4
  288. package/dist-cli/chunks/help-T5FYSVGB.js +0 -4
  289. package/dist-cli/chunks/home-paths-GT3LFNOR.js +0 -4
  290. package/dist-cli/chunks/inspect-ZA6XF5LD.js +0 -4
  291. package/dist-cli/chunks/install-desktop-TIMUDHPL.js +0 -4
  292. package/dist-cli/chunks/runtime-XOAXMSTU.js +0 -4
  293. package/dist-cli/chunks/serve-BI2NBAXG.js +0 -44
  294. package/dist-cli/chunks/store-JTHEJLAZ.js +0 -4
  295. package/dist-cli/chunks/telemetry-ZYJGD2DB.js +0 -4
  296. package/dist-cli/chunks/upload-GMSZPWM6.js +0 -4
  297. package/dist-cli/chunks/version-HOCHZ37L.js +0 -6
@@ -0,0 +1,488 @@
1
+ // sootsim 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 {
17
+ callInBridge,
18
+ checkSimHealth,
19
+ createBridge,
20
+ createBridgeFromParsed,
21
+ parseBridgeCliArgs,
22
+ } from '../ws-bridge'
23
+ import { inspectWaitReady, waitReadyReason } from './inspect/core'
24
+
25
+ interface ScreenshotOptions {
26
+ port?: number
27
+ verbose?: boolean
28
+ }
29
+
30
+ export async function runScreenshot(rawArgs: string[], opts: ScreenshotOptions) {
31
+ // resolve common aliases up front so `-o`, `--out`, `--testid`, etc. land
32
+ // in the same code path as their canonical spellings. before this, agents
33
+ // typing `-o foo.png` got "unknown flag" and lost the value.
34
+ const args = normalizeAliasFlags(rawArgs)
35
+ if (args.includes('--help') || args.includes('-h')) {
36
+ console.log(`
37
+ rnxsim screenshot — capture the canvas as a PNG
38
+
39
+ usage:
40
+ rnxsim screenshot [output] [options]
41
+ rnxsim screenshot [options] --output <path>
42
+
43
+ options:
44
+ --output <path> output file path (default: /tmp/sootsim-inspect.png)
45
+ first positional arg works too: \`screenshot /tmp/x.png\`
46
+ --with-frame wrap the captured screen in a tight device frame
47
+ --no-frame capture the raw screen bitmap (default)
48
+ --no-shell tenant-only capture (no status bar, keyboard, toasts,
49
+ notification center, or other shell overlays). use for
50
+ clean app screenshots when a shell surface is in the way.
51
+ --shell-only capture only the shell chrome, no tenant content
52
+ --allow-loading capture after caller-owned readiness checks even when
53
+ visible app copy matches a generic loading phrase
54
+ --area x,y,w,h crop to a logical rnx rect
55
+ --id <testID> crop to a node's bounding box
56
+ --text <text> crop to a node found by text content
57
+ --gallery crawl and capture multiple screens as HTML gallery
58
+ --themes capture light and dark variants
59
+
60
+ examples:
61
+ rnxsim screenshot
62
+ rnxsim screenshot --no-shell --output app.png
63
+ rnxsim screenshot --with-frame --output framed.png
64
+ rnxsim screenshot --area 0,200,393,400 --output hero.png
65
+ rnxsim screenshot --id loginButton --output button.png
66
+ `)
67
+ process.exit(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 `rnxsim screenshot --help` for the list of supported flags')
80
+ process.exit(1)
81
+ }
82
+
83
+ const gallery = args.includes('--gallery')
84
+ const themes = args.includes('--themes')
85
+ const allowLoading = args.includes('--allow-loading')
86
+ const withFrame = resolveFrameMode(args)
87
+ const layers = resolveLayers(args)
88
+
89
+ if (gallery || themes) {
90
+ if (withFrame) {
91
+ console.error(' --with-frame is not supported with --gallery / --themes')
92
+ process.exit(1)
93
+ }
94
+ // these still go through the playwright-backed path — the bridge can't
95
+ // crawl routes on its own yet.
96
+ return runPlaywrightScreenshot(args, opts)
97
+ }
98
+
99
+ if (
100
+ withFrame &&
101
+ args.some((arg) => arg === '--area' || arg === '--id' || arg === '--text')
102
+ ) {
103
+ console.error(' --with-frame only supports full-screen capture for now')
104
+ console.error(' remove --area / --id / --text, or capture raw and compose later')
105
+ process.exit(1)
106
+ }
107
+
108
+ // share the standard bridge arg parser so --sim / --port / saved sims
109
+ // behave identically to every other bridge command.
110
+ const parsed = parseBridgeCliArgs(args, {
111
+ port: opts.port,
112
+ stripBooleanFlags: [
113
+ '--with-frame',
114
+ '--no-frame',
115
+ '--no-shell',
116
+ '--shell-only',
117
+ '--allow-loading',
118
+ ],
119
+ stripValueFlags: ['--output', '--area', '--id', '--text'],
120
+ })
121
+ const outputArg = resolveOutputPath(args)
122
+ const rect = await parseRect(args)
123
+ const usingBridge = await probeBridge(parsed.wsPort)
124
+
125
+ if (usingBridge) {
126
+ // captureViaBridge throws plain Errors for expected conditions (unknown
127
+ // --id / --text, missing device model). surface them as a one-line
128
+ // failure instead of an uncaught internal stack trace.
129
+ try {
130
+ await captureViaBridge(parsed, outputArg, rect, withFrame, layers, allowLoading)
131
+ } catch (err) {
132
+ console.error(
133
+ ` screenshot failed: ${err instanceof Error ? err.message : String(err)}`,
134
+ )
135
+ process.exit(1)
136
+ }
137
+ return
138
+ }
139
+
140
+ if (withFrame) {
141
+ console.error(' --with-frame requires a running rnx bridge')
142
+ console.error(' open the app in a live sim first, then rerun the command')
143
+ process.exit(1)
144
+ }
145
+
146
+ if (rect) {
147
+ console.error(' --area / --id / --text require a running rnx bridge')
148
+ console.error(
149
+ ' start one with `rnxsim serve`, `rnxsim desktop`, or your project dev server',
150
+ )
151
+ process.exit(1)
152
+ }
153
+
154
+ await runPlaywrightScreenshot(args, opts)
155
+ }
156
+
157
+ async function probeBridge(port: number): Promise<boolean> {
158
+ const bridge = createBridge(port, { commandTimeoutMs: 1000 })
159
+ try {
160
+ await bridge.listSims()
161
+ return true
162
+ } catch {
163
+ return false
164
+ } finally {
165
+ bridge.close()
166
+ }
167
+ }
168
+
169
+ async function parseRect(args: string[]): Promise<SampleRect | null> {
170
+ const areaArg = args.find((_, i) => args[i - 1] === '--area')
171
+ if (areaArg) {
172
+ const parts = areaArg.split(',').map((p) => Number(p.trim()))
173
+ if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) {
174
+ throw new Error(`--area expects x,y,w,h (got "${areaArg}")`)
175
+ }
176
+ const [x, y, w, h] = parts
177
+ return { x, y, w, h }
178
+ }
179
+ const idArg = args.find((_, i) => args[i - 1] === '--id')
180
+ const textArg = args.find((_, i) => args[i - 1] === '--text')
181
+ if (idArg || textArg) {
182
+ // defer to the bridge-side resolver in captureViaBridge since the node
183
+ // must be looked up at capture time.
184
+ return { __matcher: { id: idArg, text: textArg } } as unknown as SampleRect
185
+ }
186
+ return null
187
+ }
188
+
189
+ async function captureViaBridge(
190
+ parsed: ReturnType<typeof parseBridgeCliArgs>,
191
+ outputArg: string | undefined,
192
+ rect: SampleRect | null,
193
+ withFrame: boolean,
194
+ layers: 'full' | 'tenant' | 'shell' | undefined,
195
+ allowLoading: boolean,
196
+ ) {
197
+ // 45s, not 10s: a first-paint capture on a headless CI runner with software
198
+ // GL (SwiftShader) is bounded but slow. forceRenderAll caps at ~6s (two
199
+ // 3s-bounded passes) and the WebGL→2D drawImage readback + PNG encode of a
200
+ // full ~3.2M-px device frame adds several seconds more under build
201
+ // contention (metro serving a 24MB bundle, asset capture running alongside).
202
+ // every step is bounded/synchronous, so the capture always completes — but
203
+ // the prior 10s ceiling fired mid-capture, so every branch build landed with
204
+ // a null screenshotKey (no org-grid thumbnail). runtime proof: 3pc Contrast
205
+ // build 27864867412 logged `screenshot failed: command timed out after 10s`.
206
+ // a generous ceiling costs nothing on fast hardware — bridge.send resolves
207
+ // the instant the frame is ready (~1.5s), never waiting out the timeout.
208
+ const bridge = createBridgeFromParsed({ ...parsed, commandTimeoutMs: 45000 })
209
+ try {
210
+ // a hidden tab paints to a throttled buffer — the canvas is whatever was
211
+ // last drawn before rAF stopped pumping. unlike interaction warnings, this
212
+ // affects screenshot fidelity directly, so call it out explicitly so the
213
+ // user doesn't accept a stale frame as ground truth.
214
+ const health = await checkSimHealth(bridge)
215
+ if (health.hidden) {
216
+ process.stderr.write(
217
+ ' ⚠ screenshot will capture the LAST PAINTED canvas — likely stale.\n' +
218
+ ' navigation/`shell` commands sent before this screenshot may not\n' +
219
+ ' have repainted yet. focus the tab manually or `rnxsim --sim <id>`\n' +
220
+ ' to target a visible sim before capturing.\n\n',
221
+ )
222
+ }
223
+ // re-assert readiness on THIS bridge connection right before capturing.
224
+ // the readiness gate (`sootsim wait ready`) and this capture run as
225
+ // separate CLI processes; between them the engine can re-enter its
226
+ // boot/connect overlay — a late reload fires `sootsim:externalAppReloadStart`
227
+ // and clears `__sootsimExternalAppReady` — so a capture taken now can grab
228
+ // the "opening app" loading card instead of the rendered app. that race is
229
+ // why branch-build thumbnails landed showing the boot spinner over a faint
230
+ // login screen. gate on the same probe `wait ready` uses, on this very
231
+ // connection, so the capture only ever fires on real painted content; a
232
+ // genuinely-stuck app yields no image (run.sh treats that as no thumbnail,
233
+ // which beats a boot-card thumbnail). already-ready apps clear in <1s.
234
+ if (!allowLoading) {
235
+ const readyMaxMs = Number(process.env.CONTRAST_SCREENSHOT_READY_MAX_MS) || 90_000
236
+ const readiness = await inspectWaitReady(bridge, readyMaxMs)
237
+ // "don't baseline this" is not "don't capture this". a boot/loading card
238
+ // is transient and capturing it produces a thumbnail that lies. a crashed
239
+ // app is the opposite: the red box IS the evidence a boot-failure report
240
+ // needs, and it is the only screen that app will ever paint. capture it,
241
+ // and say plainly what it is so nobody blesses it as a baseline.
242
+ if (!readiness.ready && readiness.externalError) {
243
+ process.stderr.write(
244
+ ` ⚠ capturing an ERROR screen. the app failed to load: ${readiness.externalError}\n` +
245
+ ' this is failure evidence, not app content. never use it as a baseline.\n\n',
246
+ )
247
+ } else if (!readiness.ready) {
248
+ throw new Error(
249
+ `app not ready to capture after ${Math.round(readiness.elapsedMs / 1000)}s ` +
250
+ `(${waitReadyReason(readiness)})` +
251
+ '; refusing to capture the boot/loading card. retry once the app paints.',
252
+ )
253
+ }
254
+ }
255
+
256
+ let resolvedRect: SampleRect | null = rect
257
+ const matcher = (rect as { __matcher?: { id?: string; text?: string } })?.__matcher
258
+ if (matcher) {
259
+ const node = await bridge.send({
260
+ type: 'evaluate',
261
+ code: buildResolveRectEval(matcher),
262
+ })
263
+ if (!node) {
264
+ throw new Error(
265
+ matcher.id
266
+ ? `no node with id "${matcher.id}"`
267
+ : `no node matching text "${matcher.text}"`,
268
+ )
269
+ }
270
+ resolvedRect = node as SampleRect
271
+ }
272
+
273
+ const cropRect = resolvedRect
274
+ ? { x: resolvedRect.x, y: resolvedRect.y, w: resolvedRect.w, h: resolvedRect.h }
275
+ : undefined
276
+ const request: {
277
+ type: 'screenshot'
278
+ layers?: typeof layers
279
+ crop?: typeof cropRect
280
+ } = {
281
+ type: 'screenshot',
282
+ }
283
+ if (layers) request.layers = layers
284
+ if (cropRect) request.crop = cropRect
285
+ const dataUrl: string = await bridge.send(request)
286
+ const pngPrefix = 'data:image/png;base64,'
287
+ // never write a 0-byte "saved" file from an empty or malformed payload —
288
+ // the sim may be alive but not painting (check `sootsim get errors`)
289
+ if (typeof dataUrl !== 'string' || dataUrl.length <= pngPrefix.length) {
290
+ throw new Error(
291
+ 'screenshot capture returned no image data — the sim is connected but not painting; check `rnxsim get errors`',
292
+ )
293
+ }
294
+ const base64 = dataUrl.replace(/^data:image\/png;base64,/, '')
295
+ if (cropRect) {
296
+ console.log(
297
+ ` area: x=${cropRect.x} y=${cropRect.y} w=${cropRect.w} h=${cropRect.h}`,
298
+ )
299
+ }
300
+ if (layers) console.log(` layers: ${layers}`)
301
+
302
+ const outputPath = resolve(process.cwd(), outputArg || '/tmp/sootsim-inspect.png')
303
+ mkdirSync(dirname(outputPath), { recursive: true })
304
+ const rawBuffer = Buffer.from(base64, 'base64')
305
+ if (withFrame) {
306
+ const model = await readCurrentDeviceModel(bridge)
307
+ if (!model) {
308
+ throw new Error('could not read current device model from the target sim')
309
+ }
310
+ const framed = await composeFramedScreenshot(rawBuffer, model)
311
+ writeFileSync(outputPath, framed)
312
+ console.log(` frame: ${model}`)
313
+ } else {
314
+ writeFileSync(outputPath, rawBuffer)
315
+ }
316
+ console.log(` saved: ${outputPath}`)
317
+ } finally {
318
+ bridge.close()
319
+ }
320
+ }
321
+
322
+ async function readCurrentDeviceModel(
323
+ bridge: ReturnType<typeof createBridgeFromParsed>,
324
+ ): Promise<DeviceModel | null> {
325
+ const settings = (await callInBridge<Record<string, unknown> | null>(
326
+ bridge,
327
+ 'SootSim.bridges.settings.get',
328
+ )) ?? { deviceModel: null }
329
+ const model = typeof settings.deviceModel === 'string' ? settings.deviceModel : null
330
+ if (!model || !(model in devices)) return null
331
+ return model as DeviceModel
332
+ }
333
+
334
+ function resolveFrameMode(args: string[]): boolean {
335
+ let withFrame = false
336
+ for (const arg of args) {
337
+ if (arg === '--with-frame') withFrame = true
338
+ if (arg === '--no-frame') withFrame = false
339
+ }
340
+ return withFrame
341
+ }
342
+
343
+ function resolveLayers(args: string[]): 'full' | 'tenant' | 'shell' | undefined {
344
+ if (args.includes('--shell-only')) return 'shell'
345
+ if (args.includes('--no-shell')) return 'tenant'
346
+ return undefined
347
+ }
348
+
349
+ // known flags for `sootsim screenshot`. booleans take no value, value flags
350
+ // consume the next arg. kept local so this validation can't drift from the
351
+ // flags the command actually reads.
352
+ const SCREENSHOT_BOOLEAN_FLAGS = new Set([
353
+ '--help',
354
+ '-h',
355
+ '--verbose',
356
+ '-v',
357
+ '--with-frame',
358
+ '--no-frame',
359
+ '--no-shell',
360
+ '--shell-only',
361
+ '--allow-loading',
362
+ '--gallery',
363
+ '--themes',
364
+ ])
365
+ const SCREENSHOT_VALUE_FLAGS = new Set([
366
+ '--output',
367
+ '--area',
368
+ '--id',
369
+ '--text',
370
+ '--url',
371
+ '--port',
372
+ '--timeout',
373
+ '--sim',
374
+ ])
375
+ // common typos agents reach for, mapped to the real flag.
376
+ const SCREENSHOT_FLAG_SUGGESTIONS: Record<string, string> = {
377
+ '--rect': '--area',
378
+ '--crop': '--area',
379
+ '--region': '--area',
380
+ '--bounds': '--area',
381
+ '--box': '--area',
382
+ '--xywh': '--area',
383
+ '--out': '--output',
384
+ '-o': '--output',
385
+ '--file': '--output',
386
+ '--path': '--output',
387
+ '--testid': '--id',
388
+ '--test-id': '--id',
389
+ '--test_id': '--id',
390
+ }
391
+
392
+ // preprocess args: normalize common aliases to their canonical flag so the
393
+ // rest of the parser only has to think about one spelling. handled here
394
+ // rather than in findUnknownFlags so the value-consuming code paths see
395
+ // the canonical flag too — otherwise `-o /tmp/x.png` would print a
396
+ // suggestion *and* lose the value.
397
+ function normalizeAliasFlags(args: string[]): string[] {
398
+ const out: string[] = []
399
+ const valueAliases: Record<string, string> = {
400
+ '-o': '--output',
401
+ '--out': '--output',
402
+ '--file': '--output',
403
+ '--path': '--output',
404
+ '--testid': '--id',
405
+ '--test-id': '--id',
406
+ '--test_id': '--id',
407
+ '--rect': '--area',
408
+ '--crop': '--area',
409
+ '--region': '--area',
410
+ '--bounds': '--area',
411
+ '--box': '--area',
412
+ '--xywh': '--area',
413
+ }
414
+ for (const arg of args) {
415
+ const replacement = valueAliases[arg]
416
+ out.push(replacement ?? arg)
417
+ }
418
+ return out
419
+ }
420
+
421
+ function findUnknownFlags(args: string[]): Array<{ flag: string; suggestion?: string }> {
422
+ const unknown: Array<{ flag: string; suggestion?: string }> = []
423
+ for (let i = 0; i < args.length; i++) {
424
+ const arg = args[i]
425
+ if (!arg.startsWith('-')) continue
426
+ if (SCREENSHOT_VALUE_FLAGS.has(arg)) {
427
+ i++
428
+ continue
429
+ }
430
+ if (SCREENSHOT_BOOLEAN_FLAGS.has(arg)) continue
431
+ unknown.push({ flag: arg, suggestion: SCREENSHOT_FLAG_SUGGESTIONS[arg] })
432
+ }
433
+ return unknown
434
+ }
435
+
436
+ // resolve output path: prefer --output <path>, fall back to the first
437
+ // positional arg so `sootsim screenshot /tmp/foo.png` works the way every
438
+ // other screenshot tool does.
439
+ function resolveOutputPath(args: string[]): string | undefined {
440
+ const explicit = args.find((_, i) => args[i - 1] === '--output')
441
+ if (explicit) return explicit
442
+ for (let i = 0; i < args.length; i++) {
443
+ const arg = args[i]
444
+ if (arg.startsWith('-')) {
445
+ if (SCREENSHOT_VALUE_FLAGS.has(arg)) i++
446
+ continue
447
+ }
448
+ return arg
449
+ }
450
+ return undefined
451
+ }
452
+
453
+ // legacy playwright path kept for --gallery / --themes and for no-bridge
454
+ // captures from CI. unchanged except for being extracted into its own fn.
455
+ async function runPlaywrightScreenshot(args: string[], opts: ScreenshotOptions) {
456
+ const output = resolveOutputPath(args) || 'sootsim-screenshot.png'
457
+ const gallery = args.includes('--gallery')
458
+ const url =
459
+ args.find((_, i) => args[i - 1] === '--url') ||
460
+ `http://localhost:${opts.port || 5173}`
461
+
462
+ const { chromium } = await import('playwright')
463
+ const browser = await launchReapedChrome((options) => chromium.launch(options), {
464
+ headless: true,
465
+ })
466
+
467
+ try {
468
+ if (gallery) {
469
+ console.log(' capturing gallery...')
470
+ console.log(' (gallery mode not yet implemented)')
471
+ } else {
472
+ const page = await browser.newPage({ viewport: { width: 500, height: 900 } })
473
+ await page.goto(url, { waitUntil: 'networkidle' })
474
+ await page.waitForTimeout(2000)
475
+
476
+ const outputPath = resolve(process.cwd(), output)
477
+ mkdirSync(dirname(outputPath), { recursive: true })
478
+ await page.screenshot({ path: outputPath })
479
+ console.log(` saved: ${outputPath}`)
480
+ await page.close()
481
+ }
482
+ } catch (err: any) {
483
+ console.error(` screenshot failed: ${err.message}`)
484
+ process.exitCode = 1
485
+ } finally {
486
+ await browser.close()
487
+ }
488
+ }