@reshotdev/screenshot 0.0.1-beta.11 → 0.0.1-beta.13
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 +1 -1
- package/README.md +84 -51
- package/package.json +20 -16
- package/src/commands/auth.js +38 -8
- package/src/commands/capture-dom.js +50 -0
- package/src/commands/compose.js +220 -0
- package/src/commands/doctor-target.js +36 -4
- package/src/commands/drifts.js +13 -1
- package/src/commands/publish.js +137 -12
- package/src/commands/pull.js +13 -8
- package/src/commands/refresh.js +166 -0
- package/src/commands/setup-wizard.js +35 -2
- package/src/commands/status.js +22 -2
- package/src/commands/variation.js +194 -0
- package/src/index.js +189 -47
- package/src/lib/api-client.js +61 -35
- package/src/lib/auto-update/refresh.js +598 -0
- package/src/lib/auto-update/scene-runtime.compose.tsx +73 -0
- package/src/lib/auto-update/spec.js +89 -0
- package/src/lib/capture-engine.js +73 -0
- package/src/lib/capture-script-runner.js +280 -134
- package/src/lib/certification.js +23 -1
- package/src/lib/compose-context.js +156 -0
- package/src/lib/compose-pack.js +42 -0
- package/src/lib/compose-runtime.js +34 -0
- package/src/lib/compose-upload.js +142 -0
- package/src/lib/config.js +5 -5
- package/src/lib/dom-capture.js +64 -0
- package/src/lib/output-path-template.js +3 -3
- package/src/lib/record-clip.js +83 -3
- package/src/lib/record-config.js +0 -4
- package/src/lib/resolve-targets.js +60 -0
- package/src/lib/run-manifest.js +45 -0
- package/src/lib/storage-providers.js +1 -1
- package/src/lib/style-engine.js +5 -5
- package/src/lib/ui-api-helpers.js +118 -0
- package/src/lib/ui-api.js +28 -820
- package/src/lib/ui-asset-cleanup.js +62 -0
- package/src/lib/ui-output-versions.js +165 -0
- package/src/lib/ui-recorder-routes.js +341 -0
- package/src/lib/ui-scenario-metadata.js +161 -0
- package/vendor/compose/dist/auto-update.cjs +5544 -0
- package/vendor/compose/dist/auto-update.mjs +5518 -0
- package/vendor/compose/dist/capture.cjs +1450 -0
- package/vendor/compose/dist/capture.mjs +1416 -0
- package/vendor/compose/dist/eligibility.cjs +5331 -0
- package/vendor/compose/dist/eligibility.mjs +5313 -0
- package/vendor/compose/dist/index.cjs +2046 -0
- package/vendor/compose/dist/index.mjs +1997 -0
- package/vendor/compose/dist/jsx-dev-runtime.cjs +55 -0
- package/vendor/compose/dist/jsx-dev-runtime.mjs +27 -0
- package/vendor/compose/dist/jsx-runtime.cjs +58 -0
- package/vendor/compose/dist/jsx-runtime.mjs +31 -0
- package/vendor/compose/dist/render.cjs +558 -0
- package/vendor/compose/dist/render.mjs +515 -0
- package/vendor/compose/dist/verify-cli.cjs +3806 -0
- package/vendor/compose/dist/verify-cli.mjs +3812 -0
- package/vendor/compose/dist/verify.cjs +3880 -0
- package/vendor/compose/dist/verify.mjs +3858 -0
- package/web/manager/dist/assets/{index-D2qqcFNN.js → index-D0S2otug.js} +56 -56
- package/web/manager/dist/index.html +1 -1
- package/src/commands/ci-run.js +0 -178
- package/src/commands/ci-setup.js +0 -288
- package/src/commands/ingest.js +0 -458
- package/src/commands/setup.js +0 -165
- package/src/lib/playwright-runner.js +0 -252
package/LICENSE
CHANGED
|
@@ -175,7 +175,7 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
|
175
175
|
|
|
176
176
|
END OF TERMS AND CONDITIONS
|
|
177
177
|
|
|
178
|
-
Copyright 2026
|
|
178
|
+
Copyright 2026 The Plain Works Co., Ltd.
|
|
179
179
|
|
|
180
180
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
181
181
|
you may not use this file except in compliance with the License.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @reshotdev/screenshot
|
|
2
2
|
|
|
3
|
-
Product screenshots in documentation go stale within days of a
|
|
3
|
+
Product screenshots in documentation go stale within days of a UI change. Manually recapturing them across themes, viewports, and locales is tedious and error-prone. This CLI runs screenshot and video capture against a localhost build, comparing each run against the previous capture so teams can review diffs before docs change.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@reshotdev/screenshot)
|
|
6
6
|
[](https://github.com/reshotdev/screenshot/actions/workflows/ci.yml)
|
|
@@ -31,17 +31,32 @@ reshot run
|
|
|
31
31
|
# 4. Review captures in the web UI
|
|
32
32
|
reshot studio
|
|
33
33
|
|
|
34
|
-
# 5. Publish when you want hosted assets
|
|
35
|
-
reshot publish
|
|
34
|
+
# 5. Publish when you want hosted assets — pass --auto-approve on first run
|
|
35
|
+
reshot publish --auto-approve
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
For launch-grade reliability, do not treat `next dev` as the supported capture
|
|
39
39
|
runtime. Use a production-like local server and see the
|
|
40
40
|
[Supported Environments guide](https://reshot.dev/docs/cli/getting-started/supported-environments).
|
|
41
41
|
|
|
42
|
+
> **First-time setup tip:** pass `--auto-approve` to your first `reshot
|
|
43
|
+
> publish` so newly-captured visuals skip the review queue and become
|
|
44
|
+
> immediately available via `reshot pull`. Without it, every new visual
|
|
45
|
+
> lands in PENDING and is only visible in the studio.
|
|
46
|
+
|
|
47
|
+
> **OAuth / magic-link / Supabase apps:** skip `auth.loginSteps` and use
|
|
48
|
+
> `playwright codegen $YOUR_APP --save-storage=.reshot/auth-state.json`
|
|
49
|
+
> once, then set `"storageStatePath": ".reshot/auth-state.json"` in your
|
|
50
|
+
> config. See [Authentication](#authentication) below for details.
|
|
51
|
+
|
|
42
52
|
## Certified Targets
|
|
43
53
|
|
|
44
|
-
|
|
54
|
+
> **Most integrations should omit `target` entirely.** Certified targets
|
|
55
|
+
> are an opt-in contract for production-grade flows once basic capture is
|
|
56
|
+
> working — start without them and add `target` later if you need the
|
|
57
|
+
> stronger guarantees.
|
|
58
|
+
|
|
59
|
+
This release adds a **Certified Targets** contract for apps that need stronger guarantees than ad hoc capture. Certified targets declare their readiness selectors, localhost runtime, required routes, and expected published assets in `reshot.config.json`, then pass the full doctor/capture/publish/delivery pipeline before release.
|
|
45
60
|
|
|
46
61
|
## Configuration
|
|
47
62
|
|
|
@@ -120,8 +135,6 @@ Create `reshot.config.json` in your project root:
|
|
|
120
135
|
| `reshot certify` | Run the full certified-target pipeline | `--scenarios`, `--tag`, `--json` |
|
|
121
136
|
| `reshot drifts` | Manage visual drift notifications | `approve`, `reject`, `ignore`, `approve-all` |
|
|
122
137
|
| `reshot import-tests` | Import Playwright tests as scenarios | `--dry-run`, `--no-interactive` |
|
|
123
|
-
| `reshot ci setup` | Generate CI/CD workflow files | — |
|
|
124
|
-
| `reshot ci run` | Capture + publish in one step (CI) | `--tag`, `--no-publish`, `--dry-run` |
|
|
125
138
|
|
|
126
139
|
## Certification Workflow
|
|
127
140
|
|
|
@@ -276,18 +289,17 @@ The recorded scenario is appended to `reshot.config.json` automatically.
|
|
|
276
289
|
|
|
277
290
|
## Authentication
|
|
278
291
|
|
|
279
|
-
### Storage State
|
|
292
|
+
### Storage State (recommended)
|
|
280
293
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
To manually generate it:
|
|
294
|
+
For OAuth, magic-link, Supabase Auth, Clerk, Auth.js, and any other modern
|
|
295
|
+
auth flow that can't be scripted with form fields, capture a Playwright
|
|
296
|
+
storage state once and let reshot reuse it for every scenario:
|
|
285
297
|
|
|
286
298
|
```bash
|
|
287
|
-
npx playwright codegen http://localhost:3000 --save-storage
|
|
299
|
+
npx playwright codegen http://localhost:3000 --save-storage=.reshot/auth-state.json
|
|
288
300
|
```
|
|
289
301
|
|
|
290
|
-
|
|
302
|
+
Then point your config at it:
|
|
291
303
|
|
|
292
304
|
```json
|
|
293
305
|
{
|
|
@@ -295,7 +307,22 @@ Or reference a project-local path in your config:
|
|
|
295
307
|
}
|
|
296
308
|
```
|
|
297
309
|
|
|
298
|
-
|
|
310
|
+
`reshot record` does the same thing interactively and writes to
|
|
311
|
+
`~/.reshot/session-state.json` by default.
|
|
312
|
+
|
|
313
|
+
### Test backdoors
|
|
314
|
+
|
|
315
|
+
If your app has a dev/test backdoor that bypasses auth at the server layer
|
|
316
|
+
(for example a `/api/devtools` fixture endpoint, a header-based
|
|
317
|
+
impersonation hook, or a localhost-only cookie), you can point reshot at it
|
|
318
|
+
via `baseUrl` and skip storage state entirely. This is often the cleanest
|
|
319
|
+
option for first-party apps that already maintain such an endpoint for
|
|
320
|
+
testing.
|
|
321
|
+
|
|
322
|
+
### Login Steps (password forms only)
|
|
323
|
+
|
|
324
|
+
If your app still uses a traditional username/password form, you can
|
|
325
|
+
script the login directly:
|
|
299
326
|
|
|
300
327
|
```json
|
|
301
328
|
{
|
|
@@ -312,6 +339,8 @@ Or reference a project-local path in your config:
|
|
|
312
339
|
```
|
|
313
340
|
|
|
314
341
|
Environment variables (`${EMAIL}`, `${PASSWORD}`) are interpolated at runtime.
|
|
342
|
+
`loginSteps` cannot drive OAuth, magic-link, or any redirect-based flow —
|
|
343
|
+
use **Storage State** above for those.
|
|
315
344
|
|
|
316
345
|
## Output Formats
|
|
317
346
|
|
|
@@ -343,63 +372,67 @@ Set `"format": "summary-video"` in scenario output config to record the full bro
|
|
|
343
372
|
|
|
344
373
|
Crops the screenshot to the bounding box of the selected element.
|
|
345
374
|
|
|
346
|
-
##
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
jobs:
|
|
357
|
-
capture:
|
|
358
|
-
runs-on: ubuntu-latest
|
|
359
|
-
steps:
|
|
360
|
-
- uses: actions/checkout@v4
|
|
361
|
-
- uses: actions/setup-node@v4
|
|
362
|
-
with: { node-version: 20 }
|
|
363
|
-
- run: npm ci
|
|
364
|
-
- run: npm start & # start your app
|
|
365
|
-
- run: npx @reshotdev/screenshot ci run --tag ${{ github.sha }} --message "Deploy ${{ github.sha }}"
|
|
366
|
-
env:
|
|
367
|
-
RESHOT_TOKEN: ${{ secrets.RESHOT_TOKEN }}
|
|
375
|
+
## Automation in Scripts
|
|
376
|
+
|
|
377
|
+
Use headless mode with environment variables to integrate into build scripts or local workflows:
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
# In your Makefile or build script
|
|
381
|
+
export RESHOT_API_KEY=$(cat .reshot/api-key)
|
|
382
|
+
reshot run --scenarios dashboard --no-headless false
|
|
383
|
+
reshot publish --tag v1.2.0
|
|
368
384
|
```
|
|
369
385
|
|
|
370
|
-
|
|
386
|
+
Set `RESHOT_API_KEY` and `RESHOT_PROJECT_ID` to run without interactive auth:
|
|
371
387
|
|
|
372
388
|
```bash
|
|
373
|
-
reshot
|
|
389
|
+
RESHOT_API_KEY=your-key RESHOT_PROJECT_ID=your-project reshot run
|
|
374
390
|
```
|
|
375
391
|
|
|
376
|
-
|
|
392
|
+
For headless execution, ensure:
|
|
393
|
+
- Your app is running on localhost (e.g., `npm run build && npm run start`)
|
|
394
|
+
- `headless: true` is set in `reshot.config.json`
|
|
395
|
+
- API credentials are available as environment variables
|
|
377
396
|
|
|
378
397
|
## Asset Map for Builds
|
|
379
398
|
|
|
380
399
|
Generate a manifest of captured assets for use in documentation sites or marketing pages:
|
|
381
400
|
|
|
382
401
|
```bash
|
|
383
|
-
reshot pull --format json --output assets.json
|
|
402
|
+
reshot pull --format json --output assets.json
|
|
384
403
|
```
|
|
385
404
|
|
|
405
|
+
The output is keyed by scenario, visual, and context (variant). `meta` mirrors
|
|
406
|
+
the API response; `assets` is a 3-level nested object — `scenarioKey →
|
|
407
|
+
visualKey → context`:
|
|
408
|
+
|
|
386
409
|
```json
|
|
387
410
|
{
|
|
388
|
-
"
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
"
|
|
396
|
-
|
|
411
|
+
"meta": {
|
|
412
|
+
"projectId": "...",
|
|
413
|
+
"exportedAt": "2026-04-30T12:00:00.000Z",
|
|
414
|
+
"totalVisuals": 12
|
|
415
|
+
},
|
|
416
|
+
"assets": {
|
|
417
|
+
"dashboard": {
|
|
418
|
+
"overview": {
|
|
419
|
+
"themeLight": {
|
|
420
|
+
"type": "image/png",
|
|
421
|
+
"alt": "Dashboard overview, light theme",
|
|
422
|
+
"steps": [
|
|
423
|
+
{ "src": "https://cdn.reshot.dev/abc123/overview.png", "step": "overview" }
|
|
424
|
+
]
|
|
425
|
+
},
|
|
426
|
+
"themeDark": { "...": "..." }
|
|
427
|
+
}
|
|
397
428
|
}
|
|
398
|
-
|
|
429
|
+
}
|
|
399
430
|
}
|
|
400
431
|
```
|
|
401
432
|
|
|
402
|
-
|
|
433
|
+
By default `pull` returns visuals in all states (approved + pending). Pass
|
|
434
|
+
`--status approved` to filter to released visuals only. Also supports
|
|
435
|
+
`--format ts` (TypeScript with full metadata) and `--format csv`.
|
|
403
436
|
|
|
404
437
|
## Drift Management
|
|
405
438
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reshotdev/screenshot",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.1-beta.13",
|
|
4
|
+
"description": "Screenshot and video capture CLI",
|
|
5
5
|
"author": "Reshot <hello@reshot.dev>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://reshot.dev",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"screenshots",
|
|
18
|
-
"
|
|
18
|
+
"cli",
|
|
19
19
|
"documentation",
|
|
20
20
|
"visual-testing",
|
|
21
21
|
"automation",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
28
|
"src/",
|
|
29
|
+
"vendor/compose/dist/",
|
|
29
30
|
"web/manager/dist/",
|
|
30
31
|
"web/cropper/",
|
|
31
32
|
"web/subtitle-editor/",
|
|
@@ -35,24 +36,13 @@
|
|
|
35
36
|
"engines": {
|
|
36
37
|
"node": ">=18.0.0"
|
|
37
38
|
},
|
|
38
|
-
"scripts": {
|
|
39
|
-
"test": "node scripts/test.js",
|
|
40
|
-
"test:unit": "node --test tests/unit/*.test.js",
|
|
41
|
-
"test:integration": "node --test tests/integration/*.test.js",
|
|
42
|
-
"test:all": "node --test tests/unit/*.test.js tests/integration/*.test.js",
|
|
43
|
-
"test:dry-run": "node src/index.js --help && node src/index.js --version",
|
|
44
|
-
"prepublishOnly": "cd web/manager && npm install && npm run build",
|
|
45
|
-
"ui:build": "cd web/manager && npm install && npm run build",
|
|
46
|
-
"ui:dev": "cd web/manager && npm install && npm run dev",
|
|
47
|
-
"lint": "echo 'No linting configured yet'",
|
|
48
|
-
"pack:check": "npm pack --dry-run"
|
|
49
|
-
},
|
|
50
39
|
"dependencies": {
|
|
51
40
|
"axios": "^1.6.2",
|
|
52
41
|
"chalk": "^4.1.2",
|
|
53
42
|
"commander": "^11.1.0",
|
|
54
43
|
"cors": "^2.8.5",
|
|
55
44
|
"dotenv": "^16.3.1",
|
|
45
|
+
"esbuild": "^0.27.2",
|
|
56
46
|
"express": "^4.18.2",
|
|
57
47
|
"form-data": "^4.0.0",
|
|
58
48
|
"fs-extra": "^11.2.0",
|
|
@@ -62,10 +52,24 @@
|
|
|
62
52
|
"ora": "^7.0.1",
|
|
63
53
|
"pixelmatch": "^5.3.0",
|
|
64
54
|
"playwright": "^1.40.0",
|
|
55
|
+
"playwright-core": "^1.57.0",
|
|
65
56
|
"pngjs": "^7.0.0",
|
|
66
57
|
"sharp": "^0.33.2",
|
|
67
58
|
"socket.io": "^4.7.2",
|
|
68
59
|
"uuid": "^9.0.1",
|
|
69
60
|
"@aws-sdk/client-s3": "^3.650.0"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"test": "node scripts/test.js",
|
|
64
|
+
"test:unit": "node --test tests/unit/*.test.js",
|
|
65
|
+
"test:integration": "node --test tests/integration/*.test.js",
|
|
66
|
+
"test:all": "node --test tests/unit/*.test.js tests/integration/*.test.js",
|
|
67
|
+
"test:dry-run": "node src/index.js --help && node src/index.js --version",
|
|
68
|
+
"test:source": "node src/index.js",
|
|
69
|
+
"ui:build": "cd web/manager && npm install && npm run build",
|
|
70
|
+
"ui:dev": "cd web/manager && npm install && npm run dev",
|
|
71
|
+
"lint": "echo 'No linting configured yet'",
|
|
72
|
+
"build": "pnpm run ui:build",
|
|
73
|
+
"pack:check": "npm pack --dry-run"
|
|
70
74
|
}
|
|
71
|
-
}
|
|
75
|
+
}
|
package/src/commands/auth.js
CHANGED
|
@@ -14,7 +14,7 @@ const pkg = require("../../package.json");
|
|
|
14
14
|
|
|
15
15
|
const DEFAULT_CALLBACK_PORT = 3721;
|
|
16
16
|
const POLL_INTERVAL_MS = 2000;
|
|
17
|
-
const DEFAULT_AUTH_TIMEOUT_MS =
|
|
17
|
+
const DEFAULT_AUTH_TIMEOUT_MS = 15 * 60 * 1000;
|
|
18
18
|
|
|
19
19
|
const unwrapResponse = (payload) => {
|
|
20
20
|
if (!payload) {
|
|
@@ -158,9 +158,9 @@ async function verifyApiKey(apiBaseUrl, apiKey, httpClient = axios) {
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
async function authCommand(options = {}) {
|
|
161
|
-
// Support non-interactive auth via environment variables
|
|
162
|
-
const envApiKey = process.env.RESHOT_API_KEY;
|
|
163
|
-
const envProjectId = process.env.RESHOT_PROJECT_ID;
|
|
161
|
+
// Support non-interactive auth via environment variables
|
|
162
|
+
const envApiKey = options.apiKey || process.env.RESHOT_API_KEY;
|
|
163
|
+
const envProjectId = options.projectId || process.env.RESHOT_PROJECT_ID;
|
|
164
164
|
const httpClient = options.httpClient || axios;
|
|
165
165
|
const openFn = options.openFn || open;
|
|
166
166
|
const writeSettingsFn = options.writeSettingsFn || writeSettings;
|
|
@@ -189,6 +189,33 @@ async function authCommand(options = {}) {
|
|
|
189
189
|
};
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// The browser approval flow requires an interactive session (a human approves
|
|
193
|
+
// in the browser while the CLI polls). With no TTY and no env credentials we
|
|
194
|
+
// would otherwise open a browser nobody sees and poll for up to 15 minutes —
|
|
195
|
+
// i.e. hang. Detect that and exit promptly with actionable guidance.
|
|
196
|
+
const stdinIsInteractive =
|
|
197
|
+
typeof options.isInteractive === "boolean"
|
|
198
|
+
? options.isInteractive
|
|
199
|
+
: Boolean(process.stdin && process.stdin.isTTY);
|
|
200
|
+
if (!stdinIsInteractive) {
|
|
201
|
+
console.error(
|
|
202
|
+
chalk.red("✖ `reshot auth` needs an interactive terminal."),
|
|
203
|
+
);
|
|
204
|
+
console.error(
|
|
205
|
+
chalk.gray(
|
|
206
|
+
" Run it in an interactive shell to approve in the browser, or set",
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
console.error(
|
|
210
|
+
chalk.gray(
|
|
211
|
+
" RESHOT_API_KEY and RESHOT_PROJECT_ID for non-interactive (CI) auth.",
|
|
212
|
+
),
|
|
213
|
+
);
|
|
214
|
+
const err = new Error("Interactive terminal required for browser auth");
|
|
215
|
+
err.code = "ENOTTY";
|
|
216
|
+
throw err;
|
|
217
|
+
}
|
|
218
|
+
|
|
192
219
|
const apiBaseUrl = options.apiBaseUrl || getApiBaseUrl();
|
|
193
220
|
const explicitPortEnv =
|
|
194
221
|
process.env.RESHOT_CLI_CALLBACK_PORT || "";
|
|
@@ -232,12 +259,15 @@ async function authCommand(options = {}) {
|
|
|
232
259
|
}
|
|
233
260
|
}
|
|
234
261
|
|
|
262
|
+
const initiatePayload = {
|
|
263
|
+
callbackPort,
|
|
264
|
+
clientVersion: pkg.version,
|
|
265
|
+
...(options.projectId ? { projectId: options.projectId } : {}),
|
|
266
|
+
};
|
|
267
|
+
|
|
235
268
|
const initiateResponse = await httpClient.post(
|
|
236
269
|
`${apiBaseUrl}/auth/cli/initiate`,
|
|
237
|
-
|
|
238
|
-
callbackPort,
|
|
239
|
-
clientVersion: pkg.version,
|
|
240
|
-
},
|
|
270
|
+
initiatePayload,
|
|
241
271
|
{ headers: { "Content-Type": "application/json" } }
|
|
242
272
|
);
|
|
243
273
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// capture-dom.js - Capture a self-contained DOM reconstruction artifact from a
|
|
2
|
+
// live URL (Tier-3 Phase 2). Emits <slug>.dom.html (+ sidecars), remounted.png,
|
|
3
|
+
// and live.png so the calibrated quality gate can diff them:
|
|
4
|
+
//
|
|
5
|
+
// reshot capture-dom <url> --out /tmp/cap
|
|
6
|
+
// pnpm --dir packages/compose verify diff /tmp/cap/remounted.png /tmp/cap/live.png
|
|
7
|
+
|
|
8
|
+
const chalk = require("chalk");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const { captureDomFromUrl } = require("../lib/dom-capture");
|
|
11
|
+
|
|
12
|
+
function registerCaptureDom(program) {
|
|
13
|
+
program
|
|
14
|
+
.command("capture-dom <url>")
|
|
15
|
+
.description("Capture a self-contained DOM reconstruction artifact from a live URL")
|
|
16
|
+
.option("-o, --out <dir>", "Output directory", "./.reshot/capture-dom")
|
|
17
|
+
.option("-s, --slug <slug>", "Artifact slug", "capture")
|
|
18
|
+
.option("--width <px>", "Viewport width", (v) => parseInt(v, 10))
|
|
19
|
+
.option("--height <px>", "Viewport height", (v) => parseInt(v, 10))
|
|
20
|
+
.option("--dpr <n>", "Device scale factor", (v) => parseInt(v, 10))
|
|
21
|
+
.action(async (url, options) => {
|
|
22
|
+
const outDir = path.resolve(options.out);
|
|
23
|
+
const settings =
|
|
24
|
+
options.width || options.height || options.dpr
|
|
25
|
+
? {
|
|
26
|
+
width: options.width || 1000,
|
|
27
|
+
height: options.height || 800,
|
|
28
|
+
deviceScaleFactor: options.dpr || 2,
|
|
29
|
+
}
|
|
30
|
+
: undefined;
|
|
31
|
+
|
|
32
|
+
console.log(chalk.cyan(`\n Capturing DOM from ${url} ...\n`));
|
|
33
|
+
const result = await captureDomFromUrl({ url, outDir, slug: options.slug, settings });
|
|
34
|
+
|
|
35
|
+
console.log(chalk.green(` ✔ method: ${result.method}`));
|
|
36
|
+
console.log(` artifact: ${result.artifact}`);
|
|
37
|
+
console.log(` remounted: ${result.remounted}`);
|
|
38
|
+
console.log(` live: ${result.live}`);
|
|
39
|
+
if (result.sidecars.length) {
|
|
40
|
+
console.log(` sidecars: ${result.sidecars.map((s) => `${s.kind}(rasterized=${s.rasterized})`).join(", ")}`);
|
|
41
|
+
}
|
|
42
|
+
console.log(
|
|
43
|
+
chalk.gray(
|
|
44
|
+
`\n Verify: pnpm --dir packages/compose verify diff ${result.remounted} ${result.live}\n`,
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { registerCaptureDom };
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// compose.js - Render a local @reshot/compose file into a video pack
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const fs = require("fs-extra");
|
|
4
|
+
const chalk = require("chalk");
|
|
5
|
+
const { composeDistDir } = require("../lib/compose-runtime");
|
|
6
|
+
const {
|
|
7
|
+
DEFAULT_FORMATS,
|
|
8
|
+
assertCaptureExists,
|
|
9
|
+
deriveSlug,
|
|
10
|
+
parseFormats,
|
|
11
|
+
parseSize,
|
|
12
|
+
resolveCapturePath,
|
|
13
|
+
resolveComposeContext,
|
|
14
|
+
resolveMetadataPath,
|
|
15
|
+
resolveOutBase,
|
|
16
|
+
} = require("../lib/compose-context");
|
|
17
|
+
const {
|
|
18
|
+
assertUploadPackExists,
|
|
19
|
+
packFromExistingOutputs,
|
|
20
|
+
} = require("../lib/compose-pack");
|
|
21
|
+
const {
|
|
22
|
+
buildDashboardUrl,
|
|
23
|
+
getComposeApiBaseUrl,
|
|
24
|
+
humanizeName,
|
|
25
|
+
resolveComposeProjectContext,
|
|
26
|
+
uploadComposition,
|
|
27
|
+
} = require("../lib/compose-upload");
|
|
28
|
+
|
|
29
|
+
async function runCompose(file, options = {}, deps = {}) {
|
|
30
|
+
const context = await resolveComposeContext(file, options);
|
|
31
|
+
|
|
32
|
+
const size = parseSize(options.size || "1440x900");
|
|
33
|
+
const formats = parseFormats(options.formats, options.gif);
|
|
34
|
+
const outBase = resolveOutBase(context.compositionPath, options.out);
|
|
35
|
+
|
|
36
|
+
const { render } = deps.renderModule || loadComposeRender();
|
|
37
|
+
|
|
38
|
+
console.log(chalk.cyan(`\n-> Rendering ${chalk.bold(context.slug)} (${size.width}x${size.height})`));
|
|
39
|
+
console.log(chalk.gray(` composition ${relativePath(context.compositionPath)}`));
|
|
40
|
+
console.log(chalk.gray(` metadata ${relativePath(context.metadataPath)}`));
|
|
41
|
+
console.log(chalk.gray(` capture ${relativePath(context.capturePath)}`));
|
|
42
|
+
console.log(chalk.gray(` out ${relativePath(outBase)}`));
|
|
43
|
+
|
|
44
|
+
const result = await withComposeEnvironment(
|
|
45
|
+
{
|
|
46
|
+
RESHOT_COMPOSE_SLUG: context.slug,
|
|
47
|
+
RESHOT_COMPOSE_METADATA_PATH: context.metadataPath,
|
|
48
|
+
RESHOT_COMPOSE_CAPTURE_PATH: context.capturePath,
|
|
49
|
+
},
|
|
50
|
+
() =>
|
|
51
|
+
render(context.compositionPath, {
|
|
52
|
+
slug: context.slug,
|
|
53
|
+
out: outBase,
|
|
54
|
+
size,
|
|
55
|
+
formats,
|
|
56
|
+
metadataPath: context.metadataPath,
|
|
57
|
+
capturePath: context.capturePath,
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
console.log(chalk.green(`\nRendered ${context.slug}`));
|
|
62
|
+
printPack(result.pack || {});
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function runComposePush(file, options = {}, deps = {}) {
|
|
67
|
+
const context = await resolveComposeContext(file, options);
|
|
68
|
+
const outBase = resolveOutBase(context.compositionPath, options.out);
|
|
69
|
+
const pack = options.skipRender
|
|
70
|
+
? await packFromExistingOutputs(outBase)
|
|
71
|
+
: (await runCompose(file, options, deps)).pack || {};
|
|
72
|
+
|
|
73
|
+
await assertUploadPackExists(pack, Boolean(options.skipRender));
|
|
74
|
+
|
|
75
|
+
const projectContext = resolveComposeProjectContext({
|
|
76
|
+
projectOption: options.project,
|
|
77
|
+
settings: deps.settings,
|
|
78
|
+
});
|
|
79
|
+
const apiBaseUrl = getComposeApiBaseUrl(deps.apiBaseUrl);
|
|
80
|
+
const name = options.name || humanizeName(context.slug);
|
|
81
|
+
const upload = deps.uploadComposition || uploadComposition;
|
|
82
|
+
|
|
83
|
+
console.log(chalk.cyan(`\n-> Uploading ${chalk.bold(name)} to project ${projectContext.projectId}`));
|
|
84
|
+
const response = await upload({
|
|
85
|
+
apiBaseUrl,
|
|
86
|
+
apiKey: projectContext.apiKey,
|
|
87
|
+
projectId: projectContext.projectId,
|
|
88
|
+
name,
|
|
89
|
+
slug: context.slug,
|
|
90
|
+
sourceTsx: await fs.readFile(context.compositionPath, "utf8"),
|
|
91
|
+
metadataJson: await fs.readFile(context.metadataPath, "utf8"),
|
|
92
|
+
pack,
|
|
93
|
+
autoApprove: Boolean(options.autoApprove),
|
|
94
|
+
httpClient: deps.httpClient,
|
|
95
|
+
});
|
|
96
|
+
const dashboardUrl = buildDashboardUrl(response, apiBaseUrl, projectContext.projectId);
|
|
97
|
+
|
|
98
|
+
console.log(chalk.green(`\nUploaded ${context.slug}`));
|
|
99
|
+
console.log(chalk.gray(` Dashboard: ${dashboardUrl}`));
|
|
100
|
+
printPublicUrls(response?.publicUrls);
|
|
101
|
+
if (response?.attributionWarning === "legacy-key-no-user-attribution") {
|
|
102
|
+
console.log(
|
|
103
|
+
chalk.yellow(
|
|
104
|
+
" Attribution: re-issue your API key to enable per-engineer render attribution.",
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { response, dashboardUrl, projectId: projectContext.projectId };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function registerCompose(program) {
|
|
113
|
+
const compose = program
|
|
114
|
+
.command("compose")
|
|
115
|
+
.description("Render and upload local JSX compositions")
|
|
116
|
+
.argument("<file>", "Composition file to render")
|
|
117
|
+
.option("--slug <slug>", "Matrix variant slug; defaults to the composition filename")
|
|
118
|
+
.option("--out <path>", "Output base path; defaults to <file-stem>.composed")
|
|
119
|
+
.option("--size <size>", "Viewport size as WIDTHxHEIGHT", "1440x900")
|
|
120
|
+
.option("--formats <formats>", "Comma-separated output formats", DEFAULT_FORMATS.join(","))
|
|
121
|
+
.option("--gif", "Also emit a gif")
|
|
122
|
+
.action(async (file, options) => {
|
|
123
|
+
try {
|
|
124
|
+
await runCompose(file, normalizeCommandOptions(options));
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error(chalk.red("Error:"), error.message);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
compose
|
|
132
|
+
.command("push <file>")
|
|
133
|
+
.description("Render and upload a local JSX composition to the dashboard")
|
|
134
|
+
.option("--name <name>", "Composition display name")
|
|
135
|
+
.option("--project <projectId>", "Project id; defaults to local Reshot settings")
|
|
136
|
+
.option("--out <path>", "Output base path to upload when --skip-render is used")
|
|
137
|
+
.option("--skip-render", "Upload existing local outputs without re-rendering")
|
|
138
|
+
.option("--auto-approve", "Immediately approve this composition render and update live embed URLs")
|
|
139
|
+
.action(async (file, options) => {
|
|
140
|
+
try {
|
|
141
|
+
await runComposePush(file, normalizeCommandOptions(options));
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error(chalk.red("Error:"), error.message);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeCommandOptions(options) {
|
|
150
|
+
return typeof options?.opts === "function" ? options.opts() : options || {};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function loadComposeRender() {
|
|
154
|
+
return require(path.join(composeDistDir(), "render.cjs"));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function withComposeEnvironment(env, callback) {
|
|
158
|
+
const previous = {};
|
|
159
|
+
for (const [key, value] of Object.entries(env)) {
|
|
160
|
+
previous[key] = process.env[key];
|
|
161
|
+
process.env[key] = value;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
return await callback();
|
|
166
|
+
} finally {
|
|
167
|
+
for (const [key, value] of Object.entries(previous)) {
|
|
168
|
+
if (value === undefined) {
|
|
169
|
+
delete process.env[key];
|
|
170
|
+
} else {
|
|
171
|
+
process.env[key] = value;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function printPack(pack) {
|
|
178
|
+
for (const format of ["mp4", "webm", "poster", "gif"]) {
|
|
179
|
+
if (pack[format]) {
|
|
180
|
+
console.log(chalk.gray(` ${format.padEnd(6)} ${relativePath(pack[format])}`));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function printPublicUrls(publicUrls) {
|
|
186
|
+
if (!publicUrls || typeof publicUrls !== "object") return;
|
|
187
|
+
if (publicUrls.embed) {
|
|
188
|
+
console.log(chalk.gray(` Live embed: ${publicUrls.embed}`));
|
|
189
|
+
}
|
|
190
|
+
if (publicUrls.live?.mp4) {
|
|
191
|
+
console.log(chalk.gray(` Live MP4: ${publicUrls.live.mp4}`));
|
|
192
|
+
}
|
|
193
|
+
if (publicUrls.pinned?.mp4) {
|
|
194
|
+
console.log(chalk.gray(` Pinned MP4: ${publicUrls.pinned.mp4}`));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function relativePath(filePath) {
|
|
199
|
+
const relative = path.relative(process.cwd(), filePath);
|
|
200
|
+
return relative && !relative.startsWith("..") ? relative : filePath;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = {
|
|
204
|
+
DEFAULT_FORMATS,
|
|
205
|
+
assertCaptureExists,
|
|
206
|
+
deriveSlug,
|
|
207
|
+
normalizeCommandOptions,
|
|
208
|
+
parseFormats,
|
|
209
|
+
parseSize,
|
|
210
|
+
printPublicUrls,
|
|
211
|
+
registerCompose,
|
|
212
|
+
resolveComposeContext,
|
|
213
|
+
resolveComposeProjectContext,
|
|
214
|
+
resolveCapturePath,
|
|
215
|
+
resolveMetadataPath,
|
|
216
|
+
resolveOutBase,
|
|
217
|
+
runCompose,
|
|
218
|
+
runComposePush,
|
|
219
|
+
uploadComposition,
|
|
220
|
+
};
|