@reshotdev/screenshot 0.0.1-beta.2 → 0.0.1-beta.20

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.
Files changed (81) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +138 -47
  3. package/package.json +27 -16
  4. package/src/commands/auth.js +159 -30
  5. package/src/commands/capture-dom.js +50 -0
  6. package/src/commands/certify.js +62 -0
  7. package/src/commands/compose.js +220 -0
  8. package/src/commands/doctor-release.js +74 -0
  9. package/src/commands/doctor-target.js +108 -0
  10. package/src/commands/drifts.js +16 -69
  11. package/src/commands/import-tests.js +13 -13
  12. package/src/commands/init.js +16 -277
  13. package/src/commands/publish.js +484 -257
  14. package/src/commands/pull.js +302 -35
  15. package/src/commands/refresh.js +166 -0
  16. package/src/commands/run.js +292 -12
  17. package/src/commands/setup-wizard.js +348 -496
  18. package/src/commands/status.js +334 -126
  19. package/src/commands/sync.js +28 -236
  20. package/src/commands/ui.js +1 -1
  21. package/src/commands/variation.js +194 -0
  22. package/src/commands/verify-publish.js +46 -0
  23. package/src/index.js +383 -118
  24. package/src/lib/api-client.js +172 -60
  25. package/src/lib/auto-update/refresh.js +598 -0
  26. package/src/lib/auto-update/scene-runtime.compose.tsx +73 -0
  27. package/src/lib/auto-update/spec.js +89 -0
  28. package/src/lib/capture-engine.js +179 -9
  29. package/src/lib/capture-script-runner.js +639 -214
  30. package/src/lib/certification.js +887 -0
  31. package/src/lib/compose-context.js +156 -0
  32. package/src/lib/compose-pack.js +42 -0
  33. package/src/lib/compose-runtime.js +34 -0
  34. package/src/lib/compose-upload.js +142 -0
  35. package/src/lib/config.js +186 -81
  36. package/src/lib/dom-capture.js +64 -0
  37. package/src/lib/ensure-browser.js +147 -0
  38. package/src/lib/output-path-template.js +3 -3
  39. package/src/lib/record-cdp.js +288 -16
  40. package/src/lib/record-clip.js +83 -3
  41. package/src/lib/record-config.js +1 -5
  42. package/src/lib/release-doctor.js +321 -0
  43. package/src/lib/resolve-targets.js +60 -0
  44. package/src/lib/run-manifest.js +148 -0
  45. package/src/lib/standalone-mode.js +1 -1
  46. package/src/lib/storage-providers.js +5 -5
  47. package/src/lib/style-engine.js +5 -5
  48. package/src/lib/target-contract.js +292 -0
  49. package/src/lib/ui-api-helpers.js +118 -0
  50. package/src/lib/ui-api.js +31 -824
  51. package/src/lib/ui-asset-cleanup.js +62 -0
  52. package/src/lib/ui-output-versions.js +165 -0
  53. package/src/lib/ui-recorder-routes.js +341 -0
  54. package/src/lib/ui-scenario-metadata.js +161 -0
  55. package/vendor/compose/dist/auto-update.cjs +5544 -0
  56. package/vendor/compose/dist/auto-update.mjs +5518 -0
  57. package/vendor/compose/dist/capture.cjs +1450 -0
  58. package/vendor/compose/dist/capture.mjs +1416 -0
  59. package/vendor/compose/dist/eligibility.cjs +5331 -0
  60. package/vendor/compose/dist/eligibility.mjs +5313 -0
  61. package/vendor/compose/dist/index.cjs +2046 -0
  62. package/vendor/compose/dist/index.mjs +1997 -0
  63. package/vendor/compose/dist/jsx-dev-runtime.cjs +55 -0
  64. package/vendor/compose/dist/jsx-dev-runtime.mjs +27 -0
  65. package/vendor/compose/dist/jsx-runtime.cjs +58 -0
  66. package/vendor/compose/dist/jsx-runtime.mjs +31 -0
  67. package/vendor/compose/dist/render.cjs +558 -0
  68. package/vendor/compose/dist/render.mjs +515 -0
  69. package/vendor/compose/dist/verify-cli.cjs +3806 -0
  70. package/vendor/compose/dist/verify-cli.mjs +3812 -0
  71. package/vendor/compose/dist/verify.cjs +3880 -0
  72. package/vendor/compose/dist/verify.mjs +3858 -0
  73. package/web/manager/dist/assets/index-D0S2otug.js +507 -0
  74. package/web/manager/dist/index.html +1 -1
  75. package/src/commands/ci-run.js +0 -123
  76. package/src/commands/ci-setup.js +0 -288
  77. package/src/commands/ingest.js +0 -458
  78. package/src/commands/setup.js +0 -137
  79. package/src/commands/validate-docs.js +0 -529
  80. package/src/lib/playwright-runner.js +0 -252
  81. package/web/manager/dist/assets/index--ZgioErz.js +0 -507
@@ -1,6 +1,4 @@
1
- // sync.js - Upload Playwright traces and documentation for processing
2
- // This is the renamed/enhanced version of ingest.js
3
- // Implements the "Smart Handoff" protocol from the DocSync specification
1
+ // sync.js - Upload Playwright traces to Reshot platform
4
2
 
5
3
  const chalk = require("chalk");
6
4
  const crypto = require("crypto");
@@ -9,127 +7,12 @@ const path = require("path");
9
7
  const { execSync } = require("child_process");
10
8
  const config = require("../lib/config");
11
9
  const apiClient = require("../lib/api-client");
12
- const { hashFile } = require("../lib/hash");
13
10
  const pkg = require("../../package.json");
14
11
 
15
12
  // File extension allowlists
16
13
  const TRACE_EXTENSIONS = [".zip"];
17
- const DOC_EXTENSIONS = [".md", ".mdx"];
18
- const MAX_DOC_SIZE = 5 * 1024 * 1024; // 5MB per doc file
19
14
  const MAX_TRACE_SIZE = 100 * 1024 * 1024; // 100MB per trace
20
15
 
21
- /**
22
- * Parse frontmatter from a markdown file
23
- */
24
- function parseFrontmatter(content) {
25
- const frontmatterRegex = /^---\n([\s\S]*?)\n---/;
26
- const match = content.match(frontmatterRegex);
27
-
28
- if (!match) return { frontmatter: {}, content };
29
-
30
- const frontmatter = {};
31
- const lines = match[1].split("\n");
32
-
33
- for (const line of lines) {
34
- const colonIndex = line.indexOf(":");
35
- if (colonIndex > 0) {
36
- const key = line.slice(0, colonIndex).trim();
37
- let value = line.slice(colonIndex + 1).trim();
38
- if (
39
- (value.startsWith('"') && value.endsWith('"')) ||
40
- (value.startsWith("'") && value.endsWith("'"))
41
- ) {
42
- value = value.slice(1, -1);
43
- }
44
- frontmatter[key] = value;
45
- }
46
- }
47
-
48
- return {
49
- frontmatter,
50
- content: content.slice(match[0].length).trim(),
51
- };
52
- }
53
-
54
- /**
55
- * Discover documentation files based on config
56
- */
57
- async function discoverDocumentation(docConfig, projectRoot) {
58
- const files = [];
59
- const root = path.resolve(projectRoot, docConfig.root || "./docs");
60
-
61
- if (!fs.existsSync(root)) {
62
- return files;
63
- }
64
-
65
- const include = docConfig.include || ["**/*.md", "**/*.mdx"];
66
- const exclude = docConfig.exclude || ["**/_*.mdx", "node_modules"];
67
- const mappings = docConfig.mappings || {};
68
-
69
- function walkDir(dir, relativePath = "") {
70
- const items = fs.readdirSync(dir);
71
-
72
- for (const item of items) {
73
- const fullPath = path.join(dir, item);
74
- const relPath = path.join(relativePath, item);
75
- const stat = fs.statSync(fullPath);
76
-
77
- const shouldExclude = exclude.some((pattern) => {
78
- if (pattern.includes("*")) {
79
- const regex = new RegExp(
80
- pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*"),
81
- );
82
- return regex.test(relPath);
83
- }
84
- return relPath.includes(pattern) || item === pattern;
85
- });
86
-
87
- if (shouldExclude) continue;
88
-
89
- if (stat.isDirectory()) {
90
- walkDir(fullPath, relPath);
91
- } else {
92
- const ext = path.extname(item).toLowerCase();
93
- if (DOC_EXTENSIONS.includes(ext)) {
94
- const content = fs.readFileSync(fullPath, "utf-8");
95
- const { frontmatter } = parseFrontmatter(content);
96
-
97
- const journeyKey =
98
- frontmatter.reshot_journey ||
99
- mappings[relPath] ||
100
- mappings[fullPath];
101
-
102
- if (journeyKey) {
103
- const fileSize = stat.size;
104
-
105
- if (fileSize > MAX_DOC_SIZE) {
106
- console.log(
107
- chalk.yellow(` ⚠ Skipping ${relPath}: exceeds size limit`),
108
- );
109
- continue;
110
- }
111
-
112
- files.push({
113
- path: fullPath,
114
- relativePath: relPath,
115
- journeyKey,
116
- contentHash: crypto
117
- .createHash("sha256")
118
- .update(content)
119
- .digest("hex"),
120
- size: fileSize,
121
- frontmatter,
122
- });
123
- }
124
- }
125
- }
126
- }
127
- }
128
-
129
- walkDir(root);
130
- return files;
131
- }
132
-
133
16
  /**
134
17
  * Extract journey key from Playwright test-results directory name
135
18
  * Playwright creates directories like: "test-name-chromium" or "describe-test-name-chromium"
@@ -191,11 +74,6 @@ function extractJourneyKey(dirName, journeyMappings = {}) {
191
74
  }
192
75
 
193
76
  // Check if the key parts of the test name appear in both
194
- // Match "student-login-successfully" to pattern "should-complete-student-login-successfully"
195
- const patternStr = pattern.toLowerCase();
196
- const normalizedStr = normalized;
197
-
198
- // Extract meaningful words (4+ chars) and check overlap
199
77
  const meaningfulFromPattern = patternParts.filter((p) => p.length >= 4);
200
78
  const meaningfulFromDir = normalizedParts.filter((p) => p.length >= 4);
201
79
 
@@ -323,28 +201,24 @@ function getGitMetadata() {
323
201
  * Main sync command
324
202
  * @param {Object} options - Command options
325
203
  * @param {string} options.traceDir - Override trace directory
326
- * @param {boolean} options.traces - Sync traces only
327
- * @param {boolean} options.docs - Sync documentation only
328
204
  * @param {boolean} options.dryRun - Preview without uploading
329
205
  * @param {boolean} options.verbose - Show detailed output
330
206
  */
331
207
  async function syncCommand(options = {}) {
332
208
  const {
333
209
  traceDir: traceDirOverride,
334
- traces: tracesOnly = false,
335
- docs: docsOnly = false,
336
210
  dryRun = false,
337
211
  verbose = false,
338
212
  } = options;
339
213
 
340
214
  console.log(chalk.cyan.bold("\nšŸ”„ Reshot Sync\n"));
341
215
 
342
- // Load configuration (use readDocSyncConfig which is less strict about scenarios)
343
- let docSyncConfig;
216
+ // Load configuration
217
+ let reshotConfig;
344
218
  try {
345
- docSyncConfig = config.readDocSyncConfig();
219
+ reshotConfig = config.readConfigLenient();
346
220
  } catch (error) {
347
- console.error(chalk.red("āœ– No docsync.config.json found."));
221
+ console.error(chalk.red("āœ– No reshot.config.json found."));
348
222
  console.log(
349
223
  chalk.gray(" Run"),
350
224
  chalk.cyan("reshot setup"),
@@ -361,7 +235,7 @@ async function syncCommand(options = {}) {
361
235
  settings = null;
362
236
  }
363
237
 
364
- const projectId = docSyncConfig.projectId || settings?.projectId;
238
+ const projectId = reshotConfig.projectId || settings?.projectId;
365
239
  const apiKey = settings?.apiKey;
366
240
 
367
241
  if (!projectId || !apiKey) {
@@ -374,99 +248,55 @@ async function syncCommand(options = {}) {
374
248
  process.exit(1);
375
249
  }
376
250
 
377
- const features = docSyncConfig._metadata?.features || {
378
- visuals: true,
379
- docsync: true,
380
- };
381
251
  const projectRoot = process.cwd();
382
252
 
383
- // Determine what to sync
384
- const syncTraces = !docsOnly && features.visuals !== false;
385
- const syncDocs =
386
- !tracesOnly && features.docsync !== false && docSyncConfig.documentation;
387
-
388
253
  if (dryRun) {
389
254
  console.log(chalk.yellow("DRY RUN - No files will be uploaded\n"));
390
255
  }
391
256
 
392
- let traceFiles = [];
393
- let docFiles = [];
394
-
395
257
  // ========================================
396
258
  // PHASE 1: Discover Traces
397
259
  // ========================================
398
- if (syncTraces) {
399
- const traceDir =
400
- traceDirOverride || docSyncConfig.visuals?.traceDir || "./test-results";
401
- const resolvedTraceDir = path.resolve(projectRoot, traceDir);
402
- const journeyMappings = docSyncConfig.visuals?.journeyMappings || {};
403
-
404
- console.log(chalk.gray(`Scanning traces: ${traceDir}`));
405
- traceFiles = await discoverTraces(resolvedTraceDir, journeyMappings);
406
-
407
- if (traceFiles.length === 0) {
408
- console.log(chalk.yellow(" No trace files found"));
409
- if (!fs.existsSync(resolvedTraceDir)) {
410
- console.log(
411
- chalk.gray(" Run your Playwright tests first: npx playwright test"),
412
- );
413
- }
414
- } else {
415
- console.log(chalk.green(` Found ${traceFiles.length} trace file(s)`));
416
- if (verbose) {
417
- for (const trace of traceFiles) {
418
- console.log(
419
- chalk.gray(` → ${trace.parentDir || trace.relativePath}`),
420
- );
421
- console.log(chalk.gray(` journey: ${trace.journeyKey}`));
422
- }
423
- }
424
- }
425
- console.log();
426
- }
260
+ const traceDir =
261
+ traceDirOverride || reshotConfig.visuals?.traceDir || "./test-results";
262
+ const resolvedTraceDir = path.resolve(projectRoot, traceDir);
263
+ const journeyMappings = reshotConfig.visuals?.journeyMappings || {};
427
264
 
428
- // ========================================
429
- // PHASE 2: Discover Documentation
430
- // ========================================
431
- if (syncDocs) {
432
- console.log(
433
- chalk.gray(`Scanning docs: ${docSyncConfig.documentation.root}`),
434
- );
435
- docFiles = await discoverDocumentation(
436
- docSyncConfig.documentation,
437
- projectRoot,
438
- );
265
+ console.log(chalk.gray(`Scanning traces: ${traceDir}`));
266
+ const traceFiles = await discoverTraces(resolvedTraceDir, journeyMappings);
439
267
 
440
- if (docFiles.length === 0) {
441
- console.log(chalk.yellow(" No bound documentation files found"));
268
+ if (traceFiles.length === 0) {
269
+ console.log(chalk.yellow(" No trace files found"));
270
+ if (!fs.existsSync(resolvedTraceDir)) {
442
271
  console.log(
443
- chalk.gray(" Add reshot_journey frontmatter to your markdown files"),
272
+ chalk.gray(" Run your Playwright tests first: npx playwright test"),
444
273
  );
445
- } else {
446
- console.log(chalk.green(` Found ${docFiles.length} bound doc file(s)`));
447
- if (verbose) {
448
- for (const doc of docFiles) {
449
- console.log(
450
- chalk.gray(` → ${doc.relativePath} → ${doc.journeyKey}`),
451
- );
452
- }
274
+ }
275
+ console.log();
276
+ } else {
277
+ console.log(chalk.green(` Found ${traceFiles.length} trace file(s)`));
278
+ if (verbose) {
279
+ for (const trace of traceFiles) {
280
+ console.log(
281
+ chalk.gray(` → ${trace.parentDir || trace.relativePath}`),
282
+ );
283
+ console.log(chalk.gray(` journey: ${trace.journeyKey}`));
453
284
  }
454
285
  }
455
286
  console.log();
456
287
  }
457
288
 
458
289
  // ========================================
459
- // PHASE 3: Upload to Platform
290
+ // PHASE 2: Upload to Platform
460
291
  // ========================================
461
292
  if (dryRun) {
462
293
  console.log(chalk.cyan("━━━ Dry Run Summary ━━━\n"));
463
294
  console.log(chalk.gray(` Traces: ${traceFiles.length} files`));
464
- console.log(chalk.gray(` Docs: ${docFiles.length} files`));
465
295
  console.log(chalk.yellow("\nNo files uploaded (dry run).\n"));
466
296
  return;
467
297
  }
468
298
 
469
- if (traceFiles.length === 0 && docFiles.length === 0) {
299
+ if (traceFiles.length === 0) {
470
300
  console.log(chalk.yellow("Nothing to sync.\n"));
471
301
  return;
472
302
  }
@@ -476,12 +306,6 @@ async function syncCommand(options = {}) {
476
306
  try {
477
307
  // Initialize sync job with manifest
478
308
  const manifest = {
479
- docs: docFiles.map((d) => ({
480
- relativePath: d.relativePath,
481
- journeyKey: d.journeyKey,
482
- contentHash: d.contentHash,
483
- size: d.size,
484
- })),
485
309
  traces: traceFiles.map((t) => ({
486
310
  filename: t.filename,
487
311
  journeyKey: t.journeyKey,
@@ -545,31 +369,6 @@ async function syncCommand(options = {}) {
545
369
  }
546
370
  }
547
371
 
548
- // Upload docs to presigned URLs
549
- let docsUploaded = 0;
550
- for (const doc of docFiles) {
551
- if (skippedFiles.includes(doc.contentHash)) {
552
- if (verbose) {
553
- console.log(chalk.gray(` ⊘ ${doc.relativePath} (cached)`));
554
- }
555
- docsUploaded++;
556
- continue;
557
- }
558
-
559
- const presigned = presignedUrls[doc.contentHash];
560
- if (presigned) {
561
- const content = fs.readFileSync(doc.path, "utf-8");
562
- await apiClient.uploadToPresignedUrl(presigned.url, content, {
563
- contentType: presigned.contentType || "text/markdown",
564
- headers: { Authorization: `Bearer ${apiKey}` },
565
- });
566
- docsUploaded++;
567
- if (verbose) {
568
- console.log(chalk.gray(` āœ” ${doc.relativePath}`));
569
- }
570
- }
571
- }
572
-
573
372
  // Get git metadata
574
373
  const git = getGitMetadata();
575
374
 
@@ -579,12 +378,6 @@ async function syncCommand(options = {}) {
579
378
  {
580
379
  projectId,
581
380
  uploadResults: {
582
- docs: docFiles.map((d) => ({
583
- relativePath: d.relativePath,
584
- journeyKey: d.journeyKey,
585
- storageKey:
586
- presignedUrls[d.contentHash]?.storageKey || d.contentHash,
587
- })),
588
381
  traces: traceFiles.map((t) => ({
589
382
  filename: t.filename,
590
383
  journeyKey: t.journeyKey,
@@ -605,7 +398,6 @@ async function syncCommand(options = {}) {
605
398
 
606
399
  console.log(chalk.green(`\nāœ” Sync complete!`));
607
400
  console.log(chalk.gray(` Traces: ${tracesUploaded} uploaded`));
608
- console.log(chalk.gray(` Docs: ${docsUploaded} uploaded`));
609
401
  console.log();
610
402
  console.log(chalk.gray(" View status:"), chalk.cyan("reshot status"));
611
403
  console.log(chalk.gray(" Check drifts:"), chalk.cyan("reshot drifts\n"));
@@ -127,7 +127,7 @@ module.exports = async function uiCommand(options = {}) {
127
127
  } else {
128
128
  console.log(
129
129
  chalk.yellow("Info:"),
130
- "No docsync.config.json found. Use the UI to pull config from platform or create a new one."
130
+ "No reshot.config.json found. Use the UI to pull config from platform or create a new one."
131
131
  );
132
132
  }
133
133
 
@@ -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;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+
3
+ const chalk = require("chalk");
4
+ const { runVerifyPublish } = require("../lib/certification");
5
+
6
+ async function verifyPublishCommand(options = {}) {
7
+ const scenarioKeys = options.scenarios
8
+ ? String(options.scenarios)
9
+ .split(",")
10
+ .map((value) => value.trim())
11
+ .filter(Boolean)
12
+ : null;
13
+
14
+ const report = await runVerifyPublish({
15
+ scenarioKeys,
16
+ tag: options.tag,
17
+ message: options.message,
18
+ });
19
+
20
+ if (options.json) {
21
+ console.log(JSON.stringify(report, null, 2));
22
+ } else {
23
+ console.log(chalk.cyan("\nšŸ“¦ Publish Verification\n"));
24
+ console.log(
25
+ report.ok
26
+ ? chalk.green(" āœ” Publish, pull, and hosted delivery verified")
27
+ : chalk.red(" āœ– Publish verification failed"),
28
+ );
29
+ console.log(chalk.gray(` Published: ${report.publishResult.assetsProcessed || 0}`));
30
+ console.log(chalk.gray(` Pull repairs: ${report.pullResult.normalizationRepairs || 0}`));
31
+ const failedChecks = (report.deliveryChecks || []).filter((check) => !check.ok);
32
+ if (failedChecks.length > 0) {
33
+ for (const check of failedChecks.slice(0, 10)) {
34
+ console.log(chalk.red(` āœ– ${check.scenario}/${check.assetKey}: ${check.reason}`));
35
+ }
36
+ }
37
+ }
38
+
39
+ if (!report.ok) {
40
+ process.exitCode = 1;
41
+ }
42
+
43
+ return report;
44
+ }
45
+
46
+ module.exports = verifyPublishCommand;