@reshotdev/screenshot 0.0.1-beta.0

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 (59) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +388 -0
  3. package/package.json +64 -0
  4. package/src/commands/auth.js +259 -0
  5. package/src/commands/chrome.js +140 -0
  6. package/src/commands/ci-run.js +123 -0
  7. package/src/commands/ci-setup.js +288 -0
  8. package/src/commands/drifts.js +423 -0
  9. package/src/commands/import-tests.js +309 -0
  10. package/src/commands/ingest.js +458 -0
  11. package/src/commands/init.js +633 -0
  12. package/src/commands/publish.js +1721 -0
  13. package/src/commands/pull.js +303 -0
  14. package/src/commands/record.js +94 -0
  15. package/src/commands/run.js +476 -0
  16. package/src/commands/setup-wizard.js +740 -0
  17. package/src/commands/setup.js +137 -0
  18. package/src/commands/status.js +275 -0
  19. package/src/commands/sync.js +621 -0
  20. package/src/commands/ui.js +248 -0
  21. package/src/commands/validate-docs.js +529 -0
  22. package/src/index.js +462 -0
  23. package/src/lib/api-client.js +815 -0
  24. package/src/lib/capture-engine.js +1623 -0
  25. package/src/lib/capture-script-runner.js +3120 -0
  26. package/src/lib/ci-detect.js +137 -0
  27. package/src/lib/config.js +1240 -0
  28. package/src/lib/diff-engine.js +642 -0
  29. package/src/lib/hash.js +74 -0
  30. package/src/lib/image-crop.js +396 -0
  31. package/src/lib/matrix.js +89 -0
  32. package/src/lib/output-path-template.js +318 -0
  33. package/src/lib/playwright-runner.js +252 -0
  34. package/src/lib/polished-clip.js +553 -0
  35. package/src/lib/privacy-engine.js +408 -0
  36. package/src/lib/progress-tracker.js +142 -0
  37. package/src/lib/record-browser-injection.js +654 -0
  38. package/src/lib/record-cdp.js +612 -0
  39. package/src/lib/record-clip.js +343 -0
  40. package/src/lib/record-config.js +623 -0
  41. package/src/lib/record-screenshot.js +360 -0
  42. package/src/lib/record-terminal.js +123 -0
  43. package/src/lib/recorder-service.js +781 -0
  44. package/src/lib/secrets.js +51 -0
  45. package/src/lib/selector-strategies.js +859 -0
  46. package/src/lib/standalone-mode.js +400 -0
  47. package/src/lib/storage-providers.js +569 -0
  48. package/src/lib/style-engine.js +684 -0
  49. package/src/lib/ui-api.js +4677 -0
  50. package/src/lib/ui-assets.js +373 -0
  51. package/src/lib/ui-executor.js +587 -0
  52. package/src/lib/variant-injector.js +591 -0
  53. package/src/lib/viewport-presets.js +454 -0
  54. package/src/lib/worker-pool.js +118 -0
  55. package/web/cropper/index.html +436 -0
  56. package/web/manager/dist/assets/index--ZgioErz.js +507 -0
  57. package/web/manager/dist/assets/index-n468W0Wr.css +1 -0
  58. package/web/manager/dist/index.html +27 -0
  59. package/web/subtitle-editor/index.html +295 -0
@@ -0,0 +1,476 @@
1
+ // run.js - Execute all scenarios from config using the robust capture engine
2
+ const chalk = require("chalk");
3
+ const fs = require("fs-extra");
4
+ const path = require("path");
5
+ const config = require("../lib/config");
6
+ const { runAllScenarios, generateVersionTimestamp, detectOptimalConcurrency } = require("../lib/capture-script-runner");
7
+ const { getBaselines } = require("../lib/api-client");
8
+ const {
9
+ downloadBaselines,
10
+ compareImages,
11
+ writeManifest,
12
+ compareWithPreviousVersion,
13
+ compareWithCloudBaselines,
14
+ writeLocalDiffManifest,
15
+ CACHE_DIR,
16
+ } = require("../lib/diff-engine");
17
+
18
+ /**
19
+ * Generate all variant combinations from dimensions config
20
+ * @param {Object} dimensions - Dimensions configuration
21
+ * @param {string[]} dimensionKeys - Which dimensions to expand (default: all)
22
+ * @returns {Object[]} Array of variant objects
23
+ */
24
+ function generateVariantCombinations(dimensions, dimensionKeys = null) {
25
+ if (!dimensions || Object.keys(dimensions).length === 0) {
26
+ return [];
27
+ }
28
+
29
+ // Use provided dimension keys or all available
30
+ const keysToUse = dimensionKeys || Object.keys(dimensions);
31
+
32
+ // Filter to only dimensions that have options
33
+ const validKeys = keysToUse.filter(key => {
34
+ const dim = dimensions[key];
35
+ return dim?.options && Object.keys(dim.options).length > 0;
36
+ });
37
+
38
+ if (validKeys.length === 0) {
39
+ return [];
40
+ }
41
+
42
+ // Get options for each dimension
43
+ const dimensionOptions = validKeys.map((key) => {
44
+ const dim = dimensions[key];
45
+ return Object.keys(dim.options).map((optKey) => ({
46
+ dimension: key,
47
+ option: optKey,
48
+ }));
49
+ });
50
+
51
+ // Generate cartesian product of all dimension options
52
+ const cartesian = (...arrays) => {
53
+ return arrays.reduce(
54
+ (acc, arr) => acc.flatMap((combo) => arr.map((item) => [...combo, item])),
55
+ [[]]
56
+ );
57
+ };
58
+
59
+ const combinations = cartesian(...dimensionOptions);
60
+
61
+ // Convert to variant objects
62
+ return combinations.map((combo) => {
63
+ const variant = {};
64
+ for (const { dimension, option } of combo) {
65
+ variant[dimension] = option;
66
+ }
67
+ return variant;
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Run scenarios from config
73
+ * @param {Object} options - Run options
74
+ * @param {string[]} options.scenarioKeys - Filter to specific scenario keys (optional)
75
+ * @param {boolean} options.headless - Run in headless mode (default: true)
76
+ * @param {Object} options.variant - Override variant for all scenarios (e.g., { locale: 'ko', role: 'admin' })
77
+ * @param {boolean} options.allVariants - Run all configured variant combinations
78
+ * @param {boolean} options.noVariants - Skip variant expansion entirely
79
+ * @param {string} options.format - Override output format: 'step-by-step-images' | 'summary-video'
80
+ * @param {boolean} options.diff - Enable baseline diffing (default: use config)
81
+ * @param {boolean} options.cloud - Enable cloud baseline sync (default: false, use local)
82
+ * @param {number} options.concurrency - Number of parallel browser workers (optional)
83
+ */
84
+ async function runCommand(options = {}) {
85
+ const {
86
+ scenarioKeys,
87
+ headless = true,
88
+ variant,
89
+ allVariants = false,
90
+ noVariants = false,
91
+ noPrivacy = false,
92
+ noStyle = false,
93
+ format,
94
+ diff,
95
+ cloud = false,
96
+ concurrency,
97
+ noExit = false,
98
+ } = options;
99
+
100
+ const docSyncConfig = config.readConfig();
101
+ const diffingConfig = config.getDiffingConfig();
102
+
103
+ // Check feature toggles
104
+ const features = docSyncConfig._metadata?.features || { visuals: true };
105
+ if (features.visuals !== true) {
106
+ console.log(
107
+ chalk.yellow(
108
+ "⚠ Visual generation is disabled for this project. Skipping scenario execution."
109
+ )
110
+ );
111
+ return;
112
+ }
113
+
114
+ // Determine if diffing is enabled
115
+ // CLI flag takes precedence, then config, default is TRUE (always diff locally)
116
+ const shouldDiff =
117
+ diff !== undefined ? diff : diffingConfig.enabled !== false;
118
+
119
+ // Parse variant if passed as JSON string
120
+ let variantOverride = variant;
121
+ if (typeof variant === "string") {
122
+ try {
123
+ variantOverride = JSON.parse(variant);
124
+ } catch (e) {
125
+ console.error(chalk.red(`Invalid variant JSON: ${variant}`));
126
+ process.exitCode = 1;
127
+ return;
128
+ }
129
+ }
130
+
131
+ // If format is specified, override scenarios' output format
132
+ let configToUse = docSyncConfig;
133
+ if (format) {
134
+ console.log(chalk.cyan(`šŸ“· Using capture format: ${format}\n`));
135
+ configToUse = {
136
+ ...docSyncConfig,
137
+ scenarios: (docSyncConfig.scenarios || []).map((scenario) => ({
138
+ ...scenario,
139
+ output: {
140
+ ...scenario.output,
141
+ format,
142
+ },
143
+ })),
144
+ };
145
+ }
146
+
147
+ // Determine concurrency: CLI flag > config > auto-detect
148
+ const autoConcurrency = detectOptimalConcurrency();
149
+ const effectiveConcurrency = concurrency || docSyncConfig.concurrency || autoConcurrency;
150
+ if (!concurrency && !docSyncConfig.concurrency) {
151
+ console.log(chalk.gray(` Auto-detected concurrency: ${effectiveConcurrency} (${require("os").cpus().length} CPUs, ${Math.round(require("os").freemem() / 1024 / 1024)}MB free)\n`));
152
+ }
153
+
154
+ // ============================================
155
+ // VARIANT EXPANSION: Run all variant combinations if configured
156
+ // ============================================
157
+ const variantsConfig = docSyncConfig.variants || {};
158
+ const dimensions = variantsConfig.dimensions || {};
159
+ const hasVariants = Object.keys(dimensions).length > 0;
160
+
161
+ // Determine if we should expand variants:
162
+ // - If specific variant is provided: use it only
163
+ // - If --no-variants: skip expansion, run without variants
164
+ // - If --all-variants or variants are configured: expand all combinations
165
+ const shouldExpandVariants = !noVariants && !variantOverride && hasVariants;
166
+
167
+ if (shouldExpandVariants) {
168
+ const combinations = generateVariantCombinations(dimensions);
169
+
170
+ if (combinations.length > 0) {
171
+ console.log(chalk.cyan(`šŸŽØ Expanding ${combinations.length} variant combination(s):\n`));
172
+
173
+ // Show variant combinations
174
+ for (const combo of combinations) {
175
+ const label = Object.entries(combo)
176
+ .map(([dim, opt]) => {
177
+ const dimension = dimensions[dim];
178
+ const option = dimension?.options?.[opt];
179
+ return option?.name || opt;
180
+ })
181
+ .join(' • ');
182
+ console.log(chalk.gray(` → ${label}`));
183
+ }
184
+ console.log();
185
+
186
+ // Generate a shared timestamp for all variant runs
187
+ const sharedTimestamp = generateVersionTimestamp();
188
+ console.log(chalk.gray(`šŸ“ Shared timestamp: ${sharedTimestamp}\n`));
189
+
190
+ // Run scenarios for each variant combination
191
+ const allResults = [];
192
+ let allSuccess = true;
193
+
194
+ for (const variantCombo of combinations) {
195
+ const variantLabel = Object.entries(variantCombo)
196
+ .map(([dim, opt]) => {
197
+ const dimension = dimensions[dim];
198
+ const option = dimension?.options?.[opt];
199
+ return option?.name || opt;
200
+ })
201
+ .join(' • ');
202
+
203
+ console.log(chalk.cyan(`\n━━━ Variant: ${variantLabel} ━━━\n`));
204
+
205
+ const result = await runAllScenarios(configToUse, {
206
+ scenarioKeys,
207
+ headless,
208
+ variantOverride: variantCombo,
209
+ concurrency: effectiveConcurrency,
210
+ sharedTimestamp,
211
+ noPrivacy,
212
+ noStyle,
213
+ });
214
+
215
+ allResults.push({ variant: variantCombo, ...result });
216
+ if (!result.success) {
217
+ allSuccess = false;
218
+ }
219
+ }
220
+
221
+ // Summary
222
+ console.log(chalk.cyan(`\n━━━ Variant Capture Summary ━━━\n`));
223
+ for (const result of allResults) {
224
+ const variantLabel = Object.entries(result.variant)
225
+ .map(([dim, opt]) => {
226
+ const dimension = dimensions[dim];
227
+ const option = dimension?.options?.[opt];
228
+ return option?.name || opt;
229
+ })
230
+ .join(' • ');
231
+ const statusIcon = result.success ? 'āœ”' : 'āœ–';
232
+ const statusColor = result.success ? chalk.green : chalk.red;
233
+ const assetCount = result.results?.reduce((sum, r) => sum + (r.assets?.length || 0), 0) || 0;
234
+ console.log(statusColor(` ${statusIcon} ${variantLabel}: ${assetCount} assets`));
235
+ }
236
+
237
+ if (!allSuccess) {
238
+ process.exitCode = 1;
239
+ }
240
+
241
+ if (!allSuccess) {
242
+ process.exitCode = 1;
243
+ }
244
+
245
+ // Exit after variant expansion completes unless called programmatically
246
+ if (!noExit) {
247
+ process.exit(allSuccess ? 0 : 1);
248
+ }
249
+ return { success: allSuccess, results: allResults };
250
+ }
251
+ }
252
+
253
+ // Single variant or no variants - use original logic
254
+ const result = await runAllScenarios(configToUse, {
255
+ scenarioKeys,
256
+ headless,
257
+ variantOverride,
258
+ concurrency: effectiveConcurrency,
259
+ noPrivacy,
260
+ noStyle,
261
+ });
262
+
263
+ // ============================================
264
+ // POST-PROCESSING: Local Version-to-Version Diffing
265
+ // ============================================
266
+ const outputBaseDir = path.join(
267
+ process.cwd(),
268
+ docSyncConfig.assetDir || ".reshot/output"
269
+ );
270
+
271
+ // ============================================
272
+ // CLOUD BASELINE SYNC: Download approved baselines from platform
273
+ // ============================================
274
+ let cloudBaselines = null;
275
+ if (cloud && shouldDiff) {
276
+ console.log(chalk.cyan("\nā˜ļø Syncing cloud baselines...\n"));
277
+
278
+ try {
279
+ const settings = config.readSettings();
280
+ const projectId =
281
+ settings?.projectId || docSyncConfig._metadata?.projectId;
282
+ const apiKey = process.env.RESHOT_API_KEY || settings?.apiKey;
283
+
284
+ if (projectId && apiKey) {
285
+ const baselineUrls = await getBaselines(projectId, apiKey);
286
+ const baselineCount = Object.keys(baselineUrls).length;
287
+
288
+ if (baselineCount > 0) {
289
+ console.log(
290
+ chalk.gray(` Found ${baselineCount} approved baseline(s) in cloud`)
291
+ );
292
+ cloudBaselines = await downloadBaselines(baselineUrls);
293
+ console.log(
294
+ chalk.green(
295
+ ` āœ” Downloaded ${
296
+ Object.keys(cloudBaselines).length
297
+ } baseline(s)\n`
298
+ )
299
+ );
300
+ } else {
301
+ console.log(
302
+ chalk.gray(
303
+ " No approved baselines found in cloud (first publish?)\n"
304
+ )
305
+ );
306
+ }
307
+ } else {
308
+ console.log(
309
+ chalk.yellow(
310
+ " ⚠ Cloud sync requires authentication. Run 'reshot auth' first.\n"
311
+ )
312
+ );
313
+ }
314
+ } catch (error) {
315
+ console.log(
316
+ chalk.yellow(` ⚠ Cloud baseline sync failed: ${error.message}`)
317
+ );
318
+ console.log(
319
+ chalk.gray(" Falling back to local version-to-version diffing.\n")
320
+ );
321
+ }
322
+ }
323
+
324
+ if (shouldDiff && result.results) {
325
+ const diffMode = cloudBaselines
326
+ ? "cloud baselines"
327
+ : "previous local versions";
328
+ console.log(
329
+ chalk.cyan(`\nšŸ” Computing visual diffs against ${diffMode}...\n`)
330
+ );
331
+
332
+ let totalScenarios = 0;
333
+ let totalDiffs = 0;
334
+ let totalCompared = 0;
335
+ let totalNew = 0;
336
+
337
+ for (const scenarioResult of result.results) {
338
+ if (!scenarioResult.success) continue;
339
+
340
+ const scenarioKey = scenarioResult.key;
341
+ const currentTimestamp = scenarioResult.timestamp;
342
+ const currentOutputDir = scenarioResult.outputDir; // Full path including variant
343
+
344
+ if (!currentTimestamp || !currentOutputDir) {
345
+ console.log(
346
+ chalk.gray(
347
+ ` → ${scenarioKey}: No timestamp/outputDir found, skipping diff`
348
+ )
349
+ );
350
+ continue;
351
+ }
352
+
353
+ totalScenarios++;
354
+ console.log(chalk.white(` šŸ“ ${scenarioKey}:`));
355
+
356
+ let diffData;
357
+
358
+ if (cloudBaselines && Object.keys(cloudBaselines).length > 0) {
359
+ // Use cloud baselines for comparison
360
+ diffData = await compareWithCloudBaselines(
361
+ currentOutputDir,
362
+ scenarioKey,
363
+ cloudBaselines,
364
+ diffingConfig
365
+ );
366
+ console.log(chalk.gray(` ↳ Comparing against cloud baselines`));
367
+ } else {
368
+ // Fall back to local version-to-version comparison
369
+ diffData = await compareWithPreviousVersion(
370
+ outputBaseDir,
371
+ scenarioKey,
372
+ currentTimestamp,
373
+ diffingConfig
374
+ );
375
+
376
+ if (!diffData.previousVersion) {
377
+ console.log(
378
+ chalk.gray(` ↳ First version - no previous version to compare`)
379
+ );
380
+ console.log(
381
+ chalk.gray(
382
+ ` (Diff percentages will appear after your next run)\n`
383
+ )
384
+ );
385
+ continue;
386
+ }
387
+
388
+ console.log(
389
+ chalk.gray(` ↳ Comparing against ${diffData.previousVersion}`)
390
+ );
391
+ }
392
+
393
+ // Log individual results
394
+ for (const [assetKey, assetResult] of Object.entries(diffData.results)) {
395
+ if (assetResult.status === "new") {
396
+ console.log(chalk.blue(` ✚ New: ${assetKey}`));
397
+ totalNew++;
398
+ } else if (assetResult.hasDiff) {
399
+ const percentage = (assetResult.score * 100).toFixed(2);
400
+ const reason = assetResult.reason ? ` (${assetResult.reason})` : "";
401
+ console.log(
402
+ chalk.yellow(
403
+ ` ⚠ Changed: ${assetKey} - ${percentage}%${reason}`
404
+ )
405
+ );
406
+ totalDiffs++;
407
+ } else {
408
+ console.log(chalk.green(` āœ” Unchanged: ${assetKey}`));
409
+ }
410
+ totalCompared++;
411
+ }
412
+
413
+ // Write diff manifest to the ACTUAL output directory (includes variant)
414
+ await writeLocalDiffManifest(currentOutputDir, diffData);
415
+
416
+ console.log(""); // Blank line between scenarios
417
+ }
418
+
419
+ // Summary
420
+ console.log(chalk.cyan("šŸ“Š Diff Summary:"));
421
+ console.log(chalk.white(` Scenarios: ${totalScenarios}`));
422
+ console.log(chalk.white(` Assets compared: ${totalCompared}`));
423
+ if (totalNew > 0) {
424
+ console.log(chalk.blue(` New assets: ${totalNew}`));
425
+ }
426
+ if (totalDiffs > 0) {
427
+ console.log(chalk.yellow(` Changed: ${totalDiffs}`));
428
+ }
429
+ console.log(
430
+ chalk.green(` Unchanged: ${totalCompared - totalDiffs - totalNew}`)
431
+ );
432
+ console.log("");
433
+
434
+ // Helpful guidance about diff percentages
435
+ if (totalNew > 0 && totalCompared === totalNew) {
436
+ console.log(
437
+ chalk.gray(
438
+ "šŸ’” Tip: All assets are new (first capture). Diff percentages will"
439
+ )
440
+ );
441
+ console.log(
442
+ chalk.gray(
443
+ " appear in the platform after your next run & publish cycle."
444
+ )
445
+ );
446
+ console.log("");
447
+ } else if (totalDiffs > 0) {
448
+ console.log(
449
+ chalk.gray(
450
+ "šŸ’” Tip: Changed assets will show diff percentages in the platform"
451
+ )
452
+ );
453
+ console.log(chalk.gray(" after you run 'reshot publish'."));
454
+ console.log("");
455
+ }
456
+ }
457
+
458
+ if (!result.success) {
459
+ const failed = result.results.filter((r) => !r.success);
460
+ console.error(chalk.red(`\nāŒ ${failed.length} scenario(s) failed`));
461
+ process.exitCode = 1;
462
+ } else {
463
+ console.log(chalk.green("\n✨ All scenarios completed successfully!"));
464
+ }
465
+
466
+ console.log(chalk.gray(`\nOutput saved to: ${outputBaseDir}`));
467
+
468
+ // Ensure process exits cleanly (Playwright CDP connections can keep event loop alive)
469
+ if (!noExit) {
470
+ setImmediate(() => process.exit(process.exitCode || 0));
471
+ }
472
+
473
+ return { success: result.success, results: result.results };
474
+ }
475
+
476
+ module.exports = runCommand;