@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.
Files changed (81) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +138 -47
  3. package/package.json +27 -16
  4. package/src/commands/auth.js +159 -30
  5. package/src/commands/capture-dom.js +50 -0
  6. package/src/commands/certify.js +62 -0
  7. package/src/commands/compose.js +220 -0
  8. package/src/commands/doctor-release.js +74 -0
  9. package/src/commands/doctor-target.js +108 -0
  10. package/src/commands/drifts.js +16 -69
  11. package/src/commands/import-tests.js +13 -13
  12. package/src/commands/init.js +16 -277
  13. package/src/commands/publish.js +484 -257
  14. package/src/commands/pull.js +302 -35
  15. package/src/commands/refresh.js +166 -0
  16. package/src/commands/run.js +292 -12
  17. package/src/commands/setup-wizard.js +348 -496
  18. package/src/commands/status.js +334 -126
  19. package/src/commands/sync.js +28 -236
  20. package/src/commands/ui.js +1 -1
  21. package/src/commands/variation.js +194 -0
  22. package/src/commands/verify-publish.js +46 -0
  23. package/src/index.js +383 -118
  24. package/src/lib/api-client.js +172 -60
  25. package/src/lib/auto-update/refresh.js +598 -0
  26. package/src/lib/auto-update/scene-runtime.compose.tsx +73 -0
  27. package/src/lib/auto-update/spec.js +89 -0
  28. package/src/lib/capture-engine.js +179 -9
  29. package/src/lib/capture-script-runner.js +639 -214
  30. package/src/lib/certification.js +887 -0
  31. package/src/lib/compose-context.js +156 -0
  32. package/src/lib/compose-pack.js +42 -0
  33. package/src/lib/compose-runtime.js +34 -0
  34. package/src/lib/compose-upload.js +142 -0
  35. package/src/lib/config.js +186 -81
  36. package/src/lib/dom-capture.js +64 -0
  37. package/src/lib/ensure-browser.js +147 -0
  38. package/src/lib/output-path-template.js +3 -3
  39. package/src/lib/record-cdp.js +288 -16
  40. package/src/lib/record-clip.js +83 -3
  41. package/src/lib/record-config.js +1 -5
  42. package/src/lib/release-doctor.js +321 -0
  43. package/src/lib/resolve-targets.js +60 -0
  44. package/src/lib/run-manifest.js +148 -0
  45. package/src/lib/standalone-mode.js +1 -1
  46. package/src/lib/storage-providers.js +5 -5
  47. package/src/lib/style-engine.js +5 -5
  48. package/src/lib/target-contract.js +292 -0
  49. package/src/lib/ui-api-helpers.js +118 -0
  50. package/src/lib/ui-api.js +31 -824
  51. package/src/lib/ui-asset-cleanup.js +62 -0
  52. package/src/lib/ui-output-versions.js +165 -0
  53. package/src/lib/ui-recorder-routes.js +341 -0
  54. package/src/lib/ui-scenario-metadata.js +161 -0
  55. package/vendor/compose/dist/auto-update.cjs +5544 -0
  56. package/vendor/compose/dist/auto-update.mjs +5518 -0
  57. package/vendor/compose/dist/capture.cjs +1450 -0
  58. package/vendor/compose/dist/capture.mjs +1416 -0
  59. package/vendor/compose/dist/eligibility.cjs +5331 -0
  60. package/vendor/compose/dist/eligibility.mjs +5313 -0
  61. package/vendor/compose/dist/index.cjs +2046 -0
  62. package/vendor/compose/dist/index.mjs +1997 -0
  63. package/vendor/compose/dist/jsx-dev-runtime.cjs +55 -0
  64. package/vendor/compose/dist/jsx-dev-runtime.mjs +27 -0
  65. package/vendor/compose/dist/jsx-runtime.cjs +58 -0
  66. package/vendor/compose/dist/jsx-runtime.mjs +31 -0
  67. package/vendor/compose/dist/render.cjs +558 -0
  68. package/vendor/compose/dist/render.mjs +515 -0
  69. package/vendor/compose/dist/verify-cli.cjs +3806 -0
  70. package/vendor/compose/dist/verify-cli.mjs +3812 -0
  71. package/vendor/compose/dist/verify.cjs +3880 -0
  72. package/vendor/compose/dist/verify.mjs +3858 -0
  73. package/web/manager/dist/assets/index-D0S2otug.js +507 -0
  74. package/web/manager/dist/index.html +1 -1
  75. package/src/commands/ci-run.js +0 -123
  76. package/src/commands/ci-setup.js +0 -288
  77. package/src/commands/ingest.js +0 -458
  78. package/src/commands/setup.js +0 -137
  79. package/src/commands/validate-docs.js +0 -529
  80. package/src/lib/playwright-runner.js +0 -252
  81. 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;
@@ -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 validate Validate journey bindings against visual inventory
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: "PENDING" });
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
- console.error(chalk.red("Error:"), error.message);
272
- process.exit(1);
273
- }
274
- }
275
-
276
- /**
277
- * Validate journey bindings against visual inventory
278
- */
279
- async function validateBindings(apiKey, projectId, options = {}) {
280
- console.log(chalk.blue("\n🔍 Validating Journey Bindings\n"));
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 docSyncConfig;
293
+ let reshotConfig;
342
294
  try {
343
- docSyncConfig = config.readDocSyncConfig();
295
+ reshotConfig = config.readConfigLenient();
344
296
  } catch (error) {
345
- console.error(chalk.red("Error:"), "docsync.config.json not found. Run `reshot init` first.");
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
- docSyncConfig._metadata?.projectId;
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