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.
- 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-7YBDCYMA.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-JJYYW2WH.js → agent-wrapper-2GHFBHCR.js} +2 -2
- package/dist-cli/chunks/{app-fonts-IXRNQG6B.js → app-fonts-RSNVPQSU.js} +2 -2
- package/dist-cli/chunks/{assert-54T5SK5F.js → assert-XCMX3XJX.js} +2 -2
- package/dist-cli/chunks/{auth-FI5UDI45.js → auth-B442HRAX.js} +2 -2
- package/dist-cli/chunks/{beta-JV6UKADW.js → beta-XJ55JK3M.js} +2 -2
- package/dist-cli/chunks/camera-UGYSSLIK.js +33 -0
- package/dist-cli/chunks/{chunk-3NV2NCNX.js → chunk-277AEQZX.js} +2 -2
- package/dist-cli/chunks/{chunk-2YR5BGA5.js → chunk-2JNSK774.js} +2 -2
- package/dist-cli/chunks/{chunk-RSZWCKNT.js → chunk-32WOTSTR.js} +3 -3
- package/dist-cli/chunks/{chunk-WEXDAC74.js → chunk-3S753SNQ.js} +2 -2
- package/dist-cli/chunks/{chunk-IJO63TDP.js → chunk-46ZOLOYA.js} +2 -2
- package/dist-cli/chunks/{chunk-WUSWBCWA.js → chunk-5L7ELDQL.js} +8 -9
- package/dist-cli/chunks/{chunk-WF3T4SVI.js → chunk-7OOPFSQS.js} +2 -2
- package/dist-cli/chunks/{chunk-TZFFR3SD.js → chunk-7SV3RPRW.js} +2 -2
- package/dist-cli/chunks/{chunk-WWZIXIRD.js → chunk-7ZC35MOU.js} +1 -1
- package/dist-cli/chunks/chunk-AMG5E6CC.js +9 -0
- package/dist-cli/chunks/{chunk-WINYQ44O.js → chunk-APWNH3A4.js} +1 -1
- package/dist-cli/chunks/chunk-B57XUKY3.js +4 -0
- package/dist-cli/chunks/{chunk-YDGQTMQL.js → chunk-C2NL26TD.js} +1 -1
- package/dist-cli/chunks/{chunk-NMF2ZMZQ.js → chunk-D2FNUWAB.js} +4 -4
- package/dist-cli/chunks/{chunk-YIFT42WN.js → chunk-E5T4XSJ3.js} +2 -2
- package/dist-cli/chunks/{chunk-46EUUFJ5.js → chunk-EAC34EQS.js} +1 -1
- package/dist-cli/chunks/{chunk-OVFJFXUD.js → chunk-EG32ML36.js} +2 -2
- package/dist-cli/chunks/{chunk-5YJCOWCH.js → chunk-FXUAC6D5.js} +1 -1
- package/dist-cli/chunks/chunk-GQSL4USA.js +6 -0
- package/dist-cli/chunks/{chunk-VFCMSYZK.js → chunk-HAXW27SS.js} +2 -2
- package/dist-cli/chunks/{chunk-2D2UPBBR.js → chunk-IZAHPAN6.js} +1 -1
- package/dist-cli/chunks/{chunk-7GN3LVWB.js → chunk-J62KM5TB.js} +2 -2
- package/dist-cli/chunks/{chunk-BTWORNNG.js → chunk-JEMCD5E4.js} +1 -1
- package/dist-cli/chunks/{chunk-DCEMHR2Y.js → chunk-JZS3Q37N.js} +2 -2
- package/dist-cli/chunks/{chunk-D4FFVGI5.js → chunk-KR4JON7D.js} +1 -1
- package/dist-cli/chunks/chunk-KWCYKQIQ.js +4 -0
- package/dist-cli/chunks/{chunk-VNQEB4L7.js → chunk-MCWPL644.js} +2 -2
- package/dist-cli/chunks/chunk-MJYK3N2I.js +4 -0
- package/dist-cli/chunks/{chunk-5TEF3ET3.js → chunk-O6TRIZNS.js} +2 -2
- package/dist-cli/chunks/{chunk-QKDWYITG.js → chunk-P7XL2E73.js} +3 -3
- package/dist-cli/chunks/{chunk-BBULZ7CG.js → chunk-PFQTUKQ4.js} +62 -87
- package/dist-cli/chunks/{chunk-GGRX24GF.js → chunk-PG5RZCTN.js} +2 -2
- package/dist-cli/chunks/{chunk-W6K4EFPH.js → chunk-QGRI2Z4M.js} +2 -2
- package/dist-cli/chunks/{chunk-ZMJD5GEC.js → chunk-QLXXE7GE.js} +1 -1
- package/dist-cli/chunks/{chunk-VZXWHRUZ.js → chunk-QOJJJWJE.js} +89 -133
- package/dist-cli/chunks/{chunk-OZSSI4WN.js → chunk-RWZY5427.js} +2 -2
- package/dist-cli/chunks/{chunk-UC6U3MML.js → chunk-RZKU2K3J.js} +2 -2
- package/dist-cli/chunks/{chunk-IJ5CAZZC.js → chunk-SBV4IK4H.js} +1 -1
- package/dist-cli/chunks/{chunk-DZS6WPUI.js → chunk-SGMVFFMK.js} +1 -1
- package/dist-cli/chunks/{chunk-OHAZNXLK.js → chunk-TAX4UT2N.js} +1 -1
- package/dist-cli/chunks/{chunk-5DHC6KHQ.js → chunk-TUOFAWXT.js} +1 -1
- package/dist-cli/chunks/{chunk-GASE6UBA.js → chunk-UHZLOHGP.js} +1 -1
- package/dist-cli/chunks/{chunk-F5ZRSS3C.js → chunk-VC7V76U3.js} +1 -1
- package/dist-cli/chunks/{chunk-RTN5C5RL.js → chunk-VQVMLW4U.js} +1 -1
- package/dist-cli/chunks/{chunk-WMIIKMGK.js → chunk-VUKKYPZN.js} +2 -2
- package/dist-cli/chunks/{chunk-5TPRP5QT.js → chunk-WGGRJDRE.js} +1 -1
- package/dist-cli/chunks/chunk-X6H76EKP.js +15 -0
- package/dist-cli/chunks/chunk-XLZ5FNRT.js +27 -0
- package/dist-cli/chunks/{chunk-HI5TFJWN.js → chunk-XULEACM4.js} +2 -2
- package/dist-cli/chunks/{chunk-MJRLLB4R.js → chunk-YFSDM7AX.js} +4 -4
- package/dist-cli/chunks/chunk-YWI3UEVX.js +5 -0
- package/dist-cli/chunks/{chunk-XEVZYVIW.js → chunk-ZBSJO4NB.js} +10 -9
- package/dist-cli/chunks/{cleanup-P27PA6JI.js → cleanup-EYLGCA6Z.js} +2 -2
- package/dist-cli/chunks/cli-version-LL2UGIHE.js +4 -0
- package/dist-cli/chunks/{compat-ZD65FED3.js → compat-TLJYHB4E.js} +2 -2
- package/dist-cli/chunks/{config-XMJRNM2A.js → config-4JWUOEXK.js} +2 -2
- package/dist-cli/chunks/{control-KMIQT3QP.js → control-HPAOYF4N.js} +2 -2
- package/dist-cli/chunks/daemon-OYLASXLE.js +4 -0
- package/dist-cli/chunks/{debug-PT4HOP7N.js → debug-4BKXF6KI.js} +5 -5
- package/dist-cli/chunks/{desktop-S3FG72AK.js → desktop-ZQ6ZD2S6.js} +3 -3
- package/dist-cli/chunks/{detox-B3D4IFCN.js → detox-WNPASSS3.js} +2 -2
- package/dist-cli/chunks/{device-XBNDSB2R.js → device-BXLXVG3V.js} +2 -2
- package/dist-cli/chunks/{diagnose-HMQXJE5N.js → diagnose-3QX7W5RW.js} +2 -2
- package/dist-cli/chunks/{disk-cleanup-BLCZ5BSZ.js → disk-cleanup-KSWKI7WB.js} +2 -2
- package/dist-cli/chunks/drivers-EPIEFF7P.js +4 -0
- package/dist-cli/chunks/{film-BJGTBYZB.js → film-GDMR33VO.js} +3 -3
- package/dist-cli/chunks/flow-NKMPBYCJ.js +4 -0
- package/dist-cli/chunks/help-OGCBHOKA.js +4 -0
- package/dist-cli/chunks/{hidden-runtime-alias-ANOYADHM.js → hidden-runtime-alias-S2GTDX3T.js} +2 -2
- package/dist-cli/chunks/home-paths-QRCDLTTV.js +4 -0
- package/dist-cli/chunks/inspect-FUYMOZPY.js +4 -0
- package/dist-cli/chunks/install-desktop-FR6YKW7Y.js +4 -0
- package/dist-cli/chunks/{login-FJ737MWG.js → login-IJAPUZHI.js} +4 -4
- package/dist-cli/chunks/{logout-ZCNMMHMY.js → logout-QMXDFU2X.js} +2 -2
- package/dist-cli/chunks/{maestro-SZTNKLDF.js → maestro-ZXU3YCVX.js} +3 -3
- package/dist-cli/chunks/{maestro-generate-DCFAIZ4H.js → maestro-generate-PYB5QY7K.js} +3 -3
- package/dist-cli/chunks/{mode-GRMQCRXR.js → mode-WGUCL5FZ.js} +2 -2
- package/dist-cli/chunks/{optional-demo-registry-W36EWFFB.js → optional-demo-registry-WH2O6H36.js} +2 -2
- package/dist-cli/chunks/{perf-QYBAAUZG.js → perf-TOD3UFAH.js} +2 -2
- package/dist-cli/chunks/{permissions-3QCQ6VF4.js → permissions-I5BRJGTB.js} +2 -2
- package/dist-cli/chunks/{record-QPPC2S4E.js → record-ZYL2FYSK.js} +3 -3
- package/dist-cli/chunks/{report-issue-7NMFP4HK.js → report-issue-TAI6DYZO.js} +2 -2
- package/dist-cli/chunks/reset-7YGYKQPQ.js +4 -0
- package/dist-cli/chunks/runtime-B4JO6QRI.js +4 -0
- package/dist-cli/chunks/{screenshot-command-67AECJFB.js → screenshot-command-7ANLODZY.js} +7 -7
- package/dist-cli/chunks/{screenshot-layers-ASWBYPJL.js → screenshot-layers-A7FYXSVU.js} +3 -3
- package/dist-cli/chunks/{screenshots-capture-PXA3HFQK.js → screenshots-capture-WT2ZY6CB.js} +2 -2
- package/dist-cli/chunks/serve-TG5WKA4V.js +44 -0
- package/dist-cli/chunks/{setup-7DWPMRSB.js → setup-PCWLD22V.js} +2 -2
- package/dist-cli/chunks/{skills-S3Y22TUA.js → skills-27ZHWCQS.js} +2 -2
- package/dist-cli/chunks/state-ZQVY46ZO.js +14 -0
- package/dist-cli/chunks/{storage-XUIMJWAJ.js → storage-OYC57C4X.js} +6 -6
- package/dist-cli/chunks/store-32HSPZHI.js +4 -0
- package/dist-cli/chunks/telemetry-PBJR7XHR.js +4 -0
- package/dist-cli/chunks/{timeline-TMPLQPSP.js → timeline-RAJJFQT6.js} +2 -2
- package/dist-cli/chunks/{upgrade-7HDSIM7K.js → upgrade-OEFNZRIP.js} +2 -2
- package/dist-cli/chunks/upload-LQJITLKK.js +4 -0
- package/dist-cli/chunks/version-NIXTY6PL.js +6 -0
- package/dist-cli/chunks/{web-DG3WBYD3.js → web-XBUTBVGR.js} +2 -2
- package/dist-cli/chunks/{what-happened-XFVUTZR7.js → what-happened-YNHRFUDX.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,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
|
+
}
|