@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,373 @@
1
+ // ui-assets.js - Asset file utilities for UI
2
+ const fs = require("fs-extra");
3
+ const path = require("path");
4
+
5
+ /**
6
+ * Find all asset files in output directory structure
7
+ *
8
+ * Handles multiple output structures:
9
+ * 1. .reshot/output/<scenarioKey>/latest/<files> (latest version)
10
+ * 2. .reshot/output/<scenarioKey>/default/<files> (default variation)
11
+ * 3. .reshot/output/<scenarioKey>/<timestamp>/<files> (timestamped version)
12
+ * 4. .reshot/output/<scenarioKey>/<timestamp>/<variantSlug>/<files> (variant within timestamp)
13
+ *
14
+ * Default behavior:
15
+ * - Shows named folders (latest, default, etc.)
16
+ * - For timestamped folders, shows the MOST RECENT one
17
+ * - If timestamp contains variant subfolders, shows all variants from most recent timestamp
18
+ *
19
+ * @param {string} dir - Output base directory
20
+ * @param {string[]} extensions - File extensions to include
21
+ * @param {Object} options - Additional options
22
+ * @param {boolean} options.includeAllVersions - Include all timestamped versions (default: false)
23
+ * @param {boolean} options.latestJobOnly - Only include the most recent timestamp folder, exclude named folders (default: false)
24
+ * @returns {string[]} Array of absolute file paths
25
+ */
26
+ function findAssetFiles(
27
+ dir,
28
+ extensions = [".png", ".gif", ".mp4", ".jpg", ".jpeg", ".webm"],
29
+ options = {}
30
+ ) {
31
+ const { includeAllVersions = false, latestJobOnly = false } = options;
32
+ const files = [];
33
+
34
+ if (!fs.existsSync(dir)) {
35
+ return files;
36
+ }
37
+
38
+ // Patterns to exclude from asset collection (debug artifacts, diff images)
39
+ const EXCLUDED_FILENAMES = ['debug-failure.png', 'debug-failure.jpg'];
40
+ const EXCLUDED_DIRS = ['diffs'];
41
+ const EXCLUDED_SUFFIXES = ['.diff.png', '.diff.jpg'];
42
+
43
+ function shouldExcludeFile(filename, filePath) {
44
+ if (EXCLUDED_FILENAMES.includes(filename)) return true;
45
+ for (const suffix of EXCLUDED_SUFFIXES) {
46
+ if (filename.endsWith(suffix)) return true;
47
+ }
48
+ for (const dir of EXCLUDED_DIRS) {
49
+ if (filePath.includes(path.sep + dir + path.sep) || filePath.includes('/' + dir + '/')) return true;
50
+ }
51
+ return false;
52
+ }
53
+
54
+ // Helper to recursively collect asset files from a folder
55
+ function collectAssets(folder) {
56
+ const collected = [];
57
+ if (!fs.existsSync(folder)) return collected;
58
+
59
+ function walk(currentDir) {
60
+ try {
61
+ const items = fs.readdirSync(currentDir);
62
+ for (const item of items) {
63
+ const fullPath = path.join(currentDir, item);
64
+ const stat = fs.statSync(fullPath);
65
+
66
+ if (stat.isDirectory()) {
67
+ // Skip excluded directories
68
+ if (EXCLUDED_DIRS.includes(item)) continue;
69
+ walk(fullPath);
70
+ } else {
71
+ const ext = path.extname(item).toLowerCase();
72
+ if (extensions.includes(ext) && !shouldExcludeFile(item, fullPath)) {
73
+ collected.push(fullPath);
74
+ }
75
+ }
76
+ }
77
+ } catch (e) {
78
+ // Ignore permission errors
79
+ }
80
+ }
81
+
82
+ walk(folder);
83
+ return collected;
84
+ }
85
+
86
+ // Check if a folder is a timestamp
87
+ function isTimestamp(name) {
88
+ return /^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$/.test(name);
89
+ }
90
+
91
+ // Get scenario-level folders
92
+ const scenarioFolders = fs.readdirSync(dir).filter((item) => {
93
+ const fullPath = path.join(dir, item);
94
+ try {
95
+ return fs.statSync(fullPath).isDirectory();
96
+ } catch {
97
+ return false;
98
+ }
99
+ });
100
+
101
+ for (const scenarioFolder of scenarioFolders) {
102
+ const scenarioPath = path.join(dir, scenarioFolder);
103
+
104
+ let subFolders = [];
105
+ try {
106
+ subFolders = fs.readdirSync(scenarioPath).filter((item) => {
107
+ const fullPath = path.join(scenarioPath, item);
108
+ try {
109
+ return fs.statSync(fullPath).isDirectory();
110
+ } catch {
111
+ return false;
112
+ }
113
+ });
114
+ } catch {
115
+ continue;
116
+ }
117
+
118
+ // Categorize subfolders
119
+ const timestampedFolders = subFolders
120
+ .filter((f) => isTimestamp(f))
121
+ .sort()
122
+ .reverse();
123
+ const namedFolders = subFolders.filter((f) => !isTimestamp(f)); // 'latest', 'default', etc.
124
+
125
+ if (includeAllVersions) {
126
+ // Include everything
127
+ for (const folder of subFolders) {
128
+ const folderPath = path.join(scenarioPath, folder);
129
+ files.push(...collectAssets(folderPath));
130
+ }
131
+ } else if (latestJobOnly) {
132
+ // Only include the most recent timestamp folder (for publish preview)
133
+ // This excludes named folders like "latest" to avoid showing ALL historical assets
134
+ if (timestampedFolders.length > 0) {
135
+ let foundImages = false;
136
+
137
+ for (const timestamp of timestampedFolders) {
138
+ const timestampPath = path.join(scenarioPath, timestamp);
139
+ const timestampAssets = collectAssets(timestampPath);
140
+ const hasImages = timestampAssets.some((f) =>
141
+ /\.(png|jpg|jpeg|gif)$/i.test(f)
142
+ );
143
+
144
+ if (hasImages) {
145
+ files.push(...timestampAssets);
146
+ foundImages = true;
147
+ break; // Use only the most recent timestamp with images
148
+ }
149
+ }
150
+
151
+ // If no timestamp has images, use the most recent one (might have video)
152
+ if (!foundImages) {
153
+ const mostRecentTimestamp = timestampedFolders[0];
154
+ const timestampPath = path.join(scenarioPath, mostRecentTimestamp);
155
+ files.push(...collectAssets(timestampPath));
156
+ }
157
+ }
158
+ } else {
159
+ // Smart selection (default behavior for /assets page)
160
+
161
+ // Always include named folders
162
+ for (const folder of namedFolders) {
163
+ const folderPath = path.join(scenarioPath, folder);
164
+ files.push(...collectAssets(folderPath));
165
+ }
166
+
167
+ // For timestamped folders, find the most recent one that has IMAGE assets
168
+ // (not just video files)
169
+ if (timestampedFolders.length > 0) {
170
+ let foundImages = false;
171
+
172
+ for (const timestamp of timestampedFolders) {
173
+ const timestampPath = path.join(scenarioPath, timestamp);
174
+ const timestampAssets = collectAssets(timestampPath);
175
+ const hasImages = timestampAssets.some((f) =>
176
+ /\.(png|jpg|jpeg|gif)$/i.test(f)
177
+ );
178
+
179
+ if (hasImages) {
180
+ files.push(...timestampAssets);
181
+ foundImages = true;
182
+ break; // Use only the most recent timestamp with images
183
+ }
184
+ }
185
+
186
+ // If no timestamp has images, use the most recent one (might have video)
187
+ if (!foundImages) {
188
+ const mostRecentTimestamp = timestampedFolders[0];
189
+ const timestampPath = path.join(scenarioPath, mostRecentTimestamp);
190
+ files.push(...collectAssets(timestampPath));
191
+ }
192
+ }
193
+ }
194
+ }
195
+
196
+ return files;
197
+ }
198
+
199
+ /**
200
+ * Extract metadata from file path
201
+ *
202
+ * Handles multiple path structures:
203
+ * 1. .reshot/output/<scenarioKey>/<variationSlug>/<filename>
204
+ * 2. .reshot/output/<scenarioKey>/<timestamp>/<filename>
205
+ * 3. .reshot/output/<scenarioKey>/<timestamp>/<variantSlug>/<filename>
206
+ *
207
+ * @param {string} filePath - Absolute file path
208
+ * @param {string} outputBaseDir - Base output directory
209
+ * @returns {Object} Metadata object
210
+ */
211
+ function extractMetadata(filePath, outputBaseDir) {
212
+ const relativePath = path.relative(outputBaseDir, filePath);
213
+ const parts = relativePath.split(path.sep);
214
+
215
+ // parts[0] = scenarioKey
216
+ const scenarioKey = parts[0];
217
+
218
+ // Check if parts[1] is a timestamp
219
+ const isTimestamp = /^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$/.test(parts[1]);
220
+
221
+ let variationSlug;
222
+ let filename;
223
+
224
+ if (isTimestamp && parts.length > 3) {
225
+ // Structure: scenarioKey/timestamp/variantSlug/filename
226
+ // Use variantSlug as variation, not the timestamp
227
+ variationSlug = parts[2];
228
+ filename = parts.slice(3).join("/");
229
+ } else if (isTimestamp) {
230
+ // Structure: scenarioKey/timestamp/filename (no variant)
231
+ // Normalize timestamp to 'latest' for better UX
232
+ variationSlug = "latest";
233
+ filename = parts.slice(2).join("/");
234
+ } else {
235
+ // Structure: scenarioKey/variationSlug/filename
236
+ variationSlug = parts[1];
237
+ filename = parts.slice(2).join("/");
238
+ }
239
+
240
+ // Extract visual key from filename (remove extension)
241
+ const captureKey = path.basename(filename, path.extname(filename));
242
+
243
+ return {
244
+ scenarioKey,
245
+ variationSlug,
246
+ captureKey,
247
+ filename,
248
+ // Include raw path info for debugging
249
+ rawParts: parts,
250
+ };
251
+ }
252
+
253
+ /**
254
+ * Group assets by scenario and variation
255
+ * @param {string[]} assetFiles - Array of absolute file paths
256
+ * @param {string} outputBaseDir - Base output directory
257
+ * @returns {Array} Array of grouped asset objects
258
+ */
259
+ function groupAssetsByScenario(assetFiles, outputBaseDir) {
260
+ const groups = new Map();
261
+
262
+ for (const assetPath of assetFiles) {
263
+ const metadata = extractMetadata(assetPath, outputBaseDir);
264
+ const { scenarioKey, variationSlug, captureKey } = metadata;
265
+
266
+ if (!scenarioKey || !variationSlug || !captureKey) {
267
+ continue;
268
+ }
269
+
270
+ const groupKey = `${scenarioKey}::${variationSlug}`;
271
+ if (!groups.has(groupKey)) {
272
+ groups.set(groupKey, {
273
+ scenarioKey,
274
+ variationSlug,
275
+ assets: [],
276
+ });
277
+ }
278
+
279
+ const stat = fs.statSync(assetPath);
280
+ groups.get(groupKey).assets.push({
281
+ captureKey,
282
+ path: assetPath,
283
+ filename: metadata.filename,
284
+ size: stat.size,
285
+ mtime: stat.mtime.toISOString(),
286
+ url: `/assets/${path
287
+ .relative(outputBaseDir, assetPath)
288
+ .replace(/\\/g, "/")}`,
289
+ });
290
+ }
291
+
292
+ return Array.from(groups.values());
293
+ }
294
+
295
+ /**
296
+ * Get all version timestamps per scenario
297
+ * @param {string} outputBaseDir - Base output directory
298
+ * @returns {Object} Map of scenarioKey -> array of version timestamps (sorted newest first)
299
+ */
300
+ function getVersionsPerScenario(outputBaseDir) {
301
+ const versions = {};
302
+
303
+ if (!fs.existsSync(outputBaseDir)) {
304
+ return versions;
305
+ }
306
+
307
+ // Check if a folder name is a timestamp
308
+ function isTimestamp(name) {
309
+ return /^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$/.test(name);
310
+ }
311
+
312
+ // Get scenario-level folders
313
+ const scenarioFolders = fs.readdirSync(outputBaseDir).filter((item) => {
314
+ const fullPath = path.join(outputBaseDir, item);
315
+ try {
316
+ return fs.statSync(fullPath).isDirectory();
317
+ } catch {
318
+ return false;
319
+ }
320
+ });
321
+
322
+ for (const scenarioFolder of scenarioFolders) {
323
+ const scenarioPath = path.join(outputBaseDir, scenarioFolder);
324
+
325
+ try {
326
+ const subFolders = fs.readdirSync(scenarioPath).filter((item) => {
327
+ const fullPath = path.join(scenarioPath, item);
328
+ try {
329
+ return fs.statSync(fullPath).isDirectory();
330
+ } catch {
331
+ return false;
332
+ }
333
+ });
334
+
335
+ // Get only timestamp folders, sorted newest first
336
+ const timestampedFolders = subFolders
337
+ .filter((f) => isTimestamp(f))
338
+ .sort()
339
+ .reverse();
340
+
341
+ if (timestampedFolders.length > 0) {
342
+ versions[scenarioFolder] = timestampedFolders.map((ts) => {
343
+ // Parse timestamp to human-readable format
344
+ const parts = ts.match(
345
+ /(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})/
346
+ );
347
+ if (parts) {
348
+ const date = new Date(
349
+ `${parts[1]}-${parts[2]}-${parts[3]}T${parts[4]}:${parts[5]}:${parts[6]}`
350
+ );
351
+ return {
352
+ timestamp: ts,
353
+ label: date.toLocaleString(),
354
+ date: date.toISOString(),
355
+ };
356
+ }
357
+ return { timestamp: ts, label: ts, date: ts };
358
+ });
359
+ }
360
+ } catch {
361
+ // Skip if can't read
362
+ }
363
+ }
364
+
365
+ return versions;
366
+ }
367
+
368
+ module.exports = {
369
+ findAssetFiles,
370
+ extractMetadata,
371
+ groupAssetsByScenario,
372
+ getVersionsPerScenario,
373
+ };