@reshotdev/screenshot 0.0.1-beta.6 → 0.0.1-beta.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reshotdev/screenshot",
3
- "version": "0.0.1-beta.6",
3
+ "version": "0.0.1-beta.7",
4
4
  "description": "CI/CD screenshot and video capture CLI",
5
5
  "author": "Reshot <hello@reshot.dev>",
6
6
  "license": "MIT",
@@ -217,7 +217,7 @@ async function authCommand() {
217
217
  await verifyApiKey(apiBaseUrl, status.project.apiKey);
218
218
 
219
219
  // Derive platformUrl from apiBaseUrl (remove /api suffix)
220
- const platformUrl = apiBaseUrl.replace(/\/api\/?$/, '') || 'http://localhost:3000';
220
+ const platformUrl = apiBaseUrl.replace(/\/api\/?$/, '') || 'https://reshot.dev';
221
221
 
222
222
  writeSettings({
223
223
  projectId: status.project.id,
@@ -578,6 +578,7 @@ async function publishWithTransactionalFlow(
578
578
  docSyncConfig,
579
579
  gitInfo,
580
580
  diffManifests = null,
581
+ { autoApprove = false } = {},
581
582
  ) {
582
583
  console.log(
583
584
  chalk.cyan(" 🚀 Using transactional upload (direct to R2)...\n"),
@@ -807,7 +808,10 @@ async function publishWithTransactionalFlow(
807
808
  });
808
809
  }
809
810
 
810
- // Commit each group
811
+ // Build all commits for batch request
812
+ const MAX_BATCH_SIZE = 200;
813
+ const commits = [];
814
+
811
815
  for (const { group, scenarioConfig, assets } of groupMap.values()) {
812
816
  const contextObj = buildContextForVariation(
813
817
  scenarioConfig,
@@ -824,38 +828,58 @@ async function publishWithTransactionalFlow(
824
828
  });
825
829
 
826
830
  if (metadata.cli) {
827
- metadata.cli.features = ["steps", "transactional"];
831
+ metadata.cli.features = ["steps", "transactional", "batch"];
828
832
  }
829
833
 
834
+ commits.push({ metadata, assets });
835
+ }
836
+
837
+ // Send in batches
838
+ console.log(
839
+ chalk.gray(` Committing ${commits.length} scenario(s) to platform...`),
840
+ );
841
+
842
+ for (let i = 0; i < commits.length; i += MAX_BATCH_SIZE) {
843
+ const chunk = commits.slice(i, i + MAX_BATCH_SIZE);
844
+
830
845
  try {
831
- const result = await apiClient.publishTransactional(apiKey, {
832
- metadata,
833
- assets,
846
+ const batchResult = await apiClient.publishBatch(apiKey, {
847
+ commits: chunk,
848
+ autoApprove: autoApprove || false,
834
849
  });
835
850
 
836
- const processedCount = result?.assetsProcessed ?? assets.length;
837
- console.log(
838
- chalk.green(
839
- ` ✔ Committed "${group.scenarioKey}" (${group.variationSlug}): ${processedCount} asset(s)`,
840
- ),
841
- );
842
- successCount += processedCount;
851
+ for (const r of batchResult.results || []) {
852
+ if (r.status === "ok") {
853
+ const count = r.assetsProcessed || 0;
854
+ console.log(
855
+ chalk.green(
856
+ ` ✔ Committed "${r.scenario}" (${r.context}): ${count} asset(s)`,
857
+ ),
858
+ );
859
+ successCount += count;
843
860
 
844
- // Handle skipped assets (visual limit)
845
- if (result?.skippedAssets?.length > 0) {
846
- for (const key of result.skippedAssets) {
847
- console.log(chalk.yellow(` ⚠ Skipped "${key}" (plan limit reached)`));
861
+ if (r.skippedAssets?.length > 0) {
862
+ for (const key of r.skippedAssets) {
863
+ console.log(chalk.yellow(` ⚠ Skipped "${key}" (plan limit reached)`));
864
+ }
865
+ skippedCount += r.skippedAssets.length;
866
+ }
867
+ } else {
868
+ console.log(
869
+ chalk.red(
870
+ ` ✖ "${r.scenario}" (${r.context}): ${r.error || "Unknown error"}`,
871
+ ),
872
+ );
873
+ failCount++;
848
874
  }
849
- skippedCount += result.skippedAssets.length;
850
875
  }
851
876
 
852
- // Capture viewUrl from first successful response
853
- if (!viewUrl && result?.viewUrl) {
854
- viewUrl = result.viewUrl;
877
+ if (!viewUrl && batchResult.viewUrl) {
878
+ viewUrl = batchResult.viewUrl;
855
879
  }
856
880
  } catch (error) {
857
- console.log(chalk.red(` ✖ Commit failed: ${error.message}`));
858
- failCount += assets.length;
881
+ console.log(chalk.red(` ✖ Batch request failed: ${error.message}`));
882
+ failCount += chunk.length;
859
883
  }
860
884
  }
861
885
 
@@ -1204,7 +1228,7 @@ function parseFrontmatter(content) {
1204
1228
  }
1205
1229
 
1206
1230
  async function publishCommand(options = {}) {
1207
- const { tag, message, dryRun, force, video, outputJson } = options;
1231
+ const { tag, message, dryRun, force, video, outputJson, autoApprove } = options;
1208
1232
 
1209
1233
  // Result tracking for --output-json and programmatic callers
1210
1234
  const publishResult = {
@@ -1230,6 +1254,10 @@ async function publishCommand(options = {}) {
1230
1254
  console.log(chalk.yellow("🔍 DRY RUN MODE - No assets will be uploaded\n"));
1231
1255
  }
1232
1256
 
1257
+ if (autoApprove) {
1258
+ console.log(chalk.cyan(" ✅ Auto-approve enabled: visuals will be approved immediately\n"));
1259
+ }
1260
+
1233
1261
  // Read config + settings (if available)
1234
1262
  const settings = readSettingsSafe();
1235
1263
  let docSyncConfig = null;
@@ -1413,6 +1441,7 @@ async function publishCommand(options = {}) {
1413
1441
  docSyncConfig,
1414
1442
  { commitHash, commitMessage, publishSessionId },
1415
1443
  diffManifests,
1444
+ { autoApprove },
1416
1445
  );
1417
1446
  successCount = result.successCount;
1418
1447
  failCount = result.failCount;
package/src/index.js CHANGED
@@ -221,12 +221,14 @@ program
221
221
  .option("--dry-run", "Preview without uploading")
222
222
  .option("-f, --force", "Skip confirmation prompts")
223
223
  .option("--output-json", "Write structured result to .reshot/output/publish-result.json")
224
+ .option("--auto-approve", "Automatically approve published visuals (skip review queue)")
224
225
  .action(async (options) => {
225
226
  try {
226
227
  const publishCommand = require("./commands/publish");
227
228
  await publishCommand({
228
229
  ...options,
229
230
  outputJson: options.outputJson,
231
+ autoApprove: options.autoApprove,
230
232
  });
231
233
  } catch (error) {
232
234
  console.error(chalk.red("Error:"), error.message);
@@ -3,15 +3,35 @@ const axios = require("axios");
3
3
  const FormData = require("form-data");
4
4
  const fs = require("fs");
5
5
 
6
- const baseUrl =
7
- process.env.RESHOT_API_BASE_URL ||
8
- process.env.RESHOT_API_BASE_URL ||
9
- "http://localhost:3000/api";
6
+ const PRODUCTION_API_URL = "https://reshot.dev/api";
10
7
 
11
8
  function getApiBaseUrl() {
12
- return baseUrl;
9
+ // 1. Explicit env var override (for CI or local dev)
10
+ if (process.env.RESHOT_API_BASE_URL) {
11
+ return process.env.RESHOT_API_BASE_URL;
12
+ }
13
+
14
+ // 2. Read from settings.json (set during auth/setup)
15
+ try {
16
+ const path = require("path");
17
+ const settingsPath = path.join(process.cwd(), ".reshot", "settings.json");
18
+ if (fs.existsSync(settingsPath)) {
19
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
20
+ if (settings.platformUrl) {
21
+ return settings.platformUrl.replace(/\/+$/, "") + "/api";
22
+ }
23
+ }
24
+ } catch {
25
+ // Settings don't exist yet (first auth) — fall through to default
26
+ }
27
+
28
+ // 3. Default to production
29
+ return PRODUCTION_API_URL;
13
30
  }
14
31
 
32
+ // Resolved once at module load — all API calls use this
33
+ const baseUrl = getApiBaseUrl();
34
+
15
35
  /**
16
36
  * Sleep helper for retry delays
17
37
  */
@@ -586,6 +606,30 @@ async function publishTransactional(apiKey, payload) {
586
606
  return response.data;
587
607
  }
588
608
 
609
+ /**
610
+ * Batch publish metadata for multiple scenarios in one request
611
+ * @param {string} apiKey - API key for authentication
612
+ * @param {Object} payload - { commits: Array<{ metadata, assets }> }
613
+ * @returns {Promise<Object>}
614
+ */
615
+ async function publishBatch(apiKey, payload) {
616
+ if (!apiKey) {
617
+ throw new Error("API key is required to publish");
618
+ }
619
+
620
+ const response = await axios.post(`${baseUrl}/v1/publish/batch`, payload, {
621
+ headers: {
622
+ "Content-Type": "application/json",
623
+ Authorization: `Bearer ${apiKey}`,
624
+ },
625
+ timeout: 60000,
626
+ maxBodyLength: Infinity,
627
+ maxContentLength: Infinity,
628
+ });
629
+
630
+ return response.data;
631
+ }
632
+
589
633
  /**
590
634
  * Check which hashes already exist in storage (for deduplication)
591
635
  * @param {string} apiKey - API key for authentication
@@ -800,6 +844,7 @@ module.exports = {
800
844
  signAssets,
801
845
  uploadToPresignedUrl,
802
846
  publishTransactional,
847
+ publishBatch,
803
848
  checkExistingHashes,
804
849
  // Diffing support
805
850
  getBaselines,
@@ -1133,11 +1133,25 @@ async function runScenarioWithStepByStepCapture(scenario, options = {}) {
1133
1133
  `Page error detected: ${errMsg}. The page rendered an error UI instead of expected content.`
1134
1134
  );
1135
1135
  } else if (retryResult.status === "timeout") {
1136
+ const currentUrl = engine.page.url();
1136
1137
  console.log(
1137
1138
  chalk.yellow(
1138
- ` ⚠ Ready selector not found after ${retryResult.attempts} attempt(s), proceeding with current state`
1139
+ ` ⚠ Ready selector not found after ${retryResult.attempts} attempt(s): ${readySelector}`
1140
+ )
1141
+ );
1142
+ console.log(
1143
+ chalk.gray(` URL: ${currentUrl}`)
1144
+ );
1145
+ console.log(
1146
+ chalk.gray(
1147
+ ` Hint: The page loaded but this selector does not exist. Check your readySelector in reshot.config.json.`
1139
1148
  )
1140
1149
  );
1150
+ throw new Error(
1151
+ `Scenario readySelector "${readySelector}" not found after ${retryResult.attempts} attempt(s). ` +
1152
+ `The page loaded at ${currentUrl} but the selector does not exist. ` +
1153
+ `Update readySelector in reshot.config.json or remove it to skip this check.`
1154
+ );
1141
1155
  } else if (retryResult.attempts > 1) {
1142
1156
  console.log(
1143
1157
  chalk.green(
@@ -2185,8 +2199,16 @@ async function runScenarioWithVideoCapture(scenario, options = {}) {
2185
2199
  });
2186
2200
  } catch (e) {
2187
2201
  if (!isOptional) {
2202
+ const currentUrl = page.url();
2203
+ console.warn(
2204
+ chalk.yellow(` ⚠ Element not found: ${params.target}`)
2205
+ );
2206
+ console.warn(chalk.gray(` URL: ${currentUrl}`));
2207
+ console.warn(chalk.gray(` Timeout: ${waitTimeout}ms`));
2188
2208
  console.warn(
2189
- chalk.yellow(` ⚠ Wait for ${params.target} timed out`)
2209
+ chalk.gray(
2210
+ ` Hint: Verify the selector exists on the page. Run 'reshot record' to inspect.`
2211
+ )
2190
2212
  );
2191
2213
  }
2192
2214
  }
package/src/lib/ui-api.js CHANGED
@@ -43,17 +43,16 @@ const {
43
43
  * @returns {string} Platform URL
44
44
  */
45
45
  function getPlatformUrl(settings) {
46
- // Priority: settings.platformUrl > env var > localhost default
46
+ // Priority: settings.platformUrl > env var > production default
47
47
  if (settings?.platformUrl) {
48
48
  return settings.platformUrl;
49
49
  }
50
- const envUrl =
51
- process.env.RESHOT_API_BASE_URL || process.env.RESHOT_API_BASE_URL;
50
+ const envUrl = process.env.RESHOT_API_BASE_URL;
52
51
  if (envUrl) {
53
52
  // Remove /api suffix if present to get platform URL
54
53
  return envUrl.replace(/\/api\/?$/, "");
55
54
  }
56
- return "http://localhost:3000";
55
+ return "https://reshot.dev";
57
56
  }
58
57
 
59
58
  /**
@@ -2852,7 +2851,7 @@ function attachApiRoutes(app, context) {
2852
2851
  const apiBaseUrl = getApiBaseUrl();
2853
2852
  // Derive platformUrl from apiBaseUrl (remove /api suffix)
2854
2853
  const platformUrl =
2855
- apiBaseUrl.replace(/\/api\/?$/, "") || "http://localhost:3000";
2854
+ apiBaseUrl.replace(/\/api\/?$/, "") || "https://reshot.dev";
2856
2855
 
2857
2856
  config.writeSettings({
2858
2857
  projectId: project.id,