@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 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. 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
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 (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.7",
3
+ "version": "0.0.1-beta.9",
4
4
  "description": "CI/CD screenshot and video capture CLI",
5
5
  "author": "Reshot <hello@reshot.dev>",
6
6
  "license": "MIT",
@@ -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,23 +140,56 @@ 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
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 = ora("Requesting authentication session…").start();
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 startLocalStatusServer(basePort, {
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 startLocalStatusServer(basePort);
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 startLocalStatusServer(0);
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 axios.post(
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
- await open(authUrl, { wait: false });
254
+ console.log(chalk.gray("Auth URL:"));
255
+ console.log(chalk.cyan(authUrl));
210
256
  console.log(
211
- chalk.blue(
212
- "A browser window has been opened. Complete the flow to continue."
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
- const status = await waitForCompletion(apiBaseUrl, authToken, expiresAt);
217
- 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);
218
290
 
219
291
  // Derive platformUrl from apiBaseUrl (remove /api suffix)
220
292
  const platformUrl = apiBaseUrl.replace(/\/api\/?$/, '') || 'https://reshot.dev';
221
293
 
222
- writeSettings({
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;
@@ -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
@@ -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
@@ -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;
@@ -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
- const MAX_BATCH_SIZE = 200;
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 batchResult = await apiClient.publishBatch(apiKey, {
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 { tag, message, dryRun, force, video, outputJson, autoApprove } = options;
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/{key}?tag=${tag}\n`),
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 publishResult;
1578
+ return {
1579
+ ...publishResult,
1580
+ success: publishResult.assetsFailed === 0 && publishResult.assetsProcessed > 0,
1581
+ };
1547
1582
  }
1548
1583
 
1549
1584
  module.exports = publishCommand;