rnxsim 0.1.313 → 0.1.315
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.
- package/LICENSE +11 -10
- package/README.md +5 -0
- package/cli/app-config.ts +65 -0
- package/cli/app-fonts.ts +408 -0
- package/cli/app-project.ts +231 -0
- package/cli/app-splash.ts +185 -0
- package/cli/app-state-reset.ts +24 -0
- package/cli/auth.ts +155 -0
- package/cli/bin.ts +594 -0
- package/cli/bridge-diagnostics.ts +226 -0
- package/cli/bridge-flow-runner.ts +2830 -0
- package/cli/browser-evals.ts +96 -0
- package/cli/commands/agent-wrapper.ts +986 -0
- package/cli/commands/agent.ts +423 -0
- package/cli/commands/app-fonts.ts +98 -0
- package/cli/commands/assert.ts +541 -0
- package/cli/commands/auth.ts +59 -0
- package/cli/commands/camera.ts +266 -0
- package/cli/commands/cleanup.ts +169 -0
- package/cli/commands/compat.ts +87 -0
- package/cli/commands/config.ts +32 -0
- package/cli/commands/control.ts +2142 -0
- package/cli/commands/cpu-profile.ts +269 -0
- package/cli/commands/daemon-mac-app.ts +169 -0
- package/cli/commands/daemon.ts +874 -0
- package/cli/commands/debug.ts +719 -0
- package/cli/commands/desktop.ts +39 -0
- package/cli/commands/detect.ts +197 -0
- package/cli/commands/detox.ts +385 -0
- package/cli/commands/device.ts +133 -0
- package/cli/commands/diagnose.ts +589 -0
- package/cli/commands/electron.ts +95 -0
- package/cli/commands/film.ts +379 -0
- package/cli/commands/flow.ts +1124 -0
- package/cli/commands/inspect/actions.ts +622 -0
- package/cli/commands/inspect/core.ts +2405 -0
- package/cli/commands/inspect/count.ts +17 -0
- package/cli/commands/inspect/describe.ts +192 -0
- package/cli/commands/inspect/env.ts +23 -0
- package/cli/commands/inspect/find.ts +171 -0
- package/cli/commands/inspect/get-layout.ts +39 -0
- package/cli/commands/inspect/keyboard.ts +52 -0
- package/cli/commands/inspect/list.ts +58 -0
- package/cli/commands/inspect/memory.ts +215 -0
- package/cli/commands/inspect/redaction.ts +39 -0
- package/cli/commands/inspect/resolve-target.ts +82 -0
- package/cli/commands/inspect/screens.ts +78 -0
- package/cli/commands/inspect/settle.ts +22 -0
- package/cli/commands/inspect/settling.ts +158 -0
- package/cli/commands/inspect/shared.ts +353 -0
- package/cli/commands/inspect/sleep.ts +14 -0
- package/cli/commands/inspect/tree.ts +32 -0
- package/cli/commands/inspect/url.ts +17 -0
- package/cli/commands/inspect/wait-event.ts +210 -0
- package/cli/commands/inspect/wait-idle.ts +24 -0
- package/cli/commands/inspect/wait-ready.ts +74 -0
- package/cli/commands/inspect/wait-selector.ts +54 -0
- package/cli/commands/inspect/wait.ts +31 -0
- package/cli/commands/inspect.ts +4519 -0
- package/cli/commands/install-desktop.ts +351 -0
- package/cli/commands/login.ts +331 -0
- package/cli/commands/logout.ts +31 -0
- package/cli/commands/maestro-generate.ts +361 -0
- package/cli/commands/maestro.ts +453 -0
- package/cli/commands/mode.ts +57 -0
- package/cli/commands/no-bridge-hint.ts +80 -0
- package/cli/commands/perf.ts +66 -0
- package/cli/commands/permissions.ts +203 -0
- package/cli/commands/profile.ts +108 -0
- package/cli/commands/react.ts +353 -0
- package/cli/commands/record.ts +1434 -0
- package/cli/commands/report-issue.ts +305 -0
- package/cli/commands/reset.ts +85 -0
- package/cli/commands/runtime.ts +351 -0
- package/cli/commands/screenshot-command.ts +106 -0
- package/cli/commands/screenshot-layers.ts +143 -0
- package/cli/commands/screenshot-mode.ts +37 -0
- package/cli/commands/screenshot.ts +488 -0
- package/cli/commands/screenshots-capture.ts +607 -0
- package/cli/commands/screenshots.ts +127 -0
- package/cli/commands/serve.ts +168 -0
- package/cli/commands/setup.ts +545 -0
- package/cli/commands/shell-boolean-mode.ts +81 -0
- package/cli/commands/skills.ts +467 -0
- package/cli/commands/slides.ts +361 -0
- package/cli/commands/state.ts +87 -0
- package/cli/commands/storage.ts +58 -0
- package/cli/commands/telemetry.ts +54 -0
- package/cli/commands/three-mode.ts +763 -0
- package/cli/commands/timeline.ts +122 -0
- package/cli/commands/upgrade.ts +208 -0
- package/cli/commands/upload.ts +1225 -0
- package/cli/commands/version.ts +54 -0
- package/cli/commands/what-happened.ts +327 -0
- package/cli/current-sim.ts +204 -0
- package/cli/desktop-companion.ts +300 -0
- package/cli/drivers/electron.ts +70 -0
- package/cli/drivers/index.ts +20 -0
- package/cli/drivers/playwright-provisioning.ts +180 -0
- package/cli/drivers/playwright.ts +698 -0
- package/cli/drivers/registry.ts +65 -0
- package/cli/drivers/types.ts +102 -0
- package/cli/flow-file.ts +142 -0
- package/cli/flow-live-status.ts +120 -0
- package/cli/flow-session.ts +187 -0
- package/cli/help.ts +80 -0
- package/cli/hidden-runtime-alias.ts +19 -0
- package/cli/hints.ts +216 -0
- package/cli/inspect-notice-state.ts +114 -0
- package/cli/maestro-js.ts +334 -0
- package/cli/open-url.ts +8 -0
- package/cli/parent-pid.ts +204 -0
- package/cli/parse-args.ts +211 -0
- package/cli/prompt.ts +51 -0
- package/cli/recording-access.ts +107 -0
- package/cli/registry.ts +1 -0
- package/cli/resolve-assets.ts +63 -0
- package/cli/run-registry.ts +226 -0
- package/cli/runtime-notes.ts +66 -0
- package/cli/runtime-summary.ts +25 -0
- package/cli/setup-repository.ts +187 -0
- package/cli/telemetry.ts +187 -0
- package/cli/ws-bridge.ts +798 -0
- package/dist-cli/bin.js +5 -5
- package/dist-cli/chunks/{agent-XZ2KTPCU.js → agent-CBU2WDKV.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-JJYYW2WH.js → agent-wrapper-UNB4R27L.js} +2 -2
- package/dist-cli/chunks/{app-fonts-IXRNQG6B.js → app-fonts-YMLRF3JD.js} +2 -2
- package/dist-cli/chunks/{assert-54T5SK5F.js → assert-5N6HUCL5.js} +2 -2
- package/dist-cli/chunks/{auth-FI5UDI45.js → auth-BHDR3A6G.js} +2 -2
- package/dist-cli/chunks/{beta-JV6UKADW.js → beta-LWUVCPYK.js} +2 -2
- package/dist-cli/chunks/camera-CNO6B2JZ.js +33 -0
- package/dist-cli/chunks/{chunk-ZMJD5GEC.js → chunk-25VCR44B.js} +1 -1
- package/dist-cli/chunks/{chunk-WUSWBCWA.js → chunk-26CSIR76.js} +8 -9
- package/dist-cli/chunks/{chunk-WINYQ44O.js → chunk-26WMWUH4.js} +1 -1
- package/dist-cli/chunks/{chunk-BTWORNNG.js → chunk-2JZ53D4A.js} +1 -1
- package/dist-cli/chunks/{chunk-DCEMHR2Y.js → chunk-2POODCTW.js} +2 -2
- package/dist-cli/chunks/{chunk-5TEF3ET3.js → chunk-4LTBK3ZN.js} +2 -2
- package/dist-cli/chunks/{chunk-NMF2ZMZQ.js → chunk-54MIRTFE.js} +4 -4
- package/dist-cli/chunks/{chunk-WEXDAC74.js → chunk-56H6EVO4.js} +2 -2
- package/dist-cli/chunks/{chunk-F5ZRSS3C.js → chunk-657SQG4D.js} +1 -1
- package/dist-cli/chunks/{chunk-IJ5CAZZC.js → chunk-6ZMRJPKI.js} +1 -1
- package/dist-cli/chunks/{chunk-W6K4EFPH.js → chunk-AOGGOC4V.js} +2 -2
- package/dist-cli/chunks/chunk-BPCDO7ST.js +15 -0
- package/dist-cli/chunks/{chunk-D4FFVGI5.js → chunk-C5O5CTXJ.js} +1 -1
- package/dist-cli/chunks/{chunk-RTN5C5RL.js → chunk-CMM64PPR.js} +1 -1
- package/dist-cli/chunks/{chunk-BBULZ7CG.js → chunk-D3M36D7O.js} +62 -87
- package/dist-cli/chunks/{chunk-5DHC6KHQ.js → chunk-DNCNFZ7V.js} +1 -1
- package/dist-cli/chunks/{chunk-YIFT42WN.js → chunk-FNTTIR2P.js} +2 -2
- package/dist-cli/chunks/chunk-H362IX2I.js +4 -0
- package/dist-cli/chunks/{chunk-IJO63TDP.js → chunk-J6HMTUZH.js} +2 -2
- package/dist-cli/chunks/{chunk-RSZWCKNT.js → chunk-JZ6VTZ5B.js} +3 -3
- package/dist-cli/chunks/{chunk-MJRLLB4R.js → chunk-KQSJVNFE.js} +4 -4
- package/dist-cli/chunks/chunk-L4JZKVCC.js +6 -0
- package/dist-cli/chunks/{chunk-VFCMSYZK.js → chunk-LP2BTEJ6.js} +2 -2
- package/dist-cli/chunks/{chunk-2YR5BGA5.js → chunk-LT6UC7I3.js} +2 -2
- package/dist-cli/chunks/{chunk-OVFJFXUD.js → chunk-MCKQH4AM.js} +2 -2
- package/dist-cli/chunks/{chunk-TZFFR3SD.js → chunk-MF7WUHQ3.js} +2 -2
- package/dist-cli/chunks/{chunk-46EUUFJ5.js → chunk-MSSY4EEZ.js} +1 -1
- package/dist-cli/chunks/{chunk-WMIIKMGK.js → chunk-N5ZCYP5F.js} +2 -2
- package/dist-cli/chunks/{chunk-3NV2NCNX.js → chunk-NCKAD3V7.js} +2 -2
- package/dist-cli/chunks/{chunk-VNQEB4L7.js → chunk-O2KRHVTU.js} +2 -2
- package/dist-cli/chunks/{chunk-OZSSI4WN.js → chunk-OF2HTH25.js} +2 -2
- package/dist-cli/chunks/{chunk-GGRX24GF.js → chunk-OXS2IWO2.js} +2 -2
- package/dist-cli/chunks/{chunk-WWZIXIRD.js → chunk-PVL7XSPN.js} +1 -1
- package/dist-cli/chunks/{chunk-DZS6WPUI.js → chunk-PVPAP3TV.js} +1 -1
- package/dist-cli/chunks/{chunk-5YJCOWCH.js → chunk-PXNAZYGV.js} +1 -1
- package/dist-cli/chunks/chunk-QR3HTFRB.js +5 -0
- package/dist-cli/chunks/{chunk-5TPRP5QT.js → chunk-QR7MWAWE.js} +1 -1
- package/dist-cli/chunks/chunk-RJIEZ2NY.js +4 -0
- package/dist-cli/chunks/{chunk-UC6U3MML.js → chunk-RVM4EZ4E.js} +2 -2
- package/dist-cli/chunks/{chunk-2D2UPBBR.js → chunk-SZKSFMWS.js} +1 -1
- package/dist-cli/chunks/{chunk-7GN3LVWB.js → chunk-TEL7G234.js} +2 -2
- package/dist-cli/chunks/{chunk-GASE6UBA.js → chunk-U2HNIHBY.js} +1 -1
- package/dist-cli/chunks/chunk-UROXJVY3.js +27 -0
- package/dist-cli/chunks/{chunk-HI5TFJWN.js → chunk-WM32MGTJ.js} +2 -2
- package/dist-cli/chunks/{chunk-VZXWHRUZ.js → chunk-WMZ7NJAO.js} +89 -133
- package/dist-cli/chunks/{chunk-XEVZYVIW.js → chunk-WUXCH6WN.js} +10 -9
- package/dist-cli/chunks/chunk-XOXGC3EH.js +9 -0
- package/dist-cli/chunks/chunk-XSSHENNY.js +4 -0
- package/dist-cli/chunks/{chunk-OHAZNXLK.js → chunk-XZ6O67AR.js} +1 -1
- package/dist-cli/chunks/{chunk-QKDWYITG.js → chunk-Y2MWQKOS.js} +3 -3
- package/dist-cli/chunks/{chunk-YDGQTMQL.js → chunk-YY24QI25.js} +1 -1
- package/dist-cli/chunks/{chunk-WF3T4SVI.js → chunk-ZWX6DDIS.js} +2 -2
- package/dist-cli/chunks/{cleanup-P27PA6JI.js → cleanup-F3NUJ3SL.js} +2 -2
- package/dist-cli/chunks/cli-version-XIW6O3FG.js +4 -0
- package/dist-cli/chunks/{compat-ZD65FED3.js → compat-AX2SEPTH.js} +2 -2
- package/dist-cli/chunks/{config-XMJRNM2A.js → config-4QQO5JVU.js} +2 -2
- package/dist-cli/chunks/{control-KMIQT3QP.js → control-OCKKSOWA.js} +2 -2
- package/dist-cli/chunks/daemon-N5VLMYKQ.js +4 -0
- package/dist-cli/chunks/{debug-PT4HOP7N.js → debug-T6MWVEVX.js} +5 -5
- package/dist-cli/chunks/{desktop-S3FG72AK.js → desktop-NCW7R7CB.js} +3 -3
- package/dist-cli/chunks/{detox-B3D4IFCN.js → detox-G6GITO2X.js} +2 -2
- package/dist-cli/chunks/{device-XBNDSB2R.js → device-7BTEJCTH.js} +2 -2
- package/dist-cli/chunks/{diagnose-HMQXJE5N.js → diagnose-SC5LACE3.js} +2 -2
- package/dist-cli/chunks/{disk-cleanup-BLCZ5BSZ.js → disk-cleanup-KK56QQZA.js} +2 -2
- package/dist-cli/chunks/drivers-NPGVBWMG.js +4 -0
- package/dist-cli/chunks/{film-BJGTBYZB.js → film-WGYEHGIA.js} +3 -3
- package/dist-cli/chunks/flow-KQBIBM3N.js +4 -0
- package/dist-cli/chunks/help-G7CAKI65.js +4 -0
- package/dist-cli/chunks/{hidden-runtime-alias-ANOYADHM.js → hidden-runtime-alias-CWTZWH3L.js} +2 -2
- package/dist-cli/chunks/home-paths-EBT4XHAS.js +4 -0
- package/dist-cli/chunks/inspect-CCPZXVDU.js +4 -0
- package/dist-cli/chunks/install-desktop-NZ3VGTQZ.js +4 -0
- package/dist-cli/chunks/{login-FJ737MWG.js → login-GSYOTGVQ.js} +4 -4
- package/dist-cli/chunks/{logout-ZCNMMHMY.js → logout-JPRIR7YK.js} +2 -2
- package/dist-cli/chunks/{maestro-SZTNKLDF.js → maestro-LXKDNO5M.js} +3 -3
- package/dist-cli/chunks/{maestro-generate-DCFAIZ4H.js → maestro-generate-5ZIKOU53.js} +3 -3
- package/dist-cli/chunks/{mode-GRMQCRXR.js → mode-N3QHXCES.js} +2 -2
- package/dist-cli/chunks/{optional-demo-registry-W36EWFFB.js → optional-demo-registry-XDH6X24N.js} +2 -2
- package/dist-cli/chunks/{perf-QYBAAUZG.js → perf-VOPL26FM.js} +2 -2
- package/dist-cli/chunks/{permissions-3QCQ6VF4.js → permissions-CZFKM4UD.js} +2 -2
- package/dist-cli/chunks/{record-QPPC2S4E.js → record-FODSOGMA.js} +3 -3
- package/dist-cli/chunks/{report-issue-7NMFP4HK.js → report-issue-FBPCZKOV.js} +2 -2
- package/dist-cli/chunks/reset-3H35GFO3.js +4 -0
- package/dist-cli/chunks/runtime-4OVFRXV5.js +4 -0
- package/dist-cli/chunks/{screenshot-command-67AECJFB.js → screenshot-command-7S3FYVO7.js} +7 -7
- package/dist-cli/chunks/{screenshot-layers-ASWBYPJL.js → screenshot-layers-CDYHPNC3.js} +3 -3
- package/dist-cli/chunks/{screenshots-capture-PXA3HFQK.js → screenshots-capture-EBIIY2YU.js} +2 -2
- package/dist-cli/chunks/serve-NELNSCZP.js +44 -0
- package/dist-cli/chunks/{setup-7DWPMRSB.js → setup-DF5ZDDIV.js} +2 -2
- package/dist-cli/chunks/{skills-S3Y22TUA.js → skills-E2QZF6WZ.js} +2 -2
- package/dist-cli/chunks/state-MBZDKZ7U.js +14 -0
- package/dist-cli/chunks/{storage-XUIMJWAJ.js → storage-J5BMNVXM.js} +6 -6
- package/dist-cli/chunks/store-U3DXC7HH.js +4 -0
- package/dist-cli/chunks/telemetry-XIPJVT5E.js +4 -0
- package/dist-cli/chunks/{timeline-TMPLQPSP.js → timeline-MC6N5BHY.js} +2 -2
- package/dist-cli/chunks/{upgrade-7HDSIM7K.js → upgrade-7UTU53DH.js} +2 -2
- package/dist-cli/chunks/upload-2M2KOTYI.js +4 -0
- package/dist-cli/chunks/version-VNACNYQK.js +6 -0
- package/dist-cli/chunks/{web-DG3WBYD3.js → web-MGJ2TUKR.js} +2 -2
- package/dist-cli/chunks/{what-happened-XFVUTZR7.js → what-happened-7DLKH3OD.js} +3 -3
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract.cjs +20 -0
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +67 -28
- package/dist-lib/host/bridge-host.cjs +140 -12
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +2815 -40
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +2549 -2061
- package/dist-lib/sdk.mjs +2543 -2061
- package/dist-lib/skills.cjs +480 -280
- package/dist-lib/vite.cjs +1 -1
- package/package.json +8 -2
- package/src/bridge-constants.ts +3 -4
- package/src/bridge-contract.ts +251 -0
- package/src/connect.ts +83 -0
- package/src/disk-cleanup.ts +30 -0
- package/src/home-paths.ts +81 -38
- package/src/host/bridge-host.ts +134 -6
- package/src/index.ts +27 -1
- package/src/sdk.ts +8 -0
- package/src/sim-client.ts +660 -0
- package/dist-cli/chunks/camera-VL73YIKP.js +0 -22
- package/dist-cli/chunks/chunk-4NPPOV2N.js +0 -5
- package/dist-cli/chunks/chunk-FSUYIVJ6.js +0 -9
- package/dist-cli/chunks/chunk-G2WW6L2C.js +0 -23
- package/dist-cli/chunks/chunk-KTHV3RUS.js +0 -26
- package/dist-cli/chunks/chunk-LF2ZVT7O.js +0 -6
- package/dist-cli/chunks/chunk-NFK7T35W.js +0 -4
- package/dist-cli/chunks/chunk-TIVZIMMW.js +0 -4
- package/dist-cli/chunks/cli-version-WWLPBDQ7.js +0 -4
- package/dist-cli/chunks/daemon-G2ME7NLB.js +0 -4
- package/dist-cli/chunks/drivers-LDECZGP2.js +0 -4
- package/dist-cli/chunks/flow-UEQNVTU7.js +0 -4
- package/dist-cli/chunks/help-T5FYSVGB.js +0 -4
- package/dist-cli/chunks/home-paths-GT3LFNOR.js +0 -4
- package/dist-cli/chunks/inspect-ZA6XF5LD.js +0 -4
- package/dist-cli/chunks/install-desktop-TIMUDHPL.js +0 -4
- package/dist-cli/chunks/runtime-XOAXMSTU.js +0 -4
- package/dist-cli/chunks/serve-BI2NBAXG.js +0 -44
- package/dist-cli/chunks/store-JTHEJLAZ.js +0 -4
- package/dist-cli/chunks/telemetry-ZYJGD2DB.js +0 -4
- package/dist-cli/chunks/upload-GMSZPWM6.js +0 -4
- 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
|
+
}
|