@reshotdev/screenshot 0.0.1-beta.12 → 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/README.md +67 -22
- package/package.json +18 -14
- package/src/commands/auth.js +37 -7
- 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 +9 -4
- 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 +187 -9
- 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 +2 -2
- package/src/lib/dom-capture.js +64 -0
- 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/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-CvleJUur.js → index-D0S2otug.js} +56 -56
- package/web/manager/dist/index.html +1 -1
- package/src/commands/ingest.js +0 -458
- package/src/commands/setup.js +0 -165
- package/src/lib/playwright-runner.js +0 -252
package/README.md
CHANGED
|
@@ -31,16 +31,31 @@ 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
|
|
|
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
|
+
|
|
44
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
|
|
@@ -274,18 +289,17 @@ The recorded scenario is appended to `reshot.config.json` automatically.
|
|
|
274
289
|
|
|
275
290
|
## Authentication
|
|
276
291
|
|
|
277
|
-
### Storage State
|
|
278
|
-
|
|
279
|
-
The CLI stores browser session state at `~/.reshot/session-state.json` (global).
|
|
280
|
-
This is automatically captured when you run `reshot record`.
|
|
292
|
+
### Storage State (recommended)
|
|
281
293
|
|
|
282
|
-
|
|
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:
|
|
283
297
|
|
|
284
298
|
```bash
|
|
285
|
-
npx playwright codegen http://localhost:3000 --save-storage
|
|
299
|
+
npx playwright codegen http://localhost:3000 --save-storage=.reshot/auth-state.json
|
|
286
300
|
```
|
|
287
301
|
|
|
288
|
-
|
|
302
|
+
Then point your config at it:
|
|
289
303
|
|
|
290
304
|
```json
|
|
291
305
|
{
|
|
@@ -293,7 +307,22 @@ Or reference a project-local path in your config:
|
|
|
293
307
|
}
|
|
294
308
|
```
|
|
295
309
|
|
|
296
|
-
|
|
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:
|
|
297
326
|
|
|
298
327
|
```json
|
|
299
328
|
{
|
|
@@ -310,6 +339,8 @@ Or reference a project-local path in your config:
|
|
|
310
339
|
```
|
|
311
340
|
|
|
312
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.
|
|
313
344
|
|
|
314
345
|
## Output Formats
|
|
315
346
|
|
|
@@ -368,26 +399,40 @@ For headless execution, ensure:
|
|
|
368
399
|
Generate a manifest of captured assets for use in documentation sites or marketing pages:
|
|
369
400
|
|
|
370
401
|
```bash
|
|
371
|
-
reshot pull --format json --output assets.json
|
|
402
|
+
reshot pull --format json --output assets.json
|
|
372
403
|
```
|
|
373
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
|
+
|
|
374
409
|
```json
|
|
375
410
|
{
|
|
376
|
-
"
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
"
|
|
384
|
-
|
|
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
|
+
}
|
|
385
428
|
}
|
|
386
|
-
|
|
429
|
+
}
|
|
387
430
|
}
|
|
388
431
|
```
|
|
389
432
|
|
|
390
|
-
|
|
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`.
|
|
391
436
|
|
|
392
437
|
## Drift Management
|
|
393
438
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reshotdev/screenshot",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
3
|
+
"version": "0.0.1-beta.13",
|
|
4
4
|
"description": "Screenshot and video capture CLI",
|
|
5
5
|
"author": "Reshot <hello@reshot.dev>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -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) {
|
|
@@ -159,8 +159,8 @@ async function verifyApiKey(apiBaseUrl, apiKey, httpClient = axios) {
|
|
|
159
159
|
|
|
160
160
|
async function authCommand(options = {}) {
|
|
161
161
|
// Support non-interactive auth via environment variables
|
|
162
|
-
const envApiKey = process.env.RESHOT_API_KEY;
|
|
163
|
-
const envProjectId = process.env.RESHOT_PROJECT_ID;
|
|
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
|
+
};
|
|
@@ -11,14 +11,46 @@ async function doctorTargetCommand(options = {}) {
|
|
|
11
11
|
.filter(Boolean)
|
|
12
12
|
: null;
|
|
13
13
|
|
|
14
|
+
// Emit an immediate banner BEFORE any async work so the command is never
|
|
15
|
+
// silent — previously it produced zero output while preparing fixtures and
|
|
16
|
+
// launching a browser, which read as a hang.
|
|
17
|
+
if (!options.json) {
|
|
18
|
+
console.log(chalk.cyan("🩺 Running target doctor…"));
|
|
19
|
+
console.log(
|
|
20
|
+
chalk.gray(
|
|
21
|
+
scenarioKeys
|
|
22
|
+
? ` scenarios: ${scenarioKeys.join(", ")}`
|
|
23
|
+
: " scenarios: all certified",
|
|
24
|
+
),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
const progressLogger = options.json
|
|
15
29
|
? null
|
|
16
30
|
: (message) => console.log(chalk.gray(` → ${message}`));
|
|
17
31
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
32
|
+
let report;
|
|
33
|
+
try {
|
|
34
|
+
report = await runDoctorTarget({
|
|
35
|
+
scenarioKeys,
|
|
36
|
+
onProgress: progressLogger,
|
|
37
|
+
timeoutMs: options.timeout ? Number(options.timeout) : undefined,
|
|
38
|
+
});
|
|
39
|
+
} catch (error) {
|
|
40
|
+
// Fail fast with an actionable message + report path rather than hanging.
|
|
41
|
+
if (!options.json) {
|
|
42
|
+
console.error(chalk.red(`\n ✖ Target doctor aborted: ${error.message}`));
|
|
43
|
+
console.error(
|
|
44
|
+
chalk.gray(
|
|
45
|
+
" Confirm the dev server is reachable and the target is configured (see .reshot/reports).",
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
} else {
|
|
49
|
+
console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
|
|
50
|
+
}
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
return { ok: false, error: error.message };
|
|
53
|
+
}
|
|
22
54
|
|
|
23
55
|
if (options.json) {
|
|
24
56
|
console.log(JSON.stringify(report, null, 2));
|
package/src/commands/drifts.js
CHANGED
|
@@ -234,7 +234,7 @@ async function batchDriftAction(apiKey, projectId, action, options = {}) {
|
|
|
234
234
|
|
|
235
235
|
try {
|
|
236
236
|
// Fetch all pending drifts
|
|
237
|
-
const response = await apiClient.getDrifts(apiKey, projectId, { status: "
|
|
237
|
+
const response = await apiClient.getDrifts(apiKey, projectId, { status: "pending" });
|
|
238
238
|
const drifts = response.drifts || [];
|
|
239
239
|
|
|
240
240
|
if (drifts.length === 0) {
|
|
@@ -268,6 +268,18 @@ async function batchDriftAction(apiKey, projectId, action, options = {}) {
|
|
|
268
268
|
console.log(chalk.red(` ${failed} drift(s) failed`));
|
|
269
269
|
}
|
|
270
270
|
} catch (error) {
|
|
271
|
+
const status = error.reshot?.status ?? error.response?.status;
|
|
272
|
+
if (status === 400 || status === 404) {
|
|
273
|
+
console.log(chalk.green(" ✓ No pending drifts to approve."));
|
|
274
|
+
console.log(
|
|
275
|
+
chalk.gray(
|
|
276
|
+
" Drifts only exist when a capture differs from a baseline. New captures\n" +
|
|
277
|
+
" with no prior version go to the review queue — use `reshot publish\n" +
|
|
278
|
+
" --auto-approve` or approve them in the studio."
|
|
279
|
+
)
|
|
280
|
+
);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
271
283
|
console.error(chalk.red("Error:"), error.message);
|
|
272
284
|
process.exit(1);
|
|
273
285
|
}
|