@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
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// variation.js - Render variations from a captured DOM scene.
|
|
2
|
+
//
|
|
3
|
+
// Workflow:
|
|
4
|
+
// 1. Resolve the source MHTML — either from the local .reshot/output tree
|
|
5
|
+
// or by downloading from the platform CDN (R2 public URL stored on
|
|
6
|
+
// the VisualVersion as domSceneS3Path).
|
|
7
|
+
// 2. Open the MHTML in a fresh headless Chromium.
|
|
8
|
+
// 3. Apply the variation manifest (JS mutations + viewport).
|
|
9
|
+
// 4. Screenshot.
|
|
10
|
+
//
|
|
11
|
+
// This is the v1 marketing-operator entry point. The dashboard UI on top
|
|
12
|
+
// of it is the next deliverable; the CLI exists so the loop is usable
|
|
13
|
+
// the moment Phase 1 ships.
|
|
14
|
+
//
|
|
15
|
+
// Manifest format (JSON):
|
|
16
|
+
//
|
|
17
|
+
// {
|
|
18
|
+
// "viewport": { "width": 1440, "height": 900, "deviceScaleFactor": 2 },
|
|
19
|
+
// "colorScheme": "light" | "dark",
|
|
20
|
+
// "mutations": [
|
|
21
|
+
// { "remove": "aside, nav" }, // delete elements
|
|
22
|
+
// { "replaceText": [
|
|
23
|
+
// { "find": "ari@tempo.example", "with": "alice@acme.com" }
|
|
24
|
+
// ]},
|
|
25
|
+
// { "limit": { "selector": "tbody tr", "count": 5 } }, // keep first N
|
|
26
|
+
// { "setText": { "selector": "[data-author]", "text": "alice@acme.com" } },
|
|
27
|
+
// { "setAttr": { "selector": "img.logo", "name": "src", "value": "..." } },
|
|
28
|
+
// { "evaluate": "/* arbitrary page.evaluate script */" }
|
|
29
|
+
// ]
|
|
30
|
+
// }
|
|
31
|
+
|
|
32
|
+
const fs = require("fs-extra");
|
|
33
|
+
const path = require("node:path");
|
|
34
|
+
const chalk = require("chalk");
|
|
35
|
+
const { chromium } = require("playwright");
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Runs in the browser context. Pure function — no closures over Node
|
|
39
|
+
* state. `manifestJson` is the only input. Keep this synchronous; if
|
|
40
|
+
* the page has React rehydration scripts, give them time before calling
|
|
41
|
+
* by waiting in the caller, then apply mutations as the *last* DOM
|
|
42
|
+
* write so they aren't immediately overwritten.
|
|
43
|
+
*/
|
|
44
|
+
function applyMutationsInBrowser(manifestJson) {
|
|
45
|
+
const m = JSON.parse(manifestJson);
|
|
46
|
+
for (const mut of m.mutations || []) {
|
|
47
|
+
if (mut.remove) {
|
|
48
|
+
document.querySelectorAll(mut.remove).forEach((el) => el.remove());
|
|
49
|
+
}
|
|
50
|
+
if (mut.replaceText) {
|
|
51
|
+
for (const r of mut.replaceText) {
|
|
52
|
+
document.body.innerHTML = document.body.innerHTML
|
|
53
|
+
.split(r.find)
|
|
54
|
+
.join(r.with);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (mut.limit) {
|
|
58
|
+
const els = document.querySelectorAll(mut.limit.selector);
|
|
59
|
+
els.forEach((el, i) => {
|
|
60
|
+
if (i >= mut.limit.count) el.remove();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (mut.setText) {
|
|
64
|
+
document.querySelectorAll(mut.setText.selector).forEach((el) => {
|
|
65
|
+
el.textContent = mut.setText.text;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (mut.setAttr) {
|
|
69
|
+
document.querySelectorAll(mut.setAttr.selector).forEach((el) => {
|
|
70
|
+
el.setAttribute(mut.setAttr.name, mut.setAttr.value);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (mut.evaluate) {
|
|
74
|
+
// eslint-disable-next-line no-new-func
|
|
75
|
+
new Function(mut.evaluate)();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function resolveSourceMhtml({ source, scenarioKey, captureKey, theme }) {
|
|
81
|
+
// 1) Explicit HTTPS URL → download to tmp, return local path. The local
|
|
82
|
+
// file MUST end in .mhtml — Chromium detects MHTML by extension, not by
|
|
83
|
+
// sniffing magic bytes, and our CDN serves under .related (the extension
|
|
84
|
+
// derived from multipart/related contentType).
|
|
85
|
+
if (source && /^https?:\/\//.test(source)) {
|
|
86
|
+
const tmpDir = path.join(require("os").tmpdir(), "reshot-variation");
|
|
87
|
+
fs.ensureDirSync(tmpDir);
|
|
88
|
+
const localPath = path.join(tmpDir, `${Date.now()}-variation.mhtml`);
|
|
89
|
+
const res = await fetch(source);
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
throw new Error(`Failed to fetch source MHTML: ${res.status} ${res.statusText}`);
|
|
92
|
+
}
|
|
93
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
94
|
+
await fs.writeFile(localPath, buf);
|
|
95
|
+
return { kind: "remote", path: localPath };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 2) Explicit local path
|
|
99
|
+
if (source && fs.existsSync(source)) {
|
|
100
|
+
return { kind: "local", path: source };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 3) Look in .reshot/output tree
|
|
104
|
+
if (scenarioKey && captureKey) {
|
|
105
|
+
const root = path.join(process.cwd(), ".reshot", "output", scenarioKey);
|
|
106
|
+
if (fs.existsSync(root)) {
|
|
107
|
+
const runs = fs.readdirSync(root)
|
|
108
|
+
.filter((d) => /^\d{4}-\d{2}-\d{2}/.test(d))
|
|
109
|
+
.sort()
|
|
110
|
+
.reverse();
|
|
111
|
+
for (const run of runs) {
|
|
112
|
+
const candidate = path.join(
|
|
113
|
+
root,
|
|
114
|
+
run,
|
|
115
|
+
theme ? `theme-${theme}` : "default",
|
|
116
|
+
`${captureKey}.mhtml`,
|
|
117
|
+
);
|
|
118
|
+
if (fs.existsSync(candidate)) {
|
|
119
|
+
return { kind: "local", path: candidate };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function variationCommand(options) {
|
|
129
|
+
const {
|
|
130
|
+
source,
|
|
131
|
+
scenario,
|
|
132
|
+
capture,
|
|
133
|
+
theme = "light",
|
|
134
|
+
manifest: manifestPath,
|
|
135
|
+
output,
|
|
136
|
+
headless = true,
|
|
137
|
+
} = options;
|
|
138
|
+
|
|
139
|
+
if (!manifestPath || !fs.existsSync(manifestPath)) {
|
|
140
|
+
throw new Error(`Manifest not found: ${manifestPath}`);
|
|
141
|
+
}
|
|
142
|
+
if (!output) {
|
|
143
|
+
throw new Error("--output <path.png> is required");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
147
|
+
const viewport = manifest.viewport || {
|
|
148
|
+
width: 1440,
|
|
149
|
+
height: 900,
|
|
150
|
+
deviceScaleFactor: 2,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
console.log(chalk.cyan("🎨 Variation render"));
|
|
154
|
+
console.log(chalk.gray(` manifest: ${manifestPath}`));
|
|
155
|
+
|
|
156
|
+
const sourceRef = await resolveSourceMhtml({
|
|
157
|
+
source,
|
|
158
|
+
scenarioKey: scenario,
|
|
159
|
+
captureKey: capture,
|
|
160
|
+
theme,
|
|
161
|
+
});
|
|
162
|
+
if (!sourceRef) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
"Could not resolve source MHTML. Provide --source <path> or --scenario <key> --capture <key>.",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
console.log(chalk.gray(` source: ${sourceRef.path}`));
|
|
168
|
+
|
|
169
|
+
const browser = await chromium.launch({ headless });
|
|
170
|
+
const ctx = await browser.newContext({
|
|
171
|
+
viewport: { width: viewport.width, height: viewport.height },
|
|
172
|
+
deviceScaleFactor: viewport.deviceScaleFactor || 2,
|
|
173
|
+
colorScheme: manifest.colorScheme === "dark" ? "dark" : "light",
|
|
174
|
+
javaScriptEnabled: true,
|
|
175
|
+
});
|
|
176
|
+
const page = await ctx.newPage();
|
|
177
|
+
|
|
178
|
+
const fileUrl = `file://${path.resolve(sourceRef.path)}`;
|
|
179
|
+
await page.goto(fileUrl, { waitUntil: "domcontentloaded" });
|
|
180
|
+
await page.waitForLoadState("load");
|
|
181
|
+
await page.waitForTimeout(400);
|
|
182
|
+
|
|
183
|
+
if (manifest.mutations?.length) {
|
|
184
|
+
await page.evaluate(applyMutationsInBrowser, JSON.stringify(manifest));
|
|
185
|
+
await page.waitForTimeout(200);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fs.ensureDirSync(path.dirname(output));
|
|
189
|
+
await page.screenshot({ path: output });
|
|
190
|
+
console.log(chalk.green(`✔ Rendered: ${output}`));
|
|
191
|
+
await browser.close();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = variationCommand;
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,8 @@ require("dotenv").config({
|
|
|
8
8
|
|
|
9
9
|
const { Command } = require("commander");
|
|
10
10
|
const chalk = require("chalk");
|
|
11
|
+
const fs = require("fs-extra");
|
|
12
|
+
const path = require("path");
|
|
11
13
|
const pkg = require("../package.json");
|
|
12
14
|
|
|
13
15
|
const program = new Command();
|
|
@@ -26,6 +28,8 @@ program
|
|
|
26
28
|
.command("setup")
|
|
27
29
|
.description("Set up the local or hosted workflow from scratch")
|
|
28
30
|
.option("--offline", "Stay local-only and skip hosted authentication")
|
|
31
|
+
.option("--project <id>", "Connect setup to an existing Reshot project")
|
|
32
|
+
.option("--token <token>", "Publish token for non-interactive project linking")
|
|
29
33
|
.option("--no-studio", "Skip offering to launch Studio after setup")
|
|
30
34
|
.option("--force", "Force re-initialization even if already set up")
|
|
31
35
|
.action(async (options) => {
|
|
@@ -38,6 +42,43 @@ program
|
|
|
38
42
|
}
|
|
39
43
|
});
|
|
40
44
|
|
|
45
|
+
program
|
|
46
|
+
.command("record-clip [target]")
|
|
47
|
+
.description("Record a scenario as a summary MP4 (alias for `reshot run --format summary-video`)")
|
|
48
|
+
.option("-s, --scenarios <keys>", "Comma-separated scenario keys")
|
|
49
|
+
.option("--out <dir>", "Copy the generated MP4 and metadata into this directory")
|
|
50
|
+
.option("--no-headless", "Run browser in visible mode")
|
|
51
|
+
.option("--debug", "Enable verbose debug logging")
|
|
52
|
+
.action(async (target, options) => {
|
|
53
|
+
if (options.debug) {
|
|
54
|
+
process.env.RESHOT_DEBUG = "1";
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const runCommand = require("./commands/run");
|
|
58
|
+
const scenarioKeys = resolveRecordClipScenarioKeys(target, options);
|
|
59
|
+
const result = await runCommand({
|
|
60
|
+
scenarioKeys,
|
|
61
|
+
headless: options.headless,
|
|
62
|
+
format: "summary-video",
|
|
63
|
+
noExit: true,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (options.out && result?.success !== false) {
|
|
67
|
+
await copyRecordClipOutputs(result, target, options.out);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (result?.success === false) {
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error(chalk.red("Error:"), error.message);
|
|
75
|
+
if (options.debug && error.stack) {
|
|
76
|
+
console.error(chalk.gray(error.stack));
|
|
77
|
+
}
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
41
82
|
program.addHelpText(
|
|
42
83
|
"after",
|
|
43
84
|
`
|
|
@@ -113,7 +154,7 @@ program
|
|
|
113
154
|
|
|
114
155
|
// Run: Execute scenarios from config (automated visual capture)
|
|
115
156
|
program
|
|
116
|
-
.command("run")
|
|
157
|
+
.command("run [target]")
|
|
117
158
|
.description("Execute visual capture scenarios from config")
|
|
118
159
|
.option("-s, --scenarios <keys>", "Comma-separated list of scenario keys")
|
|
119
160
|
.option("--no-headless", "Run browser in visible mode")
|
|
@@ -135,15 +176,13 @@ program
|
|
|
135
176
|
.option("--no-style", "Disable style processing")
|
|
136
177
|
.option("--cloud", "Compare against cloud baselines")
|
|
137
178
|
.option("--debug", "Enable verbose debug logging")
|
|
138
|
-
.action(async (options) => {
|
|
179
|
+
.action(async (target, options) => {
|
|
139
180
|
if (options.debug) {
|
|
140
181
|
process.env.RESHOT_DEBUG = "1";
|
|
141
182
|
}
|
|
142
183
|
try {
|
|
143
184
|
const runCommand = require("./commands/run");
|
|
144
|
-
const scenarioKeys = options
|
|
145
|
-
? options.scenarios.split(",").map((s) => s.trim())
|
|
146
|
-
: null;
|
|
185
|
+
const scenarioKeys = resolveScenarioKeysFromTarget(target, options);
|
|
147
186
|
await runCommand({
|
|
148
187
|
scenarioKeys,
|
|
149
188
|
headless: options.headless,
|
|
@@ -297,6 +336,18 @@ program
|
|
|
297
336
|
}
|
|
298
337
|
});
|
|
299
338
|
|
|
339
|
+
// Compose: Render a local JSX composition into a video pack
|
|
340
|
+
const { registerCompose } = require("./commands/compose");
|
|
341
|
+
registerCompose(program);
|
|
342
|
+
|
|
343
|
+
// Capture-DOM: Capture a self-contained DOM reconstruction artifact from a URL
|
|
344
|
+
const { registerCaptureDom } = require("./commands/capture-dom");
|
|
345
|
+
registerCaptureDom(program);
|
|
346
|
+
|
|
347
|
+
// Refresh: Phase 5 auto-update loop — recapture, drift-check, re-publish or flag
|
|
348
|
+
const { registerRefresh } = require("./commands/refresh");
|
|
349
|
+
registerRefresh(program);
|
|
350
|
+
|
|
300
351
|
// ============================================================================
|
|
301
352
|
// PUBLISHING & INTEGRATION COMMANDS
|
|
302
353
|
// ============================================================================
|
|
@@ -317,26 +368,51 @@ program
|
|
|
317
368
|
.action(async (options) => {
|
|
318
369
|
try {
|
|
319
370
|
const publishCommand = require("./commands/publish");
|
|
320
|
-
await publishCommand({
|
|
371
|
+
const result = await publishCommand({
|
|
321
372
|
...options,
|
|
322
373
|
outputJson: options.outputJson,
|
|
323
374
|
autoApprove: options.autoApprove,
|
|
324
375
|
skipReleaseDoctor: options.skipReleaseDoctor,
|
|
325
376
|
});
|
|
377
|
+
if (result && result.success === false) {
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
326
380
|
} catch (error) {
|
|
327
381
|
console.error(chalk.red("Error:"), error.message);
|
|
328
382
|
process.exit(1);
|
|
329
383
|
}
|
|
330
384
|
});
|
|
331
385
|
|
|
332
|
-
//
|
|
386
|
+
// Variation: Render a variation from a captured DOM scene (MHTML).
|
|
387
|
+
// Beta — see docs/variation-pipeline.md.
|
|
388
|
+
program
|
|
389
|
+
.command("variation")
|
|
390
|
+
.description("Render a variation from a captured DOM scene (beta)")
|
|
391
|
+
.option("-s, --source <path>", "Path to source .mhtml (overrides --scenario/--capture)")
|
|
392
|
+
.option("--scenario <key>", "Scenario key under .reshot/output/")
|
|
393
|
+
.option("--capture <key>", "Capture key (e.g., 'observation-detail')")
|
|
394
|
+
.option("--theme <name>", "Theme variant: light | dark", "light")
|
|
395
|
+
.option("-m, --manifest <path>", "Path to variation manifest (.json)")
|
|
396
|
+
.option("-o, --output <path>", "Output PNG path")
|
|
397
|
+
.option("--no-headless", "Run browser visibly for debugging")
|
|
398
|
+
.action(async (options) => {
|
|
399
|
+
try {
|
|
400
|
+
const variationCommand = require("./commands/variation");
|
|
401
|
+
await variationCommand(options);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
console.error(chalk.red("Error:"), error.message);
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// Pull: Generate asset map for local workflows
|
|
333
409
|
program
|
|
334
410
|
.command("pull")
|
|
335
|
-
.description("Pull asset map for your
|
|
411
|
+
.description("Pull asset map for your capture workflow")
|
|
336
412
|
.option("-f, --format <format>", "Output format: json, ts, csv", "json")
|
|
337
413
|
.option("-o, --output <path>", "Output file path")
|
|
338
414
|
.option("--full", "Include full metadata in TypeScript output")
|
|
339
|
-
.option("--status <status>", "Filter: approved, pending, all", "
|
|
415
|
+
.option("--status <status>", "Filter: approved, pending, all", "all")
|
|
340
416
|
.action(async (options) => {
|
|
341
417
|
try {
|
|
342
418
|
const pullCommand = require("./commands/pull");
|
|
@@ -359,6 +435,7 @@ doctor
|
|
|
359
435
|
.command("target")
|
|
360
436
|
.description("Audit the certified target contract before capture")
|
|
361
437
|
.option("-s, --scenarios <keys>", "Comma-separated list of scenario keys")
|
|
438
|
+
.option("--timeout <ms>", "Per-step timeout in milliseconds (default 15000)")
|
|
362
439
|
.option("--json", "Output JSON report")
|
|
363
440
|
.action(async (options) => {
|
|
364
441
|
try {
|
|
@@ -424,42 +501,6 @@ program
|
|
|
424
501
|
}
|
|
425
502
|
});
|
|
426
503
|
|
|
427
|
-
// CI: CI/CD integration commands
|
|
428
|
-
const ciCommand = program
|
|
429
|
-
.command("ci")
|
|
430
|
-
.description("CI/CD integration commands");
|
|
431
|
-
|
|
432
|
-
ciCommand
|
|
433
|
-
.command("setup")
|
|
434
|
-
.description("Interactive CI/CD setup wizard (GitHub Actions, etc.)")
|
|
435
|
-
.action(async () => {
|
|
436
|
-
try {
|
|
437
|
-
const ciSetup = require("./commands/ci-setup");
|
|
438
|
-
await ciSetup();
|
|
439
|
-
} catch (error) {
|
|
440
|
-
console.error(chalk.red("Error:"), error.message);
|
|
441
|
-
process.exit(1);
|
|
442
|
-
}
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
ciCommand
|
|
446
|
-
.command("run")
|
|
447
|
-
.description("Run capture + publish in one step (CI-optimized)")
|
|
448
|
-
.option("-c, --config <path>", "Path to reshot.config.json")
|
|
449
|
-
.option("--tag <tag>", "Version tag for publish")
|
|
450
|
-
.option("-m, --message <message>", "Commit message for publish")
|
|
451
|
-
.option("--dry-run", "Preview without uploading")
|
|
452
|
-
.option("--no-publish", "Run capture only, skip publish")
|
|
453
|
-
.option("--skip-release-doctor", "Skip the composed release gate precheck")
|
|
454
|
-
.action(async (options) => {
|
|
455
|
-
try {
|
|
456
|
-
const ciRun = require("./commands/ci-run");
|
|
457
|
-
await ciRun(options);
|
|
458
|
-
} catch (error) {
|
|
459
|
-
console.error(chalk.red("Error:"), error.message);
|
|
460
|
-
process.exit(1);
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
504
|
|
|
464
505
|
// ============================================================================
|
|
465
506
|
// DRIFT MANAGEMENT COMMANDS
|
|
@@ -505,7 +546,23 @@ Actions:
|
|
|
505
546
|
// Auth: Standalone authentication (for re-auth scenarios)
|
|
506
547
|
const auth = program
|
|
507
548
|
.command("auth")
|
|
508
|
-
.description(
|
|
549
|
+
.description(
|
|
550
|
+
"Link this CLI to a Reshot project. Opens a browser to approve the session and stores a project API key locally.",
|
|
551
|
+
)
|
|
552
|
+
.addHelpText(
|
|
553
|
+
"after",
|
|
554
|
+
`
|
|
555
|
+
Authentication paths:
|
|
556
|
+
Interactive (default) Opens your browser, you approve the session, and the
|
|
557
|
+
CLI saves a project API key to .reshot/settings.json.
|
|
558
|
+
Non-interactive (CI) Set RESHOT_API_KEY and RESHOT_PROJECT_ID and the CLI
|
|
559
|
+
links without any browser or prompt.
|
|
560
|
+
|
|
561
|
+
Examples:
|
|
562
|
+
reshot auth Browser-based login
|
|
563
|
+
RESHOT_API_KEY=… RESHOT_PROJECT_ID=… reshot auth Headless / CI login
|
|
564
|
+
`,
|
|
565
|
+
)
|
|
509
566
|
.action(async () => {
|
|
510
567
|
try {
|
|
511
568
|
const authCommand = require("./commands/auth");
|
|
@@ -518,7 +575,24 @@ const auth = program
|
|
|
518
575
|
|
|
519
576
|
auth
|
|
520
577
|
.command("login")
|
|
521
|
-
.description(
|
|
578
|
+
.description(
|
|
579
|
+
"Alias for `reshot auth` — opens the browser approval flow (or uses RESHOT_API_KEY in CI).",
|
|
580
|
+
)
|
|
581
|
+
.action(async () => {
|
|
582
|
+
try {
|
|
583
|
+
const authCommand = require("./commands/auth");
|
|
584
|
+
await authCommand();
|
|
585
|
+
} catch (error) {
|
|
586
|
+
console.error(chalk.red("Error:"), error.message);
|
|
587
|
+
process.exit(1);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
program
|
|
592
|
+
.command("login")
|
|
593
|
+
.description(
|
|
594
|
+
"Alias for `reshot auth` — link this CLI to a project via browser approval (or RESHOT_API_KEY in CI).",
|
|
595
|
+
)
|
|
522
596
|
.action(async () => {
|
|
523
597
|
try {
|
|
524
598
|
const authCommand = require("./commands/auth");
|
|
@@ -559,4 +633,72 @@ program
|
|
|
559
633
|
}
|
|
560
634
|
});
|
|
561
635
|
|
|
636
|
+
function resolveRecordClipScenarioKeys(target, options = {}) {
|
|
637
|
+
return resolveScenarioKeysFromTarget(target, options);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function resolveScenarioKeysFromTarget(target, options = {}) {
|
|
641
|
+
if (options.scenarios) {
|
|
642
|
+
return options.scenarios.split(",").map((value) => value.trim()).filter(Boolean);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (!target) {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const absoluteTarget = path.resolve(process.cwd(), target);
|
|
650
|
+
if (!fs.existsSync(absoluteTarget)) {
|
|
651
|
+
return [target];
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const source = fs.readFileSync(absoluteTarget, "utf8");
|
|
655
|
+
const config = require("./lib/config").readConfig();
|
|
656
|
+
const scenarios = config.scenarios || [];
|
|
657
|
+
const mentioned = scenarios.find((scenario) => source.includes(scenario.key));
|
|
658
|
+
if (mentioned) {
|
|
659
|
+
return [mentioned.key];
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const basename = path.basename(target).replace(/\.(spec\.)?[cm]?[tj]sx?$/i, "");
|
|
663
|
+
const byName = scenarios.find((scenario) => {
|
|
664
|
+
const normalizedKey = String(scenario.key || "").replace(/^dogfood-/, "");
|
|
665
|
+
return scenario.key === basename || normalizedKey === basename;
|
|
666
|
+
});
|
|
667
|
+
if (byName) {
|
|
668
|
+
return [byName.key];
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
throw new Error(
|
|
672
|
+
`Could not map ${target} to a configured scenario. Add the scenario key to the spec file or pass --scenarios <key>.`,
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async function copyRecordClipOutputs(result, target, outDir) {
|
|
677
|
+
const firstScenario = (result.results || []).find((item) => item?.success !== false);
|
|
678
|
+
const outputDir = firstScenario?.outputDir;
|
|
679
|
+
if (!outputDir) {
|
|
680
|
+
throw new Error("No summary-video output directory was produced.");
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const slug = target
|
|
684
|
+
? path.basename(target).replace(/\.(spec\.)?[cm]?[tj]sx?$/i, "")
|
|
685
|
+
: firstScenario.key || "summary-video";
|
|
686
|
+
const destinationDir = path.resolve(process.cwd(), outDir);
|
|
687
|
+
await fs.ensureDir(destinationDir);
|
|
688
|
+
|
|
689
|
+
const copies = [
|
|
690
|
+
["summary-video.mp4", `${slug}.mp4`],
|
|
691
|
+
["summary-video.metadata.json", `${slug}.metadata.json`],
|
|
692
|
+
["sentinels.json", `${slug}.sentinels.json`],
|
|
693
|
+
];
|
|
694
|
+
|
|
695
|
+
for (const [fromName, toName] of copies) {
|
|
696
|
+
const sourcePath = path.join(outputDir, fromName);
|
|
697
|
+
if (await fs.pathExists(sourcePath)) {
|
|
698
|
+
await fs.copy(sourcePath, path.join(destinationDir, toName));
|
|
699
|
+
console.log(chalk.gray(` copied ${toName}`));
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
562
704
|
program.parse(process.argv);
|
package/src/lib/api-client.js
CHANGED
|
@@ -119,6 +119,17 @@ async function withRetry(fn, options = {}) {
|
|
|
119
119
|
throw error;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// Respect Retry-After header on 429 rate limit responses
|
|
123
|
+
const retryAfterHeader = error.response?.headers?.["retry-after"];
|
|
124
|
+
if (retryAfterHeader && statusCode === 429) {
|
|
125
|
+
const retryMs = (parseInt(retryAfterHeader, 10) || 5) * 1000;
|
|
126
|
+
console.log(
|
|
127
|
+
` ⚠ Rate limited (attempt ${attempt}/${maxRetries}), retrying in ${retryMs / 1000}s...`,
|
|
128
|
+
);
|
|
129
|
+
await sleep(retryMs);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
122
133
|
// Calculate delay with exponential backoff + jitter
|
|
123
134
|
const delay = Math.min(
|
|
124
135
|
initialDelay * Math.pow(2, attempt - 1) + Math.random() * 1000,
|
|
@@ -649,17 +660,21 @@ async function publishBatch(apiKey, payload) {
|
|
|
649
660
|
throw new Error("API key is required to publish");
|
|
650
661
|
}
|
|
651
662
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
663
|
+
return withRetry(
|
|
664
|
+
async () => {
|
|
665
|
+
const response = await axios.post(`${baseUrl}/v1/publish/batch`, payload, {
|
|
666
|
+
headers: {
|
|
667
|
+
"Content-Type": "application/json",
|
|
668
|
+
Authorization: `Bearer ${apiKey}`,
|
|
669
|
+
},
|
|
670
|
+
timeout: 60000,
|
|
671
|
+
maxBodyLength: Infinity,
|
|
672
|
+
maxContentLength: Infinity,
|
|
673
|
+
});
|
|
674
|
+
return response.data;
|
|
656
675
|
},
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
maxContentLength: Infinity,
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
return response.data;
|
|
676
|
+
{ maxRetries: 2, retryOn: [500, 502, 503, 504, 429, "ECONNRESET", "ETIMEDOUT"] },
|
|
677
|
+
);
|
|
663
678
|
}
|
|
664
679
|
|
|
665
680
|
/**
|
|
@@ -809,40 +824,49 @@ async function commitIngest(apiKey, projectId, uploadResults, git, cli) {
|
|
|
809
824
|
* Get drift records for a project
|
|
810
825
|
*/
|
|
811
826
|
async function getDrifts(apiKey, projectId, options = {}) {
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
827
|
+
const params = new URLSearchParams();
|
|
828
|
+
if (options.status) params.set("status", options.status);
|
|
829
|
+
if (options.journeyKey) params.set("journeyKey", options.journeyKey);
|
|
830
|
+
const endpoint = `${baseUrl}/v1/projects/${projectId}/drifts?${params.toString()}`;
|
|
816
831
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
{
|
|
832
|
+
try {
|
|
833
|
+
return await withRetry(async () => {
|
|
834
|
+
const response = await axios.get(endpoint, {
|
|
820
835
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
821
836
|
timeout: 30000,
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
})
|
|
837
|
+
});
|
|
838
|
+
return response.data;
|
|
839
|
+
});
|
|
840
|
+
} catch (error) {
|
|
841
|
+
// Surface HTTP status + endpoint + server message instead of axios's
|
|
842
|
+
// bare "Request failed with status code N".
|
|
843
|
+
throw createApiError("Drifts request failed", endpoint, error);
|
|
844
|
+
}
|
|
826
845
|
}
|
|
827
846
|
|
|
828
847
|
/**
|
|
829
848
|
* Get sync jobs for a project
|
|
830
849
|
*/
|
|
831
850
|
async function getSyncJobs(apiKey, projectId, options = {}) {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
851
|
+
const endpoint = `${baseUrl}/v1/projects/${projectId}/sync-jobs`;
|
|
852
|
+
try {
|
|
853
|
+
return await withRetry(async () => {
|
|
854
|
+
const response = await axios.post(
|
|
855
|
+
endpoint,
|
|
856
|
+
{
|
|
857
|
+
limit: options.limit || 10,
|
|
858
|
+
status: options.status,
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
862
|
+
timeout: 30000,
|
|
863
|
+
},
|
|
864
|
+
);
|
|
865
|
+
return response.data;
|
|
866
|
+
});
|
|
867
|
+
} catch (error) {
|
|
868
|
+
throw createApiError("Sync jobs request failed", endpoint, error);
|
|
869
|
+
}
|
|
846
870
|
}
|
|
847
871
|
|
|
848
872
|
/**
|
|
@@ -898,4 +922,6 @@ module.exports = {
|
|
|
898
922
|
getDrifts,
|
|
899
923
|
getSyncJobs,
|
|
900
924
|
driftAction,
|
|
925
|
+
// Testing
|
|
926
|
+
withRetry,
|
|
901
927
|
};
|