@reshotdev/screenshot 0.0.1-beta.8 → 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.
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+
3
+ const chalk = require("chalk");
4
+ const { runVerifyPublish } = require("../lib/certification");
5
+
6
+ async function verifyPublishCommand(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 runVerifyPublish({
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šŸ“¦ Publish Verification\n"));
24
+ console.log(
25
+ report.ok
26
+ ? chalk.green(" āœ” Publish, pull, and hosted delivery verified")
27
+ : chalk.red(" āœ– Publish verification failed"),
28
+ );
29
+ console.log(chalk.gray(` Published: ${report.publishResult.assetsProcessed || 0}`));
30
+ console.log(chalk.gray(` Pull repairs: ${report.pullResult.normalizationRepairs || 0}`));
31
+ const failedChecks = (report.deliveryChecks || []).filter((check) => !check.ok);
32
+ if (failedChecks.length > 0) {
33
+ for (const check of failedChecks.slice(0, 10)) {
34
+ console.log(chalk.red(` āœ– ${check.scenario}/${check.assetKey}: ${check.reason}`));
35
+ }
36
+ }
37
+ }
38
+
39
+ if (!report.ok) {
40
+ process.exitCode = 1;
41
+ }
42
+
43
+ return report;
44
+ }
45
+
46
+ module.exports = verifyPublishCommand;
package/src/index.js CHANGED
@@ -153,10 +153,70 @@ program
153
153
  }
154
154
  });
155
155
 
156
+ // Capture: Compatibility alias for older docs and snippets
157
+ program
158
+ .command("capture")
159
+ .description("Compatibility alias for `reshot run` (prefer `reshot run` and `reshot publish`)")
160
+ .option("-s, --scenarios <keys>", "Comma-separated list of scenario keys")
161
+ .option("--mode <mode>", "Compatibility mode: preview | publish", "preview")
162
+ .option("--config <path>", "Compatibility config path (defaults to reshot.config.json)")
163
+ .option("--base-url <url>", "Temporary base URL override for compatibility flows")
164
+ .option("--tag <tag>", "Version tag when mode=publish")
165
+ .option("-m, --message <message>", "Publish message when mode=publish")
166
+ .option("--no-headless", "Run browser in visible mode")
167
+ .option("-c, --concurrency <n>", "Number of parallel browser workers", parseInt)
168
+ .option("--debug", "Enable verbose debug logging")
169
+ .action(async (options) => {
170
+ if (options.debug) {
171
+ process.env.RESHOT_DEBUG = "1";
172
+ }
173
+ if (options.config && options.config !== "reshot.config.json") {
174
+ console.log(
175
+ chalk.yellow(
176
+ "Compatibility mode currently uses reshot.config.json from the working directory.",
177
+ ),
178
+ );
179
+ }
180
+ if (options.baseUrl) {
181
+ process.env.RESHOT_BASE_URL = options.baseUrl;
182
+ console.log(
183
+ chalk.gray(
184
+ `Using temporary base URL override for this run: ${options.baseUrl}`,
185
+ ),
186
+ );
187
+ }
188
+ try {
189
+ const runCommand = require("./commands/run");
190
+ const scenarioKeys = options.scenarios
191
+ ? options.scenarios.split(",").map((s) => s.trim())
192
+ : null;
193
+ await runCommand({
194
+ scenarioKeys,
195
+ headless: options.headless,
196
+ concurrency: options.concurrency,
197
+ });
198
+
199
+ if (String(options.mode || "preview").toLowerCase() === "publish") {
200
+ const publishCommand = require("./commands/publish");
201
+ await publishCommand({
202
+ tag: options.tag,
203
+ message: options.message,
204
+ });
205
+ }
206
+ } catch (error) {
207
+ console.error(chalk.red("Error:"), error.message);
208
+ if (options.debug && error.stack) {
209
+ console.error(chalk.gray(error.stack));
210
+ }
211
+ process.exit(1);
212
+ }
213
+ });
214
+
156
215
  // Record: Interactive visual recording from live browser
157
216
  program
158
217
  .command("record [title]")
159
218
  .description("Interactively record visuals from a live browser session")
219
+ .option("--name <title>", "Compatibility alias for the scenario title")
160
220
  .option("--browser", "Launch Chrome with remote debugging before recording")
161
221
  .option("-p, --port <port>", "Chrome debugging port (default: 9222)")
162
222
  .option("--url <url>", "URL to open when launching browser")
@@ -166,14 +226,15 @@ program
166
226
  if (options.debug) {
167
227
  process.env.RESHOT_DEBUG = "1";
168
228
  }
229
+ const resolvedTitle = title || options.name;
169
230
  try {
170
231
  // If --refresh-session, just sync the session and exit
171
232
  if (options.refreshSession) {
172
233
  const { autoSyncSessionFromCDP, getDefaultSessionPath } = require("./lib/record-cdp");
173
234
  const sessionPath = getDefaultSessionPath();
174
235
  console.log(chalk.gray(" Syncing session from active browser..."));
175
- const synced = await autoSyncSessionFromCDP(sessionPath);
176
- if (synced) {
236
+ const result = await autoSyncSessionFromCDP(sessionPath);
237
+ if (result.synced) {
177
238
  console.log(chalk.green(" āœ” Session refreshed at " + sessionPath));
178
239
  } else {
179
240
  console.log(chalk.yellow(" ⚠ No active CDP browser found. Launch Chrome with remote debugging first:"));
@@ -194,7 +255,7 @@ program
194
255
  }
195
256
 
196
257
  const recordCommand = require("./commands/record");
197
- await recordCommand(title);
258
+ await recordCommand(resolvedTitle);
198
259
  } catch (error) {
199
260
  console.error(chalk.red("Error:"), error.message);
200
261
  if (options.debug && error.stack) {
@@ -275,6 +336,62 @@ program
275
336
  }
276
337
  });
277
338
 
339
+ // Doctor: Validate target contract and readiness
340
+ const doctor = program.command("doctor").description("Validate target configuration and readiness");
341
+
342
+ doctor
343
+ .command("target")
344
+ .description("Audit the certified target contract before capture")
345
+ .option("-s, --scenarios <keys>", "Comma-separated list of scenario keys")
346
+ .option("--json", "Output JSON report")
347
+ .action(async (options) => {
348
+ try {
349
+ const doctorTargetCommand = require("./commands/doctor-target");
350
+ await doctorTargetCommand(options);
351
+ } catch (error) {
352
+ console.error(chalk.red("Error:"), error.message);
353
+ process.exit(1);
354
+ }
355
+ });
356
+
357
+ // Verify: Publish/pull/hosted delivery verification
358
+ const verify = program.command("verify").description("Verify publish and delivery flows");
359
+
360
+ verify
361
+ .command("publish")
362
+ .description("Verify publish, pull, and hosted asset delivery")
363
+ .option("-s, --scenarios <keys>", "Comma-separated list of scenario keys")
364
+ .option("--tag <tag>", "Version tag for verification publish")
365
+ .option("-m, --message <message>", "Publish message override")
366
+ .option("--json", "Output JSON report")
367
+ .action(async (options) => {
368
+ try {
369
+ const verifyPublishCommand = require("./commands/verify-publish");
370
+ await verifyPublishCommand(options);
371
+ } catch (error) {
372
+ console.error(chalk.red("Error:"), error.message);
373
+ process.exit(1);
374
+ }
375
+ });
376
+
377
+ // Certify: full certified-target release gate
378
+ program
379
+ .command("certify")
380
+ .description("Run the full certified target pipeline")
381
+ .option("-s, --scenarios <keys>", "Comma-separated list of scenario keys")
382
+ .option("--tag <tag>", "Version tag for certification publish")
383
+ .option("-m, --message <message>", "Publish message override")
384
+ .option("--json", "Output JSON report")
385
+ .action(async (options) => {
386
+ try {
387
+ const certifyCommand = require("./commands/certify");
388
+ await certifyCommand(options);
389
+ } catch (error) {
390
+ console.error(chalk.red("Error:"), error.message);
391
+ process.exit(1);
392
+ }
393
+ });
394
+
278
395
  // CI: CI/CD integration commands
279
396
  const ciCommand = program
280
397
  .command("ci")
@@ -353,9 +470,22 @@ Actions:
353
470
  // ============================================================================
354
471
 
355
472
  // Auth: Standalone authentication (for re-auth scenarios)
356
- program
473
+ const auth = program
357
474
  .command("auth")
358
- .description("Authenticate with Reshot Cloud")
475
+ .description("Authenticate with Reshot Cloud (or set RESHOT_API_KEY + RESHOT_PROJECT_ID env vars)")
476
+ .action(async () => {
477
+ try {
478
+ const authCommand = require("./commands/auth");
479
+ await authCommand();
480
+ } catch (error) {
481
+ console.error(chalk.red("Error:"), error.message);
482
+ process.exit(1);
483
+ }
484
+ });
485
+
486
+ auth
487
+ .command("login")
488
+ .description("Compatibility alias for `reshot auth`")
359
489
  .action(async () => {
360
490
  try {
361
491
  const authCommand = require("./commands/auth");
@@ -5,6 +5,51 @@ const fs = require("fs");
5
5
 
6
6
  const PRODUCTION_API_URL = "https://reshot.dev/api";
7
7
 
8
+ function summarizeApiBody(body) {
9
+ if (body == null) return "";
10
+ if (typeof body === "string") return body.trim();
11
+ if (typeof body !== "object") return String(body);
12
+
13
+ const candidates = [
14
+ body.message,
15
+ body.error?.message,
16
+ body.error,
17
+ body.reason,
18
+ body.details,
19
+ ].filter(Boolean);
20
+
21
+ for (const candidate of candidates) {
22
+ if (typeof candidate === "string" && candidate.trim()) {
23
+ return candidate.trim();
24
+ }
25
+ }
26
+
27
+ try {
28
+ return JSON.stringify(body);
29
+ } catch {
30
+ return String(body);
31
+ }
32
+ }
33
+
34
+ function createApiError(kind, endpoint, error) {
35
+ const status = error.response?.status || null;
36
+ const statusText = error.response?.statusText || "";
37
+ const bodySummary = summarizeApiBody(error.response?.data);
38
+ const baseMessage = bodySummary || error.message || "Unknown API error";
39
+ const statusLabel = status ? `${status}${statusText ? ` ${statusText}` : ""}` : "request failed";
40
+ const wrapped = new Error(`${kind} (${statusLabel}) at ${endpoint}: ${baseMessage}`);
41
+ wrapped.response = error.response;
42
+ wrapped.code = error.code;
43
+ wrapped.reshot = {
44
+ kind,
45
+ endpoint,
46
+ status,
47
+ statusText,
48
+ bodySummary,
49
+ };
50
+ return wrapped;
51
+ }
52
+
8
53
  function getApiBaseUrl() {
9
54
  // 1. Explicit env var override (for CI or local dev)
10
55
  if (process.env.RESHOT_API_BASE_URL) {
@@ -210,20 +255,7 @@ async function publishAssetsV1(apiKey, metadata, assets) {
210
255
  });
211
256
  return response.data;
212
257
  } catch (error) {
213
- if (error.response) {
214
- const status = error.response.status;
215
- const errorMsg = error.response.data?.error || error.message;
216
-
217
- // Create an error that preserves the response for auth detection
218
- const err = new Error(
219
- status === 401 || status === 403
220
- ? `Authentication failed: ${errorMsg}`
221
- : `Failed to publish assets: ${errorMsg}`,
222
- );
223
- err.response = error.response;
224
- throw err;
225
- }
226
- throw new Error(`Failed to publish assets: ${error.message}`);
258
+ throw createApiError("publish_ingest_failure", `${baseUrl}/v1/publish`, error);
227
259
  }
228
260
  }
229
261
 
@@ -700,15 +732,23 @@ async function exportVisuals(projectId, options = {}) {
700
732
  }
701
733
 
702
734
  return withRetry(async () => {
703
- const response = await axios.get(
704
- `${baseUrl}/projects/${projectId}/visuals/export`,
705
- {
706
- params: { format, status },
707
- headers: { Authorization: `Bearer ${apiKey}` },
708
- timeout: 60000,
709
- },
710
- );
711
- return response.data;
735
+ try {
736
+ const response = await axios.get(
737
+ `${baseUrl}/projects/${projectId}/visuals/export`,
738
+ {
739
+ params: { format, status },
740
+ headers: { Authorization: `Bearer ${apiKey}` },
741
+ timeout: 60000,
742
+ },
743
+ );
744
+ return response.data;
745
+ } catch (error) {
746
+ throw createApiError(
747
+ "export_visuals_failure",
748
+ `${baseUrl}/projects/${projectId}/visuals/export`,
749
+ error,
750
+ );
751
+ }
712
752
  });
713
753
  }
714
754
 
@@ -838,6 +878,7 @@ module.exports = {
838
878
  getProjectConfig,
839
879
  postChangelogDrafts,
840
880
  getApiBaseUrl,
881
+ createApiError,
841
882
  syncPushAssets,
842
883
  getSyncStatus,
843
884
  // New transactional flow
@@ -98,6 +98,8 @@ class CaptureEngine {
98
98
  this.capturedAssets = [];
99
99
  this.logger = options.logger || console.log;
100
100
  this.headless = options.headless !== false; // Default to headless
101
+ this.injectWorkspaceStore = options.injectWorkspaceStore !== false;
102
+ this.diagnostics = [];
101
103
 
102
104
  // Storage state path for authenticated sessions
103
105
  // If provided, loads cookies/localStorage from file to preserve auth state
@@ -289,7 +291,9 @@ class CaptureEngine {
289
291
  logVariantSummary(this.variantConfig, this.logger);
290
292
  }
291
293
 
292
- await this._injectWorkspaceStore();
294
+ if (this.injectWorkspaceStore) {
295
+ await this._injectWorkspaceStore();
296
+ }
293
297
 
294
298
  // Inject privacy masking CSS (after variant injection, before captures)
295
299
  this._privacyInjectionOk = true;
@@ -306,24 +310,76 @@ class CaptureEngine {
306
310
  this._authResponseDetected = false;
307
311
  this.page.on("response", (response) => {
308
312
  const status = response.status();
313
+ const url = response.url();
309
314
  if (
310
315
  (status === 401 || status === 403) &&
311
316
  response.request().resourceType() === "document"
312
317
  ) {
313
318
  this._authResponseDetected = true;
314
319
  }
320
+
321
+ if (status >= 400 && response.request().resourceType() === "document") {
322
+ this._recordDiagnostic("response_error", "error", {
323
+ url,
324
+ status,
325
+ resourceType: response.request().resourceType(),
326
+ });
327
+ }
315
328
  });
316
329
 
317
330
  // Set up error handling
318
331
  this.page.on("pageerror", (err) => {
319
332
  const firstLine = (err.message || '').split('\n')[0].slice(0, 200);
333
+ this._recordDiagnostic("pageerror", "error", {
334
+ message: err.message || String(err),
335
+ });
320
336
  this.logger(chalk.yellow(` [Page Error] ${firstLine}`));
321
337
  });
338
+ this.page.on("console", (message) => {
339
+ const type = message.type();
340
+ const text = message.text();
341
+ const severity =
342
+ type === "error"
343
+ ? "error"
344
+ : type === "warning"
345
+ ? "warning"
346
+ : "info";
347
+
348
+ if (severity === "error" || severity === "warning") {
349
+ this._recordDiagnostic("console", severity, {
350
+ message: text,
351
+ consoleType: type,
352
+ cspViolation: /content security policy|csp/i.test(text),
353
+ });
354
+ }
355
+ });
356
+ this.page.on("requestfailed", (request) => {
357
+ this._recordDiagnostic("requestfailed", "error", {
358
+ url: request.url(),
359
+ method: request.method(),
360
+ resourceType: request.resourceType(),
361
+ message: request.failure()?.errorText || "Request failed",
362
+ });
363
+ });
322
364
 
323
365
  this.logger(chalk.green(" āœ” Browser initialized"));
324
366
  return this;
325
367
  }
326
368
 
369
+ _recordDiagnostic(kind, severity, details = {}) {
370
+ this.diagnostics.push({
371
+ id: `${Date.now()}-${this.diagnostics.length + 1}`,
372
+ kind,
373
+ severity,
374
+ ...details,
375
+ capturedAt: new Date().toISOString(),
376
+ });
377
+ }
378
+
379
+ getDiagnostics() {
380
+ return [...this.diagnostics];
381
+ }
382
+
327
383
  /**
328
384
  * Hide development overlays (Next.js devtools, Vercel toolbar, etc.)
329
385
  * Injects CSS to hide common development UI elements before each navigation
@@ -512,10 +568,15 @@ class CaptureEngine {
512
568
  }
513
569
 
514
570
  // Check for auth redirect after navigation (URL patterns + HTTP 401/403)
571
+ // Skip the check if the scenario explicitly targeted this URL (e.g. capturing /login)
515
572
  const currentUrl = this.page.url();
573
+ const targetPath = url.startsWith("http") ? new URL(url).pathname : url;
574
+ const currentPath = (() => { try { return new URL(currentUrl).pathname; } catch { return currentUrl; } })();
575
+ const isIntentionalTarget = currentPath === targetPath;
516
576
  const isAuthRedirect =
517
- isAuthRedirectUrl(currentUrl, this._customAuthPatterns) ||
518
- this._authResponseDetected;
577
+ !isIntentionalTarget &&
578
+ (isAuthRedirectUrl(currentUrl, this._customAuthPatterns) ||
579
+ this._authResponseDetected);
519
580
  if (isAuthRedirect) {
520
581
  const errorMsg = `Auth redirect detected: navigated to ${currentUrl}. Session may be expired. Re-run \`reshot record\` to refresh session, or export a fresh Playwright storage state to .reshot/auth-state.json.`;
521
582
  this.logger(chalk.red(` āœ– ${errorMsg}`));
@@ -251,7 +251,11 @@ async function executeWithRetry(engine, readySelector, options = {}) {
251
251
  * @returns {Promise<{ok: boolean, message?: string}>}
252
252
  */
253
253
  async function preflightAuthCheck(baseUrl, options = {}) {
254
- const { storageStatePath, viewport = { width: 1280, height: 720 } } = options;
254
+ const {
255
+ storageStatePath,
256
+ viewport = { width: 1280, height: 720 },
257
+ authCheckUrl = "/app/projects",
258
+ } = options;
255
259
 
256
260
  if (!storageStatePath || !fs.existsSync(storageStatePath)) {
257
261
  return { ok: true }; // No session to verify
@@ -272,8 +276,12 @@ async function preflightAuthCheck(baseUrl, options = {}) {
272
276
  try {
273
277
  await engine.init();
274
278
 
275
- // Navigate to projects page (a page that requires auth + data)
276
- await engine.page.goto(`${baseUrl}/app/projects`, {
279
+ const preflightPath = authCheckUrl.startsWith("http")
280
+ ? authCheckUrl
281
+ : `${baseUrl}${authCheckUrl}`;
282
+
283
+ // Navigate to a known authenticated page and validate session/data loading.
284
+ await engine.page.goto(preflightPath, {
277
285
  waitUntil: "domcontentloaded",
278
286
  timeout: 15000,
279
287
  });
@@ -290,6 +298,19 @@ async function preflightAuthCheck(baseUrl, options = {}) {
290
298
  };
291
299
  }
292
300
 
301
+ // Also detect login page via DOM (catches SPA redirects where URL hasn't changed)
302
+ const hasLoginForm = await engine.page.evaluate(() => {
303
+ const h = document.querySelector("h1, h2");
304
+ return h && /sign\s*in|log\s*in/i.test(h.textContent);
305
+ }).catch(() => false);
306
+ if (hasLoginForm) {
307
+ return {
308
+ ok: false,
309
+ message:
310
+ "Auth session expired (login form detected). Run `reshot record` to refresh.",
311
+ };
312
+ }
313
+
293
314
  // Wait for data to settle
294
315
  await engine.page.waitForTimeout(3000);
295
316
  await engine._waitForStability();
@@ -418,6 +439,14 @@ async function retryInteractiveStep(engine, action, params, context) {
418
439
  }
419
440
  }
420
441
 
442
+ function promoteLastGotoUrl(lastGotoUrl, currentUrl) {
443
+ if (!currentUrl || currentUrl === "about:blank") {
444
+ return lastGotoUrl;
445
+ }
446
+
447
+ return currentUrl !== lastGotoUrl ? currentUrl : lastGotoUrl;
448
+ }
449
+
421
450
  /**
422
451
  * Calculate a perceptual hash for an image buffer
423
452
  * This is a simple hash based on resizing the image to a small grid
@@ -854,6 +883,7 @@ async function runScenarioWithStepByStepCapture(scenario, options = {}) {
854
883
  waitForReady: scenario.waitForReady || null, // Custom loading-state hook
855
884
  privacyConfig: hasPrivacy ? scenarioPrivacyConfig : null, // Privacy masking
856
885
  styleConfig: hasStyle ? scenarioStyleConfig : null, // Image beautification
886
+ injectWorkspaceStore: scenario.needsWorkspaceInjection !== false,
857
887
  logger: quiet ? () => {} : (msg) => console.log(msg),
858
888
  });
859
889
 
@@ -1278,6 +1308,11 @@ async function runScenarioWithStepByStepCapture(scenario, options = {}) {
1278
1308
  }
1279
1309
  }
1280
1310
 
1311
+ // Promote the current page URL after successful interactive steps so
1312
+ // retries restore the page we actually navigated to, not the last
1313
+ // explicit goto target.
1314
+ lastGotoUrl = promoteLastGotoUrl(lastGotoUrl, engine.page.url());
1315
+
1281
1316
  // Wait for animations/transitions - longer wait for multi-step flows
1282
1317
  const isMultiStep = script.length > 3;
1283
1318
  await engine.page.waitForTimeout(isMultiStep ? 500 : 150);
@@ -1443,7 +1478,17 @@ async function runScenarioWithStepByStepCapture(scenario, options = {}) {
1443
1478
  // Non-critical — don't fail the capture
1444
1479
  }
1445
1480
 
1446
- return { success: failedSteps.length === 0, assets, skippedSteps, duplicatesSkipped, failedSteps, retriedSteps, privacy: privacyMeta, style: styleMeta };
1481
+ return {
1482
+ success: failedSteps.length === 0,
1483
+ assets,
1484
+ skippedSteps,
1485
+ duplicatesSkipped,
1486
+ failedSteps,
1487
+ retriedSteps,
1488
+ privacy: privacyMeta,
1489
+ style: styleMeta,
1490
+ diagnostics: engine.getDiagnostics(),
1491
+ };
1447
1492
  })(); // End of scenarioExecution async IIFE
1448
1493
 
1449
1494
  // Race scenario execution against timeout
@@ -1472,7 +1517,15 @@ async function runScenarioWithStepByStepCapture(scenario, options = {}) {
1472
1517
  // Ignore
1473
1518
  }
1474
1519
 
1475
- return { success: false, error: error.message, assets, skippedSteps, failedSteps, retriedSteps };
1520
+ return {
1521
+ success: false,
1522
+ error: error.message,
1523
+ assets,
1524
+ skippedSteps,
1525
+ failedSteps,
1526
+ retriedSteps,
1527
+ diagnostics: engine.getDiagnostics(),
1528
+ };
1476
1529
  } finally {
1477
1530
  await engine.close();
1478
1531
  }
@@ -2606,6 +2659,7 @@ async function runScenarioWithEngine(scenario, options = {}) {
2606
2659
  storageStatePath: hasSession ? sessionPath : null, // Use saved session if available
2607
2660
  storageStateData, // Pre-loaded auth state
2608
2661
  hideDevtools: true, // Always hide dev overlays in captures
2662
+ injectWorkspaceStore: scenario.needsWorkspaceInjection !== false,
2609
2663
  logger: quiet ? () => {} : (msg) => console.log(msg),
2610
2664
  });
2611
2665
 
@@ -2617,7 +2671,7 @@ async function runScenarioWithEngine(scenario, options = {}) {
2617
2671
  chalk.green(`\n āœ” Scenario completed: ${assets.length} assets captured`)
2618
2672
  );
2619
2673
 
2620
- return { success: true, assets };
2674
+ return { success: true, assets, diagnostics: engine.getDiagnostics() };
2621
2675
  } catch (error) {
2622
2676
  console.error(chalk.red(`\n āŒ Scenario failed: ${error.message}`));
2623
2677
 
@@ -2637,7 +2691,7 @@ async function runScenarioWithEngine(scenario, options = {}) {
2637
2691
  // Ignore screenshot errors
2638
2692
  }
2639
2693
 
2640
- return { success: false, error: error.message };
2694
+ return { success: false, error: error.message, diagnostics: engine.getDiagnostics() };
2641
2695
  } finally {
2642
2696
  await engine.close();
2643
2697
  }
@@ -2882,7 +2936,14 @@ async function runAllScenarios(config, options = {}) {
2882
2936
  // Run auth pre-flight check if any scenario requires auth
2883
2937
  const captureConfig = getCaptureConfig(config.capture || {});
2884
2938
  const scenarios = config.scenarios || [];
2885
- const hasAuthScenarios = scenarios.some((s) => s.requiresAuth);
2939
+ const hasAuthScenarios = scenarios.some((s) => s.captureClass !== "public");
2940
+ const authPreflightScenario = scenarios.find(
2941
+ (scenario) => scenario.captureClass === "live-auth" || scenario.requiresAuth,
2942
+ );
2943
+ const authCheckUrl =
2944
+ config.target?.authPreflightUrl ||
2945
+ authPreflightScenario?.url ||
2946
+ "/app/projects";
2886
2947
 
2887
2948
  if (captureConfig.preflightCheck && hasAuthScenarios) {
2888
2949
  const sessionPath = getDefaultSessionPath();
@@ -2893,6 +2954,7 @@ async function runAllScenarios(config, options = {}) {
2893
2954
  {
2894
2955
  storageStatePath: sessionPath,
2895
2956
  viewport: config.viewport || { width: 1280, height: 720 },
2957
+ authCheckUrl,
2896
2958
  }
2897
2959
  );
2898
2960
  if (!preflightResult.ok) {
@@ -3160,6 +3222,7 @@ async function runAllScenarios(config, options = {}) {
3160
3222
 
3161
3223
  module.exports = {
3162
3224
  convertLegacySteps,
3225
+ substituteUrlVariables,
3163
3226
  runScenarioWithEngine,
3164
3227
  runScenarioWithStepByStepCapture,
3165
3228
  runScenarioWithVideoCapture,
@@ -3171,6 +3234,7 @@ module.exports = {
3171
3234
  waitForVisualStability,
3172
3235
  // Error detection & retry
3173
3236
  retryInteractiveStep,
3237
+ promoteLastGotoUrl,
3174
3238
  executeWithRetry,
3175
3239
  preflightAuthCheck,
3176
3240
  // New exports for output templating