@reshotdev/screenshot 0.0.1-beta.12 → 0.0.1-beta.14
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/README.md +67 -22
- package/package.json +18 -14
- package/src/commands/auth.js +37 -7
- package/src/commands/capture-dom.js +50 -0
- package/src/commands/compose.js +220 -0
- package/src/commands/doctor-release.js +7 -0
- package/src/commands/doctor-target.js +36 -4
- package/src/commands/drifts.js +13 -1
- package/src/commands/publish.js +183 -21
- package/src/commands/pull.js +9 -4
- package/src/commands/refresh.js +166 -0
- package/src/commands/setup-wizard.js +57 -3
- package/src/commands/status.js +22 -2
- package/src/commands/variation.js +194 -0
- package/src/index.js +190 -10
- package/src/lib/api-client.js +61 -35
- 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 +76 -2
- package/src/lib/capture-script-runner.js +289 -138
- package/src/lib/certification.js +23 -1
- 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 +2 -2
- package/src/lib/dom-capture.js +64 -0
- package/src/lib/ensure-browser.js +147 -0
- package/src/lib/record-clip.js +83 -3
- package/src/lib/record-config.js +0 -4
- package/src/lib/release-doctor.js +11 -3
- package/src/lib/resolve-targets.js +60 -0
- package/src/lib/run-manifest.js +45 -0
- package/src/lib/ui-api-helpers.js +118 -0
- package/src/lib/ui-api.js +28 -820
- 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-CvleJUur.js → index-D0S2otug.js} +56 -56
- package/web/manager/dist/index.html +1 -1
- package/src/commands/ingest.js +0 -458
- package/src/commands/setup.js +0 -165
- package/src/lib/playwright-runner.js +0 -252
package/src/lib/record-clip.js
CHANGED
|
@@ -8,13 +8,27 @@ const { chromium } = require('playwright');
|
|
|
8
8
|
const { updateBrowserMode } = require('./record-browser-injection');
|
|
9
9
|
const { runPolishedClip } = require('./polished-clip');
|
|
10
10
|
const { saveScenarioProgress } = require('./record-config');
|
|
11
|
+
const { resolveTargets } = require('./resolve-targets');
|
|
12
|
+
const { captureDomArtifact } = require('./dom-capture');
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Start clip recording flow
|
|
14
16
|
* @param {Object} sessionState - Recording session state
|
|
15
17
|
* @param {Page} page - Playwright page object
|
|
16
18
|
*/
|
|
17
|
-
async function startClipRecording(sessionState, page) {
|
|
19
|
+
async function startClipRecording(sessionState, page, config = {}) {
|
|
20
|
+
const metadataRecorder = config.emitMetadata
|
|
21
|
+
? createClipMetadataRecorder({
|
|
22
|
+
slug: config.slug || sessionState.visualKey,
|
|
23
|
+
captureSize: config.captureSize || { width: 1280, height: 720 },
|
|
24
|
+
})
|
|
25
|
+
: null;
|
|
26
|
+
|
|
27
|
+
if (metadataRecorder) {
|
|
28
|
+
sessionState.logEvent = metadataRecorder.logEvent;
|
|
29
|
+
metadataRecorder.logEvent('workflow_start');
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
// Ask about container element
|
|
19
33
|
const { useContainer } = await inquirer.prompt([
|
|
20
34
|
{
|
|
@@ -127,6 +141,10 @@ async function startClipRecording(sessionState, page) {
|
|
|
127
141
|
|
|
128
142
|
// Set up action replay handler to sync video with real user actions
|
|
129
143
|
sessionState.replayActionToRecording = async (action, selector, text) => {
|
|
144
|
+
if (metadataRecorder) {
|
|
145
|
+
metadataRecorder.logEvent(action, { selector, text });
|
|
146
|
+
}
|
|
147
|
+
|
|
130
148
|
try {
|
|
131
149
|
if (action === 'click') {
|
|
132
150
|
await recordingPage.click(selector);
|
|
@@ -233,6 +251,17 @@ async function startClipRecording(sessionState, page) {
|
|
|
233
251
|
|
|
234
252
|
sessionState.capturedSteps.push(clipStep);
|
|
235
253
|
await saveScenarioProgress(sessionState, page, { finalize: false });
|
|
254
|
+
|
|
255
|
+
if (metadataRecorder) {
|
|
256
|
+
await writeClipMetadata({
|
|
257
|
+
page,
|
|
258
|
+
outputDir: config.outputDir || sessionState.outputDir || process.cwd(),
|
|
259
|
+
slug: metadataRecorder.slug,
|
|
260
|
+
captureSize: metadataRecorder.captureSize,
|
|
261
|
+
timeline: metadataRecorder.timeline,
|
|
262
|
+
targets: config.targets,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
236
265
|
|
|
237
266
|
// Clean up temp video
|
|
238
267
|
fs.removeSync(rawVideoPath);
|
|
@@ -242,6 +271,9 @@ async function startClipRecording(sessionState, page) {
|
|
|
242
271
|
sessionState.clipEvents = null;
|
|
243
272
|
sessionState.recordingStart = null;
|
|
244
273
|
sessionState.stopClipRecording = false;
|
|
274
|
+
if (metadataRecorder) {
|
|
275
|
+
delete sessionState.logEvent;
|
|
276
|
+
}
|
|
245
277
|
|
|
246
278
|
console.log(
|
|
247
279
|
chalk.green(
|
|
@@ -250,6 +282,53 @@ async function startClipRecording(sessionState, page) {
|
|
|
250
282
|
);
|
|
251
283
|
}
|
|
252
284
|
|
|
285
|
+
function createClipMetadataRecorder({ slug, captureSize }) {
|
|
286
|
+
const startedAt = Date.now();
|
|
287
|
+
const timeline = [];
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
slug,
|
|
291
|
+
captureSize,
|
|
292
|
+
timeline,
|
|
293
|
+
logEvent(type, payload = {}) {
|
|
294
|
+
timeline.push({
|
|
295
|
+
tMs: Date.now() - startedAt,
|
|
296
|
+
type,
|
|
297
|
+
payload,
|
|
298
|
+
});
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function writeClipMetadata({
|
|
304
|
+
page,
|
|
305
|
+
outputDir,
|
|
306
|
+
slug,
|
|
307
|
+
captureSize,
|
|
308
|
+
timeline,
|
|
309
|
+
targets,
|
|
310
|
+
}) {
|
|
311
|
+
const resolvedTargets = targets ? await resolveTargets(page, targets) : {};
|
|
312
|
+
|
|
313
|
+
// Tier-3: alongside the video, emit a self-contained DOM reconstruction
|
|
314
|
+
// artifact (<slug>.dom.html + sidecars) for eligible screens. Additive and
|
|
315
|
+
// best-effort — it must never break the video path, so failures are swallowed.
|
|
316
|
+
await fs.ensureDir(outputDir);
|
|
317
|
+
const domArtifact = await captureDomArtifact({ page, outputDir, slug });
|
|
318
|
+
|
|
319
|
+
const metadata = {
|
|
320
|
+
slug,
|
|
321
|
+
version: 1,
|
|
322
|
+
captureSize,
|
|
323
|
+
timeline,
|
|
324
|
+
targets: resolvedTargets,
|
|
325
|
+
...(domArtifact ? { domArtifact } : {}),
|
|
326
|
+
};
|
|
327
|
+
const metadataPath = path.join(outputDir, `${slug}.metadata.json`);
|
|
328
|
+
await fs.writeJson(metadataPath, metadata, { spaces: 2 });
|
|
329
|
+
return metadataPath;
|
|
330
|
+
}
|
|
331
|
+
|
|
253
332
|
/**
|
|
254
333
|
* Run subtitle editor mini-server
|
|
255
334
|
* @param {Array} events - Clip events with timestamps
|
|
@@ -338,6 +417,7 @@ function buildClipKey(visualKey, filename) {
|
|
|
338
417
|
}
|
|
339
418
|
|
|
340
419
|
module.exports = {
|
|
341
|
-
|
|
420
|
+
createClipMetadataRecorder,
|
|
421
|
+
startClipRecording,
|
|
422
|
+
writeClipMetadata
|
|
342
423
|
};
|
|
343
|
-
|
package/src/lib/record-config.js
CHANGED
|
@@ -492,10 +492,6 @@ async function persistFinalScenario(sessionState, page, options = {}) {
|
|
|
492
492
|
// Default output configuration for automatic step-by-step image generation
|
|
493
493
|
const defaultOutput = {
|
|
494
494
|
format: "step-by-step-images",
|
|
495
|
-
highlight: {
|
|
496
|
-
color: "rgba(255, 255, 0, 0.5)",
|
|
497
|
-
style: "box",
|
|
498
|
-
},
|
|
499
495
|
};
|
|
500
496
|
|
|
501
497
|
// Parse groupPath from session state or existing scenario
|
|
@@ -275,13 +275,21 @@ async function runReleaseDoctor(options = {}) {
|
|
|
275
275
|
for (const issue of targetDoctor.summary?.advisories || []) {
|
|
276
276
|
advisories.push({ scope: "target-doctor", ...issue });
|
|
277
277
|
}
|
|
278
|
-
|
|
278
|
+
// A stale/mismatched docs asset map (e.g. src/data/reshot-assets.json left
|
|
279
|
+
// behind by an earlier `reshot pull`) describes a generated artifact, not the
|
|
280
|
+
// config being published. It must NOT hard-block a publish of the current
|
|
281
|
+
// config — surface it as an advisory with a concrete remedy instead.
|
|
282
|
+
if (!docsAssetMap.skipped && !docsAssetMap.ok) {
|
|
283
|
+
const remedy =
|
|
284
|
+
docsAssetMap.path
|
|
285
|
+
? `Re-run \`reshot pull\` to regenerate it, or delete ${docsAssetMap.path}.`
|
|
286
|
+
: "Re-run `reshot pull` to regenerate it, or delete the stale src/data/reshot-assets.json.";
|
|
279
287
|
for (const issue of docsAssetMap.issues) {
|
|
280
|
-
|
|
288
|
+
advisories.push({ scope: "docs-asset-map", message: `${issue} ${remedy}` });
|
|
281
289
|
}
|
|
282
290
|
}
|
|
283
291
|
|
|
284
|
-
const ok = preflight.ok && targetDoctor.ok
|
|
292
|
+
const ok = preflight.ok && targetDoctor.ok;
|
|
285
293
|
const report = {
|
|
286
294
|
type: "ReleaseDoctorReport",
|
|
287
295
|
stage: "doctor-release",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// resolve-targets.js - Resolve compose target coordinates from a Playwright page
|
|
2
|
+
|
|
3
|
+
async function resolveTargets(page, targets = {}) {
|
|
4
|
+
const resolved = {};
|
|
5
|
+
|
|
6
|
+
for (const [name, config] of Object.entries(targets || {})) {
|
|
7
|
+
const target = normalizeTarget(config);
|
|
8
|
+
|
|
9
|
+
for (const step of target.navigate) {
|
|
10
|
+
if (step.clickText) {
|
|
11
|
+
await page.getByText(step.clickText, { exact: true }).first().click();
|
|
12
|
+
} else if (step.selector) {
|
|
13
|
+
await page.locator(step.selector).first().click();
|
|
14
|
+
}
|
|
15
|
+
if (step.waitMs) {
|
|
16
|
+
await page.waitForTimeout(step.waitMs);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const locator = page.locator(target.selector).first();
|
|
21
|
+
await locator.waitFor({ state: 'visible', timeout: target.timeoutMs });
|
|
22
|
+
const box = await locator.boundingBox();
|
|
23
|
+
if (!box) {
|
|
24
|
+
throw new Error(`Could not resolve target "${name}"`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
resolved[name] = {
|
|
28
|
+
x: Math.round(box.x),
|
|
29
|
+
y: Math.round(box.y),
|
|
30
|
+
w: Math.round(box.width),
|
|
31
|
+
h: Math.round(box.height),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return resolved;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeTarget(config) {
|
|
39
|
+
if (typeof config === 'string') {
|
|
40
|
+
return {
|
|
41
|
+
selector: config,
|
|
42
|
+
navigate: [],
|
|
43
|
+
timeoutMs: 10000,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!config || typeof config !== 'object' || !config.selector) {
|
|
48
|
+
throw new Error('Target config must be a selector string or an object with selector');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
selector: config.selector,
|
|
53
|
+
navigate: Array.isArray(config.navigate) ? config.navigate : [],
|
|
54
|
+
timeoutMs: config.timeoutMs || 10000,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
resolveTargets,
|
|
60
|
+
};
|
package/src/lib/run-manifest.js
CHANGED
|
@@ -91,6 +91,50 @@ function getLatestSuccessfulRunManifest() {
|
|
|
91
91
|
return null;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Returns the latest run manifest that has at least one successful scenario,
|
|
96
|
+
* even if the overall run was not fully successful. This prevents falling back
|
|
97
|
+
* to stale manifests when only some scenarios failed.
|
|
98
|
+
*
|
|
99
|
+
* Returns { manifest, isFallback, isPartialSuccess } where:
|
|
100
|
+
* - isFallback: true if the returned manifest is NOT the latest run
|
|
101
|
+
* - isPartialSuccess: true if the manifest has both succeeded and failed scenarios
|
|
102
|
+
*/
|
|
103
|
+
function getLatestUsableRunManifest() {
|
|
104
|
+
const latest = readRunManifest(LATEST_RUN_MANIFEST_PATH);
|
|
105
|
+
|
|
106
|
+
if (latest) {
|
|
107
|
+
const successfulScenarios = (latest.scenarios || []).filter(
|
|
108
|
+
(s) => s.success !== false,
|
|
109
|
+
);
|
|
110
|
+
if (successfulScenarios.length > 0) {
|
|
111
|
+
return {
|
|
112
|
+
manifest: latest,
|
|
113
|
+
isFallback: false,
|
|
114
|
+
isPartialSuccess: !latest.success,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Latest has zero successful scenarios — search historical manifests
|
|
120
|
+
for (const manifestPath of listRunManifestPaths()) {
|
|
121
|
+
const manifest = readRunManifest(manifestPath);
|
|
122
|
+
if (!manifest) continue;
|
|
123
|
+
const successfulScenarios = (manifest.scenarios || []).filter(
|
|
124
|
+
(s) => s.success !== false,
|
|
125
|
+
);
|
|
126
|
+
if (successfulScenarios.length > 0) {
|
|
127
|
+
return {
|
|
128
|
+
manifest,
|
|
129
|
+
isFallback: true,
|
|
130
|
+
isPartialSuccess: !manifest.success,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
94
138
|
module.exports = {
|
|
95
139
|
RUN_MANIFEST_DIR,
|
|
96
140
|
LATEST_RUN_MANIFEST_PATH,
|
|
@@ -99,5 +143,6 @@ module.exports = {
|
|
|
99
143
|
readRunManifest,
|
|
100
144
|
listRunManifestPaths,
|
|
101
145
|
getLatestSuccessfulRunManifest,
|
|
146
|
+
getLatestUsableRunManifest,
|
|
102
147
|
normalizeScenarioResults,
|
|
103
148
|
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const config = require("./config");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get the platform URL from settings, falling back to production.
|
|
6
|
+
* @param {Object} settings - CLI settings object
|
|
7
|
+
* @returns {string} Platform URL
|
|
8
|
+
*/
|
|
9
|
+
function getPlatformUrl(settings) {
|
|
10
|
+
if (settings?.platformUrl) {
|
|
11
|
+
return settings.platformUrl;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const envUrl = process.env.RESHOT_API_BASE_URL;
|
|
15
|
+
if (envUrl) {
|
|
16
|
+
return envUrl.replace(/\/api\/?$/, "");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return "https://reshot.dev";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Handle API errors and detect if re-auth is needed.
|
|
24
|
+
* @param {Error} error - The error from API call
|
|
25
|
+
* @param {Object} res - Express response object
|
|
26
|
+
* @returns {Object|null} Response if error was handled, null otherwise
|
|
27
|
+
*/
|
|
28
|
+
function handleApiError(error, res) {
|
|
29
|
+
if (config.isAuthError(error)) {
|
|
30
|
+
const errorMsg =
|
|
31
|
+
error.response?.data?.error ||
|
|
32
|
+
error.message ||
|
|
33
|
+
"API key is invalid or expired";
|
|
34
|
+
return res.status(401).json(config.createAuthErrorResponse(errorMsg));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Generate all possible variant combinations from dimensions.
|
|
42
|
+
* @param {Object} dimensions - Variant dimensions config
|
|
43
|
+
* @param {string[]} dimensionKeys - Which dimensions to include
|
|
44
|
+
* @returns {Array<Object>} Array of variant objects
|
|
45
|
+
*/
|
|
46
|
+
function generateVariantCombinations(dimensions, dimensionKeys = []) {
|
|
47
|
+
if (!dimensions || dimensionKeys.length === 0) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const dimensionOptions = dimensionKeys
|
|
52
|
+
.map((key) => {
|
|
53
|
+
const dim = dimensions[key];
|
|
54
|
+
if (!dim?.options) return [];
|
|
55
|
+
return Object.keys(dim.options).map((optKey) => ({
|
|
56
|
+
dimension: key,
|
|
57
|
+
option: optKey,
|
|
58
|
+
}));
|
|
59
|
+
})
|
|
60
|
+
.filter((opts) => opts.length > 0);
|
|
61
|
+
|
|
62
|
+
if (dimensionOptions.length === 0) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const combinations = cartesian(...dimensionOptions);
|
|
67
|
+
|
|
68
|
+
return combinations.map((combo) => {
|
|
69
|
+
const variant = {};
|
|
70
|
+
for (const { dimension, option } of combo) {
|
|
71
|
+
variant[dimension] = option;
|
|
72
|
+
}
|
|
73
|
+
return variant;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function cartesian(...arrays) {
|
|
78
|
+
return arrays.reduce(
|
|
79
|
+
(acc, arr) => acc.flatMap((combo) => arr.map((item) => [...combo, item])),
|
|
80
|
+
[[]],
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Validate a path segment to prevent directory traversal attacks.
|
|
86
|
+
* @param {string} segment - Path segment to validate
|
|
87
|
+
* @returns {boolean} True if safe, false if potentially malicious
|
|
88
|
+
*/
|
|
89
|
+
function isValidPathSegment(segment) {
|
|
90
|
+
if (!segment || typeof segment !== "string") return false;
|
|
91
|
+
if (segment === "." || segment === "..") return false;
|
|
92
|
+
if (segment.includes("/") || segment.includes("\\")) return false;
|
|
93
|
+
if (segment.includes("\0")) return false;
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Validate that a resolved path stays within the expected base directory.
|
|
99
|
+
* @param {string} resolvedPath - Fully resolved path
|
|
100
|
+
* @param {string} baseDir - Expected base directory
|
|
101
|
+
* @returns {boolean} True if path is within base, false otherwise
|
|
102
|
+
*/
|
|
103
|
+
function isPathWithinBase(resolvedPath, baseDir) {
|
|
104
|
+
const normalizedBase = path.resolve(baseDir);
|
|
105
|
+
const normalizedPath = path.resolve(resolvedPath);
|
|
106
|
+
return (
|
|
107
|
+
normalizedPath.startsWith(normalizedBase + path.sep) ||
|
|
108
|
+
normalizedPath === normalizedBase
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
generateVariantCombinations,
|
|
114
|
+
getPlatformUrl,
|
|
115
|
+
handleApiError,
|
|
116
|
+
isPathWithinBase,
|
|
117
|
+
isValidPathSegment,
|
|
118
|
+
};
|