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,607 @@
1
+ // sootsim screenshot appstore capture — headlessly (re)capture an app's App-Store
2
+ // slides as EDITABLE 3-layer refs (captureRef + cleanRef + screenRef, plus
3
+ // threeScene for 3d) and let the shell's existing auto-upload push them to the
4
+ // signed-in user's org deck. NOT a reimplementation of capture/upload: it
5
+ // launches a GPU headless Chrome, loads the org-mode build shell, seeds the
6
+ // `sootsim login` session into the page so the in-browser auto-upload PUTs
7
+ // authenticate as the user, drives the same `SootSim.bridges.screenshotControl`
8
+ // surface the rail UI uses, then waits for the deck/capture PUTs to flush.
9
+ //
10
+ // auth: `sootsim login` (resolveCliAuth → session token). the token is seeded
11
+ // into the shell page's localStorage (`sootsim.session.token`) — the exact key
12
+ // `getSession()` reads, which both the deck PUT (projects-persistence) and the
13
+ // per-ref capture PUT (capture-store) attach as `Authorization: Bearer`.
14
+ //
15
+ // safety: upload is the default, but the command REFUSES to overwrite a
16
+ // non-empty org deck unless `--force` is passed (the deck PUT has snapshot
17
+ // semantics — it replaces the whole deck). `--dry-run` captures and reports the
18
+ // refs without uploading (PUTs are intercepted + aborted in the page).
19
+
20
+ import { execSync } from 'node:child_process'
21
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
22
+ import { tmpdir } from 'node:os'
23
+ import { dirname, isAbsolute, join, resolve } from 'node:path'
24
+ import { chromium, type Frame, type Page } from 'playwright-core'
25
+ import { launchReapedChromeScoped } from '../../../../scripts/lib/reap-browser'
26
+ import { rnxPublicBrand } from '../../src/public-brand'
27
+ import { resolveCliAuth, type CliAuth } from '../auth'
28
+
29
+ const HELP = `
30
+ rnxsim screenshot appstore capture — headlessly capture + upload App Store slides
31
+
32
+ usage:
33
+ rnxsim screenshot appstore capture --org <org> --repo <repo> [options]
34
+
35
+ what it does:
36
+ boots the org-mode build shell in headless GPU Chrome (authed via your
37
+ \`rnxsim login\` session), captures each slide through the live screenshot
38
+ composer so every slide gets editable layers (captureRef + cleanRef +
39
+ screenRef), and lets the shell auto-upload them to your org deck.
40
+
41
+ options:
42
+ --org <org> github org slug (required)
43
+ --repo <repo> repo name (required)
44
+ --branch <branch> build branch to boot (default: main)
45
+ --platform <p> ios | android (default: ios)
46
+ --slides <list> 1-based indices to capture, e.g. 1,3,5 (default: all)
47
+ --dry-run capture + report refs, do NOT upload (PUTs aborted)
48
+ --force allow overwriting a NON-EMPTY existing deck
49
+ --out <dir> also write composed App-Store PNGs locally
50
+ --shell-origin <url> origin serving /build (default: your session origin)
51
+ --api-origin <url> origin for deck/capture APIs (default: session origin)
52
+ --timeout <ms> per-step bridge timeout (default: 45000)
53
+ --json machine-readable summary
54
+
55
+ examples:
56
+ rnxsim screenshot appstore capture --org acme --repo my-app --dry-run
57
+ rnxsim screenshot appstore capture --org acme --repo my-app --branch main --force
58
+ `
59
+
60
+ interface CaptureSlide {
61
+ index: number
62
+ id: string
63
+ title: string
64
+ captureRef: string | null
65
+ cleanRef: string | null
66
+ screenRef: string | null
67
+ captureMode: '2d' | '3d' | null
68
+ }
69
+
70
+ interface ControlState {
71
+ projectName: string
72
+ deviceModel: string
73
+ slides: CaptureSlide[]
74
+ }
75
+
76
+ interface Flags {
77
+ org: string
78
+ repo: string
79
+ branch: string
80
+ platform: string
81
+ slides: number[] | null
82
+ dryRun: boolean
83
+ force: boolean
84
+ out: string | null
85
+ shellOrigin: string | null
86
+ apiOrigin: string | null
87
+ timeoutMs: number
88
+ json: boolean
89
+ }
90
+
91
+ function parseFlags(args: string[]): Flags {
92
+ const get = (name: string): string | undefined => {
93
+ const i = args.indexOf(name)
94
+ return i >= 0 ? args[i + 1] : undefined
95
+ }
96
+ const has = (name: string) => args.includes(name)
97
+ const slidesRaw = get('--slides')
98
+ const slides = slidesRaw
99
+ ? slidesRaw
100
+ .split(',')
101
+ .map((s) => Number(s.trim()))
102
+ .filter((n) => Number.isInteger(n) && n > 0)
103
+ : null
104
+ return {
105
+ org: get('--org') ?? '',
106
+ repo: get('--repo') ?? '',
107
+ branch: get('--branch') ?? 'main',
108
+ platform: get('--platform') ?? 'ios',
109
+ slides: slides && slides.length ? slides : null,
110
+ dryRun: has('--dry-run'),
111
+ force: has('--force'),
112
+ out: get('--out') ?? null,
113
+ shellOrigin: get('--shell-origin') ?? null,
114
+ apiOrigin: get('--api-origin') ?? null,
115
+ timeoutMs: Number(get('--timeout') ?? '45000') || 45000,
116
+ json: has('--json'),
117
+ }
118
+ }
119
+
120
+ // the session token + origin power both the page auth seed and the API calls.
121
+ // only a logged-in session works here (the in-browser PUTs read a bearer); an
122
+ // api-key/github CliAuth has no browser session origin, so we direct those to
123
+ // `sootsim login` rather than silently failing mid-capture.
124
+ function resolveSessionAuth(): { token: string; origin: string } {
125
+ const auth: CliAuth | null = resolveCliAuth()
126
+ if (auth && auth.kind === 'session') return { token: auth.token, origin: auth.origin }
127
+ process.stderr.write(
128
+ '\n rnxsim screenshot appstore capture needs a logged-in session.\n' +
129
+ ' run `rnxsim login` first (the captured slides upload to that account).\n\n',
130
+ )
131
+ process.exit(1)
132
+ }
133
+
134
+ function isLoopback(hostname: string): boolean {
135
+ const h = hostname.replace(/^\[|\]$/g, '').toLowerCase()
136
+ return (
137
+ h === 'localhost' ||
138
+ h.endsWith('.localhost') ||
139
+ h === '::1' ||
140
+ /^127(?:\.\d{1,3}){3}$/.test(h)
141
+ )
142
+ }
143
+
144
+ // the build shell (/build/...) is served by the sootsim PAGES site, a DIFFERENT
145
+ // origin from the contrast API/auth origin. derive it from the api origin:
146
+ // loopback dev pairs the 3000(+offset) app with the 5173(+offset) shell (the
147
+ // inverse of engine origin.ts's pairedLocalAppOrigin); prod's contrast.dev is
148
+ // fronted by sootsim.com for /build. the engine itself resolves where the
149
+ // deck/capture PUTs go from wherever the shell loads, so we only need to land
150
+ // the shell on the right origin and seed the token there.
151
+ function deriveShellOrigin(apiOrigin: string): string {
152
+ const u = new URL(apiOrigin)
153
+ if (isLoopback(u.hostname)) {
154
+ const port = Number(u.port) || 3000
155
+ const offset = port - 3000
156
+ if (offset >= 0 && offset <= 1000) {
157
+ u.port = String(5173 + offset)
158
+ return u.origin
159
+ }
160
+ }
161
+ if (u.hostname === 'contrast.dev' || u.hostname.endsWith('.contrast.dev')) {
162
+ return rnxPublicBrand.origin
163
+ }
164
+ // single-origin deploys serve /build from the same origin as the api.
165
+ return apiOrigin
166
+ }
167
+
168
+ function loadAvg1(): number {
169
+ try {
170
+ return Number(execSync('sysctl -n vm.loadavg').toString().trim().split(/\s+/)[1]) || 0
171
+ } catch {
172
+ return 0
173
+ }
174
+ }
175
+
176
+ // a deck frame's persisted 3d scene (pose + look). present only on 3d slides.
177
+ interface DeckFrame {
178
+ threeScene?: Record<string, unknown> | null
179
+ screenRef?: string | null
180
+ }
181
+
182
+ // fetch the org deck's frames (in slide order) so a re-bake can restore each
183
+ // slide's EXACT authored pose before capturing. without this the 3d bake
184
+ // snapshots whatever pose the async focus-restore happens to have settled on —
185
+ // which races to the default pose and silently flattens a slide (real bug).
186
+ async function fetchDeckFrames(
187
+ apiOrigin: string,
188
+ token: string,
189
+ org: string,
190
+ repo: string,
191
+ ): Promise<DeckFrame[]> {
192
+ const res = await fetch(
193
+ `${apiOrigin}/api/sootsim/screenshot-decks?org=${encodeURIComponent(org)}&repo=${encodeURIComponent(repo)}`,
194
+ { headers: { authorization: `Bearer ${token}` } },
195
+ )
196
+ if (!res.ok) return []
197
+ const body = (await res.json().catch(() => null)) as {
198
+ decks?: Array<{ config?: { frames?: DeckFrame[] } }>
199
+ } | null
200
+ const frames: DeckFrame[] = []
201
+ for (const deck of body?.decks ?? []) {
202
+ if (Array.isArray(deck.config?.frames)) frames.push(...deck.config.frames)
203
+ }
204
+ return frames
205
+ }
206
+
207
+ // the bare saved screen (screenRef) decoded to a data url, so a device-free
208
+ // re-bake textures the posed phone with the slide's own screen.
209
+ async function fetchScreenImage(
210
+ apiOrigin: string,
211
+ token: string,
212
+ org: string,
213
+ ref: string,
214
+ ): Promise<string | null> {
215
+ const res = await fetch(
216
+ `${apiOrigin}/api/sootsim/screenshot-capture?ref=${encodeURIComponent(ref)}&org=${encodeURIComponent(org)}`,
217
+ { headers: { authorization: `Bearer ${token}` } },
218
+ )
219
+ if (!res.ok) return null
220
+ const buf = Buffer.from(await res.arrayBuffer())
221
+ return `data:image/png;base64,${buf.toString('base64')}`
222
+ }
223
+
224
+ // find the frame that hosts the screenshot plugin bridge. the build shell mounts
225
+ // a /sootsim/ iframe and the screenshot plugin (contexts:['host']) publishes
226
+ // screenshotControl on that frame's window — origin-agnostic detection by
227
+ // probing for the bridge rather than matching a url pattern.
228
+ async function waitForShellFrame(page: Page, timeoutMs: number): Promise<Frame> {
229
+ const deadline = Date.now() + timeoutMs
230
+ while (Date.now() < deadline) {
231
+ for (const frame of page.frames()) {
232
+ const ready = await frame
233
+ .evaluate(() => {
234
+ const ss = (
235
+ globalThis as { SootSim?: { bridges?: { screenshotControl?: unknown } } }
236
+ ).SootSim
237
+ return !!ss?.bridges?.screenshotControl
238
+ })
239
+ .catch(() => false)
240
+ if (ready) return frame
241
+ }
242
+ await page.waitForTimeout(250)
243
+ }
244
+ throw new Error(
245
+ 'screenshot bridge never appeared — the build shell did not boot screenshot mode (check --branch/--platform has a ready build, and you are logged in)',
246
+ )
247
+ }
248
+
249
+ async function getState(frame: Frame): Promise<ControlState> {
250
+ return frame.evaluate(async () => {
251
+ const ss = (
252
+ globalThis as {
253
+ SootSim?: { bridges?: { screenshotControl?: { getState: () => unknown } } }
254
+ }
255
+ ).SootSim
256
+ const control = ss?.bridges?.screenshotControl
257
+ if (!control) throw new Error('screenshotControl missing')
258
+ return control.getState() as unknown
259
+ }) as Promise<ControlState>
260
+ }
261
+
262
+ // ssMode=1 in the url makes the screenshot plugin auto-enter the mode at boot;
263
+ // wait for that, and only if it never lands, set it directly through the
264
+ // published settings bridge (the same store-routing the plugin uses — never a
265
+ // raw shell-command event, which is a cross-worker footgun).
266
+ async function ensureScreenshotMode(frame: Frame): Promise<void> {
267
+ await frame.evaluate(async () => {
268
+ const settings = (
269
+ globalThis as {
270
+ SootSim?: {
271
+ bridges?: {
272
+ settings?: {
273
+ get: () => { screenshotMode?: boolean; showFrame?: boolean }
274
+ set: (key: string, value: boolean) => void
275
+ }
276
+ }
277
+ }
278
+ }
279
+ ).SootSim?.bridges?.settings
280
+ const deadline = Date.now() + 3000
281
+ while (Date.now() < deadline) {
282
+ if (settings?.get?.()?.screenshotMode) return
283
+ await new Promise((r) => setTimeout(r, 50))
284
+ }
285
+ if (settings && !settings.get().screenshotMode) {
286
+ settings.set('screenshotMode', true)
287
+ if (!settings.get().showFrame) settings.set('showFrame', true)
288
+ }
289
+ })
290
+ }
291
+
292
+ // capture one slide into editable layers. for a 3d slide (has a persisted
293
+ // scene) we EXPLICITLY restore the scene + pose and texture the saved screen
294
+ // onto the phone BEFORE baking, rather than relying on the composer's async
295
+ // focus-restore having settled — that race silently snapshots the default pose
296
+ // and flattens the slide. for a 2d/fresh slide (no scene) this is a plain
297
+ // capture, unchanged.
298
+ async function captureSlide(
299
+ frame: Frame,
300
+ index: number,
301
+ scene: Record<string, unknown> | null,
302
+ screenDataUrl: string | null,
303
+ ): Promise<void> {
304
+ await frame.evaluate(
305
+ async ([n, sc, screen]) => {
306
+ const ss = (
307
+ globalThis as {
308
+ SootSim?: {
309
+ bridges?: {
310
+ screenshotControl?: {
311
+ capture: (ref: number) => Promise<unknown>
312
+ focusSlide: (ref: number) => unknown
313
+ }
314
+ threeMode?: {
315
+ configure?: (opts: unknown) => unknown
316
+ setScreenImage?: (dataUrl: string | null) => void
317
+ captureStage?: (opts?: unknown) => unknown
318
+ getState?: () => { ready?: boolean } | null
319
+ }
320
+ }
321
+ }
322
+ }
323
+ ).SootSim
324
+ const control = ss?.bridges?.screenshotControl
325
+ if (!control) throw new Error('screenshotControl missing')
326
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
327
+
328
+ if (sc) {
329
+ // focus → composer auto-toggles 3d + mounts the stage (whose bridge
330
+ // methods land a few frames later).
331
+ control.focusSlide(n as number)
332
+ const deadline = Date.now() + 30000
333
+ let three = ss?.bridges?.threeMode
334
+ while (Date.now() < deadline) {
335
+ three = ss?.bridges?.threeMode
336
+ if (three?.setScreenImage && three?.captureStage && three?.getState?.()?.ready)
337
+ break
338
+ await sleep(150)
339
+ }
340
+ await sleep(1000)
341
+ const s = sc as Record<string, unknown>
342
+ // re-apply the EXACT saved scene+pose so the bake's getPose() snapshot
343
+ // reads the authored pose, not a transient.
344
+ three?.configure?.({
345
+ colorway: s.colorway,
346
+ background: s.background,
347
+ ...(s.customGradient ? { customGradient: s.customGradient } : {}),
348
+ setting: s.setting,
349
+ focus: s.focus,
350
+ environment: s.environmentActive
351
+ ? { pins: s.environmentPins ?? {}, effects: s.environmentEffects ?? {} }
352
+ : false,
353
+ pose: {
354
+ phonePosition: s.phonePosition,
355
+ phoneRotation: s.phoneRotation,
356
+ cameraPosition: s.cameraPosition,
357
+ target: s.target,
358
+ },
359
+ })
360
+ await sleep(900)
361
+ if (screen && three?.setScreenImage) {
362
+ three.setScreenImage(screen as string)
363
+ await sleep(1200)
364
+ }
365
+ }
366
+
367
+ await control.capture(n as number)
368
+ },
369
+ [index, scene, screenDataUrl] as const,
370
+ )
371
+ }
372
+
373
+ export async function runScreenshotsCapture(args: string[]): Promise<number> {
374
+ if (args.includes('--help') || args.includes('-h')) {
375
+ console.log(HELP)
376
+ return 0
377
+ }
378
+ const flags = parseFlags(args)
379
+ if (!flags.org || !flags.repo) {
380
+ process.stderr.write(' --org and --repo are required (see --help)\n')
381
+ return 1
382
+ }
383
+
384
+ const { token, origin } = resolveSessionAuth()
385
+ const apiOrigin = (flags.apiOrigin ?? origin).replace(/\/$/, '')
386
+ const shellOrigin = (flags.shellOrigin ?? deriveShellOrigin(apiOrigin)).replace(
387
+ /\/$/,
388
+ '',
389
+ )
390
+
391
+ // fetch the deck once: drives both the clobber guard and the per-slide scene
392
+ // restore (so a 3d re-bake keeps each slide's exact authored pose).
393
+ const deckFrames = await fetchDeckFrames(apiOrigin, token, flags.org, flags.repo)
394
+
395
+ // guard against clobbering a non-empty deck (PUT replaces the whole deck).
396
+ if (!flags.dryRun && !flags.force && deckFrames.length > 0) {
397
+ process.stderr.write(
398
+ ` refusing to overwrite the existing ${flags.org}/${flags.repo} deck ` +
399
+ `(${deckFrames.length} slide(s)). re-run with --force to replace it, or --dry-run to preview.\n`,
400
+ )
401
+ return 1
402
+ }
403
+
404
+ // heavy-process guard: a headless engine + canvaskit boot pegs cores; bail if
405
+ // the box is already saturated rather than piling on.
406
+ const load = loadAvg1()
407
+ const cores = Number(execSync('sysctl -n hw.ncpu').toString().trim()) || 8
408
+ if (load > cores) {
409
+ process.stderr.write(
410
+ ` load average ${load.toFixed(1)} exceeds ${cores} cores — tear down other work first.\n`,
411
+ )
412
+ return 1
413
+ }
414
+
415
+ const url =
416
+ `${shellOrigin}/build/${encodeURIComponent(flags.org)}/${encodeURIComponent(flags.repo)}/` +
417
+ `${encodeURIComponent(flags.branch)}?platform=${encodeURIComponent(flags.platform)}&ssMode=1`
418
+
419
+ const profileDir = mkdtempSync(join(tmpdir(), 'sootsim-capture-'))
420
+ const { browser: ctx, reapNow } = await launchReapedChromeScoped(
421
+ (options) => chromium.launchPersistentContext(profileDir, options),
422
+ {
423
+ headless: true,
424
+ viewport: { width: 393, height: 852 },
425
+ deviceScaleFactor: 3,
426
+ args: ['--hide-scrollbars'],
427
+ },
428
+ )
429
+
430
+ // seed the login session into every frame's localStorage BEFORE any script
431
+ // runs, so the shell boots already authenticated and the auto-upload PUTs
432
+ // carry the bearer. this is the exact key getSession() reads.
433
+ await ctx.addInitScript((t: string) => {
434
+ try {
435
+ localStorage.setItem('sootsim.session.token', t)
436
+ } catch {}
437
+ }, token)
438
+
439
+ // track the auto-upload PUTs so we can PROVE the refs round-tripped (not just
440
+ // that we captured locally). in dry-run, abort them so nothing is written.
441
+ const uploads: Array<{ kind: 'deck' | 'capture'; status: number }> = []
442
+ const page = ctx.pages()[0] ?? (await ctx.newPage())
443
+ if (flags.dryRun) {
444
+ // abort only the WRITES (PUT). the GET that loads the org deck must go
445
+ // through, or the shell falls back to an empty default project and there's
446
+ // nothing to capture.
447
+ await page.route(/\/api\/sootsim\/screenshot-(decks|capture)/, (route) =>
448
+ route.request().method() === 'PUT' ? route.abort() : route.continue(),
449
+ )
450
+ } else {
451
+ page.on('response', (res) => {
452
+ const u = res.url()
453
+ const method = res.request().method()
454
+ if (method !== 'PUT') return
455
+ if (u.includes('/api/sootsim/screenshot-decks'))
456
+ uploads.push({ kind: 'deck', status: res.status() })
457
+ else if (u.includes('/api/sootsim/screenshot-capture'))
458
+ uploads.push({ kind: 'capture', status: res.status() })
459
+ })
460
+ }
461
+
462
+ const pageErrors: string[] = []
463
+ page.on('pageerror', (e) => pageErrors.push(e.message))
464
+
465
+ // the build page forwards ssOrg/ssRepo into the shell, which GETs the team
466
+ // deck on boot. wait for that load so we capture against the real deck, not
467
+ // the empty default project the shell starts with.
468
+ const deckLoaded = page
469
+ .waitForResponse(
470
+ (r) =>
471
+ r.url().includes('/api/sootsim/screenshot-decks') &&
472
+ r.request().method() === 'GET',
473
+ { timeout: flags.timeoutMs },
474
+ )
475
+ .catch(() => null)
476
+
477
+ try {
478
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: flags.timeoutMs })
479
+ const frame = await waitForShellFrame(page, flags.timeoutMs)
480
+ await ensureScreenshotMode(frame)
481
+ // let the org deck GET resolve + the projects store apply it before reading.
482
+ await deckLoaded
483
+ await page.waitForTimeout(800)
484
+
485
+ const before = await getState(frame)
486
+ if (before.slides.length === 0) {
487
+ process.stderr.write(
488
+ ' the deck has no slides to capture — add slides in the composer first.\n',
489
+ )
490
+ return 1
491
+ }
492
+ const targets = flags.slides
493
+ ? before.slides.filter((s) => flags.slides!.includes(s.index))
494
+ : before.slides
495
+
496
+ console.log(
497
+ ` ${before.projectName} on ${before.deviceModel} — capturing ${targets.length}/${before.slides.length} slide(s)${flags.dryRun ? ' (dry-run, no upload)' : ''}`,
498
+ )
499
+
500
+ for (const slide of targets) {
501
+ try {
502
+ // restore the slide's authored 3d scene + its saved bare screen so the
503
+ // re-bake keeps the exact pose and shows the right screen (pose-accurate,
504
+ // device-free). 2d/fresh slides have no scene and capture live.
505
+ const deckFrame = deckFrames[slide.index - 1]
506
+ const scene = deckFrame?.threeScene ?? null
507
+ const screenDataUrl =
508
+ scene && deckFrame?.screenRef
509
+ ? await fetchScreenImage(apiOrigin, token, flags.org, deckFrame.screenRef)
510
+ : null
511
+ await captureSlide(frame, slide.index, scene, screenDataUrl)
512
+ console.log(` ✓ slide ${slide.index} "${slide.title.slice(0, 40)}"`)
513
+ } catch (err) {
514
+ console.log(
515
+ ` ✗ slide ${slide.index} "${slide.title.slice(0, 40)}" — ${(err as Error).message}`,
516
+ )
517
+ }
518
+ }
519
+
520
+ // let the debounced deck (800ms) + capture (400ms) PUTs flush.
521
+ if (!flags.dryRun) await page.waitForTimeout(2500)
522
+
523
+ const after = await getState(frame)
524
+ const captured = after.slides.filter((s) => targets.some((t) => t.id === s.id))
525
+
526
+ // optional local PNG export.
527
+ const written: string[] = []
528
+ if (flags.out) {
529
+ const outDir = isAbsolute(flags.out) ? flags.out : resolve(process.cwd(), flags.out)
530
+ for (const slide of captured) {
531
+ if (!slide.captureRef) continue
532
+ const png = await frame.evaluate(async (n: number) => {
533
+ const ss = (
534
+ globalThis as {
535
+ SootSim?: {
536
+ bridges?: {
537
+ screenshotControl?: {
538
+ exportSlide: (
539
+ ref: number,
540
+ ) => Promise<{ name: string; dataUrl: string }>
541
+ }
542
+ }
543
+ }
544
+ }
545
+ ).SootSim
546
+ const control = ss?.bridges?.screenshotControl
547
+ if (!control) throw new Error('screenshotControl missing')
548
+ return control.exportSlide(n)
549
+ }, slide.index)
550
+ const m = png.dataUrl.match(/^data:[^;]+;base64,(.+)$/)
551
+ if (m) {
552
+ const path = join(outDir, png.name)
553
+ mkdirSync(dirname(path), { recursive: true })
554
+ writeFileSync(path, Buffer.from(m[1], 'base64'))
555
+ written.push(path)
556
+ }
557
+ }
558
+ }
559
+
560
+ const editable = captured.filter((s) => s.captureRef && s.cleanRef)
561
+ const summary = {
562
+ org: flags.org,
563
+ repo: flags.repo,
564
+ branch: flags.branch,
565
+ dryRun: flags.dryRun,
566
+ slides: captured.map((s) => ({
567
+ index: s.index,
568
+ title: s.title,
569
+ captureRef: !!s.captureRef,
570
+ cleanRef: !!s.cleanRef,
571
+ screenRef: !!s.screenRef,
572
+ captureMode: s.captureMode,
573
+ })),
574
+ editableCount: editable.length,
575
+ uploads: flags.dryRun ? 'skipped (dry-run)' : uploads,
576
+ written,
577
+ pageErrors: pageErrors.slice(0, 5),
578
+ }
579
+
580
+ if (flags.json) {
581
+ console.log(JSON.stringify(summary, null, 2))
582
+ } else {
583
+ console.log(
584
+ `\n ${editable.length}/${captured.length} slide(s) now have editable layers (captureRef + cleanRef)`,
585
+ )
586
+ const flat = captured.filter((s) => s.captureRef && !s.cleanRef)
587
+ if (flat.length) {
588
+ console.log(
589
+ ` ⚠ ${flat.length} slide(s) baked a flat capture only (no cleanRef) — typically 3d slides whose scene didn't restore for a re-bake.`,
590
+ )
591
+ }
592
+ if (!flags.dryRun) {
593
+ const ok = uploads.filter((u) => u.status >= 200 && u.status < 300).length
594
+ console.log(` uploads: ${ok}/${uploads.length} PUT(s) succeeded`)
595
+ }
596
+ if (written.length)
597
+ console.log(` wrote ${written.length} local PNG(s) to ${flags.out}`)
598
+ if (pageErrors.length)
599
+ console.log(` ⚠ ${pageErrors.length} page error(s) during capture`)
600
+ }
601
+ return 0
602
+ } finally {
603
+ await ctx.close().catch(() => {})
604
+ reapNow()
605
+ rmSync(profileDir, { recursive: true, force: true })
606
+ }
607
+ }