@reshotdev/screenshot 0.0.1-beta.2 → 0.0.1-beta.21
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 +138 -47
- package/package.json +27 -16
- package/src/commands/auth.js +159 -30
- package/src/commands/capture-dom.js +50 -0
- package/src/commands/certify.js +62 -0
- package/src/commands/compose.js +220 -0
- package/src/commands/doctor-release.js +74 -0
- package/src/commands/doctor-target.js +108 -0
- package/src/commands/drifts.js +16 -69
- package/src/commands/import-tests.js +13 -13
- package/src/commands/init.js +16 -277
- package/src/commands/publish.js +484 -257
- package/src/commands/pull.js +302 -35
- package/src/commands/refresh.js +166 -0
- package/src/commands/run.js +292 -12
- package/src/commands/setup-wizard.js +348 -496
- package/src/commands/status.js +334 -126
- package/src/commands/sync.js +28 -236
- package/src/commands/ui.js +1 -1
- package/src/commands/variation.js +194 -0
- package/src/commands/verify-publish.js +46 -0
- package/src/index.js +383 -118
- package/src/lib/api-client.js +172 -60
- 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 +179 -9
- package/src/lib/capture-script-runner.js +639 -214
- package/src/lib/certification.js +887 -0
- 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 +186 -81
- package/src/lib/dom-capture.js +64 -0
- package/src/lib/ensure-browser.js +147 -0
- package/src/lib/output-path-template.js +3 -3
- package/src/lib/record-cdp.js +288 -16
- package/src/lib/record-clip.js +83 -3
- package/src/lib/record-config.js +1 -5
- package/src/lib/release-doctor.js +321 -0
- package/src/lib/resolve-targets.js +60 -0
- package/src/lib/run-manifest.js +148 -0
- package/src/lib/standalone-mode.js +1 -1
- package/src/lib/storage-providers.js +5 -5
- package/src/lib/style-engine.js +5 -5
- package/src/lib/target-contract.js +292 -0
- package/src/lib/ui-api-helpers.js +118 -0
- package/src/lib/ui-api.js +31 -824
- 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-D0S2otug.js +507 -0
- package/web/manager/dist/index.html +1 -1
- package/src/commands/ci-run.js +0 -123
- package/src/commands/ci-setup.js +0 -288
- package/src/commands/ingest.js +0 -458
- package/src/commands/setup.js +0 -137
- package/src/commands/validate-docs.js +0 -529
- package/src/lib/playwright-runner.js +0 -252
- package/web/manager/dist/assets/index--ZgioErz.js +0 -507
package/src/commands/pull.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// pull.js - Pull asset map from Reshot platform
|
|
2
|
-
// Generates JSON, TypeScript, or CSV files for
|
|
2
|
+
// Generates JSON, TypeScript, or CSV files for local workflows
|
|
3
3
|
const chalk = require("chalk");
|
|
4
4
|
const fs = require("fs-extra");
|
|
5
5
|
const path = require("path");
|
|
@@ -16,6 +16,160 @@ function toCamelCase(str) {
|
|
|
16
16
|
.replace(/^[A-Z]/, (chr) => chr.toLowerCase());
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function normalizeHostedAssetUrl(urlString) {
|
|
20
|
+
if (!urlString || typeof urlString !== "string") {
|
|
21
|
+
return urlString;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let normalized = urlString;
|
|
25
|
+
if (!normalized.includes("?") && normalized.includes("&")) {
|
|
26
|
+
const firstAmp = normalized.indexOf("&");
|
|
27
|
+
normalized = `${normalized.slice(0, firstAmp)}?${normalized.slice(firstAmp + 1)}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const parsed = new URL(normalized);
|
|
31
|
+
const capture = parsed.searchParams.get("capture");
|
|
32
|
+
if (capture && !parsed.searchParams.get("step")) {
|
|
33
|
+
parsed.searchParams.set("step", capture);
|
|
34
|
+
}
|
|
35
|
+
parsed.searchParams.delete("capture");
|
|
36
|
+
|
|
37
|
+
return parsed.toString();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeHostedAssetUrlWithMeta(urlString) {
|
|
41
|
+
const normalized = normalizeHostedAssetUrl(urlString);
|
|
42
|
+
return {
|
|
43
|
+
url: normalized,
|
|
44
|
+
repaired: normalized !== urlString,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeAssetEntry(entry) {
|
|
49
|
+
if (!entry || typeof entry !== "object") {
|
|
50
|
+
return { entry, repairs: 0 };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (Array.isArray(entry.steps)) {
|
|
54
|
+
return {
|
|
55
|
+
entry: {
|
|
56
|
+
...entry,
|
|
57
|
+
steps: entry.steps.map((step) => {
|
|
58
|
+
const src = normalizeHostedAssetUrlWithMeta(step.src);
|
|
59
|
+
const poster = step.poster
|
|
60
|
+
? normalizeHostedAssetUrlWithMeta(step.poster)
|
|
61
|
+
: null;
|
|
62
|
+
return {
|
|
63
|
+
...step,
|
|
64
|
+
src: src.url,
|
|
65
|
+
poster: poster ? poster.url : step.poster,
|
|
66
|
+
};
|
|
67
|
+
}),
|
|
68
|
+
},
|
|
69
|
+
repairs: entry.steps.reduce((count, step) => {
|
|
70
|
+
const src = normalizeHostedAssetUrlWithMeta(step.src);
|
|
71
|
+
const poster = step.poster
|
|
72
|
+
? normalizeHostedAssetUrlWithMeta(step.poster)
|
|
73
|
+
: null;
|
|
74
|
+
return count + (src.repaired ? 1 : 0) + (poster?.repaired ? 1 : 0);
|
|
75
|
+
}, 0),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
entry: {
|
|
81
|
+
...entry,
|
|
82
|
+
src: entry.src ? normalizeHostedAssetUrl(entry.src) : entry.src,
|
|
83
|
+
poster: entry.poster ? normalizeHostedAssetUrl(entry.poster) : entry.poster,
|
|
84
|
+
},
|
|
85
|
+
repairs:
|
|
86
|
+
(entry.src && normalizeHostedAssetUrlWithMeta(entry.src).repaired ? 1 : 0) +
|
|
87
|
+
(entry.poster && normalizeHostedAssetUrlWithMeta(entry.poster).repaired ? 1 : 0),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeAssetMap(assets) {
|
|
92
|
+
const normalized = {};
|
|
93
|
+
let repairs = 0;
|
|
94
|
+
|
|
95
|
+
for (const [group, visuals] of Object.entries(assets || {})) {
|
|
96
|
+
normalized[group] = {};
|
|
97
|
+
|
|
98
|
+
for (const [visualKey, variants] of Object.entries(visuals || {})) {
|
|
99
|
+
normalized[group][visualKey] = {};
|
|
100
|
+
|
|
101
|
+
for (const [variant, entry] of Object.entries(variants || {})) {
|
|
102
|
+
const normalizedEntry = normalizeAssetEntry(entry);
|
|
103
|
+
normalized[group][visualKey][variant] = normalizedEntry.entry;
|
|
104
|
+
repairs += normalizedEntry.repairs;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { assets: normalized, repairs };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Pick a real {group, visualKey, context} from a normalized asset map so the
|
|
114
|
+
* printed usage example is copy-pasteable for THIS project (prefers the
|
|
115
|
+
* "default" context). Returns null if the map is empty.
|
|
116
|
+
*/
|
|
117
|
+
function deriveUsageSample(assets) {
|
|
118
|
+
for (const group of Object.keys(assets || {})) {
|
|
119
|
+
for (const visualKey of Object.keys(assets[group] || {})) {
|
|
120
|
+
const contexts = Object.keys(assets[group][visualKey] || {});
|
|
121
|
+
if (contexts.length === 0) continue;
|
|
122
|
+
const context = contexts.includes("default") ? "default" : contexts[0];
|
|
123
|
+
// A capture entry is either flat ({src,width,height}) or step-based
|
|
124
|
+
// ({steps:[{src,...}]}). Tell the caller which, so the printed example
|
|
125
|
+
// references the right path (`.steps[0].src` vs `.src`) and doesn't
|
|
126
|
+
// produce an undefined src (audit run-12 MED-1).
|
|
127
|
+
const entry = assets[group][visualKey][context];
|
|
128
|
+
const stepped = !!(entry && Array.isArray(entry.steps));
|
|
129
|
+
return { group, visualKey, context, stepped };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function validateHostedAssetUrl(urlString) {
|
|
136
|
+
if (!urlString || typeof urlString !== "string") {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!urlString.includes("?") && (urlString.includes("&step=") || urlString.includes("&poster="))) {
|
|
141
|
+
throw new Error(`Malformed asset URL emitted without query delimiter: ${urlString}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const parsed = new URL(urlString);
|
|
145
|
+
if (parsed.searchParams.has("capture")) {
|
|
146
|
+
throw new Error(`Legacy capture param emitted in normalized output: ${urlString}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function validateAssetMap(assets) {
|
|
151
|
+
for (const visuals of Object.values(assets || {})) {
|
|
152
|
+
for (const variants of Object.values(visuals || {})) {
|
|
153
|
+
for (const entry of Object.values(variants || {})) {
|
|
154
|
+
if (entry && typeof entry === "object" && Array.isArray(entry.steps)) {
|
|
155
|
+
for (const step of entry.steps) {
|
|
156
|
+
validateHostedAssetUrl(step.src);
|
|
157
|
+
if (step.poster) {
|
|
158
|
+
validateHostedAssetUrl(step.poster);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
validateHostedAssetUrl(entry?.src);
|
|
165
|
+
if (entry?.poster) {
|
|
166
|
+
validateHostedAssetUrl(entry.poster);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
19
173
|
/**
|
|
20
174
|
* Generate TypeScript code from the assets object
|
|
21
175
|
* This provides type safety - if a visual is deleted, the build will fail
|
|
@@ -39,12 +193,27 @@ function generateTypeScript(assets, includeMetadata = false) {
|
|
|
39
193
|
|
|
40
194
|
for (const [variant, asset] of Object.entries(variants)) {
|
|
41
195
|
lines.push(` ${variant}: {`);
|
|
42
|
-
lines.push(` src: "${asset.src}",`);
|
|
43
196
|
lines.push(` type: "${asset.type}",`);
|
|
44
|
-
if (asset.width) lines.push(` width: ${asset.width},`);
|
|
45
|
-
if (asset.height) lines.push(` height: ${asset.height},`);
|
|
46
197
|
lines.push(` alt: "${asset.alt.replace(/"/g, '\\"')}",`);
|
|
47
|
-
if (asset.
|
|
198
|
+
if (Array.isArray(asset.steps)) {
|
|
199
|
+
lines.push(` steps: [`);
|
|
200
|
+
for (const step of asset.steps) {
|
|
201
|
+
lines.push(" {");
|
|
202
|
+
lines.push(` step: "${step.step}",`);
|
|
203
|
+
lines.push(` src: "${step.src}",`);
|
|
204
|
+
lines.push(` type: "${step.type}",`);
|
|
205
|
+
if (step.width) lines.push(` width: ${step.width},`);
|
|
206
|
+
if (step.height) lines.push(` height: ${step.height},`);
|
|
207
|
+
if (step.poster) lines.push(` poster: "${step.poster}",`);
|
|
208
|
+
lines.push(" },");
|
|
209
|
+
}
|
|
210
|
+
lines.push(" ],");
|
|
211
|
+
} else {
|
|
212
|
+
lines.push(` src: "${asset.src}",`);
|
|
213
|
+
if (asset.width) lines.push(` width: ${asset.width},`);
|
|
214
|
+
if (asset.height) lines.push(` height: ${asset.height},`);
|
|
215
|
+
if (asset.poster) lines.push(` poster: "${asset.poster}",`);
|
|
216
|
+
}
|
|
48
217
|
lines.push(` },`);
|
|
49
218
|
}
|
|
50
219
|
|
|
@@ -67,11 +236,19 @@ function generateTypeScript(assets, includeMetadata = false) {
|
|
|
67
236
|
for (const [visualKey, variants] of Object.entries(visuals)) {
|
|
68
237
|
// If there's only a default variant, flatten it
|
|
69
238
|
if (Object.keys(variants).length === 1 && variants.default) {
|
|
70
|
-
|
|
239
|
+
if (Array.isArray(variants.default.steps)) {
|
|
240
|
+
lines.push(` ${visualKey}: ${JSON.stringify(variants.default.steps)},`);
|
|
241
|
+
} else {
|
|
242
|
+
lines.push(` ${visualKey}: "${variants.default.src}",`);
|
|
243
|
+
}
|
|
71
244
|
} else {
|
|
72
245
|
lines.push(` ${visualKey}: {`);
|
|
73
246
|
for (const [variant, asset] of Object.entries(variants)) {
|
|
74
|
-
|
|
247
|
+
if (Array.isArray(asset.steps)) {
|
|
248
|
+
lines.push(` ${variant}: ${JSON.stringify(asset.steps)},`);
|
|
249
|
+
} else {
|
|
250
|
+
lines.push(` ${variant}: "${asset.src}",`);
|
|
251
|
+
}
|
|
75
252
|
}
|
|
76
253
|
lines.push(` },`);
|
|
77
254
|
}
|
|
@@ -101,7 +278,8 @@ async function pullCommand(options = {}) {
|
|
|
101
278
|
format = "json",
|
|
102
279
|
output = null,
|
|
103
280
|
full = false,
|
|
104
|
-
status = "
|
|
281
|
+
status = "all",
|
|
282
|
+
noExit = false,
|
|
105
283
|
} = options;
|
|
106
284
|
|
|
107
285
|
console.log(chalk.blue("⬇ Pulling asset map from Reshot...\n"));
|
|
@@ -112,17 +290,33 @@ async function pullCommand(options = {}) {
|
|
|
112
290
|
projectConfig = config.readConfig();
|
|
113
291
|
} catch (e) {
|
|
114
292
|
console.error(
|
|
115
|
-
chalk.red("Error: No
|
|
293
|
+
chalk.red("Error: No reshot.config.json found. Run 'reshot init' first.")
|
|
116
294
|
);
|
|
117
|
-
process.exit(1);
|
|
295
|
+
if (!noExit) process.exit(1);
|
|
296
|
+
return { success: false, error: "No reshot.config.json found" };
|
|
118
297
|
}
|
|
119
298
|
|
|
120
|
-
|
|
299
|
+
let projectId =
|
|
300
|
+
process.env.RESHOT_PROJECT_ID ||
|
|
301
|
+
projectConfig._metadata?.projectId ||
|
|
302
|
+
projectConfig.projectId;
|
|
121
303
|
if (!projectId) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
304
|
+
// Fall back to reading projectId from .reshot/settings.json (written by `reshot auth`)
|
|
305
|
+
try {
|
|
306
|
+
const settings = config.readSettings();
|
|
307
|
+
if (settings?.projectId) {
|
|
308
|
+
projectId = settings.projectId;
|
|
309
|
+
}
|
|
310
|
+
} catch (e) {
|
|
311
|
+
// No settings file either
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (!projectId) {
|
|
315
|
+
console.error(
|
|
316
|
+
chalk.red("Error: No projectId found. Add it to reshot.config.json or run 'reshot auth' first.")
|
|
317
|
+
);
|
|
318
|
+
if (!noExit) process.exit(1);
|
|
319
|
+
return { success: false, error: "No projectId found" };
|
|
126
320
|
}
|
|
127
321
|
|
|
128
322
|
// Check authentication
|
|
@@ -133,13 +327,28 @@ async function pullCommand(options = {}) {
|
|
|
133
327
|
console.error(
|
|
134
328
|
chalk.red("Error: Not authenticated. Run 'reshot auth' first.")
|
|
135
329
|
);
|
|
136
|
-
process.exit(1);
|
|
330
|
+
if (!noExit) process.exit(1);
|
|
331
|
+
return { success: false, error: "Not authenticated" };
|
|
137
332
|
}
|
|
138
333
|
if (!settings?.apiKey) {
|
|
139
334
|
console.error(
|
|
140
335
|
chalk.red("Error: Not authenticated. Run 'reshot auth' first.")
|
|
141
336
|
);
|
|
142
|
-
process.exit(1);
|
|
337
|
+
if (!noExit) process.exit(1);
|
|
338
|
+
return { success: false, error: "Not authenticated" };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (settings.projectId && projectId !== settings.projectId) {
|
|
342
|
+
console.warn(
|
|
343
|
+
chalk.yellow(
|
|
344
|
+
` ⚠ Project ID mismatch: active project is ${projectId}, but authenticated project is ${settings.projectId}.`
|
|
345
|
+
)
|
|
346
|
+
);
|
|
347
|
+
console.warn(
|
|
348
|
+
chalk.yellow(
|
|
349
|
+
` This will cause auth failures. Run 'reshot setup --force' to re-link.\n`
|
|
350
|
+
)
|
|
351
|
+
);
|
|
143
352
|
}
|
|
144
353
|
|
|
145
354
|
try {
|
|
@@ -154,16 +363,34 @@ async function pullCommand(options = {}) {
|
|
|
154
363
|
|
|
155
364
|
if (!response || !response.assets) {
|
|
156
365
|
console.error(chalk.red("Error: Invalid response from API"));
|
|
157
|
-
process.exit(1);
|
|
366
|
+
if (!noExit) process.exit(1);
|
|
367
|
+
return { success: false, error: "Invalid response from API" };
|
|
158
368
|
}
|
|
159
369
|
|
|
160
|
-
const
|
|
161
|
-
const
|
|
370
|
+
const normalization = normalizeAssetMap(response.assets);
|
|
371
|
+
const normalizedAssets = normalization.assets;
|
|
372
|
+
validateAssetMap(normalizedAssets);
|
|
373
|
+
|
|
374
|
+
const { meta } = response;
|
|
375
|
+
const assetCount = Object.values(normalizedAssets).reduce(
|
|
162
376
|
(count, group) => count + Object.keys(group).length,
|
|
163
377
|
0
|
|
164
378
|
);
|
|
165
379
|
|
|
166
|
-
|
|
380
|
+
if (assetCount === 0) {
|
|
381
|
+
console.log(chalk.yellow(` ⚠ Fetched 0 visuals.\n`));
|
|
382
|
+
console.log(chalk.gray(` Possible reasons:`));
|
|
383
|
+
if (status === "approved") {
|
|
384
|
+
console.log(chalk.gray(` • No visuals approved yet. Try: reshot pull --status all`));
|
|
385
|
+
} else if (status === "pending") {
|
|
386
|
+
console.log(chalk.gray(` • No pending visuals. Try: reshot pull --status all`));
|
|
387
|
+
}
|
|
388
|
+
console.log(chalk.gray(` • No visuals published. Run: reshot publish`));
|
|
389
|
+
console.log(chalk.gray(` • Wrong project. Active project: ${projectId}`));
|
|
390
|
+
console.log(chalk.gray(` • Check: ${settings.platformUrl || "https://reshot.dev"}\n`));
|
|
391
|
+
} else {
|
|
392
|
+
console.log(chalk.green(` ✓ Fetched ${assetCount} visuals\n`));
|
|
393
|
+
}
|
|
167
394
|
|
|
168
395
|
// Determine output path
|
|
169
396
|
let outputPath = output;
|
|
@@ -187,7 +414,7 @@ async function pullCommand(options = {}) {
|
|
|
187
414
|
|
|
188
415
|
switch (format) {
|
|
189
416
|
case "ts":
|
|
190
|
-
content = generateTypeScript(
|
|
417
|
+
content = generateTypeScript(normalizedAssets, full);
|
|
191
418
|
contentType = "TypeScript";
|
|
192
419
|
break;
|
|
193
420
|
|
|
@@ -196,9 +423,28 @@ async function pullCommand(options = {}) {
|
|
|
196
423
|
const headers = ["Key", "Group", "Name", "Context", "Type", "Unbreakable_URL", "Width", "Height"];
|
|
197
424
|
const rows = [headers.join(",")];
|
|
198
425
|
|
|
199
|
-
for (const [group, visuals] of Object.entries(
|
|
426
|
+
for (const [group, visuals] of Object.entries(normalizedAssets)) {
|
|
200
427
|
for (const [visualKey, variants] of Object.entries(visuals)) {
|
|
201
428
|
for (const [variant, asset] of Object.entries(variants)) {
|
|
429
|
+
if (Array.isArray(asset.steps)) {
|
|
430
|
+
for (const step of asset.steps) {
|
|
431
|
+
const type = step.type.split("/")[0];
|
|
432
|
+
rows.push(
|
|
433
|
+
[
|
|
434
|
+
`${group}/${visualKey}/${step.step}`,
|
|
435
|
+
group,
|
|
436
|
+
visualKey,
|
|
437
|
+
variant,
|
|
438
|
+
type,
|
|
439
|
+
step.src,
|
|
440
|
+
step.width || "",
|
|
441
|
+
step.height || "",
|
|
442
|
+
].join(",")
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
|
|
202
448
|
const type = asset.type.split("/")[0]; // 'image' or 'video'
|
|
203
449
|
rows.push(
|
|
204
450
|
[
|
|
@@ -222,7 +468,7 @@ async function pullCommand(options = {}) {
|
|
|
222
468
|
|
|
223
469
|
case "json":
|
|
224
470
|
default:
|
|
225
|
-
content = JSON.stringify({ meta, assets }, null, 2);
|
|
471
|
+
content = JSON.stringify({ meta, assets: normalizedAssets }, null, 2);
|
|
226
472
|
contentType = "JSON";
|
|
227
473
|
break;
|
|
228
474
|
}
|
|
@@ -233,6 +479,16 @@ async function pullCommand(options = {}) {
|
|
|
233
479
|
console.log(chalk.green(`✓ Generated ${contentType} file: ${outputPath}`));
|
|
234
480
|
console.log(chalk.gray(` ${assetCount} visuals exported\n`));
|
|
235
481
|
|
|
482
|
+
// Derive a REAL key path from the pulled map so the printed usage example
|
|
483
|
+
// is copy-pasteable (was hardcoded to a non-existent `dashboard.mainView`).
|
|
484
|
+
const sample = deriveUsageSample(normalizedAssets);
|
|
485
|
+
const sGroup = sample?.group ?? "dashboard";
|
|
486
|
+
const sVisual = sample?.visualKey ?? "mainView";
|
|
487
|
+
const sCtx = sample?.context ?? "default";
|
|
488
|
+
// Step-based capture entries nest the fields under `steps[0]`; reference it
|
|
489
|
+
// so the printed example's src/width/height aren't undefined (run-12 MED-1).
|
|
490
|
+
const sStep = sample?.stepped ? ".steps[0]" : "";
|
|
491
|
+
|
|
236
492
|
// Show format-specific usage instructions
|
|
237
493
|
if (format === "ts") {
|
|
238
494
|
console.log(chalk.blue("━━━ TypeScript Usage ━━━\n"));
|
|
@@ -240,10 +496,9 @@ async function pullCommand(options = {}) {
|
|
|
240
496
|
console.log(chalk.cyan(' import { ReshotVisuals } from "./visuals";\n'));
|
|
241
497
|
console.log(chalk.white("Use with full autocomplete support:\n"));
|
|
242
498
|
console.log(chalk.cyan(" // Single context (flattened)"));
|
|
243
|
-
console.log(chalk.cyan(
|
|
499
|
+
console.log(chalk.cyan(` <img src={ReshotVisuals.${sGroup}.${sVisual}} />\n`));
|
|
244
500
|
console.log(chalk.cyan(" // Multiple contexts (dark mode, locales)"));
|
|
245
|
-
console.log(chalk.cyan(
|
|
246
|
-
console.log(chalk.cyan(" <img src={ReshotVisuals.dashboard.mainView.dark} />\n"));
|
|
501
|
+
console.log(chalk.cyan(` <img src={ReshotVisuals.${sGroup}.${sVisual}.${sCtx}} />\n`));
|
|
247
502
|
if (!full) {
|
|
248
503
|
console.log(chalk.gray(" Tip: Use --full flag to include width, height, and alt text\n"));
|
|
249
504
|
}
|
|
@@ -253,10 +508,10 @@ async function pullCommand(options = {}) {
|
|
|
253
508
|
console.log(chalk.cyan(' import assets from "./reshot-assets.json";\n'));
|
|
254
509
|
console.log(chalk.white("Access assets with dot notation:\n"));
|
|
255
510
|
console.log(chalk.cyan(" <img"));
|
|
256
|
-
console.log(chalk.cyan(
|
|
257
|
-
console.log(chalk.cyan(
|
|
258
|
-
console.log(chalk.cyan(
|
|
259
|
-
console.log(chalk.cyan(
|
|
511
|
+
console.log(chalk.cyan(` src={assets.assets.${sGroup}.${sVisual}.${sCtx}${sStep}.src}`));
|
|
512
|
+
console.log(chalk.cyan(` width={assets.assets.${sGroup}.${sVisual}.${sCtx}${sStep}.width}`));
|
|
513
|
+
console.log(chalk.cyan(` height={assets.assets.${sGroup}.${sVisual}.${sCtx}${sStep}.height}`));
|
|
514
|
+
console.log(chalk.cyan(` alt="Your descriptive alt text"`));
|
|
260
515
|
console.log(chalk.cyan(" />\n"));
|
|
261
516
|
} else if (format === "csv") {
|
|
262
517
|
console.log(chalk.blue("━━━ CSV Usage ━━━\n"));
|
|
@@ -275,9 +530,9 @@ async function pullCommand(options = {}) {
|
|
|
275
530
|
console.log(chalk.gray(" • Width/Height - Dimensions in pixels\n"));
|
|
276
531
|
}
|
|
277
532
|
|
|
278
|
-
//
|
|
279
|
-
console.log(chalk.blue("━━━
|
|
280
|
-
console.log(chalk.white("Add to your
|
|
533
|
+
// Workflow integration tip
|
|
534
|
+
console.log(chalk.blue("━━━ Workflow Integration ━━━\n"));
|
|
535
|
+
console.log(chalk.white("Add to your local workflow:\n"));
|
|
281
536
|
console.log(chalk.cyan(" # package.json"));
|
|
282
537
|
console.log(chalk.cyan(' "scripts": {'));
|
|
283
538
|
console.log(chalk.cyan(` "prebuild": "npx reshot pull --format=${format}${output ? ` --output=${output}` : ''}"`));
|
|
@@ -285,6 +540,15 @@ async function pullCommand(options = {}) {
|
|
|
285
540
|
console.log(chalk.gray(" This ensures your build always uses the latest approved visuals."));
|
|
286
541
|
console.log(chalk.gray(" If a visual is deleted, your build will fail (preventing broken images).\n"));
|
|
287
542
|
|
|
543
|
+
return {
|
|
544
|
+
success: true,
|
|
545
|
+
assetCount,
|
|
546
|
+
outputPath: path.resolve(outputPath),
|
|
547
|
+
normalizedAssets,
|
|
548
|
+
normalizationRepairs: normalization.repairs,
|
|
549
|
+
meta,
|
|
550
|
+
};
|
|
551
|
+
|
|
288
552
|
} catch (error) {
|
|
289
553
|
if (config.isAuthError(error)) {
|
|
290
554
|
console.error(
|
|
@@ -296,7 +560,10 @@ async function pullCommand(options = {}) {
|
|
|
296
560
|
console.error(chalk.gray(error.stack));
|
|
297
561
|
}
|
|
298
562
|
}
|
|
299
|
-
|
|
563
|
+
if (!noExit) {
|
|
564
|
+
process.exit(1);
|
|
565
|
+
}
|
|
566
|
+
return { success: false, error: error.message };
|
|
300
567
|
}
|
|
301
568
|
}
|
|
302
569
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// refresh.js — Phase 5 auto-update loop (CI-runnable).
|
|
2
|
+
//
|
|
3
|
+
// `reshot refresh --composition <id>` or `reshot refresh --project <id>`:
|
|
4
|
+
// recapture each composition's source screen, run the calibrated drift check, and
|
|
5
|
+
// either re-publish (data changed, structure-stable) or flag for human review
|
|
6
|
+
// (structural redesign / lost eligibility). Idempotent: a re-run with no source
|
|
7
|
+
// changes is a no-op (0 renders, 0 review items).
|
|
8
|
+
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
const chalk = require("chalk");
|
|
11
|
+
const { refresh, registerComposition, setScene } = require("../lib/auto-update/refresh");
|
|
12
|
+
|
|
13
|
+
// Read + parse the --scene <path> JSON (the spec.scene render config: camera/motion
|
|
14
|
+
// /timeline/targets/...). Returns undefined when no path was given.
|
|
15
|
+
function readSceneFile(scenePath) {
|
|
16
|
+
if (!scenePath) return undefined;
|
|
17
|
+
let raw;
|
|
18
|
+
try {
|
|
19
|
+
raw = fs.readFileSync(scenePath, "utf8");
|
|
20
|
+
} catch (error) {
|
|
21
|
+
throw new Error(`--scene: cannot read ${scenePath}: ${error.message}`);
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(raw);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw new Error(`--scene: ${scenePath} is not valid JSON: ${error.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseViewport(spec) {
|
|
31
|
+
if (!spec) return undefined;
|
|
32
|
+
const m = /^(\d+)x(\d+)(?:@(\d+(?:\.\d+)?))?$/.exec(String(spec).trim());
|
|
33
|
+
if (!m) throw new Error(`Invalid --viewport "${spec}" (expected WxH or WxH@scale, e.g. 1280x900 or 1280x900@2)`);
|
|
34
|
+
return { width: Number(m[1]), height: Number(m[2]), deviceScaleFactor: m[3] ? Number(m[3]) : 2 };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Build the capture-auth config (stored on the spec, re-resolved on every recapture)
|
|
38
|
+
// so the loop can reach authenticated /app screens. --storage-state wins if both set.
|
|
39
|
+
function buildAuth(options) {
|
|
40
|
+
if (options.storageState) return { mode: "storage-state", path: options.storageState };
|
|
41
|
+
if (options.demoAuth) {
|
|
42
|
+
return { mode: "demo-bootstrap", email: typeof options.demoAuth === "string" ? options.demoAuth : "demo@example.com" };
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const ACTION_LABEL = {
|
|
48
|
+
publish: chalk.green("published"),
|
|
49
|
+
flag: chalk.yellow("flagged"),
|
|
50
|
+
skip: chalk.gray("skipped"),
|
|
51
|
+
error: chalk.red("errored"),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function printSummary(result) {
|
|
55
|
+
console.log(chalk.cyan("\nReshot refresh — per-composition result\n"));
|
|
56
|
+
for (const s of result.summaries) {
|
|
57
|
+
const label = ACTION_LABEL[s.action] || s.action;
|
|
58
|
+
const metrics = s.metrics
|
|
59
|
+
? ` diff=${s.metrics.pixelDiffPct}% ssim=${s.metrics.ssim}`
|
|
60
|
+
: "";
|
|
61
|
+
console.log(
|
|
62
|
+
` ${label} ${chalk.bold(s.slug)} (${s.compositionId})\n` +
|
|
63
|
+
` route=${s.route} eligible=${s.eligible} structureStable=${s.structureStable} quality=${s.qualityPass}${metrics}\n` +
|
|
64
|
+
` ${chalk.gray(s.reason)}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
console.log(
|
|
68
|
+
chalk.cyan(
|
|
69
|
+
`\nrendersCreated=${result.rendersCreated} reviewItemsCreated=${result.reviewItemsCreated} ` +
|
|
70
|
+
`published=${result.published} flagged=${result.flagged} skipped=${result.skipped} errors=${result.errors}`,
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function refreshCommand(options = {}) {
|
|
76
|
+
// Attach/update the crisp <Scene>+motion render config on an already-enrolled
|
|
77
|
+
// composition (so the loop renders it animated instead of the video fallback).
|
|
78
|
+
if (options.setScene) {
|
|
79
|
+
const result = await setScene(options.composition, readSceneFile(options.scene));
|
|
80
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
81
|
+
else console.log(chalk.green("scene set ") + chalk.bold(result.compositionId) + ` (render=${result.mode})`);
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (options.register) {
|
|
86
|
+
const result = await registerComposition({
|
|
87
|
+
compositionId: options.composition,
|
|
88
|
+
projectId: options.project || process.env.RESHOT_PROJECT_ID,
|
|
89
|
+
url: options.url,
|
|
90
|
+
viewport: parseViewport(options.viewport),
|
|
91
|
+
composePath: options.compose,
|
|
92
|
+
slug: options.slug,
|
|
93
|
+
name: options.name,
|
|
94
|
+
scene: readSceneFile(options.scene),
|
|
95
|
+
auth: buildAuth(options),
|
|
96
|
+
});
|
|
97
|
+
if (options.json) {
|
|
98
|
+
console.log(JSON.stringify(result, null, 2));
|
|
99
|
+
} else {
|
|
100
|
+
console.log(
|
|
101
|
+
chalk.green("registered ") +
|
|
102
|
+
chalk.bold(result.slug || result.compositionId) +
|
|
103
|
+
` (route=${result.route}, eligible=${result.eligible}, render=${result.render})`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!options.composition && !options.project) {
|
|
110
|
+
throw new Error("Pass --composition <id> or --project <id> (or --register to enroll one).");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const result = await refresh({
|
|
114
|
+
compositionId: options.composition,
|
|
115
|
+
projectId: options.project,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (options.json) {
|
|
119
|
+
console.log(JSON.stringify(result, null, 2));
|
|
120
|
+
} else {
|
|
121
|
+
printSummary(result);
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function registerRefresh(program) {
|
|
127
|
+
program
|
|
128
|
+
.command("refresh")
|
|
129
|
+
.description("Recapture composition source screens and auto re-publish or flag drift for review")
|
|
130
|
+
.option("--composition <id>", "Refresh a single composition by id")
|
|
131
|
+
.option("--project <id>", "Refresh every composition with a stored spec in this project")
|
|
132
|
+
.option("--register", "Enroll a composition into the loop: capture its source screen as the accepted baseline")
|
|
133
|
+
.option("--set-scene", "Attach/update the crisp <Scene>+motion render config on an enrolled composition (with --composition + --scene)")
|
|
134
|
+
.option("--scene <path>", "[--register|--set-scene] JSON file with the scene render config (camera/motion/timeline/targets); without it the loop renders video")
|
|
135
|
+
.option("--url <url>", "[--register] source screen URL to recapture")
|
|
136
|
+
.option("--viewport <WxH>", "[--register] capture viewport, e.g. 1280x900 or 1280x900@2")
|
|
137
|
+
.option("--compose <path>", "[--register] path to the composition's .compose.tsx for re-rendering on publish")
|
|
138
|
+
.option("--slug <slug>", "[--register] composition slug (for the upload route + public URL)")
|
|
139
|
+
.option("--name <name>", "[--register] composition display name")
|
|
140
|
+
.option("--demo-auth [email]", "[--register] authenticate the capture via the seeded demo bootstrap (default email demo@example.com) — reaches /app screens")
|
|
141
|
+
.option("--storage-state <path>", "[--register] authenticate the capture with a Playwright storageState JSON exported from a real session")
|
|
142
|
+
.option("--json", "Output the structured result as JSON")
|
|
143
|
+
.option("--fail-on-error", "Exit non-zero (2) if any composition errored — for CI gating")
|
|
144
|
+
.action(async (options) => {
|
|
145
|
+
try {
|
|
146
|
+
const result = await refreshCommand(options);
|
|
147
|
+
if (shouldFailOnError(options, result)) {
|
|
148
|
+
console.error(chalk.red(`Refresh completed with ${result.errors} error(s) (--fail-on-error).`));
|
|
149
|
+
process.exit(2);
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error(chalk.red("Error:"), error.message);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// CI gating: a completed refresh that had per-composition errors (a screen failed
|
|
159
|
+
// to capture/upload) still exits 0 by default (the batch is isolated, fail-safe).
|
|
160
|
+
// With --fail-on-error, CI can treat that as a non-zero (exit 2) — distinct from a
|
|
161
|
+
// hard crash (exit 1). Pure + exported so it is unit-testable without process.exit.
|
|
162
|
+
function shouldFailOnError(options, result) {
|
|
163
|
+
return Boolean(options && options.failOnError && result && result.errors > 0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { refreshCommand, registerRefresh, printSummary, shouldFailOnError };
|