@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.
- package/LICENSE +1 -1
- package/README.md +138 -47
- package/package.json +27 -16
- package/src/commands/auth.js +159 -30
- package/src/commands/capture-dom.js +50 -0
- package/src/commands/certify.js +62 -0
- package/src/commands/compose.js +220 -0
- package/src/commands/doctor-release.js +74 -0
- package/src/commands/doctor-target.js +108 -0
- package/src/commands/drifts.js +16 -69
- package/src/commands/import-tests.js +13 -13
- package/src/commands/init.js +16 -277
- package/src/commands/publish.js +484 -257
- package/src/commands/pull.js +302 -35
- package/src/commands/refresh.js +166 -0
- package/src/commands/run.js +292 -12
- package/src/commands/setup-wizard.js +348 -496
- package/src/commands/status.js +334 -126
- package/src/commands/sync.js +28 -236
- package/src/commands/ui.js +1 -1
- package/src/commands/variation.js +194 -0
- package/src/commands/verify-publish.js +46 -0
- package/src/index.js +383 -118
- package/src/lib/api-client.js +172 -60
- 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 +179 -9
- package/src/lib/capture-script-runner.js +639 -214
- package/src/lib/certification.js +887 -0
- 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 +186 -81
- package/src/lib/dom-capture.js +64 -0
- package/src/lib/ensure-browser.js +147 -0
- package/src/lib/output-path-template.js +3 -3
- package/src/lib/record-cdp.js +288 -16
- package/src/lib/record-clip.js +83 -3
- package/src/lib/record-config.js +1 -5
- package/src/lib/release-doctor.js +321 -0
- package/src/lib/resolve-targets.js +60 -0
- package/src/lib/run-manifest.js +148 -0
- package/src/lib/standalone-mode.js +1 -1
- package/src/lib/storage-providers.js +5 -5
- package/src/lib/style-engine.js +5 -5
- package/src/lib/target-contract.js +292 -0
- package/src/lib/ui-api-helpers.js +118 -0
- package/src/lib/ui-api.js +31 -824
- 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-D0S2otug.js +507 -0
- package/web/manager/dist/index.html +1 -1
- package/src/commands/ci-run.js +0 -123
- package/src/commands/ci-setup.js +0 -288
- package/src/commands/ingest.js +0 -458
- package/src/commands/setup.js +0 -137
- package/src/commands/validate-docs.js +0 -529
- package/src/lib/playwright-runner.js +0 -252
- package/web/manager/dist/assets/index--ZgioErz.js +0 -507
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// capture-dom.js - Capture a self-contained DOM reconstruction artifact from a
|
|
2
|
+
// live URL (Tier-3 Phase 2). Emits <slug>.dom.html (+ sidecars), remounted.png,
|
|
3
|
+
// and live.png so the calibrated quality gate can diff them:
|
|
4
|
+
//
|
|
5
|
+
// reshot capture-dom <url> --out /tmp/cap
|
|
6
|
+
// pnpm --dir packages/compose verify diff /tmp/cap/remounted.png /tmp/cap/live.png
|
|
7
|
+
|
|
8
|
+
const chalk = require("chalk");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const { captureDomFromUrl } = require("../lib/dom-capture");
|
|
11
|
+
|
|
12
|
+
function registerCaptureDom(program) {
|
|
13
|
+
program
|
|
14
|
+
.command("capture-dom <url>")
|
|
15
|
+
.description("Capture a self-contained DOM reconstruction artifact from a live URL")
|
|
16
|
+
.option("-o, --out <dir>", "Output directory", "./.reshot/capture-dom")
|
|
17
|
+
.option("-s, --slug <slug>", "Artifact slug", "capture")
|
|
18
|
+
.option("--width <px>", "Viewport width", (v) => parseInt(v, 10))
|
|
19
|
+
.option("--height <px>", "Viewport height", (v) => parseInt(v, 10))
|
|
20
|
+
.option("--dpr <n>", "Device scale factor", (v) => parseInt(v, 10))
|
|
21
|
+
.action(async (url, options) => {
|
|
22
|
+
const outDir = path.resolve(options.out);
|
|
23
|
+
const settings =
|
|
24
|
+
options.width || options.height || options.dpr
|
|
25
|
+
? {
|
|
26
|
+
width: options.width || 1000,
|
|
27
|
+
height: options.height || 800,
|
|
28
|
+
deviceScaleFactor: options.dpr || 2,
|
|
29
|
+
}
|
|
30
|
+
: undefined;
|
|
31
|
+
|
|
32
|
+
console.log(chalk.cyan(`\n Capturing DOM from ${url} ...\n`));
|
|
33
|
+
const result = await captureDomFromUrl({ url, outDir, slug: options.slug, settings });
|
|
34
|
+
|
|
35
|
+
console.log(chalk.green(` ✔ method: ${result.method}`));
|
|
36
|
+
console.log(` artifact: ${result.artifact}`);
|
|
37
|
+
console.log(` remounted: ${result.remounted}`);
|
|
38
|
+
console.log(` live: ${result.live}`);
|
|
39
|
+
if (result.sidecars.length) {
|
|
40
|
+
console.log(` sidecars: ${result.sidecars.map((s) => `${s.kind}(rasterized=${s.rasterized})`).join(", ")}`);
|
|
41
|
+
}
|
|
42
|
+
console.log(
|
|
43
|
+
chalk.gray(
|
|
44
|
+
`\n Verify: pnpm --dir packages/compose verify diff ${result.remounted} ${result.live}\n`,
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { registerCaptureDom };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const chalk = require("chalk");
|
|
4
|
+
const { runCertification } = require("../lib/certification");
|
|
5
|
+
|
|
6
|
+
async function certifyCommand(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 runCertification({
|
|
15
|
+
scenarioKeys,
|
|
16
|
+
tag: options.tag,
|
|
17
|
+
message: options.message,
|
|
18
|
+
skipReleaseDoctor: options.skipReleaseDoctor,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (options.json) {
|
|
22
|
+
console.log(JSON.stringify(report, null, 2));
|
|
23
|
+
} else {
|
|
24
|
+
console.log(chalk.cyan("\n🏅 Certified Target Verification\n"));
|
|
25
|
+
console.log(chalk.gray(`Target: ${report.target.displayName}`));
|
|
26
|
+
console.log(
|
|
27
|
+
report.ok
|
|
28
|
+
? chalk.green(` ✔ Final status: ${report.finalStatus}`)
|
|
29
|
+
: chalk.red(` ✖ Final status: ${report.finalStatus}`),
|
|
30
|
+
);
|
|
31
|
+
console.log(
|
|
32
|
+
report.releaseDoctor?.skipped
|
|
33
|
+
? chalk.gray(" • Release doctor skipped")
|
|
34
|
+
: report.releaseDoctor?.ok
|
|
35
|
+
? chalk.green(" ✔ Release doctor passed")
|
|
36
|
+
: chalk.red(" ✖ Release doctor failed"),
|
|
37
|
+
);
|
|
38
|
+
console.log(
|
|
39
|
+
report.doctor.ok
|
|
40
|
+
? chalk.green(" ✔ Doctor passed")
|
|
41
|
+
: chalk.red(" ✖ Doctor failed"),
|
|
42
|
+
);
|
|
43
|
+
console.log(
|
|
44
|
+
report.capture.success
|
|
45
|
+
? chalk.green(" ✔ Capture passed")
|
|
46
|
+
: chalk.red(" ✖ Capture failed"),
|
|
47
|
+
);
|
|
48
|
+
console.log(
|
|
49
|
+
report.publishVerification.ok
|
|
50
|
+
? chalk.green(" ✔ Publish verification passed")
|
|
51
|
+
: chalk.red(" ✖ Publish verification failed"),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!report.ok) {
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return report;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = certifyCommand;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// compose.js - Render a local @reshot/compose file into a video pack
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const fs = require("fs-extra");
|
|
4
|
+
const chalk = require("chalk");
|
|
5
|
+
const { composeDistDir } = require("../lib/compose-runtime");
|
|
6
|
+
const {
|
|
7
|
+
DEFAULT_FORMATS,
|
|
8
|
+
assertCaptureExists,
|
|
9
|
+
deriveSlug,
|
|
10
|
+
parseFormats,
|
|
11
|
+
parseSize,
|
|
12
|
+
resolveCapturePath,
|
|
13
|
+
resolveComposeContext,
|
|
14
|
+
resolveMetadataPath,
|
|
15
|
+
resolveOutBase,
|
|
16
|
+
} = require("../lib/compose-context");
|
|
17
|
+
const {
|
|
18
|
+
assertUploadPackExists,
|
|
19
|
+
packFromExistingOutputs,
|
|
20
|
+
} = require("../lib/compose-pack");
|
|
21
|
+
const {
|
|
22
|
+
buildDashboardUrl,
|
|
23
|
+
getComposeApiBaseUrl,
|
|
24
|
+
humanizeName,
|
|
25
|
+
resolveComposeProjectContext,
|
|
26
|
+
uploadComposition,
|
|
27
|
+
} = require("../lib/compose-upload");
|
|
28
|
+
|
|
29
|
+
async function runCompose(file, options = {}, deps = {}) {
|
|
30
|
+
const context = await resolveComposeContext(file, options);
|
|
31
|
+
|
|
32
|
+
const size = parseSize(options.size || "1440x900");
|
|
33
|
+
const formats = parseFormats(options.formats, options.gif);
|
|
34
|
+
const outBase = resolveOutBase(context.compositionPath, options.out);
|
|
35
|
+
|
|
36
|
+
const { render } = deps.renderModule || loadComposeRender();
|
|
37
|
+
|
|
38
|
+
console.log(chalk.cyan(`\n-> Rendering ${chalk.bold(context.slug)} (${size.width}x${size.height})`));
|
|
39
|
+
console.log(chalk.gray(` composition ${relativePath(context.compositionPath)}`));
|
|
40
|
+
console.log(chalk.gray(` metadata ${relativePath(context.metadataPath)}`));
|
|
41
|
+
console.log(chalk.gray(` capture ${relativePath(context.capturePath)}`));
|
|
42
|
+
console.log(chalk.gray(` out ${relativePath(outBase)}`));
|
|
43
|
+
|
|
44
|
+
const result = await withComposeEnvironment(
|
|
45
|
+
{
|
|
46
|
+
RESHOT_COMPOSE_SLUG: context.slug,
|
|
47
|
+
RESHOT_COMPOSE_METADATA_PATH: context.metadataPath,
|
|
48
|
+
RESHOT_COMPOSE_CAPTURE_PATH: context.capturePath,
|
|
49
|
+
},
|
|
50
|
+
() =>
|
|
51
|
+
render(context.compositionPath, {
|
|
52
|
+
slug: context.slug,
|
|
53
|
+
out: outBase,
|
|
54
|
+
size,
|
|
55
|
+
formats,
|
|
56
|
+
metadataPath: context.metadataPath,
|
|
57
|
+
capturePath: context.capturePath,
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
console.log(chalk.green(`\nRendered ${context.slug}`));
|
|
62
|
+
printPack(result.pack || {});
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function runComposePush(file, options = {}, deps = {}) {
|
|
67
|
+
const context = await resolveComposeContext(file, options);
|
|
68
|
+
const outBase = resolveOutBase(context.compositionPath, options.out);
|
|
69
|
+
const pack = options.skipRender
|
|
70
|
+
? await packFromExistingOutputs(outBase)
|
|
71
|
+
: (await runCompose(file, options, deps)).pack || {};
|
|
72
|
+
|
|
73
|
+
await assertUploadPackExists(pack, Boolean(options.skipRender));
|
|
74
|
+
|
|
75
|
+
const projectContext = resolveComposeProjectContext({
|
|
76
|
+
projectOption: options.project,
|
|
77
|
+
settings: deps.settings,
|
|
78
|
+
});
|
|
79
|
+
const apiBaseUrl = getComposeApiBaseUrl(deps.apiBaseUrl);
|
|
80
|
+
const name = options.name || humanizeName(context.slug);
|
|
81
|
+
const upload = deps.uploadComposition || uploadComposition;
|
|
82
|
+
|
|
83
|
+
console.log(chalk.cyan(`\n-> Uploading ${chalk.bold(name)} to project ${projectContext.projectId}`));
|
|
84
|
+
const response = await upload({
|
|
85
|
+
apiBaseUrl,
|
|
86
|
+
apiKey: projectContext.apiKey,
|
|
87
|
+
projectId: projectContext.projectId,
|
|
88
|
+
name,
|
|
89
|
+
slug: context.slug,
|
|
90
|
+
sourceTsx: await fs.readFile(context.compositionPath, "utf8"),
|
|
91
|
+
metadataJson: await fs.readFile(context.metadataPath, "utf8"),
|
|
92
|
+
pack,
|
|
93
|
+
autoApprove: Boolean(options.autoApprove),
|
|
94
|
+
httpClient: deps.httpClient,
|
|
95
|
+
});
|
|
96
|
+
const dashboardUrl = buildDashboardUrl(response, apiBaseUrl, projectContext.projectId);
|
|
97
|
+
|
|
98
|
+
console.log(chalk.green(`\nUploaded ${context.slug}`));
|
|
99
|
+
console.log(chalk.gray(` Dashboard: ${dashboardUrl}`));
|
|
100
|
+
printPublicUrls(response?.publicUrls);
|
|
101
|
+
if (response?.attributionWarning === "legacy-key-no-user-attribution") {
|
|
102
|
+
console.log(
|
|
103
|
+
chalk.yellow(
|
|
104
|
+
" Attribution: re-issue your API key to enable per-engineer render attribution.",
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { response, dashboardUrl, projectId: projectContext.projectId };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function registerCompose(program) {
|
|
113
|
+
const compose = program
|
|
114
|
+
.command("compose")
|
|
115
|
+
.description("Render and upload local JSX compositions")
|
|
116
|
+
.argument("<file>", "Composition file to render")
|
|
117
|
+
.option("--slug <slug>", "Matrix variant slug; defaults to the composition filename")
|
|
118
|
+
.option("--out <path>", "Output base path; defaults to <file-stem>.composed")
|
|
119
|
+
.option("--size <size>", "Viewport size as WIDTHxHEIGHT", "1440x900")
|
|
120
|
+
.option("--formats <formats>", "Comma-separated output formats", DEFAULT_FORMATS.join(","))
|
|
121
|
+
.option("--gif", "Also emit a gif")
|
|
122
|
+
.action(async (file, options) => {
|
|
123
|
+
try {
|
|
124
|
+
await runCompose(file, normalizeCommandOptions(options));
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error(chalk.red("Error:"), error.message);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
compose
|
|
132
|
+
.command("push <file>")
|
|
133
|
+
.description("Render and upload a local JSX composition to the dashboard")
|
|
134
|
+
.option("--name <name>", "Composition display name")
|
|
135
|
+
.option("--project <projectId>", "Project id; defaults to local Reshot settings")
|
|
136
|
+
.option("--out <path>", "Output base path to upload when --skip-render is used")
|
|
137
|
+
.option("--skip-render", "Upload existing local outputs without re-rendering")
|
|
138
|
+
.option("--auto-approve", "Immediately approve this composition render and update live embed URLs")
|
|
139
|
+
.action(async (file, options) => {
|
|
140
|
+
try {
|
|
141
|
+
await runComposePush(file, normalizeCommandOptions(options));
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error(chalk.red("Error:"), error.message);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeCommandOptions(options) {
|
|
150
|
+
return typeof options?.opts === "function" ? options.opts() : options || {};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function loadComposeRender() {
|
|
154
|
+
return require(path.join(composeDistDir(), "render.cjs"));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function withComposeEnvironment(env, callback) {
|
|
158
|
+
const previous = {};
|
|
159
|
+
for (const [key, value] of Object.entries(env)) {
|
|
160
|
+
previous[key] = process.env[key];
|
|
161
|
+
process.env[key] = value;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
return await callback();
|
|
166
|
+
} finally {
|
|
167
|
+
for (const [key, value] of Object.entries(previous)) {
|
|
168
|
+
if (value === undefined) {
|
|
169
|
+
delete process.env[key];
|
|
170
|
+
} else {
|
|
171
|
+
process.env[key] = value;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function printPack(pack) {
|
|
178
|
+
for (const format of ["mp4", "webm", "poster", "gif"]) {
|
|
179
|
+
if (pack[format]) {
|
|
180
|
+
console.log(chalk.gray(` ${format.padEnd(6)} ${relativePath(pack[format])}`));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function printPublicUrls(publicUrls) {
|
|
186
|
+
if (!publicUrls || typeof publicUrls !== "object") return;
|
|
187
|
+
if (publicUrls.embed) {
|
|
188
|
+
console.log(chalk.gray(` Live embed: ${publicUrls.embed}`));
|
|
189
|
+
}
|
|
190
|
+
if (publicUrls.live?.mp4) {
|
|
191
|
+
console.log(chalk.gray(` Live MP4: ${publicUrls.live.mp4}`));
|
|
192
|
+
}
|
|
193
|
+
if (publicUrls.pinned?.mp4) {
|
|
194
|
+
console.log(chalk.gray(` Pinned MP4: ${publicUrls.pinned.mp4}`));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function relativePath(filePath) {
|
|
199
|
+
const relative = path.relative(process.cwd(), filePath);
|
|
200
|
+
return relative && !relative.startsWith("..") ? relative : filePath;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = {
|
|
204
|
+
DEFAULT_FORMATS,
|
|
205
|
+
assertCaptureExists,
|
|
206
|
+
deriveSlug,
|
|
207
|
+
normalizeCommandOptions,
|
|
208
|
+
parseFormats,
|
|
209
|
+
parseSize,
|
|
210
|
+
printPublicUrls,
|
|
211
|
+
registerCompose,
|
|
212
|
+
resolveComposeContext,
|
|
213
|
+
resolveComposeProjectContext,
|
|
214
|
+
resolveCapturePath,
|
|
215
|
+
resolveMetadataPath,
|
|
216
|
+
resolveOutBase,
|
|
217
|
+
runCompose,
|
|
218
|
+
runComposePush,
|
|
219
|
+
uploadComposition,
|
|
220
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const chalk = require("chalk");
|
|
4
|
+
const { runReleaseDoctor } = require("../lib/release-doctor");
|
|
5
|
+
|
|
6
|
+
async function doctorReleaseCommand(options = {}) {
|
|
7
|
+
const report = await runReleaseDoctor(options);
|
|
8
|
+
|
|
9
|
+
if (options.json) {
|
|
10
|
+
console.log(JSON.stringify(report, null, 2));
|
|
11
|
+
} else {
|
|
12
|
+
console.log(chalk.cyan("\n🧪 Release Doctor\n"));
|
|
13
|
+
console.log(
|
|
14
|
+
report.ok
|
|
15
|
+
? chalk.green(" ✔ Release gate checks passed")
|
|
16
|
+
: chalk.red(" ✖ Release gate checks failed"),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
console.log(
|
|
20
|
+
report.runPreflight.ok
|
|
21
|
+
? chalk.green(" ✔ Run preflight healthy")
|
|
22
|
+
: chalk.red(" ✖ Run preflight failed"),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (report.targetDoctor.skipped) {
|
|
26
|
+
console.log(chalk.gray(" • Target doctor skipped (non-certified target)"));
|
|
27
|
+
} else {
|
|
28
|
+
console.log(
|
|
29
|
+
report.targetDoctor.ok
|
|
30
|
+
? chalk.green(" ✔ Target doctor healthy")
|
|
31
|
+
: chalk.red(" ✖ Target doctor failed"),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (report.docsAssetMap.skipped) {
|
|
36
|
+
console.log(chalk.gray(" • Docs asset map skipped"));
|
|
37
|
+
} else {
|
|
38
|
+
console.log(
|
|
39
|
+
report.docsAssetMap.ok
|
|
40
|
+
? chalk.green(" ✔ Docs asset map healthy")
|
|
41
|
+
: chalk.red(" ✖ Docs asset map failed"),
|
|
42
|
+
);
|
|
43
|
+
if (report.docsAssetMap.path) {
|
|
44
|
+
console.log(chalk.gray(` ${report.docsAssetMap.path}`));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const blockingIssues = report.summary?.blockingIssues || [];
|
|
49
|
+
if (blockingIssues.length > 0) {
|
|
50
|
+
for (const issue of blockingIssues.slice(0, 10)) {
|
|
51
|
+
console.log(chalk.red(` ✖ ${issue.scope}: ${issue.message}`));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const advisories = report.summary?.advisories || [];
|
|
56
|
+
if (advisories.length > 0) {
|
|
57
|
+
for (const advisory of advisories.slice(0, 10)) {
|
|
58
|
+
console.log(chalk.yellow(` ⚠ ${advisory.scope}: ${advisory.message}`));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (report.reportPath) {
|
|
63
|
+
console.log(chalk.gray(`\n Report: ${report.reportPath}`));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!report.ok) {
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return report;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = doctorReleaseCommand;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const chalk = require("chalk");
|
|
4
|
+
const { runDoctorTarget } = require("../lib/certification");
|
|
5
|
+
const config = require("../lib/config");
|
|
6
|
+
|
|
7
|
+
async function doctorTargetCommand(options = {}) {
|
|
8
|
+
const scenarioKeys = options.scenarios
|
|
9
|
+
? String(options.scenarios)
|
|
10
|
+
.split(",")
|
|
11
|
+
.map((value) => value.trim())
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
: null;
|
|
14
|
+
|
|
15
|
+
// Check for a config BEFORE the banner so we don't print "scenarios: all
|
|
16
|
+
// certified" immediately above a "Config file not found" error (audit run-11
|
|
17
|
+
// F5). Without a config there is nothing to certify.
|
|
18
|
+
let hasConfig = false;
|
|
19
|
+
try {
|
|
20
|
+
hasConfig = config.configExists();
|
|
21
|
+
} catch {
|
|
22
|
+
hasConfig = false;
|
|
23
|
+
}
|
|
24
|
+
if (!hasConfig) {
|
|
25
|
+
if (options.json) {
|
|
26
|
+
console.log(
|
|
27
|
+
JSON.stringify({ ok: false, error: "Config file not found" }, null, 2),
|
|
28
|
+
);
|
|
29
|
+
} else {
|
|
30
|
+
console.error(
|
|
31
|
+
chalk.red("\n ✖ Target doctor aborted: Config file not found."),
|
|
32
|
+
);
|
|
33
|
+
console.error(
|
|
34
|
+
chalk.gray(" Run `reshot init` (or `reshot setup`) to create one."),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
return { ok: false, error: "Config file not found" };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Emit an immediate banner BEFORE any async work so the command is never
|
|
42
|
+
// silent — previously it produced zero output while preparing fixtures and
|
|
43
|
+
// launching a browser, which read as a hang.
|
|
44
|
+
if (!options.json) {
|
|
45
|
+
console.log(chalk.cyan("🩺 Running target doctor…"));
|
|
46
|
+
console.log(
|
|
47
|
+
chalk.gray(
|
|
48
|
+
scenarioKeys
|
|
49
|
+
? ` scenarios: ${scenarioKeys.join(", ")}`
|
|
50
|
+
: " scenarios: all certified",
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const progressLogger = options.json
|
|
56
|
+
? null
|
|
57
|
+
: (message) => console.log(chalk.gray(` → ${message}`));
|
|
58
|
+
|
|
59
|
+
let report;
|
|
60
|
+
try {
|
|
61
|
+
report = await runDoctorTarget({
|
|
62
|
+
scenarioKeys,
|
|
63
|
+
onProgress: progressLogger,
|
|
64
|
+
timeoutMs: options.timeout ? Number(options.timeout) : undefined,
|
|
65
|
+
});
|
|
66
|
+
} catch (error) {
|
|
67
|
+
// Fail fast with an actionable message + report path rather than hanging.
|
|
68
|
+
if (!options.json) {
|
|
69
|
+
console.error(chalk.red(`\n ✖ Target doctor aborted: ${error.message}`));
|
|
70
|
+
console.error(
|
|
71
|
+
chalk.gray(
|
|
72
|
+
" Confirm the dev server is reachable and the target is configured (see .reshot/reports).",
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
} else {
|
|
76
|
+
console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
|
|
77
|
+
}
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
return { ok: false, error: error.message };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (options.json) {
|
|
83
|
+
console.log(JSON.stringify(report, null, 2));
|
|
84
|
+
} else {
|
|
85
|
+
console.log(chalk.cyan("\n🩺 Certified Target Doctor\n"));
|
|
86
|
+
console.log(chalk.gray(`Target: ${report.target.displayName} (${report.target.tier})`));
|
|
87
|
+
console.log(
|
|
88
|
+
report.ok
|
|
89
|
+
? chalk.green(" ✔ Target contract is healthy")
|
|
90
|
+
: chalk.red(" ✖ Target contract check failed"),
|
|
91
|
+
);
|
|
92
|
+
for (const audit of report.readinessAudits) {
|
|
93
|
+
console.log(
|
|
94
|
+
audit.ok
|
|
95
|
+
? chalk.green(` ✔ ${audit.scenario}`)
|
|
96
|
+
: chalk.red(` ✖ ${audit.scenario}${audit.contractFailure ? ` — ${audit.contractFailure}` : ""}`),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!report.ok) {
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return report;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = doctorTargetCommand;
|
package/src/commands/drifts.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - reshot drifts sync <id> Mark as manually synced (external_host)
|
|
13
13
|
* - reshot drifts approve-all Approve all pending drifts
|
|
14
14
|
* - reshot drifts reject-all Reject all pending drifts
|
|
15
|
-
* - reshot drifts
|
|
15
|
+
* - reshot drifts approve-all Approve all pending drifts
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
const chalk = require("chalk");
|
|
@@ -234,7 +234,7 @@ async function batchDriftAction(apiKey, projectId, action, options = {}) {
|
|
|
234
234
|
|
|
235
235
|
try {
|
|
236
236
|
// Fetch all pending drifts
|
|
237
|
-
const response = await apiClient.getDrifts(apiKey, projectId, { status: "
|
|
237
|
+
const response = await apiClient.getDrifts(apiKey, projectId, { status: "pending" });
|
|
238
238
|
const drifts = response.drifts || [];
|
|
239
239
|
|
|
240
240
|
if (drifts.length === 0) {
|
|
@@ -268,66 +268,18 @@ async function batchDriftAction(apiKey, projectId, action, options = {}) {
|
|
|
268
268
|
console.log(chalk.red(` ${failed} drift(s) failed`));
|
|
269
269
|
}
|
|
270
270
|
} catch (error) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
try {
|
|
283
|
-
// Fetch visual keys from server
|
|
284
|
-
const visualKeys = await apiClient.getVisualKeys(projectId, apiKey);
|
|
285
|
-
console.log(chalk.green(` ✓ Fetched ${visualKeys.size} visual keys from project`));
|
|
286
|
-
|
|
287
|
-
// Read docsync config
|
|
288
|
-
const docSyncConfig = config.readDocSyncConfig();
|
|
289
|
-
const docConfig = docSyncConfig.documentation;
|
|
290
|
-
|
|
291
|
-
if (!docConfig || !docConfig.root) {
|
|
292
|
-
console.log(chalk.yellow(" ⚠ No documentation.root configured"));
|
|
271
|
+
const status = error.reshot?.status ?? error.response?.status;
|
|
272
|
+
if (status === 400 || status === 404) {
|
|
273
|
+
console.log(chalk.green(" ✓ No pending drifts to approve."));
|
|
274
|
+
console.log(
|
|
275
|
+
chalk.gray(
|
|
276
|
+
" Drifts only exist when a capture differs from a baseline. New captures\n" +
|
|
277
|
+
" with no prior version go to the review queue — use `reshot publish\n" +
|
|
278
|
+
" --auto-approve` or approve them in the studio."
|
|
279
|
+
)
|
|
280
|
+
);
|
|
293
281
|
return;
|
|
294
282
|
}
|
|
295
|
-
|
|
296
|
-
// Use the validate-docs module for full validation
|
|
297
|
-
const validateDocs = require("./validate-docs");
|
|
298
|
-
const result = await validateDocs.validateDocSync({
|
|
299
|
-
strict: options.strict,
|
|
300
|
-
verbose: options.verbose,
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
// Additional server-side validation: check if any journey keys in the project
|
|
304
|
-
// are not bound to any documentation
|
|
305
|
-
const mappings = docConfig.mappings || {};
|
|
306
|
-
const boundKeys = new Set(Object.values(mappings));
|
|
307
|
-
|
|
308
|
-
const orphanedVisuals = [];
|
|
309
|
-
for (const key of visualKeys) {
|
|
310
|
-
if (!boundKeys.has(key)) {
|
|
311
|
-
orphanedVisuals.push(key);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (orphanedVisuals.length > 0) {
|
|
316
|
-
console.log(chalk.yellow(`\n ⚠ ${orphanedVisuals.length} visual(s) not bound to documentation:`));
|
|
317
|
-
if (options.verbose) {
|
|
318
|
-
orphanedVisuals.slice(0, 10).forEach((key) => {
|
|
319
|
-
console.log(chalk.gray(` - ${key}`));
|
|
320
|
-
});
|
|
321
|
-
if (orphanedVisuals.length > 10) {
|
|
322
|
-
console.log(chalk.gray(` ... and ${orphanedVisuals.length - 10} more`));
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (!result.valid) {
|
|
328
|
-
process.exit(1);
|
|
329
|
-
}
|
|
330
|
-
} catch (error) {
|
|
331
283
|
console.error(chalk.red("Error:"), error.message);
|
|
332
284
|
process.exit(1);
|
|
333
285
|
}
|
|
@@ -338,11 +290,11 @@ async function validateBindings(apiKey, projectId, options = {}) {
|
|
|
338
290
|
*/
|
|
339
291
|
async function driftsCommand(subcommand, args = [], options = {}) {
|
|
340
292
|
// Read configuration
|
|
341
|
-
let
|
|
293
|
+
let reshotConfig;
|
|
342
294
|
try {
|
|
343
|
-
|
|
295
|
+
reshotConfig = config.readConfigLenient();
|
|
344
296
|
} catch (error) {
|
|
345
|
-
console.error(chalk.red("Error:"), "
|
|
297
|
+
console.error(chalk.red("Error:"), "reshot.config.json not found. Run `reshot init` first.");
|
|
346
298
|
process.exit(1);
|
|
347
299
|
}
|
|
348
300
|
|
|
@@ -352,7 +304,7 @@ async function driftsCommand(subcommand, args = [], options = {}) {
|
|
|
352
304
|
const projectId =
|
|
353
305
|
process.env.RESHOT_PROJECT_ID ||
|
|
354
306
|
settings?.projectId ||
|
|
355
|
-
|
|
307
|
+
reshotConfig._metadata?.projectId;
|
|
356
308
|
|
|
357
309
|
if (!apiKey) {
|
|
358
310
|
console.error(chalk.red("Error:"), "API key not found. Set RESHOT_API_KEY or run `reshot auth`.");
|
|
@@ -398,10 +350,6 @@ async function driftsCommand(subcommand, args = [], options = {}) {
|
|
|
398
350
|
await batchDriftAction(apiKey, projectId, "reject", options);
|
|
399
351
|
break;
|
|
400
352
|
|
|
401
|
-
case "validate":
|
|
402
|
-
await validateBindings(apiKey, projectId, options);
|
|
403
|
-
break;
|
|
404
|
-
|
|
405
353
|
default:
|
|
406
354
|
console.error(chalk.red("Error:"), `Unknown subcommand: ${subcommand}`);
|
|
407
355
|
console.log(chalk.gray("\nAvailable subcommands:"));
|
|
@@ -413,7 +361,6 @@ async function driftsCommand(subcommand, args = [], options = {}) {
|
|
|
413
361
|
console.log(chalk.white(" sync ") + chalk.gray("Mark as manually synced"));
|
|
414
362
|
console.log(chalk.white(" approve-all ") + chalk.gray("Approve all pending drifts"));
|
|
415
363
|
console.log(chalk.white(" reject-all ") + chalk.gray("Reject all pending drifts"));
|
|
416
|
-
console.log(chalk.white(" validate ") + chalk.gray("Validate journey bindings"));
|
|
417
364
|
process.exit(1);
|
|
418
365
|
}
|
|
419
366
|
|