@reshotdev/screenshot 0.0.1-beta.1 → 0.0.1-beta.10

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 (38) hide show
  1. package/README.md +65 -7
  2. package/package.json +9 -2
  3. package/src/commands/auth.js +108 -26
  4. package/src/commands/certify.js +62 -0
  5. package/src/commands/ci-run.js +57 -2
  6. package/src/commands/ci-setup.js +5 -5
  7. package/src/commands/doctor-release.js +67 -0
  8. package/src/commands/doctor-target.js +49 -0
  9. package/src/commands/drifts.js +5 -70
  10. package/src/commands/import-tests.js +13 -13
  11. package/src/commands/ingest.js +10 -10
  12. package/src/commands/init.js +16 -277
  13. package/src/commands/publish.js +204 -237
  14. package/src/commands/pull.js +253 -23
  15. package/src/commands/run.js +292 -12
  16. package/src/commands/setup-wizard.js +277 -499
  17. package/src/commands/setup.js +41 -13
  18. package/src/commands/status.js +313 -125
  19. package/src/commands/sync.js +28 -236
  20. package/src/commands/ui.js +1 -1
  21. package/src/commands/verify-publish.js +46 -0
  22. package/src/index.js +194 -94
  23. package/src/lib/api-client.js +121 -35
  24. package/src/lib/capture-engine.js +103 -7
  25. package/src/lib/capture-script-runner.js +305 -58
  26. package/src/lib/certification.js +865 -0
  27. package/src/lib/config.js +181 -76
  28. package/src/lib/record-cdp.js +288 -16
  29. package/src/lib/record-config.js +1 -1
  30. package/src/lib/release-doctor.js +313 -0
  31. package/src/lib/run-manifest.js +103 -0
  32. package/src/lib/standalone-mode.js +1 -1
  33. package/src/lib/storage-providers.js +4 -4
  34. package/src/lib/target-contract.js +292 -0
  35. package/src/lib/ui-api.js +6 -7
  36. package/web/manager/dist/assets/{index--ZgioErz.js → index-D2qqcFNN.js} +1 -1
  37. package/web/manager/dist/index.html +1 -1
  38. package/src/commands/validate-docs.js +0 -529
package/README.md CHANGED
@@ -21,20 +21,47 @@ Requires Node.js >= 18. Playwright browsers are installed automatically on first
21
21
  # 1. Interactive setup wizard
22
22
  reshot setup
23
23
 
24
- # 2. Capture screenshots from your config
24
+ # 2. Start your app with a production-like local server
25
+ npm run build
26
+ npm run start
27
+
28
+ # 3. Capture screenshots from your config
25
29
  reshot run
26
30
 
27
- # 3. Review captures in the web UI
31
+ # 4. Review captures in the web UI
28
32
  reshot studio
33
+
34
+ # 5. Publish when you want hosted assets
35
+ reshot publish
29
36
  ```
30
37
 
38
+ For launch-grade reliability, do not treat `next dev` as the supported capture
39
+ runtime. Use a production-like local server and see the
40
+ [Supported Environments guide](https://reshot.dev/docs/cli/getting-started/supported-environments).
41
+
42
+ ## Certified Targets
43
+
44
+ This release adds a **Certified Targets** contract for apps that need stronger guarantees than ad hoc capture. Certified targets declare their readiness selectors, auth mode, required routes, and expected published assets in `reshot.config.json`, then pass the full doctor/capture/publish/delivery pipeline before release.
45
+
31
46
  ## Configuration
32
47
 
33
- Create `docsync.config.json` in your project root:
48
+ Create `reshot.config.json` in your project root:
34
49
 
35
50
  ```json
36
51
  {
37
52
  "baseUrl": "http://localhost:3000",
53
+ "target": {
54
+ "key": "docs-app",
55
+ "displayName": "Docs App",
56
+ "tier": "certified",
57
+ "owner": "Docs Team",
58
+ "baseUrl": "http://localhost:3000",
59
+ "captureSafe": false,
60
+ "supportedLocalCommand": "npm run build && npm run start",
61
+ "defaultAuthMode": "fixture",
62
+ "requiredEnv": ["PROJECT_ID"],
63
+ "certificationScenarioKeys": ["dashboard"]
64
+ },
38
65
  "assetDir": ".reshot/output",
39
66
  "concurrency": 2,
40
67
  "viewport": { "width": 1280, "height": 720 },
@@ -55,6 +82,15 @@ Create `docsync.config.json` in your project root:
55
82
  "name": "Dashboard",
56
83
  "url": "/dashboard",
57
84
  "requiresAuth": true,
85
+ "captureClass": "fixture-auth",
86
+ "ready": {
87
+ "selector": "[data-loaded='true']",
88
+ "expression": "window.__APP_READY__ === true"
89
+ },
90
+ "requiredRoutes": ["/dashboard"],
91
+ "requiredSelectors": ["[data-testid='dashboard-content']"],
92
+ "expectedArtifacts": ["overview", "analytics"],
93
+ "publishPolicy": "required",
58
94
  "readySelector": "[data-loaded='true']",
59
95
  "steps": [
60
96
  { "action": "screenshot", "key": "overview", "description": "Dashboard overview" },
@@ -76,15 +112,30 @@ Create `docsync.config.json` in your project root:
76
112
  | `reshot record [title]` | Interactive recording via Chrome DevTools | `--browser`, `--url`, `--port` |
77
113
  | `reshot sync` | Upload traces/docs to Reshot platform | `--trace-dir`, `--dry-run` |
78
114
  | `reshot studio` | Launch web management UI | `--port`, `--no-open` |
79
- | `reshot validate` | Check config and bindings | `--strict`, `--fix` |
80
115
  | `reshot status` | View project status and sync history | `--jobs`, `--drifts`, `--json` |
81
116
  | `reshot publish` | Upload assets with versioning | `--tag`, `--message`, `--dry-run` |
82
117
  | `reshot pull` | Generate asset map for builds | `--format json\|ts\|csv`, `--output`, `--status` |
118
+ | `reshot doctor target` | Audit target routes, readiness, and auth contract | `--scenarios`, `--json` |
119
+ | `reshot verify publish` | Validate publish, pull/export, and hosted delivery | `--scenarios`, `--tag`, `--json` |
120
+ | `reshot certify` | Run the full certified-target pipeline | `--scenarios`, `--tag`, `--json` |
83
121
  | `reshot drifts` | Manage visual drift notifications | `approve`, `reject`, `ignore`, `approve-all` |
84
122
  | `reshot import-tests` | Import Playwright tests as scenarios | `--dry-run`, `--no-interactive` |
85
123
  | `reshot ci setup` | Generate CI/CD workflow files | — |
86
124
  | `reshot ci run` | Capture + publish in one step (CI) | `--tag`, `--no-publish`, `--dry-run` |
87
125
 
126
+ ## Certification Workflow
127
+
128
+ Use these commands when a target app needs release-grade verification:
129
+
130
+ ```bash
131
+ reshot doctor target
132
+ reshot run --scenarios dashboard
133
+ reshot verify publish --tag v1.0.0
134
+ reshot certify --tag v1.0.0
135
+ ```
136
+
137
+ Certification reports are written to `.reshot/reports/certification.json`.
138
+
88
139
  ## Step Types
89
140
 
90
141
  Steps define a sequence of browser actions within a scenario:
@@ -221,16 +272,23 @@ During recording:
221
272
  - Press **C** to start/stop a video clip
222
273
  - Press **Q** to quit and save
223
274
 
224
- The recorded scenario is appended to `docsync.config.json` automatically.
275
+ The recorded scenario is appended to `reshot.config.json` automatically.
225
276
 
226
277
  ## Authentication
227
278
 
228
- ### Storage State (from Playwright)
279
+ ### Storage State
280
+
281
+ The CLI stores browser session state at `~/.reshot/session-state.json` (global).
282
+ This is automatically captured when you run `reshot record`.
283
+
284
+ To manually generate it:
229
285
 
230
286
  ```bash
231
- npx playwright codegen http://localhost:3000 --save-storage=.reshot/auth-state.json
287
+ npx playwright codegen http://localhost:3000 --save-storage=$HOME/.reshot/session-state.json
232
288
  ```
233
289
 
290
+ Or reference a project-local path in your config:
291
+
234
292
  ```json
235
293
  {
236
294
  "storageStatePath": ".reshot/auth-state.json"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reshotdev/screenshot",
3
- "version": "0.0.1-beta.1",
3
+ "version": "0.0.1-beta.10",
4
4
  "description": "CI/CD screenshot and video capture CLI",
5
5
  "author": "Reshot <hello@reshot.dev>",
6
6
  "license": "MIT",
@@ -13,7 +13,14 @@
13
13
  "publishConfig": {
14
14
  "access": "public"
15
15
  },
16
- "keywords": ["screenshots", "ci-cd", "documentation", "visual-testing", "automation", "playwright"],
16
+ "keywords": [
17
+ "screenshots",
18
+ "ci-cd",
19
+ "documentation",
20
+ "visual-testing",
21
+ "automation",
22
+ "playwright"
23
+ ],
17
24
  "bin": {
18
25
  "reshot": "./src/index.js"
19
26
  },
@@ -14,6 +14,7 @@ const pkg = require("../../package.json");
14
14
 
15
15
  const DEFAULT_CALLBACK_PORT = 3721;
16
16
  const POLL_INTERVAL_MS = 2000;
17
+ const DEFAULT_AUTH_TIMEOUT_MS = 120000;
17
18
 
18
19
  const unwrapResponse = (payload) => {
19
20
  if (!payload) {
@@ -95,15 +96,27 @@ function startLocalStatusServer(requestedPort, options = {}) {
95
96
  });
96
97
  }
97
98
 
98
- async function waitForCompletion(apiBaseUrl, authToken, expiresAtIso) {
99
+ async function waitForCompletion(
100
+ apiBaseUrl,
101
+ authToken,
102
+ expiresAtIso,
103
+ options = {},
104
+ ) {
105
+ const httpClient = options.httpClient || axios;
106
+ const spinnerFactory = options.spinnerFactory || ora;
99
107
  const expiresAt = expiresAtIso
100
108
  ? Date.parse(expiresAtIso)
101
109
  : Date.now() + 5 * 60 * 1000;
102
- const statusSpinner = ora("Waiting for browser authentication…").start();
110
+ const timeoutMs = Math.max(
111
+ 1,
112
+ Number(options.timeoutMs || DEFAULT_AUTH_TIMEOUT_MS),
113
+ );
114
+ const deadline = Math.min(expiresAt, Date.now() + timeoutMs);
115
+ const statusSpinner = spinnerFactory("Waiting for browser authentication…").start();
103
116
 
104
117
  try {
105
- while (Date.now() < expiresAt) {
106
- const statusResponse = await axios.get(`${apiBaseUrl}/auth/cli/status`, {
118
+ while (Date.now() < deadline) {
119
+ const statusResponse = await httpClient.get(`${apiBaseUrl}/auth/cli/status`, {
107
120
  params: { token: authToken },
108
121
  });
109
122
  const payload = unwrapResponse(statusResponse.data);
@@ -127,39 +140,70 @@ async function waitForCompletion(apiBaseUrl, authToken, expiresAtIso) {
127
140
  await wait(POLL_INTERVAL_MS);
128
141
  }
129
142
 
130
- throw new Error("Authentication timed out before completion.");
143
+ throw new Error(
144
+ "Authentication timed out before completion. Re-run `reshot auth` and use the printed auth URL if the browser handoff stalls.",
145
+ );
131
146
  } catch (error) {
132
147
  statusSpinner.fail("Browser authentication failed");
133
148
  throw error;
134
149
  }
135
150
  }
136
151
 
137
- async function verifyApiKey(apiBaseUrl, apiKey) {
138
- await axios.get(`${apiBaseUrl}/auth/cli/verify`, {
152
+ async function verifyApiKey(apiBaseUrl, apiKey, httpClient = axios) {
153
+ await httpClient.get(`${apiBaseUrl}/auth/cli/verify`, {
139
154
  headers: {
140
155
  Authorization: `Bearer ${apiKey}`,
141
156
  },
142
157
  });
143
158
  }
144
159
 
145
- async function authCommand() {
146
- const apiBaseUrl = getApiBaseUrl();
160
+ async function authCommand(options = {}) {
161
+ // Support non-interactive auth via environment variables (for CI/CD)
162
+ const envApiKey = process.env.RESHOT_API_KEY;
163
+ const envProjectId = process.env.RESHOT_PROJECT_ID;
164
+ const httpClient = options.httpClient || axios;
165
+ const openFn = options.openFn || open;
166
+ const writeSettingsFn = options.writeSettingsFn || writeSettings;
167
+ const startLocalStatusServerFn =
168
+ options.startLocalStatusServerFn || startLocalStatusServer;
169
+ const waitForCompletionFn = options.waitForCompletionFn || waitForCompletion;
170
+ const verifyApiKeyFn = options.verifyApiKeyFn || verifyApiKey;
171
+ const spinnerFactory = options.spinnerFactory || ora;
172
+ const timeoutMs = Number(options.timeoutMs || DEFAULT_AUTH_TIMEOUT_MS);
173
+ if (envApiKey && envProjectId) {
174
+ const platformUrl = process.env.RESHOT_PLATFORM_URL || "https://reshot.dev";
175
+ writeSettingsFn({
176
+ projectId: envProjectId,
177
+ apiKey: envApiKey,
178
+ platformUrl,
179
+ linkedAt: new Date().toISOString(),
180
+ cliVersion: pkg.version,
181
+ });
182
+ console.log(chalk.green("✔ Authenticated via environment variables"));
183
+ console.log(chalk.gray(` Project: ${envProjectId}`));
184
+ console.log(chalk.gray(` Platform: ${platformUrl}`));
185
+ return {
186
+ mode: "cloud-connected",
187
+ projectId: envProjectId,
188
+ platformUrl,
189
+ };
190
+ }
191
+
192
+ const apiBaseUrl = options.apiBaseUrl || getApiBaseUrl();
147
193
  const explicitPortEnv =
148
- process.env.RESHOT_CLI_CALLBACK_PORT ||
149
- process.env.DOCSYNC_CLI_CALLBACK_PORT ||
150
- "";
194
+ process.env.RESHOT_CLI_CALLBACK_PORT || "";
151
195
  const basePort = parseInt(explicitPortEnv || `${DEFAULT_CALLBACK_PORT}`, 10);
152
196
  const hasExplicitPort = Boolean(explicitPortEnv);
153
197
 
154
198
  let localServer;
155
199
  let callbackPort;
156
- const spinner = ora("Requesting authentication session…").start();
200
+ const spinner = spinnerFactory("Requesting authentication session…").start();
157
201
 
158
202
  try {
159
203
  if (hasExplicitPort) {
160
204
  // Respect an explicitly configured port and fail fast with a clear error
161
205
  // if it is not available.
162
- const { server, port } = await startLocalStatusServer(basePort, {
206
+ const { server, port } = await startLocalStatusServerFn(basePort, {
163
207
  explicit: true,
164
208
  });
165
209
  localServer = server;
@@ -169,12 +213,12 @@ async function authCommand() {
169
213
  // but automatically fall back to any available port so users never have
170
214
  // to think about port conflicts.
171
215
  try {
172
- const { server, port } = await startLocalStatusServer(basePort);
216
+ const { server, port } = await startLocalStatusServerFn(basePort);
173
217
  localServer = server;
174
218
  callbackPort = port;
175
219
  } catch (error) {
176
220
  if (error && error.code === "EADDRINUSE") {
177
- const { server, port } = await startLocalStatusServer(0);
221
+ const { server, port } = await startLocalStatusServerFn(0);
178
222
  localServer = server;
179
223
  callbackPort = port;
180
224
  console.log(
@@ -188,7 +232,7 @@ async function authCommand() {
188
232
  }
189
233
  }
190
234
 
191
- const initiateResponse = await axios.post(
235
+ const initiateResponse = await httpClient.post(
192
236
  `${apiBaseUrl}/auth/cli/initiate`,
193
237
  {
194
238
  callbackPort,
@@ -207,21 +251,47 @@ async function authCommand() {
207
251
  spinner.succeed("Authentication session created");
208
252
  console.log(chalk.gray(`Token expires at ${expiresAt || "unknown time"}`));
209
253
  console.log(chalk.gray(`Settings will be stored in ${SETTINGS_PATH}`));
210
-
211
- await open(authUrl, { wait: false });
254
+ console.log(chalk.gray("Auth URL:"));
255
+ console.log(chalk.cyan(authUrl));
212
256
  console.log(
213
- chalk.blue(
214
- "A browser window has been opened. Complete the flow to continue."
215
- )
257
+ chalk.gray(
258
+ "If the browser did not open, copy the URL above into a browser and complete the approval flow there.",
259
+ ),
216
260
  );
217
261
 
218
- const status = await waitForCompletion(apiBaseUrl, authToken, expiresAt);
219
- await verifyApiKey(apiBaseUrl, status.project.apiKey);
262
+ let browserOpened = false;
263
+ try {
264
+ await openFn(authUrl, { wait: false });
265
+ browserOpened = true;
266
+ console.log(
267
+ chalk.blue(
268
+ "A browser window has been opened. Approve the session there to continue.",
269
+ )
270
+ );
271
+ } catch (error) {
272
+ console.log(
273
+ chalk.yellow(
274
+ `Could not open a browser automatically: ${error.message}`,
275
+ ),
276
+ );
277
+ console.log(
278
+ chalk.gray(
279
+ "Continue by opening the auth URL manually. The CLI will keep waiting for approval.",
280
+ ),
281
+ );
282
+ }
283
+
284
+ const status = await waitForCompletionFn(apiBaseUrl, authToken, expiresAt, {
285
+ httpClient,
286
+ spinnerFactory,
287
+ timeoutMs,
288
+ });
289
+ await verifyApiKeyFn(apiBaseUrl, status.project.apiKey, httpClient);
220
290
 
221
291
  // Derive platformUrl from apiBaseUrl (remove /api suffix)
222
- const platformUrl = apiBaseUrl.replace(/\/api\/?$/, '') || 'http://localhost:3000';
292
+ const platformUrl = apiBaseUrl.replace(/\/api\/?$/, '') || 'https://reshot.dev';
223
293
 
224
- writeSettings({
294
+ writeSettingsFn({
225
295
  projectId: status.project.id,
226
296
  projectName: status.project.name,
227
297
  apiKey: status.project.apiKey,
@@ -249,6 +319,15 @@ async function authCommand() {
249
319
  )
250
320
  );
251
321
  console.log(chalk.gray(`Settings saved to ${SETTINGS_PATH}`));
322
+ console.log(chalk.gray("Mode: cloud-connected"));
323
+ return {
324
+ mode: "cloud-connected",
325
+ browserOpened,
326
+ authUrl,
327
+ projectId: status.project.id,
328
+ projectName: status.project.name,
329
+ platformUrl,
330
+ };
252
331
  } finally {
253
332
  if (localServer) {
254
333
  localServer.close();
@@ -257,3 +336,6 @@ async function authCommand() {
257
336
  }
258
337
 
259
338
  module.exports = authCommand;
339
+ module.exports.waitForCompletion = waitForCompletion;
340
+ module.exports.verifyApiKey = verifyApiKey;
341
+ module.exports.startLocalStatusServer = startLocalStatusServer;
@@ -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;
@@ -5,7 +5,14 @@ const path = require("path");
5
5
  const { detectCI, getCIMetadata } = require("../lib/ci-detect");
6
6
 
7
7
  async function ciRunCommand(options = {}) {
8
- const { config: configPath, tag, message, dryRun, publish: shouldPublish = true } = options;
8
+ const {
9
+ config: configPath,
10
+ tag,
11
+ message,
12
+ dryRun,
13
+ publish: shouldPublish = true,
14
+ skipReleaseDoctor = false,
15
+ } = options;
9
16
 
10
17
  // Disable colors in CI for cleaner logs
11
18
  const { isCI, provider } = detectCI();
@@ -33,12 +40,47 @@ async function ciRunCommand(options = {}) {
33
40
  }
34
41
 
35
42
  const ciResult = {
43
+ releaseDoctor: null,
36
44
  run: { success: false, scenariosRun: 0, scenariosFailed: 0 },
37
45
  publish: null,
38
46
  ci: ciMeta,
39
47
  timestamp: new Date().toISOString(),
40
48
  };
41
49
 
50
+ if (!skipReleaseDoctor) {
51
+ console.log(chalk.cyan("━━━ Step 0: Release Doctor ━━━\n"));
52
+ try {
53
+ const { runReleaseDoctor } = require("../lib/release-doctor");
54
+ const releaseDoctor = await runReleaseDoctor({});
55
+ ciResult.releaseDoctor = {
56
+ success: releaseDoctor.ok,
57
+ reportPath: releaseDoctor.reportPath || null,
58
+ };
59
+
60
+ if (!releaseDoctor.ok) {
61
+ console.error(chalk.red("Release doctor failed. Skipping capture and publish."));
62
+ const outputDir = path.join(process.cwd(), ".reshot", "output");
63
+ fs.ensureDirSync(outputDir);
64
+ const resultPath = path.join(outputDir, "ci-result.json");
65
+ fs.writeJsonSync(resultPath, ciResult, { spaces: 2 });
66
+ process.exitCode = 1;
67
+ return ciResult;
68
+ }
69
+ } catch (error) {
70
+ console.error(chalk.red(`Release doctor failed: ${error.message}`));
71
+ ciResult.releaseDoctor = { success: false, error: error.message };
72
+ const outputDir = path.join(process.cwd(), ".reshot", "output");
73
+ fs.ensureDirSync(outputDir);
74
+ const resultPath = path.join(outputDir, "ci-result.json");
75
+ fs.writeJsonSync(resultPath, ciResult, { spaces: 2 });
76
+ process.exitCode = 1;
77
+ return ciResult;
78
+ }
79
+ } else {
80
+ ciResult.releaseDoctor = { skipped: true, success: true };
81
+ console.log(chalk.gray("Skipping release doctor (--skip-release-doctor)\n"));
82
+ }
83
+
42
84
  // Step 1: Run capture scenarios
43
85
  console.log(chalk.cyan("━━━ Step 1: Capture ━━━\n"));
44
86
  try {
@@ -70,6 +112,7 @@ async function ciRunCommand(options = {}) {
70
112
  dryRun,
71
113
  force: true, // Skip prompts in CI
72
114
  outputJson: true,
115
+ skipReleaseDoctor: true,
73
116
  });
74
117
 
75
118
  ciResult.publish = {
@@ -98,6 +141,15 @@ async function ciRunCommand(options = {}) {
98
141
 
99
142
  // Summary
100
143
  console.log(chalk.cyan("\n━━━ CI Summary ━━━\n"));
144
+ if (ciResult.releaseDoctor) {
145
+ const doctorIcon = ciResult.releaseDoctor.success ? chalk.green("✔") : chalk.red("✖");
146
+ const doctorLabel = ciResult.releaseDoctor.skipped
147
+ ? "skipped"
148
+ : ciResult.releaseDoctor.success
149
+ ? "passed"
150
+ : "failed";
151
+ console.log(` ${doctorIcon} Release doctor: ${doctorLabel}`);
152
+ }
101
153
  const runIcon = ciResult.run.success ? chalk.green("✔") : chalk.red("✖");
102
154
  console.log(` ${runIcon} Capture: ${ciResult.run.scenariosRun} scenario(s), ${ciResult.run.scenariosFailed} failed`);
103
155
 
@@ -112,7 +164,10 @@ async function ciRunCommand(options = {}) {
112
164
  console.log();
113
165
 
114
166
  // Exit with appropriate code
115
- const overallSuccess = ciResult.run.success && (!ciResult.publish || ciResult.publish.success);
167
+ const overallSuccess =
168
+ (ciResult.releaseDoctor?.success ?? true) &&
169
+ ciResult.run.success &&
170
+ (!ciResult.publish || ciResult.publish.success);
116
171
  if (!overallSuccess) {
117
172
  process.exitCode = 1;
118
173
  }
@@ -29,7 +29,7 @@ jobs:
29
29
  run: npm install
30
30
 
31
31
  - name: Install Reshot CLI
32
- run: npm install -g @reshot/cli
32
+ run: npm install -g @reshotdev/screenshot
33
33
 
34
34
  - name: Install Playwright browsers
35
35
  run: npx playwright install chromium
@@ -37,7 +37,7 @@ jobs:
37
37
  - name: Install ffmpeg
38
38
  run: sudo apt-get update && sudo apt-get install -y ffmpeg
39
39
 
40
- # Generate visual assets from docsync.config.json blueprint
40
+ # Generate visual assets from reshot.config.json blueprint
41
41
  # Only runs if features.visuals is enabled for the project
42
42
  - name: Run Reshot scenarios
43
43
  env:
@@ -85,7 +85,7 @@ jobs:
85
85
  name: Install dependencies
86
86
  command: |
87
87
  npm install
88
- npm install -g @reshot/cli
88
+ npm install -g @reshotdev/screenshot
89
89
 
90
90
  - run:
91
91
  name: Install Playwright browsers
@@ -130,7 +130,7 @@ docs:
130
130
  - apt-get update && apt-get install -y ffmpeg
131
131
  - npm install
132
132
  - npx playwright install chromium
133
- - npm install -g @reshot/cli
133
+ - npm install -g @reshotdev/screenshot
134
134
 
135
135
  script:
136
136
  - reshot run
@@ -205,7 +205,7 @@ async function ciSetupCommand() {
205
205
 
206
206
  switch (provider) {
207
207
  case 'github':
208
- workflowPath = path.join(process.cwd(), '.github', 'workflows', 'docsync.yml');
208
+ workflowPath = path.join(process.cwd(), '.github', 'workflows', 'reshot.yml');
209
209
  workflowContent = GITHUB_ACTIONS_WORKFLOW(secretNames);
210
210
  break;
211
211
 
@@ -0,0 +1,67 @@
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
+ if (report.reportPath) {
56
+ console.log(chalk.gray(`\n Report: ${report.reportPath}`));
57
+ }
58
+ }
59
+
60
+ if (!report.ok) {
61
+ process.exitCode = 1;
62
+ }
63
+
64
+ return report;
65
+ }
66
+
67
+ module.exports = doctorReleaseCommand;