@reshotdev/screenshot 0.0.1-beta.15 → 0.0.1-beta.17

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.15",
3
+ "version": "0.0.1-beta.17",
4
4
  "description": "Screenshot and video capture CLI",
5
5
  "author": "Reshot <hello@reshot.dev>",
6
6
  "license": "MIT",
@@ -150,11 +150,12 @@ async function waitForCompletion(
150
150
  }
151
151
 
152
152
  async function verifyApiKey(apiBaseUrl, apiKey, httpClient = axios) {
153
- await httpClient.get(`${apiBaseUrl}/auth/cli/verify`, {
153
+ const response = await httpClient.get(`${apiBaseUrl}/auth/cli/verify`, {
154
154
  headers: {
155
155
  Authorization: `Bearer ${apiKey}`,
156
156
  },
157
157
  });
158
+ return response?.data || null;
158
159
  }
159
160
 
160
161
  async function authCommand(options = {}) {
@@ -172,19 +173,35 @@ async function authCommand(options = {}) {
172
173
  const timeoutMs = Number(options.timeoutMs || DEFAULT_AUTH_TIMEOUT_MS);
173
174
  if (envApiKey && envProjectId) {
174
175
  const platformUrl = process.env.RESHOT_PLATFORM_URL || "https://reshot.dev";
176
+ // Best-effort resolve the project name so settings + the setup report
177
+ // don't record `projectName: null` on this non-interactive path.
178
+ let projectName = null;
179
+ try {
180
+ const apiBaseUrl = options.apiBaseUrl || getApiBaseUrl();
181
+ const verified = await verifyApiKeyFn(apiBaseUrl, envApiKey, httpClient);
182
+ // /auth/cli/verify wraps its payload in an { data: … } envelope.
183
+ const payload = verified?.data || verified;
184
+ projectName = payload?.project?.name || null;
185
+ } catch {
186
+ // Verification is best-effort here; keep going without the name.
187
+ }
175
188
  writeSettingsFn({
176
189
  projectId: envProjectId,
190
+ projectName,
177
191
  apiKey: envApiKey,
178
192
  platformUrl,
179
193
  linkedAt: new Date().toISOString(),
180
194
  cliVersion: pkg.version,
181
195
  });
182
196
  console.log(chalk.green("✔ Authenticated via environment variables"));
183
- console.log(chalk.gray(` Project: ${envProjectId}`));
197
+ console.log(
198
+ chalk.gray(` Project: ${projectName || envProjectId}`),
199
+ );
184
200
  console.log(chalk.gray(` Platform: ${platformUrl}`));
185
201
  return {
186
202
  mode: "cloud-connected",
187
203
  projectId: envProjectId,
204
+ projectName,
188
205
  platformUrl,
189
206
  };
190
207
  }
@@ -666,6 +666,7 @@ async function publishWithTransactionalFlow(
666
666
  let successCount = 0;
667
667
  let failCount = 0;
668
668
  let skippedCount = 0;
669
+ let reviewPendingCount = 0;
669
670
  let viewUrl = null;
670
671
 
671
672
  // Flatten all assets with metadata
@@ -878,7 +879,16 @@ async function publishWithTransactionalFlow(
878
879
  contentType: file.contentType,
879
880
  });
880
881
 
881
- console.log(chalk.green(` ✔ Uploaded ${file.visualKey}`));
882
+ // Label the artifact type so PNG + MHTML-sidecar lines for the same
883
+ // visual don't read as a duplicate-upload bug (audit run-10 F3).
884
+ const artifactLabel = file._isThumbnail
885
+ ? "thumbnail"
886
+ : file._isDomScene
887
+ ? "dom scene .mhtml"
888
+ : (path.extname(file.path || "").replace(/^\./, "") || "asset");
889
+ console.log(
890
+ chalk.green(` ✔ Uploaded ${file.visualKey} (${artifactLabel})`),
891
+ );
882
892
  return { success: true, file, s3Path: urlInfo.path };
883
893
  } catch (err) {
884
894
  console.log(
@@ -1008,6 +1018,10 @@ async function publishWithTransactionalFlow(
1008
1018
  // Unwrap API envelope: response may be { data: { results, ... } } or { results, ... }
1009
1019
  const batchResult = rawBatchResult.data || rawBatchResult;
1010
1020
 
1021
+ // Count only NEWLY-pending (new/changed) captures, so the final summary
1022
+ // doesn't imply a re-publish reset already-live captures (audit F1).
1023
+ reviewPendingCount += batchResult.reviewQueueItems || 0;
1024
+
1011
1025
  for (const r of batchResult.results || []) {
1012
1026
  if (r.status === "ok") {
1013
1027
  const count = r.assetsProcessed || 0;
@@ -1051,7 +1065,14 @@ async function publishWithTransactionalFlow(
1051
1065
  }
1052
1066
  }
1053
1067
 
1054
- return { successCount, failCount, skippedCount, viewUrl, failedUploadKeys };
1068
+ return {
1069
+ successCount,
1070
+ failCount,
1071
+ skippedCount,
1072
+ reviewPendingCount,
1073
+ viewUrl,
1074
+ failedUploadKeys,
1075
+ };
1055
1076
  }
1056
1077
 
1057
1078
  /**
@@ -1697,6 +1718,7 @@ async function publishCommand(options = {}) {
1697
1718
  let successCount = 0;
1698
1719
  let failCount = 0;
1699
1720
  let skippedCount = 0;
1721
+ let reviewPendingCount = 0;
1700
1722
  let viewUrl = null;
1701
1723
 
1702
1724
  // Load diff manifests for attaching diff data to screenshot assets
@@ -1743,6 +1765,7 @@ async function publishCommand(options = {}) {
1743
1765
  successCount = result.successCount;
1744
1766
  failCount = result.failCount;
1745
1767
  skippedCount = result.skippedCount || 0;
1768
+ reviewPendingCount = result.reviewPendingCount || 0;
1746
1769
  viewUrl = result.viewUrl || null;
1747
1770
  } catch (txError) {
1748
1771
  // Fall back to legacy flow if transactional fails
@@ -1809,21 +1832,40 @@ async function publishCommand(options = {}) {
1809
1832
  }
1810
1833
 
1811
1834
  if (!autoApprove && successCount > 0) {
1812
- console.log(
1813
- chalk.cyan(
1814
- `\n ℹ ${successCount} visual${successCount === 1 ? "" : "s"} awaiting review (PENDING)`,
1815
- ),
1816
- );
1817
- console.log(
1818
- chalk.gray(
1819
- " To skip review for first-time captures, re-run with `reshot publish --auto-approve`",
1820
- ),
1821
- );
1822
- console.log(
1823
- chalk.gray(
1824
- " or approve them in the studio link above.",
1825
- ),
1826
- );
1835
+ // Only NEW/CHANGED captures await review; unchanged captures dedup to
1836
+ // their existing (often already-approved) version and stay live. Report
1837
+ // the accurate pending count so a re-publish doesn't look like it reset
1838
+ // already-approved work (audit run-10 F1).
1839
+ const pending = reviewPendingCount;
1840
+ const keptLive = Math.max(0, successCount - pending);
1841
+ if (pending > 0) {
1842
+ console.log(
1843
+ chalk.cyan(
1844
+ `\n ℹ ${pending} new/changed visual${pending === 1 ? "" : "s"} awaiting review (PENDING)`,
1845
+ ),
1846
+ );
1847
+ if (keptLive > 0) {
1848
+ console.log(
1849
+ chalk.gray(
1850
+ ` ${keptLive} unchanged visual${keptLive === 1 ? "" : "s"} stayed live (no re-approval needed).`,
1851
+ ),
1852
+ );
1853
+ }
1854
+ console.log(
1855
+ chalk.gray(
1856
+ " To skip review for first-time captures, re-run with `reshot publish --auto-approve`",
1857
+ ),
1858
+ );
1859
+ console.log(
1860
+ chalk.gray(" or approve them in the studio link above."),
1861
+ );
1862
+ } else {
1863
+ console.log(
1864
+ chalk.cyan(
1865
+ `\n ℹ No new captures to review — all ${successCount} published visual${successCount === 1 ? "" : "s"} were unchanged and stayed live.`,
1866
+ ),
1867
+ );
1868
+ }
1827
1869
  }
1828
1870
 
1829
1871
  // Helpful guidance about diff percentages
@@ -109,6 +109,23 @@ function normalizeAssetMap(assets) {
109
109
  return { assets: normalized, repairs };
110
110
  }
111
111
 
112
+ /**
113
+ * Pick a real {group, visualKey, context} from a normalized asset map so the
114
+ * printed usage example is copy-pasteable for THIS project (prefers the
115
+ * "default" context). Returns null if the map is empty.
116
+ */
117
+ function deriveUsageSample(assets) {
118
+ for (const group of Object.keys(assets || {})) {
119
+ for (const visualKey of Object.keys(assets[group] || {})) {
120
+ const contexts = Object.keys(assets[group][visualKey] || {});
121
+ if (contexts.length === 0) continue;
122
+ const context = contexts.includes("default") ? "default" : contexts[0];
123
+ return { group, visualKey, context };
124
+ }
125
+ }
126
+ return null;
127
+ }
128
+
112
129
  function validateHostedAssetUrl(urlString) {
113
130
  if (!urlString || typeof urlString !== "string") {
114
131
  return;
@@ -456,6 +473,13 @@ async function pullCommand(options = {}) {
456
473
  console.log(chalk.green(`✓ Generated ${contentType} file: ${outputPath}`));
457
474
  console.log(chalk.gray(` ${assetCount} visuals exported\n`));
458
475
 
476
+ // Derive a REAL key path from the pulled map so the printed usage example
477
+ // is copy-pasteable (was hardcoded to a non-existent `dashboard.mainView`).
478
+ const sample = deriveUsageSample(normalizedAssets);
479
+ const sGroup = sample?.group ?? "dashboard";
480
+ const sVisual = sample?.visualKey ?? "mainView";
481
+ const sCtx = sample?.context ?? "default";
482
+
459
483
  // Show format-specific usage instructions
460
484
  if (format === "ts") {
461
485
  console.log(chalk.blue("━━━ TypeScript Usage ━━━\n"));
@@ -463,10 +487,9 @@ async function pullCommand(options = {}) {
463
487
  console.log(chalk.cyan(' import { ReshotVisuals } from "./visuals";\n'));
464
488
  console.log(chalk.white("Use with full autocomplete support:\n"));
465
489
  console.log(chalk.cyan(" // Single context (flattened)"));
466
- console.log(chalk.cyan(" <img src={ReshotVisuals.dashboard.mainView} />\n"));
490
+ console.log(chalk.cyan(` <img src={ReshotVisuals.${sGroup}.${sVisual}} />\n`));
467
491
  console.log(chalk.cyan(" // Multiple contexts (dark mode, locales)"));
468
- console.log(chalk.cyan(" <img src={ReshotVisuals.dashboard.mainView.default} />"));
469
- console.log(chalk.cyan(" <img src={ReshotVisuals.dashboard.mainView.dark} />\n"));
492
+ console.log(chalk.cyan(` <img src={ReshotVisuals.${sGroup}.${sVisual}.${sCtx}} />\n`));
470
493
  if (!full) {
471
494
  console.log(chalk.gray(" Tip: Use --full flag to include width, height, and alt text\n"));
472
495
  }
@@ -476,10 +499,10 @@ async function pullCommand(options = {}) {
476
499
  console.log(chalk.cyan(' import assets from "./reshot-assets.json";\n'));
477
500
  console.log(chalk.white("Access assets with dot notation:\n"));
478
501
  console.log(chalk.cyan(" <img"));
479
- console.log(chalk.cyan(" src={assets.assets.dashboard.mainView.default.src}"));
480
- console.log(chalk.cyan(" width={assets.assets.dashboard.mainView.default.width}"));
481
- console.log(chalk.cyan(" height={assets.assets.dashboard.mainView.default.height}"));
482
- console.log(chalk.cyan(" alt={assets.assets.dashboard.mainView.default.alt}"));
502
+ console.log(chalk.cyan(` src={assets.assets.${sGroup}.${sVisual}.${sCtx}.src}`));
503
+ console.log(chalk.cyan(` width={assets.assets.${sGroup}.${sVisual}.${sCtx}.width}`));
504
+ console.log(chalk.cyan(` height={assets.assets.${sGroup}.${sVisual}.${sCtx}.height}`));
505
+ console.log(chalk.cyan(` alt={assets.assets.${sGroup}.${sVisual}.${sCtx}.alt}`));
483
506
  console.log(chalk.cyan(" />\n"));
484
507
  } else if (format === "csv") {
485
508
  console.log(chalk.blue("━━━ CSV Usage ━━━\n"));
package/src/index.js CHANGED
@@ -159,6 +159,7 @@ program
159
159
  .command("run [target]")
160
160
  .description("Execute visual capture scenarios from config")
161
161
  .option("-s, --scenarios <keys>", "Comma-separated list of scenario keys")
162
+ .option("--scenario <keys>", "Alias for --scenarios")
162
163
  .option("--no-headless", "Run browser in visible mode")
163
164
  .option("--variant <json>", "Override variant configuration as JSON")
164
165
  .option("--all-variants", "Run all configured variant combinations")
@@ -431,7 +432,24 @@ program
431
432
  });
432
433
 
433
434
  // Doctor: Validate target contract and readiness
434
- const doctor = program.command("doctor").description("Validate target configuration and readiness");
435
+ const doctor = program
436
+ .command("doctor")
437
+ .description("Validate target configuration and readiness")
438
+ .option("-s, --scenarios <keys>", "Comma-separated list of scenario keys")
439
+ .option("--timeout <ms>", "Per-step timeout in milliseconds (default 15000)")
440
+ .option("--json", "Output JSON report")
441
+ .action(async (options) => {
442
+ // Bare `reshot doctor` runs the target readiness audit by default instead
443
+ // of just printing help (audit run-10 F5). Use `doctor release` for the
444
+ // full release gate.
445
+ try {
446
+ const doctorTargetCommand = require("./commands/doctor-target");
447
+ await doctorTargetCommand(options || {});
448
+ } catch (error) {
449
+ console.error(chalk.red("Error:"), error.message);
450
+ process.exit(1);
451
+ }
452
+ });
435
453
 
436
454
  doctor
437
455
  .command("target")
@@ -640,8 +658,11 @@ function resolveRecordClipScenarioKeys(target, options = {}) {
640
658
  }
641
659
 
642
660
  function resolveScenarioKeysFromTarget(target, options = {}) {
643
- if (options.scenarios) {
644
- return options.scenarios.split(",").map((value) => value.trim()).filter(Boolean);
661
+ // Accept the singular `--scenario` as an alias for `--scenarios` — an easy
662
+ // trap that otherwise hard-fails the run (audit run-10 F2).
663
+ const scenarioList = options.scenarios || options.scenario;
664
+ if (scenarioList) {
665
+ return scenarioList.split(",").map((value) => value.trim()).filter(Boolean);
645
666
  }
646
667
 
647
668
  if (!target) {