@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.
- package/LICENSE +190 -0
- package/README.md +388 -0
- package/package.json +64 -0
- package/src/commands/auth.js +259 -0
- package/src/commands/chrome.js +140 -0
- package/src/commands/ci-run.js +123 -0
- package/src/commands/ci-setup.js +288 -0
- package/src/commands/drifts.js +423 -0
- package/src/commands/import-tests.js +309 -0
- package/src/commands/ingest.js +458 -0
- package/src/commands/init.js +633 -0
- package/src/commands/publish.js +1721 -0
- package/src/commands/pull.js +303 -0
- package/src/commands/record.js +94 -0
- package/src/commands/run.js +476 -0
- package/src/commands/setup-wizard.js +740 -0
- package/src/commands/setup.js +137 -0
- package/src/commands/status.js +275 -0
- package/src/commands/sync.js +621 -0
- package/src/commands/ui.js +248 -0
- package/src/commands/validate-docs.js +529 -0
- package/src/index.js +462 -0
- package/src/lib/api-client.js +815 -0
- package/src/lib/capture-engine.js +1623 -0
- package/src/lib/capture-script-runner.js +3120 -0
- package/src/lib/ci-detect.js +137 -0
- package/src/lib/config.js +1240 -0
- package/src/lib/diff-engine.js +642 -0
- package/src/lib/hash.js +74 -0
- package/src/lib/image-crop.js +396 -0
- package/src/lib/matrix.js +89 -0
- package/src/lib/output-path-template.js +318 -0
- package/src/lib/playwright-runner.js +252 -0
- package/src/lib/polished-clip.js +553 -0
- package/src/lib/privacy-engine.js +408 -0
- package/src/lib/progress-tracker.js +142 -0
- package/src/lib/record-browser-injection.js +654 -0
- package/src/lib/record-cdp.js +612 -0
- package/src/lib/record-clip.js +343 -0
- package/src/lib/record-config.js +623 -0
- package/src/lib/record-screenshot.js +360 -0
- package/src/lib/record-terminal.js +123 -0
- package/src/lib/recorder-service.js +781 -0
- package/src/lib/secrets.js +51 -0
- package/src/lib/selector-strategies.js +859 -0
- package/src/lib/standalone-mode.js +400 -0
- package/src/lib/storage-providers.js +569 -0
- package/src/lib/style-engine.js +684 -0
- package/src/lib/ui-api.js +4677 -0
- package/src/lib/ui-assets.js +373 -0
- package/src/lib/ui-executor.js +587 -0
- package/src/lib/variant-injector.js +591 -0
- package/src/lib/viewport-presets.js +454 -0
- package/src/lib/worker-pool.js +118 -0
- package/web/cropper/index.html +436 -0
- package/web/manager/dist/assets/index--ZgioErz.js +507 -0
- package/web/manager/dist/assets/index-n468W0Wr.css +1 -0
- package/web/manager/dist/index.html +27 -0
- 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
|
+
};
|