@reshotdev/screenshot 0.0.1-beta.7 → 0.0.1-beta.9
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 +63 -5
- package/package.json +1 -1
- package/src/commands/auth.js +106 -22
- package/src/commands/certify.js +54 -0
- package/src/commands/ci-setup.js +3 -3
- package/src/commands/doctor-target.js +42 -0
- package/src/commands/publish.js +45 -10
- package/src/commands/pull.js +252 -22
- package/src/commands/setup-wizard.js +187 -29
- package/src/commands/setup.js +35 -7
- package/src/commands/verify-publish.js +46 -0
- package/src/index.js +149 -3
- package/src/lib/api-client.js +64 -23
- package/src/lib/capture-engine.js +64 -3
- package/src/lib/capture-script-runner.js +96 -10
- package/src/lib/certification.js +739 -0
- package/src/lib/config.js +16 -3
- package/src/lib/record-cdp.js +16 -2
- package/src/lib/target-contract.js +278 -0
- package/web/manager/dist/assets/{index-8H7P9ANi.js → index-D2qqcFNN.js} +1 -1
- package/web/manager/dist/index.html +1 -1
package/README.md
CHANGED
|
@@ -21,13 +21,28 @@ 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.
|
|
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
|
-
#
|
|
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
48
|
Create `reshot.config.json` in your project root:
|
|
@@ -35,6 +50,18 @@ Create `reshot.config.json` in your project root:
|
|
|
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 `reshot.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 `reshot.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:
|
|
@@ -225,12 +276,19 @@ The recorded scenario is appended to `reshot.config.json` automatically.
|
|
|
225
276
|
|
|
226
277
|
## Authentication
|
|
227
278
|
|
|
228
|
-
### Storage State
|
|
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
|
|
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
package/src/commands/auth.js
CHANGED
|
@@ -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(
|
|
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
|
|
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() <
|
|
106
|
-
const statusResponse = await
|
|
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,23 +140,56 @@ async function waitForCompletion(apiBaseUrl, authToken, expiresAtIso) {
|
|
|
127
140
|
await wait(POLL_INTERVAL_MS);
|
|
128
141
|
}
|
|
129
142
|
|
|
130
|
-
throw new Error(
|
|
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
|
|
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
|
-
|
|
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
194
|
process.env.RESHOT_CLI_CALLBACK_PORT || "";
|
|
149
195
|
const basePort = parseInt(explicitPortEnv || `${DEFAULT_CALLBACK_PORT}`, 10);
|
|
@@ -151,13 +197,13 @@ async function authCommand() {
|
|
|
151
197
|
|
|
152
198
|
let localServer;
|
|
153
199
|
let callbackPort;
|
|
154
|
-
const spinner =
|
|
200
|
+
const spinner = spinnerFactory("Requesting authentication session…").start();
|
|
155
201
|
|
|
156
202
|
try {
|
|
157
203
|
if (hasExplicitPort) {
|
|
158
204
|
// Respect an explicitly configured port and fail fast with a clear error
|
|
159
205
|
// if it is not available.
|
|
160
|
-
const { server, port } = await
|
|
206
|
+
const { server, port } = await startLocalStatusServerFn(basePort, {
|
|
161
207
|
explicit: true,
|
|
162
208
|
});
|
|
163
209
|
localServer = server;
|
|
@@ -167,12 +213,12 @@ async function authCommand() {
|
|
|
167
213
|
// but automatically fall back to any available port so users never have
|
|
168
214
|
// to think about port conflicts.
|
|
169
215
|
try {
|
|
170
|
-
const { server, port } = await
|
|
216
|
+
const { server, port } = await startLocalStatusServerFn(basePort);
|
|
171
217
|
localServer = server;
|
|
172
218
|
callbackPort = port;
|
|
173
219
|
} catch (error) {
|
|
174
220
|
if (error && error.code === "EADDRINUSE") {
|
|
175
|
-
const { server, port } = await
|
|
221
|
+
const { server, port } = await startLocalStatusServerFn(0);
|
|
176
222
|
localServer = server;
|
|
177
223
|
callbackPort = port;
|
|
178
224
|
console.log(
|
|
@@ -186,7 +232,7 @@ async function authCommand() {
|
|
|
186
232
|
}
|
|
187
233
|
}
|
|
188
234
|
|
|
189
|
-
const initiateResponse = await
|
|
235
|
+
const initiateResponse = await httpClient.post(
|
|
190
236
|
`${apiBaseUrl}/auth/cli/initiate`,
|
|
191
237
|
{
|
|
192
238
|
callbackPort,
|
|
@@ -205,21 +251,47 @@ async function authCommand() {
|
|
|
205
251
|
spinner.succeed("Authentication session created");
|
|
206
252
|
console.log(chalk.gray(`Token expires at ${expiresAt || "unknown time"}`));
|
|
207
253
|
console.log(chalk.gray(`Settings will be stored in ${SETTINGS_PATH}`));
|
|
208
|
-
|
|
209
|
-
|
|
254
|
+
console.log(chalk.gray("Auth URL:"));
|
|
255
|
+
console.log(chalk.cyan(authUrl));
|
|
210
256
|
console.log(
|
|
211
|
-
chalk.
|
|
212
|
-
"
|
|
213
|
-
)
|
|
257
|
+
chalk.gray(
|
|
258
|
+
"If the browser did not open, copy the URL above into a browser and complete the approval flow there.",
|
|
259
|
+
),
|
|
214
260
|
);
|
|
215
261
|
|
|
216
|
-
|
|
217
|
-
|
|
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);
|
|
218
290
|
|
|
219
291
|
// Derive platformUrl from apiBaseUrl (remove /api suffix)
|
|
220
292
|
const platformUrl = apiBaseUrl.replace(/\/api\/?$/, '') || 'https://reshot.dev';
|
|
221
293
|
|
|
222
|
-
|
|
294
|
+
writeSettingsFn({
|
|
223
295
|
projectId: status.project.id,
|
|
224
296
|
projectName: status.project.name,
|
|
225
297
|
apiKey: status.project.apiKey,
|
|
@@ -247,6 +319,15 @@ async function authCommand() {
|
|
|
247
319
|
)
|
|
248
320
|
);
|
|
249
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
|
+
};
|
|
250
331
|
} finally {
|
|
251
332
|
if (localServer) {
|
|
252
333
|
localServer.close();
|
|
@@ -255,3 +336,6 @@ async function authCommand() {
|
|
|
255
336
|
}
|
|
256
337
|
|
|
257
338
|
module.exports = authCommand;
|
|
339
|
+
module.exports.waitForCompletion = waitForCompletion;
|
|
340
|
+
module.exports.verifyApiKey = verifyApiKey;
|
|
341
|
+
module.exports.startLocalStatusServer = startLocalStatusServer;
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
});
|
|
19
|
+
|
|
20
|
+
if (options.json) {
|
|
21
|
+
console.log(JSON.stringify(report, null, 2));
|
|
22
|
+
} else {
|
|
23
|
+
console.log(chalk.cyan("\n🏅 Certified Target Verification\n"));
|
|
24
|
+
console.log(chalk.gray(`Target: ${report.target.displayName}`));
|
|
25
|
+
console.log(
|
|
26
|
+
report.ok
|
|
27
|
+
? chalk.green(` ✔ Final status: ${report.finalStatus}`)
|
|
28
|
+
: chalk.red(` ✖ Final status: ${report.finalStatus}`),
|
|
29
|
+
);
|
|
30
|
+
console.log(
|
|
31
|
+
report.doctor.ok
|
|
32
|
+
? chalk.green(" ✔ Doctor passed")
|
|
33
|
+
: chalk.red(" ✖ Doctor failed"),
|
|
34
|
+
);
|
|
35
|
+
console.log(
|
|
36
|
+
report.capture.success
|
|
37
|
+
? chalk.green(" ✔ Capture passed")
|
|
38
|
+
: chalk.red(" ✖ Capture failed"),
|
|
39
|
+
);
|
|
40
|
+
console.log(
|
|
41
|
+
report.publishVerification.ok
|
|
42
|
+
? chalk.green(" ✔ Publish verification passed")
|
|
43
|
+
: chalk.red(" ✖ Publish verification failed"),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!report.ok) {
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return report;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = certifyCommand;
|
package/src/commands/ci-setup.js
CHANGED
|
@@ -29,7 +29,7 @@ jobs:
|
|
|
29
29
|
run: npm install
|
|
30
30
|
|
|
31
31
|
- name: Install Reshot CLI
|
|
32
|
-
run: npm install -g @
|
|
32
|
+
run: npm install -g @reshotdev/screenshot
|
|
33
33
|
|
|
34
34
|
- name: Install Playwright browsers
|
|
35
35
|
run: npx playwright install chromium
|
|
@@ -85,7 +85,7 @@ jobs:
|
|
|
85
85
|
name: Install dependencies
|
|
86
86
|
command: |
|
|
87
87
|
npm install
|
|
88
|
-
npm install -g @
|
|
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 @
|
|
133
|
+
- npm install -g @reshotdev/screenshot
|
|
134
134
|
|
|
135
135
|
script:
|
|
136
136
|
- reshot run
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const chalk = require("chalk");
|
|
4
|
+
const { runDoctorTarget } = require("../lib/certification");
|
|
5
|
+
|
|
6
|
+
async function doctorTargetCommand(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 runDoctorTarget({ scenarioKeys });
|
|
15
|
+
|
|
16
|
+
if (options.json) {
|
|
17
|
+
console.log(JSON.stringify(report, null, 2));
|
|
18
|
+
} else {
|
|
19
|
+
console.log(chalk.cyan("\n🩺 Certified Target Doctor\n"));
|
|
20
|
+
console.log(chalk.gray(`Target: ${report.target.displayName} (${report.target.tier})`));
|
|
21
|
+
console.log(
|
|
22
|
+
report.ok
|
|
23
|
+
? chalk.green(" ✔ Target contract is healthy")
|
|
24
|
+
: chalk.red(" ✖ Target contract check failed"),
|
|
25
|
+
);
|
|
26
|
+
for (const audit of report.readinessAudits) {
|
|
27
|
+
console.log(
|
|
28
|
+
audit.ok
|
|
29
|
+
? chalk.green(` ✔ ${audit.scenario}`)
|
|
30
|
+
: chalk.red(` ✖ ${audit.scenario}${audit.contractFailure ? ` — ${audit.contractFailure}` : ""}`),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!report.ok) {
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return report;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = doctorTargetCommand;
|
package/src/commands/publish.js
CHANGED
|
@@ -539,17 +539,20 @@ function buildScenarioDefinition(scenario) {
|
|
|
539
539
|
function buildPublishMetadata({
|
|
540
540
|
projectId,
|
|
541
541
|
publishSessionId,
|
|
542
|
+
tag,
|
|
542
543
|
scenarioKey,
|
|
543
544
|
scenarioConfig,
|
|
544
545
|
variationSlug,
|
|
545
546
|
contextData,
|
|
546
547
|
gitInfo,
|
|
548
|
+
autoApprove = false,
|
|
547
549
|
}) {
|
|
548
550
|
const scenarioDefinition = buildScenarioDefinition(scenarioConfig);
|
|
549
551
|
|
|
550
552
|
return {
|
|
551
553
|
projectId,
|
|
552
554
|
publishSessionId, // Unique ID for this CLI publish run
|
|
555
|
+
tag: tag || undefined,
|
|
553
556
|
scenarioName: scenarioConfig?.name || scenarioKey,
|
|
554
557
|
scenario: scenarioDefinition,
|
|
555
558
|
context: {
|
|
@@ -557,6 +560,9 @@ function buildPublishMetadata({
|
|
|
557
560
|
data: contextData,
|
|
558
561
|
},
|
|
559
562
|
autoCreateVisuals: true,
|
|
563
|
+
publish: {
|
|
564
|
+
autoApprove,
|
|
565
|
+
},
|
|
560
566
|
git: {
|
|
561
567
|
commitHash: gitInfo.commitHash,
|
|
562
568
|
commitMessage: gitInfo.commitMessage,
|
|
@@ -809,7 +815,8 @@ async function publishWithTransactionalFlow(
|
|
|
809
815
|
}
|
|
810
816
|
|
|
811
817
|
// Build all commits for batch request
|
|
812
|
-
|
|
818
|
+
// Vercel serverless functions have ~60s timeout; keep batches small enough to complete
|
|
819
|
+
const MAX_BATCH_SIZE = 25;
|
|
813
820
|
const commits = [];
|
|
814
821
|
|
|
815
822
|
for (const { group, scenarioConfig, assets } of groupMap.values()) {
|
|
@@ -820,11 +827,13 @@ async function publishWithTransactionalFlow(
|
|
|
820
827
|
const metadata = buildPublishMetadata({
|
|
821
828
|
projectId,
|
|
822
829
|
publishSessionId: gitInfo.publishSessionId,
|
|
830
|
+
tag: gitInfo.tag,
|
|
823
831
|
scenarioKey: group.scenarioKey,
|
|
824
832
|
scenarioConfig,
|
|
825
833
|
variationSlug: group.variationSlug,
|
|
826
834
|
contextData: contextObj,
|
|
827
835
|
gitInfo,
|
|
836
|
+
autoApprove,
|
|
828
837
|
});
|
|
829
838
|
|
|
830
839
|
if (metadata.cli) {
|
|
@@ -839,14 +848,21 @@ async function publishWithTransactionalFlow(
|
|
|
839
848
|
chalk.gray(` Committing ${commits.length} scenario(s) to platform...`),
|
|
840
849
|
);
|
|
841
850
|
|
|
851
|
+
const totalBatches = Math.ceil(commits.length / MAX_BATCH_SIZE);
|
|
842
852
|
for (let i = 0; i < commits.length; i += MAX_BATCH_SIZE) {
|
|
843
853
|
const chunk = commits.slice(i, i + MAX_BATCH_SIZE);
|
|
854
|
+
const batchNum = Math.floor(i / MAX_BATCH_SIZE) + 1;
|
|
855
|
+
if (totalBatches > 1) {
|
|
856
|
+
console.log(chalk.gray(` Batch ${batchNum}/${totalBatches}...`));
|
|
857
|
+
}
|
|
844
858
|
|
|
845
859
|
try {
|
|
846
|
-
const
|
|
860
|
+
const rawBatchResult = await apiClient.publishBatch(apiKey, {
|
|
847
861
|
commits: chunk,
|
|
848
862
|
autoApprove: autoApprove || false,
|
|
849
863
|
});
|
|
864
|
+
// Unwrap API envelope: response may be { data: { results, ... } } or { results, ... }
|
|
865
|
+
const batchResult = rawBatchResult.data || rawBatchResult;
|
|
850
866
|
|
|
851
867
|
for (const r of batchResult.results || []) {
|
|
852
868
|
if (r.status === "ok") {
|
|
@@ -895,6 +911,7 @@ async function publishWithLegacyFlow(
|
|
|
895
911
|
groupedAssets,
|
|
896
912
|
docSyncConfig,
|
|
897
913
|
gitInfo,
|
|
914
|
+
{ autoApprove = false } = {},
|
|
898
915
|
) {
|
|
899
916
|
let successCount = 0;
|
|
900
917
|
let failCount = 0;
|
|
@@ -911,11 +928,13 @@ async function publishWithLegacyFlow(
|
|
|
911
928
|
const metadata = buildPublishMetadata({
|
|
912
929
|
projectId,
|
|
913
930
|
publishSessionId: gitInfo.publishSessionId,
|
|
931
|
+
tag: gitInfo.tag,
|
|
914
932
|
scenarioKey: group.scenarioKey,
|
|
915
933
|
scenarioConfig,
|
|
916
934
|
variationSlug: group.variationSlug,
|
|
917
935
|
contextData: contextObj,
|
|
918
936
|
gitInfo,
|
|
937
|
+
autoApprove,
|
|
919
938
|
});
|
|
920
939
|
|
|
921
940
|
if (metadata.cli) {
|
|
@@ -1228,7 +1247,16 @@ function parseFrontmatter(content) {
|
|
|
1228
1247
|
}
|
|
1229
1248
|
|
|
1230
1249
|
async function publishCommand(options = {}) {
|
|
1231
|
-
const {
|
|
1250
|
+
const {
|
|
1251
|
+
tag,
|
|
1252
|
+
message,
|
|
1253
|
+
dryRun,
|
|
1254
|
+
force,
|
|
1255
|
+
video,
|
|
1256
|
+
outputJson,
|
|
1257
|
+
autoApprove,
|
|
1258
|
+
noExit = false,
|
|
1259
|
+
} = options;
|
|
1232
1260
|
|
|
1233
1261
|
// Result tracking for --output-json and programmatic callers
|
|
1234
1262
|
const publishResult = {
|
|
@@ -1288,7 +1316,8 @@ async function publishCommand(options = {}) {
|
|
|
1288
1316
|
console.log(chalk.red(` • ${error}`));
|
|
1289
1317
|
}
|
|
1290
1318
|
console.log(getStorageSetupHelp(storageConfig?.type || "reshot"));
|
|
1291
|
-
process.exit(1);
|
|
1319
|
+
if (!noExit) process.exit(1);
|
|
1320
|
+
return { ...publishResult, success: false, error: "Invalid storage configuration" };
|
|
1292
1321
|
}
|
|
1293
1322
|
|
|
1294
1323
|
// Resolve project context based on storage mode
|
|
@@ -1307,7 +1336,8 @@ async function publishCommand(options = {}) {
|
|
|
1307
1336
|
);
|
|
1308
1337
|
console.log(getStorageSetupHelp("s3"));
|
|
1309
1338
|
}
|
|
1310
|
-
process.exit(1);
|
|
1339
|
+
if (!noExit) process.exit(1);
|
|
1340
|
+
return { ...publishResult, success: false, error: error.message };
|
|
1311
1341
|
}
|
|
1312
1342
|
|
|
1313
1343
|
const { apiKey, projectId, storageMode: resolvedMode } = projectContext;
|
|
@@ -1350,7 +1380,7 @@ async function publishCommand(options = {}) {
|
|
|
1350
1380
|
});
|
|
1351
1381
|
console.log(chalk.green(` ✔ Version tag "${tag}" created`));
|
|
1352
1382
|
console.log(
|
|
1353
|
-
chalk.gray(` Pinned URL: cdn.reshot.dev/assets/{
|
|
1383
|
+
chalk.gray(` Pinned URL: cdn.reshot.dev/v1/assets/{projectId}/{visualKey}?tag=${tag}\n`),
|
|
1354
1384
|
);
|
|
1355
1385
|
} catch (tagError) {
|
|
1356
1386
|
console.log(
|
|
@@ -1439,7 +1469,7 @@ async function publishCommand(options = {}) {
|
|
|
1439
1469
|
projectId,
|
|
1440
1470
|
groupedAssets,
|
|
1441
1471
|
docSyncConfig,
|
|
1442
|
-
{ commitHash, commitMessage, publishSessionId },
|
|
1472
|
+
{ commitHash, commitMessage, publishSessionId, tag },
|
|
1443
1473
|
diffManifests,
|
|
1444
1474
|
{ autoApprove },
|
|
1445
1475
|
);
|
|
@@ -1459,7 +1489,8 @@ async function publishCommand(options = {}) {
|
|
|
1459
1489
|
projectId,
|
|
1460
1490
|
groupedAssets,
|
|
1461
1491
|
docSyncConfig,
|
|
1462
|
-
{ commitHash, commitMessage, publishSessionId },
|
|
1492
|
+
{ commitHash, commitMessage, publishSessionId, tag },
|
|
1493
|
+
{ autoApprove },
|
|
1463
1494
|
);
|
|
1464
1495
|
successCount = result.successCount;
|
|
1465
1496
|
failCount = result.failCount;
|
|
@@ -1471,7 +1502,8 @@ async function publishCommand(options = {}) {
|
|
|
1471
1502
|
projectId,
|
|
1472
1503
|
groupedAssets,
|
|
1473
1504
|
docSyncConfig,
|
|
1474
|
-
{ commitHash, commitMessage, publishSessionId },
|
|
1505
|
+
{ commitHash, commitMessage, publishSessionId, tag },
|
|
1506
|
+
{ autoApprove },
|
|
1475
1507
|
);
|
|
1476
1508
|
successCount = result.successCount;
|
|
1477
1509
|
failCount = result.failCount;
|
|
@@ -1543,7 +1575,10 @@ async function publishCommand(options = {}) {
|
|
|
1543
1575
|
|
|
1544
1576
|
console.log();
|
|
1545
1577
|
|
|
1546
|
-
return
|
|
1578
|
+
return {
|
|
1579
|
+
...publishResult,
|
|
1580
|
+
success: publishResult.assetsFailed === 0 && publishResult.assetsProcessed > 0,
|
|
1581
|
+
};
|
|
1547
1582
|
}
|
|
1548
1583
|
|
|
1549
1584
|
module.exports = publishCommand;
|