heroshot 0.5.0 → 0.6.1

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
@@ -18,24 +18,22 @@ The manual fix is tedious: open browser, navigate, log in, screenshot, crop, sav
18
18
 
19
19
  **Heroshot fixes this.** Define your screenshots once - point and click, no CSS selectors - and regenerate them with one command whenever you need.
20
20
 
21
+ ```bash
22
+ npx heroshot
23
+ ```
24
+
25
+ First run opens a browser with a visual picker. Click what you want, name it, done. Screenshots land in `heroshots/`, config saves to `.heroshot/config.json`. Next run regenerates everything headlessly.
26
+
21
27
  <table align="center">
22
28
  <tr>
23
- <th></th>
24
- <th>Light</th>
25
- <th>Dark</th>
26
- </tr>
27
- <tr>
28
- <th>Desktop</th>
29
29
  <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-desktop-light.png?raw=true" alt="Desktop Light"></td>
30
30
  <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-desktop-dark.png?raw=true" alt="Desktop Dark"></td>
31
31
  </tr>
32
32
  <tr>
33
- <th>Tablet</th>
34
33
  <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-tablet-light.png?raw=true" alt="Tablet Light"></td>
35
34
  <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-tablet-dark.png?raw=true" alt="Tablet Dark"></td>
36
35
  </tr>
37
36
  <tr>
38
- <th>Mobile</th>
39
37
  <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-mobile-light.png?raw=true" alt="Mobile Light"></td>
40
38
  <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-mobile-dark.png?raw=true" alt="Mobile Dark"></td>
41
39
  </tr>
@@ -43,17 +41,9 @@ The manual fix is tedious: open browser, navigate, log in, screenshot, crop, sav
43
41
 
44
42
  <p align="center"><em>6 screenshots from one config entry - always in sync with the live site.</em></p>
45
43
 
46
- ## Get Started
47
-
48
- ```bash
49
- npx heroshot
50
- ```
51
-
52
- First run opens a browser with a visual picker. Click what you want, name it, done. Screenshots land in `heroshots/`, config saves to `.heroshot/config.json`. Next run regenerates everything headlessly.
53
-
54
44
  ## Use in Your Docs
55
45
 
56
- **VitePress**
46
+ **VitePress** · [Full guide](https://heroshot.sh/docs/integrations/vitepress)
57
47
 
58
48
  ```ts
59
49
  // .vitepress/config.ts
@@ -62,26 +52,26 @@ export default defineConfig({ vite: { plugins: [heroshot()] } });
62
52
  ```
63
53
 
64
54
  ```vue
55
+ <script setup>
56
+ import { Heroshot } from 'heroshot/vue';
57
+ </script>
58
+
65
59
  <Heroshot name="dashboard" alt="Dashboard" />
66
60
  ```
67
61
 
68
- **Docusaurus**
62
+ **Docusaurus** · [Full guide](https://heroshot.sh/docs/integrations/docusaurus)
69
63
 
70
64
  ```js
71
65
  // docusaurus.config.js
72
66
  plugins: [['heroshot/plugins/docusaurus', {}]];
73
67
  ```
74
68
 
75
- ```mdx
69
+ ```tsx
76
70
  import { Heroshot } from 'heroshot/docusaurus';
77
- <Heroshot name="dashboard" alt="Dashboard" />
71
+ <Heroshot name="dashboard" alt="Dashboard" />;
78
72
  ```
79
73
 
80
- **MkDocs**
81
-
82
- ```bash
83
- pip install heroshot
84
- ```
74
+ **MkDocs** · [Full guide](https://heroshot.sh/docs/integrations/mkdocs)
85
75
 
86
76
  ```yaml
87
77
  # mkdocs.yml
@@ -96,53 +86,21 @@ plugins:
96
86
 
97
87
  One component/macro, all variants - light/dark mode switches automatically, responsive sizes via srcset.
98
88
 
99
- ## Automated Updates
100
-
101
- Keep screenshots always current by running heroshot in CI. Quick setup:
102
-
103
- ```bash
104
- # Get your session key (for authenticated sites)
105
- npx heroshot session-key
106
-
107
- # Add to GitHub secrets
108
- gh secret set HEROSHOT_SESSION_KEY
109
- ```
110
-
111
- Then create `.github/workflows/heroshot.yml`:
112
-
113
- ```yaml
114
- name: Update Screenshots
115
-
116
- on:
117
- workflow_dispatch:
118
-
119
- jobs:
120
- screenshots:
121
- runs-on: ubuntu-latest
122
- steps:
123
- - uses: actions/checkout@v4
124
- - uses: actions/setup-node@v4
125
- with:
126
- node-version: 20
127
- - run: npx heroshot
128
- env:
129
- HEROSHOT_SESSION_KEY: ${{ secrets.HEROSHOT_SESSION_KEY }}
130
- - run: |
131
- git config user.name "github-actions[bot]"
132
- git config user.email "github-actions[bot]@users.noreply.github.com"
133
- git add heroshots/
134
- git diff --staged --quiet || git commit -m "chore: update screenshots" && git push
135
- ```
136
-
137
- Go to Actions → Update Screenshots → Run workflow. Done.
89
+ ## Learn More
138
90
 
139
- For more options (scheduled runs, PR creation, debugging), see the [full CI guide](https://heroshot.sh/docs/guide/automated-updates).
91
+ | | |
92
+ | ------------------- | --------------------------------------------------------------------- |
93
+ | **Documentation** | [heroshot.sh](https://heroshot.sh) |
94
+ | **Getting Started** | [Quick start guide](https://heroshot.sh/docs/getting-started) |
95
+ | **Configuration** | [Config options](https://heroshot.sh/docs/config) |
96
+ | **CI/CD Setup** | [Automated updates](https://heroshot.sh/docs/guide/automated-updates) |
97
+ | **CLI Reference** | [All commands & flags](https://heroshot.sh/docs/cli) |
140
98
 
141
- ## Learn More
99
+ ## Contributing
142
100
 
143
- **Docs:** [heroshot.sh](https://heroshot.sh)
101
+ This is a community project aiming to solve screenshot automation end-to-end and any feedback is valuable. Open an [issue](https://github.com/omachala/heroshot/issues) for bugs, questions, or feature requests. Pull requests are more than welcome.
144
102
 
145
- **Status:** Early alpha. [See releases](https://github.com/omachala/heroshot/releases) for current version.
103
+ If you like it, give the repo a ⭐
146
104
 
147
105
  ## License
148
106
 
package/dist/cli.js CHANGED
@@ -46,6 +46,8 @@ var scrollPositionSchema = z.object({
46
46
  x: z.number().int().min(0).default(0),
47
47
  y: z.number().int().min(0).default(0)
48
48
  });
49
+ var paddingFillSchema = z.enum(["inherit", "solid", "transparent"]);
50
+ var elementFillSchema = z.enum(["original", "solid", "transparent"]);
49
51
  var viewportVariantSchema = z.string().refine(
50
52
  (value) => {
51
53
  if (value in VIEWPORT_PRESETS) return true;
@@ -66,8 +68,10 @@ var screenshotSchema = z.object({
66
68
  padding: paddingSchema.optional(),
67
69
  /** Scroll position to restore when capturing */
68
70
  scroll: scrollPositionSchema.optional(),
69
- /** Fill padding area with detected background color */
70
- maskPadding: z.boolean().optional(),
71
+ /** Background fill mode for padding area */
72
+ paddingFill: paddingFillSchema.optional(),
73
+ /** Background fill mode for element area */
74
+ elementFill: elementFillSchema.optional(),
71
75
  /** Viewport variants - generates screenshot for each (e.g., ["desktop", "mobile", "400x500"]) */
72
76
  viewports: z.array(viewportVariantSchema).optional(),
73
77
  /** Text overrides - selector (relative to main element) -> replacement text */
@@ -108,6 +112,8 @@ var shotCliOptionsSchema = z.object({
108
112
  quality: z.number().int().min(1).max(100).optional(),
109
113
  /** Omit background for transparent PNG */
110
114
  omitBackground: z.boolean().optional(),
115
+ /** Capture only viewport instead of full page */
116
+ viewportOnly: z.boolean().optional(),
111
117
  /** Timeout in milliseconds */
112
118
  timeout: z.number().int().positive().optional()
113
119
  });
@@ -415,7 +421,8 @@ function toConfigScreenshot(data) {
415
421
  selector: data.selector,
416
422
  ...data.padding && { padding: data.padding },
417
423
  ...data.scroll && { scroll: data.scroll },
418
- ...data.maskPadding && { maskPadding: data.maskPadding },
424
+ ...data.paddingFill && { paddingFill: data.paddingFill },
425
+ ...data.elementFill && { elementFill: data.elementFill },
419
426
  ...data.textOverrides && Object.keys(data.textOverrides).length > 0 && { textOverrides: data.textOverrides }
420
427
  };
421
428
  }
@@ -502,7 +509,8 @@ async function setup(options = {}) {
502
509
  createdAt: index,
503
510
  ...screenshot.padding && { padding: screenshot.padding },
504
511
  ...screenshot.scroll && { scroll: screenshot.scroll },
505
- ...screenshot.maskPadding && { maskPadding: screenshot.maskPadding },
512
+ ...screenshot.paddingFill && { paddingFill: screenshot.paddingFill },
513
+ ...screenshot.elementFill && { elementFill: screenshot.elementFill },
506
514
  ...screenshot.textOverrides && { textOverrides: screenshot.textOverrides }
507
515
  }));
508
516
  let pendingJob = null;
@@ -784,6 +792,58 @@ async function injectPaddingMask(page, element, padding, bgColor) {
784
792
  async function removePaddingMask(page) {
785
793
  await page.evaluate(`document.querySelector('#heroshot-padding-mask')?.remove()`);
786
794
  }
795
+ async function applyElementBackground(page, selector, bgColor) {
796
+ await page.evaluate(`
797
+ (() => {
798
+ const selector = ${JSON.stringify(selector)};
799
+ const bgColor = ${JSON.stringify(bgColor)};
800
+
801
+ const parts = selector.split('>>>').map((p) => p.trim());
802
+ let current = document;
803
+
804
+ for (const part of parts) {
805
+ if (!part) continue;
806
+ const root = current instanceof Element ? (current.shadowRoot ?? current) : current;
807
+ const found = root.querySelector(part);
808
+ if (!found) return;
809
+ current = found;
810
+ }
811
+
812
+ if (!(current instanceof Element)) return;
813
+
814
+ // Store original background for restoration
815
+ current.dataset.heroshotOriginalBg = current.style.backgroundColor;
816
+ current.style.backgroundColor = bgColor;
817
+ })()
818
+ `);
819
+ }
820
+ async function restoreElementBackground(page, selector) {
821
+ await page.evaluate(`
822
+ (() => {
823
+ const selector = ${JSON.stringify(selector)};
824
+
825
+ const parts = selector.split('>>>').map((p) => p.trim());
826
+ let current = document;
827
+
828
+ for (const part of parts) {
829
+ if (!part) continue;
830
+ const root = current instanceof Element ? (current.shadowRoot ?? current) : current;
831
+ const found = root.querySelector(part);
832
+ if (!found) return;
833
+ current = found;
834
+ }
835
+
836
+ if (!(current instanceof Element)) return;
837
+
838
+ // Restore original background
839
+ const original = current.dataset.heroshotOriginalBg;
840
+ if (original !== undefined) {
841
+ current.style.backgroundColor = original;
842
+ delete current.dataset.heroshotOriginalBg;
843
+ }
844
+ })()
845
+ `);
846
+ }
787
847
  async function findElement(page, selector, maxAttempts = 10, intervalMs = 500) {
788
848
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
789
849
  const handle = await page.evaluateHandle(`
@@ -819,22 +879,23 @@ async function findElement(page, selector, maxAttempts = 10, intervalMs = 500) {
819
879
  }
820
880
  return null;
821
881
  }
822
- async function takeScreenshot(target, outputPath, format, quality, clip) {
882
+ async function takeScreenshot(options) {
883
+ const { target, outputPath, format, quality, clip, omitBackground, fullPage = true } = options;
823
884
  const isPage = "goto" in target;
824
885
  if (format === "jpeg") {
825
886
  if (isPage && clip) {
826
887
  await target.screenshot({ path: outputPath, type: "jpeg", quality, clip });
827
888
  } else if (isPage) {
828
- await target.screenshot({ path: outputPath, type: "jpeg", quality, fullPage: true });
889
+ await target.screenshot({ path: outputPath, type: "jpeg", quality, fullPage });
829
890
  } else {
830
891
  await target.screenshot({ path: outputPath, type: "jpeg", quality });
831
892
  }
832
893
  } else if (isPage && clip) {
833
- await target.screenshot({ path: outputPath, type: "png", clip });
894
+ await target.screenshot({ path: outputPath, type: "png", clip, omitBackground });
834
895
  } else if (isPage) {
835
- await target.screenshot({ path: outputPath, type: "png", fullPage: true });
896
+ await target.screenshot({ path: outputPath, type: "png", fullPage, omitBackground });
836
897
  } else {
837
- await target.screenshot({ path: outputPath, type: "png" });
898
+ await target.screenshot({ path: outputPath, type: "png", omitBackground });
838
899
  }
839
900
  }
840
901
  async function applyTextOverrides(page, selector, textOverrides) {
@@ -867,37 +928,60 @@ async function applyTextOverrides(page, selector, textOverrides) {
867
928
  `);
868
929
  await page.waitForTimeout(50);
869
930
  }
931
+ async function getElementBackgroundColor(page, selector) {
932
+ const bgColorResult = await page.evaluate(`
933
+ (() => {
934
+ const selector = ${JSON.stringify(selector)};
935
+ const parts = selector.split('>>>').map((p) => p.trim());
936
+ let current = document;
937
+
938
+ for (const part of parts) {
939
+ if (!part) continue;
940
+ const root = current instanceof Element ? (current.shadowRoot ?? current) : current;
941
+ const found = root.querySelector(part);
942
+ if (!found) return '#ffffff';
943
+ current = found;
944
+ }
945
+
946
+ if (!(current instanceof Element)) return '#ffffff';
947
+
948
+ const detectBg = ${GET_BACKGROUND_COLOR_SCRIPT};
949
+ return detectBg(current);
950
+ })()
951
+ `);
952
+ return typeof bgColorResult === "string" ? bgColorResult : "#ffffff";
953
+ }
870
954
  async function captureElementScreenshot(options) {
871
- const { page, element, selector, outputPath, format, quality, padding, maskPadding } = options;
955
+ const {
956
+ page,
957
+ element,
958
+ selector,
959
+ outputPath,
960
+ format,
961
+ quality,
962
+ padding,
963
+ paddingFill,
964
+ elementFill
965
+ } = options;
872
966
  const hasPadding = padding && (padding.top > 0 || padding.right > 0 || padding.bottom > 0 || padding.left > 0);
967
+ const needsTransparent = format === "png" && (paddingFill === "transparent" || elementFill === "transparent");
968
+ const needsBgColor = paddingFill === "solid" || elementFill === "solid";
969
+ let bgColor = "#ffffff";
970
+ if (needsBgColor) {
971
+ bgColor = await getElementBackgroundColor(page, selector);
972
+ verbose(`Detected background color: ${bgColor}`);
973
+ }
974
+ if (elementFill === "solid") {
975
+ await applyElementBackground(page, selector, bgColor);
976
+ } else if (elementFill === "transparent") {
977
+ await applyElementBackground(page, selector, "transparent");
978
+ }
873
979
  if (hasPadding) {
874
980
  const box = await element.boundingBox();
875
981
  if (!box) {
876
982
  return { success: false, error: "Could not get element bounding box" };
877
983
  }
878
- if (maskPadding) {
879
- const bgColorResult = await page.evaluate(`
880
- (() => {
881
- const selector = ${JSON.stringify(selector)};
882
- const parts = selector.split('>>>').map((p) => p.trim());
883
- let current = document;
884
-
885
- for (const part of parts) {
886
- if (!part) continue;
887
- const root = current instanceof Element ? (current.shadowRoot ?? current) : current;
888
- const found = root.querySelector(part);
889
- if (!found) return '#ffffff';
890
- current = found;
891
- }
892
-
893
- if (!(current instanceof Element)) return '#ffffff';
894
-
895
- const detectBg = ${GET_BACKGROUND_COLOR_SCRIPT};
896
- return detectBg(current);
897
- })()
898
- `);
899
- const bgColor = typeof bgColorResult === "string" ? bgColorResult : "#ffffff";
900
- verbose(`Background color: ${bgColor}`);
984
+ if (paddingFill === "solid") {
901
985
  await injectPaddingMask(page, element, padding, bgColor);
902
986
  }
903
987
  const clip = {
@@ -906,18 +990,34 @@ async function captureElementScreenshot(options) {
906
990
  width: box.width + padding.left + padding.right,
907
991
  height: box.height + padding.top + padding.bottom
908
992
  };
909
- await takeScreenshot(page, outputPath, format, quality, clip);
910
- if (maskPadding) {
993
+ await takeScreenshot({
994
+ target: page,
995
+ outputPath,
996
+ format,
997
+ quality,
998
+ clip,
999
+ omitBackground: needsTransparent
1000
+ });
1001
+ if (paddingFill === "solid") {
911
1002
  await removePaddingMask(page);
912
1003
  }
913
1004
  } else {
914
- await takeScreenshot(element, outputPath, format, quality);
1005
+ await takeScreenshot({
1006
+ target: element,
1007
+ outputPath,
1008
+ format,
1009
+ quality,
1010
+ omitBackground: needsTransparent
1011
+ });
1012
+ }
1013
+ if (elementFill === "solid" || elementFill === "transparent") {
1014
+ await restoreElementBackground(page, selector);
915
1015
  }
916
1016
  return { success: true };
917
1017
  }
918
1018
  async function captureScreenshot(page, screenshot, outputDirectory, captureOptions, variant = {}) {
919
- const { name, url, selector, padding, scroll, maskPadding, textOverrides } = screenshot;
920
- const { format, quality } = captureOptions;
1019
+ const { name, url, selector, padding, scroll, paddingFill, elementFill, textOverrides } = screenshot;
1020
+ const { format, quality, fullPage } = captureOptions;
921
1021
  const filename = generateScreenshotFilename({
922
1022
  name,
923
1023
  viewport: variant.viewportName,
@@ -926,12 +1026,27 @@ async function captureScreenshot(page, screenshot, outputDirectory, captureOptio
926
1026
  });
927
1027
  const suffix = [variant.viewportName, variant.colorScheme].filter(Boolean).join("-");
928
1028
  verbose(`Capturing: ${name}${suffix ? ` (${suffix})` : ""}`);
1029
+ if (variant.colorScheme) {
1030
+ await page.emulateMedia({ colorScheme: variant.colorScheme });
1031
+ }
929
1032
  try {
930
1033
  await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
931
1034
  } catch (error2) {
932
1035
  const message = error2 instanceof Error ? error2.message : String(error2);
933
1036
  return { success: false, error: `Failed to navigate: ${message}`, filename };
934
1037
  }
1038
+ if (variant.colorScheme) {
1039
+ await page.evaluate(`
1040
+ (() => {
1041
+ const isDark = ${variant.colorScheme === "dark"};
1042
+ if (isDark) {
1043
+ document.documentElement.classList.add('dark');
1044
+ } else {
1045
+ document.documentElement.classList.remove('dark');
1046
+ }
1047
+ })()
1048
+ `);
1049
+ }
935
1050
  await page.waitForTimeout(2e3);
936
1051
  if (scroll) {
937
1052
  await page.evaluate(`window.scrollTo(${scroll.x}, ${scroll.y})`);
@@ -959,13 +1074,14 @@ async function captureScreenshot(page, screenshot, outputDirectory, captureOptio
959
1074
  format,
960
1075
  quality,
961
1076
  padding,
962
- maskPadding
1077
+ paddingFill,
1078
+ elementFill
963
1079
  });
964
1080
  if (!captureResult.success) {
965
1081
  return { ...captureResult, filename };
966
1082
  }
967
1083
  } else {
968
- await takeScreenshot(page, outputPath, format, quality);
1084
+ await takeScreenshot({ target: page, outputPath, format, quality, fullPage });
969
1085
  }
970
1086
  } catch (error2) {
971
1087
  const message = error2 instanceof Error ? error2.message : String(error2);
@@ -1063,7 +1179,11 @@ function showResults(results, outputDirectory, staleFiles, deletedFiles) {
1063
1179
  } else if (staleFiles.length > 0) {
1064
1180
  parts.push(colors.dim(`${staleFiles.length} stale`));
1065
1181
  }
1066
- outro(parts.join(", ") + ` to ${colors.dim(outputDirectory + "/")}`);
1182
+ outro(parts.join(", "));
1183
+ for (const result of successfulResults) {
1184
+ const fullPath = path4.join(outputDirectory, result.filename);
1185
+ log(` ${colors.dim(fullPath)}`);
1186
+ }
1067
1187
  if (staleFiles.length > 0 && deletedFiles.length === 0) {
1068
1188
  warn(`Stale files found: ${staleFiles.join(", ")}`);
1069
1189
  verbose("Run with --clean to delete stale files");
@@ -1113,7 +1233,9 @@ async function sync(options = {}) {
1113
1233
  const schemes = getColorSchemes(colorSchemeSetting);
1114
1234
  const captureOptions = {
1115
1235
  format: config.outputFormat ?? "png",
1116
- quality: config.jpegQuality
1236
+ quality: config.jpegQuality,
1237
+ fullPage: !options.viewportOnly
1238
+ // Default true, false when --viewport-only
1117
1239
  };
1118
1240
  const defaultViewport = config.browser?.viewport ?? { width: 1280, height: 800 };
1119
1241
  const deviceScaleFactor = config.browser?.deviceScaleFactor;
@@ -1197,7 +1319,7 @@ async function sync(options = {}) {
1197
1319
  captureSpinner.stop("Screenshots captured");
1198
1320
  let staleFiles = [];
1199
1321
  let deletedFiles = [];
1200
- if (!filterPattern) {
1322
+ if (!filterPattern && !options.skipStaleCheck) {
1201
1323
  const existingFiles = getExistingFiles(outputDirectory);
1202
1324
  const writtenFiles = new Set(
1203
1325
  results.filter(({ success }) => success).map(({ filename }) => filename)
@@ -1263,10 +1385,11 @@ function buildScreenshotEntry(url, options) {
1263
1385
  }
1264
1386
  return screenshot;
1265
1387
  }
1266
- function getColorScheme(options) {
1388
+ function getColorScheme(options, bothVariants) {
1389
+ if (options?.dark && options?.light) return void 0;
1267
1390
  if (options?.dark) return "dark";
1268
1391
  if (options?.light) return "light";
1269
- return void 0;
1392
+ return bothVariants ? void 0 : "light";
1270
1393
  }
1271
1394
  function getDeviceScaleFactor(options, existingConfig) {
1272
1395
  if (options?.retina) return 2;
@@ -1295,7 +1418,8 @@ function buildShotConfig(url, options, existingConfig) {
1295
1418
  jpegQuality: options?.quality ?? existingConfig?.jpegQuality ?? 80,
1296
1419
  browser: {
1297
1420
  viewport: getViewport(options, existingConfig),
1298
- colorScheme: getColorScheme(options),
1421
+ colorScheme: getColorScheme(options, false),
1422
+ // false = oneshot mode, default to light-only
1299
1423
  deviceScaleFactor: getDeviceScaleFactor(options, existingConfig)
1300
1424
  },
1301
1425
  screenshots: [screenshot]
@@ -1326,7 +1450,10 @@ async function handleUrlCapture(url, options, configPath, sessionKey) {
1326
1450
  const result = await sync({
1327
1451
  config: shotConfig,
1328
1452
  outputDirectory,
1329
- sessionKey
1453
+ sessionKey,
1454
+ skipStaleCheck: true,
1455
+ // Don't check for stale files in oneshot mode
1456
+ viewportOnly: options?.viewportOnly
1330
1457
  });
1331
1458
  if (options?.save && result.failed === 0) {
1332
1459
  const screenshot = shotConfig.screenshots[0];
@@ -1352,7 +1479,7 @@ async function handleDefaultCommand(configPath, sessionKey, hasExplicitConfig, c
1352
1479
  }
1353
1480
  return true;
1354
1481
  }
1355
- program.command("shot [url]", { isDefault: true, hidden: true }).description("Take a screenshot (URL capture mode, or sync if no URL)").option("--selector <selector...>", "CSS selector(s) to capture").option("-o, --output <file>", "Output filename").option("-p, --padding <pixels>", "Padding around element", parseInt).option("-w, --width <pixels>", "Viewport width", parseInt).option("--height <pixels>", "Viewport height", parseInt).option("--mobile", "Use mobile viewport (375x667)").option("--tablet", "Use tablet viewport (768x1024)").option("--desktop", "Use desktop viewport (1280x800)").option("--dark", "Force dark color scheme").option("--light", "Force light color scheme").option("--scale <factor>", "Device scale factor (1, 2, 3)", parseInt).option("--retina", "Use retina scale (2x)").option("-q, --quality <percent>", "JPEG quality (1-100), outputs JPEG", parseInt).option("--omit-background", "Transparent background (PNG only)").option("--timeout <ms>", "Timeout in milliseconds", parseInt).option("--save", "Save screenshot definition to config").option("--clean", "Delete stale files in output directory").action(async (url, options) => {
1482
+ program.command("shot [url]", { isDefault: true }).description("Capture URL directly, or sync all screenshots from config").option("--selector <selector...>", "CSS selector(s) to capture").option("-o, --output <file>", "Output filename").option("-p, --padding <pixels>", "Padding around element", parseInt).option("-w, --width <pixels>", "Viewport width", parseInt).option("--height <pixels>", "Viewport height", parseInt).option("--mobile", "Use mobile viewport (375x667)").option("--tablet", "Use tablet viewport (768x1024)").option("--desktop", "Use desktop viewport (1280x800)").option("--dark", "Force dark color scheme").option("--light", "Force light color scheme").option("--scale <factor>", "Device scale factor (1, 2, 3)", parseInt).option("--retina", "Use retina scale (2x)").option("-q, --quality <percent>", "JPEG quality (1-100), outputs JPEG", parseInt).option("--omit-background", "Transparent background (PNG only)").option("--viewport-only", "Capture only viewport (not full page)").option("--timeout <ms>", "Timeout in milliseconds", parseInt).option("--save", "Save screenshot definition to config").option("--clean", "Delete stale files in output directory").action(async (url, options) => {
1356
1483
  const globalOptions = program.opts();
1357
1484
  const configPath = globalOptions.config ? path5.resolve(globalOptions.config) : getConfigPath();
1358
1485
  if (url?.startsWith("http")) {