heroshot 0.0.2-alpha.1 → 0.0.2-alpha.3

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 ADDED
@@ -0,0 +1,29 @@
1
+ <p align="center">
2
+ <img src="assets/icon-128.svg" alt="heroshot logo" width="128" height="128">
3
+ </p>
4
+
5
+ <h1 align="center">heroshot</h1>
6
+
7
+ <p align="center">
8
+ <strong>Keep your product screenshots always up to date.</strong><br>
9
+ No more stale images in docs, landing pages, or blog posts.
10
+ </p>
11
+
12
+ **How it works:**
13
+
14
+ 1. Run `npx heroshot` - opens a browser with a visual picker
15
+ 2. Click on elements you want to screenshot
16
+ 3. Close browser - screenshots are captured automatically
17
+
18
+ **Why heroshot?**
19
+
20
+ - **Visual picker** - Point and click to select elements, no DevTools needed
21
+ - **Zero config** - No YAML to write, config is auto-generated
22
+ - **Element-precise** - Capture specific UI components, not just full pages
23
+ - **One command** - Regenerate all screenshots anytime your UI changes
24
+
25
+ ---
26
+
27
+ [![npm](https://img.shields.io/npm/v/heroshot)](https://www.npmjs.com/package/heroshot)
28
+
29
+ **Status:** Early alpha. [See releases](https://github.com/omachala/heroshot/releases) for current version.
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ import { chromium } from "playwright";
14
14
  import { existsSync, readFileSync, writeFileSync } from "fs";
15
15
  import path from "path";
16
16
 
17
- // src/config.ts
17
+ // src/schema.ts
18
18
  import { z } from "zod";
19
19
  function generateUid() {
20
20
  return Math.random().toString(36).slice(2, 10);
@@ -23,41 +23,47 @@ var viewportSchema = z.object({
23
23
  width: z.number().int().positive().default(1280),
24
24
  height: z.number().int().positive().default(800)
25
25
  });
26
- var beautifySchema = z.object({
27
- shadow: z.boolean().default(false),
28
- radius: z.number().int().nonnegative().default(0),
29
- background: z.string().default("transparent"),
30
- padding: z.number().int().nonnegative().default(0)
31
- });
32
26
  var colorSchemeSchema = z.enum(["light", "dark", "both"]);
27
+ var outputFormatSchema = z.enum(["png", "jpeg"]).default("png");
28
+ var paddingSchema = z.object({
29
+ top: z.number().int().min(0).default(0),
30
+ right: z.number().int().min(0).default(0),
31
+ bottom: z.number().int().min(0).default(0),
32
+ left: z.number().int().min(0).default(0)
33
+ });
34
+ var scrollPositionSchema = z.object({
35
+ x: z.number().int().min(0).default(0),
36
+ y: z.number().int().min(0).default(0)
37
+ });
33
38
  var screenshotSchema = z.object({
34
39
  id: z.string().min(1).default(generateUid),
35
40
  name: z.string().min(1),
36
41
  url: z.url(),
37
42
  filename: z.string().min(1),
38
43
  selector: z.string().optional(),
39
- viewport: viewportSchema.optional(),
40
- waitFor: z.string().optional(),
41
- delay: z.number().int().nonnegative().optional(),
42
- colorScheme: colorSchemeSchema.optional(),
43
- beautify: beautifySchema.partial().optional()
44
+ /** Padding to expand capture area beyond element bounds */
45
+ padding: paddingSchema.optional(),
46
+ /** Scroll position to restore when capturing */
47
+ scroll: scrollPositionSchema.optional()
44
48
  });
45
49
  var browserSchema = z.object({
46
50
  viewport: viewportSchema.optional(),
47
- colorScheme: colorSchemeSchema.optional(),
48
- cdp: z.url().optional()
49
- // Connect to existing Chrome
51
+ colorScheme: colorSchemeSchema.optional()
50
52
  });
51
53
  var configSchema = z.object({
52
- // Output directory for screenshots (relative to config file)
54
+ /** Output directory for screenshots (relative to config file) */
53
55
  outputDirectory: z.string().default("."),
54
- // Default browser settings
56
+ /** Output format for screenshots (png or jpeg) */
57
+ outputFormat: outputFormatSchema.optional(),
58
+ /** JPEG quality (1-100), only used when outputFormat is 'jpeg' */
59
+ jpegQuality: z.number().int().min(1).max(100).default(80),
60
+ /** Browser settings (viewport, colorScheme) */
55
61
  browser: browserSchema.optional(),
56
- // Default beautification
57
- beautify: beautifySchema.partial().optional(),
58
- // Screenshot definitions
62
+ /** Screenshot definitions */
59
63
  screenshots: z.array(screenshotSchema).default([])
60
64
  });
65
+
66
+ // src/config.ts
61
67
  function parseConfig(input) {
62
68
  return configSchema.parse(input);
63
69
  }
@@ -80,6 +86,23 @@ function saveConfig(configPath, config) {
80
86
  writeFileSync(configPath, content, "utf8");
81
87
  }
82
88
 
89
+ // src/logger.ts
90
+ var verboseEnabled = false;
91
+ function setVerbose(enabled) {
92
+ verboseEnabled = enabled;
93
+ }
94
+ function log(message) {
95
+ console.log(message);
96
+ }
97
+ log.verbose = (message) => {
98
+ if (verboseEnabled) {
99
+ console.log(message);
100
+ }
101
+ };
102
+ log.error = (message) => {
103
+ console.error(message);
104
+ };
105
+
83
106
  // src/browser.ts
84
107
  var PROFILE_DIR = path2.join(homedir(), ".heroshot", "browser-profile");
85
108
  var TOOLBAR_DIR = path2.join(import.meta.dirname, "..", "toolbar");
@@ -121,7 +144,8 @@ async function launchPersistentBrowser(options = {}) {
121
144
  throw new Error(message);
122
145
  }
123
146
  var exposedPages = /* @__PURE__ */ new WeakSet();
124
- async function injectToolbar(page, initialScreenshots, pendingJob, onEvent) {
147
+ async function injectToolbar(page, options) {
148
+ const { screenshots, pendingJob, selectedId, sidebarVisible, onEvent } = options;
125
149
  if (!exposedPages.has(page)) {
126
150
  await page.exposeFunction("__heroshotEmit", (eventJson) => {
127
151
  const event = JSON.parse(eventJson);
@@ -129,8 +153,9 @@ async function injectToolbar(page, initialScreenshots, pendingJob, onEvent) {
129
153
  });
130
154
  exposedPages.add(page);
131
155
  }
132
- const screenshotsJson = JSON.stringify(initialScreenshots);
156
+ const screenshotsJson = JSON.stringify(screenshots);
133
157
  const pendingJobJson = JSON.stringify(pendingJob);
158
+ const selectedIdJson = JSON.stringify(selectedId);
134
159
  const alreadyInitialized = await page.evaluate("globalThis.__heroshot?.initialized === true");
135
160
  if (alreadyInitialized) {
136
161
  await page.evaluate(`
@@ -145,6 +170,8 @@ async function injectToolbar(page, initialScreenshots, pendingJob, onEvent) {
145
170
  initialized: false,
146
171
  screenshots: ${screenshotsJson},
147
172
  pendingJob: ${pendingJobJson},
173
+ selectedId: ${selectedIdJson},
174
+ sidebarVisible: ${sidebarVisible},
148
175
  emit: function(event) {
149
176
  globalThis.__heroshotEmit(JSON.stringify(event));
150
177
  },
@@ -154,54 +181,64 @@ async function injectToolbar(page, initialScreenshots, pendingJob, onEvent) {
154
181
  const script = readFileSync2(scriptPath, "utf8");
155
182
  await page.addScriptTag({ content: script });
156
183
  }
157
- async function captureUrl(url, output) {
158
- console.log(`Capturing: ${url}`);
159
- const context = await launchPersistentBrowser({ headless: true });
160
- const page = await context.newPage();
161
- await page.goto(url, { waitUntil: "networkidle" });
162
- await page.screenshot({ path: output, fullPage: false });
163
- await context.close();
164
- console.log(`Saved: ${output}`);
165
- }
166
184
  async function setup() {
167
- console.log("Opening browser for setup...");
168
- console.log(`Profile will be saved to: ${PROFILE_DIR}`);
169
- console.log("");
185
+ log.verbose("Opening browser...");
186
+ log.verbose(`Profile: ${PROFILE_DIR}`);
170
187
  const configPath = getConfigPath();
171
188
  const config = loadConfig(configPath);
172
189
  const viewport = config.browser?.viewport ?? DEFAULT_VIEWPORT;
173
- const allScreenshots = config.screenshots.map((screenshot) => ({
190
+ const allScreenshots = config.screenshots.map((screenshot, index) => ({
174
191
  id: screenshot.id,
175
192
  name: screenshot.name,
176
193
  url: screenshot.url,
177
- selector: screenshot.selector ?? ""
194
+ selector: screenshot.selector ?? "",
195
+ // Use index as fallback createdAt for existing items (older items first)
196
+ createdAt: index,
197
+ ...screenshot.padding && { padding: screenshot.padding },
198
+ ...screenshot.scroll && { scroll: screenshot.scroll }
178
199
  }));
179
200
  const newlyAddedIds = /* @__PURE__ */ new Set();
180
201
  let pendingJob = null;
202
+ let selectedId = null;
203
+ let sidebarVisible = false;
181
204
  const context = await launchPersistentBrowser({ headless: false, viewport });
182
205
  const handleEvent = (event) => {
183
206
  switch (event.type) {
184
207
  case "screenshot-added": {
185
208
  allScreenshots.push(event.data);
186
209
  newlyAddedIds.add(event.data.id);
187
- console.log(`
188
- Added: ${event.data.name} (${event.data.selector})`);
189
- console.log(`URL: ${event.data.url}`);
210
+ log.verbose(`Added: ${event.data.name}`);
211
+ break;
212
+ }
213
+ case "screenshot-updated": {
214
+ const index = allScreenshots.findIndex((item) => item.id === event.data.id);
215
+ if (index !== -1) {
216
+ allScreenshots[index] = event.data;
217
+ newlyAddedIds.add(event.data.id);
218
+ log.verbose(`Renamed: ${event.data.name}`);
219
+ }
190
220
  break;
191
221
  }
192
222
  case "screenshot-selected": {
193
223
  const [currentPage] = context.pages();
194
224
  if (!currentPage) break;
225
+ selectedId = event.id;
226
+ sidebarVisible = true;
195
227
  const currentUrl = currentPage.url();
196
228
  if (currentUrl === event.url) {
197
- pendingJob = { type: "highlight", selector: event.selector };
229
+ pendingJob = { type: "highlight", selector: event.selector, screenshotId: event.id };
198
230
  void currentPage.evaluate(`
199
231
  window.dispatchEvent(new CustomEvent('heroshot-job', {
200
- detail: { type: 'highlight', selector: ${JSON.stringify(event.selector)} }
232
+ detail: { type: 'highlight', selector: ${JSON.stringify(event.selector)}, screenshotId: ${JSON.stringify(event.id)} }
201
233
  }));
202
234
  `);
203
235
  } else {
204
- pendingJob = { type: "navigate-and-highlight", url: event.url, selector: event.selector };
236
+ pendingJob = {
237
+ type: "navigate-and-highlight",
238
+ url: event.url,
239
+ selector: event.selector,
240
+ screenshotId: event.id
241
+ };
205
242
  void currentPage.goto(event.url, { waitUntil: "domcontentloaded" });
206
243
  }
207
244
  break;
@@ -221,7 +258,13 @@ Added: ${event.data.name} (${event.data.selector})`);
221
258
  const url = page2.url();
222
259
  if (!url.startsWith("http")) return;
223
260
  try {
224
- await injectToolbar(page2, allScreenshots, pendingJob, handleEvent);
261
+ await injectToolbar(page2, {
262
+ screenshots: allScreenshots,
263
+ pendingJob,
264
+ selectedId,
265
+ sidebarVisible,
266
+ onEvent: handleEvent
267
+ });
225
268
  } catch {
226
269
  }
227
270
  });
@@ -232,18 +275,13 @@ Added: ${event.data.name} (${event.data.selector})`);
232
275
  const existingPages = context.pages();
233
276
  const page = existingPages[0] ?? await context.newPage();
234
277
  setupPage(page);
235
- console.log("Browser is open.");
236
- console.log("1. Navigate to any site (e.g., https://example.com)");
237
- console.log("2. Click the picker icon in the toolbar to select elements");
238
- console.log("3. Click Done or close the browser when finished");
239
- console.log("");
278
+ await page.goto("https://heroshot.sh/welcome", { waitUntil: "domcontentloaded" });
279
+ log("Pick elements to screenshot. Close browser or click Done when finished.");
240
280
  await new Promise((resolve) => {
241
281
  context.once("close", () => resolve());
242
282
  });
243
- console.log("");
244
283
  if (newlyAddedIds.size > 0) {
245
284
  const latestConfig = loadConfig(configPath);
246
- console.log("Saved screenshots:");
247
285
  for (const element of allScreenshots) {
248
286
  if (!newlyAddedIds.has(element.id)) continue;
249
287
  const filename = element.name.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/(?:^-|-$)/g, "") + ".png";
@@ -252,22 +290,23 @@ Added: ${event.data.name} (${event.data.selector})`);
252
290
  name: element.name,
253
291
  url: element.url,
254
292
  selector: element.selector,
255
- filename
293
+ filename,
294
+ ...element.padding && { padding: element.padding },
295
+ ...element.scroll && { scroll: element.scroll }
256
296
  };
257
297
  const existingIndex = latestConfig.screenshots.findIndex((item) => item.id === element.id);
258
298
  if (existingIndex === -1) {
259
299
  latestConfig.screenshots.push(screenshot);
260
- console.log(` + ${element.name}: ${element.selector}`);
300
+ log.verbose(`+ ${element.name}`);
261
301
  } else {
262
302
  latestConfig.screenshots[existingIndex] = screenshot;
263
- console.log(` ~ ${element.name}: ${element.selector} (updated)`);
303
+ log.verbose(`~ ${element.name} (updated)`);
264
304
  }
265
305
  }
266
306
  saveConfig(configPath, latestConfig);
267
- console.log(`
268
- Config saved to: ${configPath}`);
307
+ log.verbose(`Config saved: ${configPath}`);
269
308
  }
270
- console.log("You can now use `heroshot sync` to capture screenshots.");
309
+ return { hasScreenshots: allScreenshots.length > 0 };
271
310
  }
272
311
 
273
312
  // src/sync.ts
@@ -308,60 +347,116 @@ async function findElement(page, selector, maxAttempts = 10, intervalMs = 500) {
308
347
  }
309
348
  return null;
310
349
  }
311
- async function captureScreenshot(page, screenshot, outputDirectory) {
312
- const { name, url, selector, filename } = screenshot;
313
- console.log(` ${name}...`);
350
+ function addFilenameSuffix(filename, suffix) {
351
+ const extension = path3.extname(filename);
352
+ const base = path3.basename(filename, extension);
353
+ const directory = path3.dirname(filename);
354
+ return path3.join(directory, `${base}${suffix}${extension}`);
355
+ }
356
+ async function takeScreenshot(target, outputPath, format, quality, clip) {
357
+ const isPage = "goto" in target;
358
+ if (format === "jpeg") {
359
+ if (isPage && clip) {
360
+ await target.screenshot({ path: outputPath, type: "jpeg", quality, clip });
361
+ } else if (isPage) {
362
+ await target.screenshot({ path: outputPath, type: "jpeg", quality, fullPage: false });
363
+ } else {
364
+ await target.screenshot({ path: outputPath, type: "jpeg", quality });
365
+ }
366
+ } else if (isPage && clip) {
367
+ await target.screenshot({ path: outputPath, type: "png", clip });
368
+ } else if (isPage) {
369
+ await target.screenshot({ path: outputPath, type: "png", fullPage: false });
370
+ } else {
371
+ await target.screenshot({ path: outputPath, type: "png" });
372
+ }
373
+ }
374
+ async function captureScreenshot(page, screenshot, outputDirectory, captureOptions, filenameSuffix = "") {
375
+ const { name, url, selector, filename, padding, scroll } = screenshot;
376
+ const { format, quality } = captureOptions;
377
+ const finalFilename = filenameSuffix ? addFilenameSuffix(filename, filenameSuffix) : filename;
378
+ log.verbose(` ${name}${filenameSuffix}...`);
314
379
  try {
315
- await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
380
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
316
381
  } catch (error) {
317
382
  const message = error instanceof Error ? error.message : String(error);
318
383
  return { success: false, error: `Failed to navigate: ${message}` };
319
384
  }
320
- await page.waitForTimeout(1e3);
321
- const outputPath = path3.join(outputDirectory, filename);
385
+ await page.waitForTimeout(2e3);
386
+ if (scroll) {
387
+ await page.evaluate(`window.scrollTo(${scroll.x}, ${scroll.y})`);
388
+ await page.waitForTimeout(100);
389
+ }
390
+ const outputPath = path3.join(outputDirectory, finalFilename);
322
391
  const outputDirectoryPath = path3.dirname(outputPath);
323
392
  if (!existsSync2(outputDirectoryPath)) {
324
393
  mkdirSync(outputDirectoryPath, { recursive: true });
325
394
  }
326
- if (selector) {
327
- const element = await findElement(page, selector);
328
- if (!element) {
329
- return {
330
- success: false,
331
- error: `Element not found: ${selector}`
332
- };
333
- }
334
- try {
335
- await element.screenshot({ path: outputPath });
336
- } catch (error) {
337
- const message = error instanceof Error ? error.message : String(error);
338
- return { success: false, error: `Screenshot failed: ${message}` };
339
- }
340
- } else {
341
- try {
342
- await page.screenshot({ path: outputPath, fullPage: false });
343
- } catch (error) {
344
- const message = error instanceof Error ? error.message : String(error);
345
- return { success: false, error: `Screenshot failed: ${message}` };
395
+ try {
396
+ if (selector) {
397
+ const element = await findElement(page, selector);
398
+ if (!element) {
399
+ return { success: false, error: `Element not found: ${selector}` };
400
+ }
401
+ const hasPadding = padding && (padding.top > 0 || padding.right > 0 || padding.bottom > 0 || padding.left > 0);
402
+ if (hasPadding) {
403
+ const box = await element.boundingBox();
404
+ if (!box) {
405
+ return { success: false, error: "Could not get element bounding box" };
406
+ }
407
+ const clip = {
408
+ x: Math.max(0, box.x - padding.left),
409
+ y: Math.max(0, box.y - padding.top),
410
+ width: box.width + padding.left + padding.right,
411
+ height: box.height + padding.top + padding.bottom
412
+ };
413
+ await takeScreenshot(page, outputPath, format, quality, clip);
414
+ } else {
415
+ await takeScreenshot(element, outputPath, format, quality);
416
+ }
417
+ } else {
418
+ await takeScreenshot(page, outputPath, format, quality);
346
419
  }
420
+ } catch (error) {
421
+ const message = error instanceof Error ? error.message : String(error);
422
+ return { success: false, error: `Screenshot failed: ${message}` };
347
423
  }
348
424
  return { success: true };
349
425
  }
426
+ function getColorSchemes(setting) {
427
+ if (setting === "both") return ["light", "dark"];
428
+ if (setting) return [setting];
429
+ return [];
430
+ }
431
+ async function captureAndLog(page, screenshot, outputDirectory, captureOptions, suffix) {
432
+ const result = await captureScreenshot(page, screenshot, outputDirectory, captureOptions, suffix);
433
+ const filename = suffix ? addFilenameSuffix(screenshot.filename, suffix) : screenshot.filename;
434
+ if (result.success) {
435
+ log.verbose(` Saved: ${filename}`);
436
+ } else {
437
+ log.error(` ${screenshot.name}${suffix}: ${result.error ?? "Unknown error"}`);
438
+ }
439
+ return {
440
+ id: `${screenshot.id}${suffix}`,
441
+ name: `${screenshot.name}${suffix}`,
442
+ success: result.success,
443
+ error: result.error
444
+ };
445
+ }
350
446
  async function sync(options = {}) {
351
447
  const configPath = getConfigPath();
352
448
  const config = loadConfig(configPath);
353
449
  if (config.screenshots.length === 0) {
354
- console.log("No screenshots defined in config.");
450
+ log("No screenshots defined.");
355
451
  return { total: 0, success: 0, failed: 0, results: [] };
356
452
  }
357
453
  const { id: filterId } = options;
358
454
  const screenshots = filterId ? config.screenshots.filter((screenshot) => screenshot.id === filterId) : config.screenshots;
359
455
  if (filterId && screenshots.length === 0) {
360
- console.log(`No screenshot found with ID: ${filterId}`);
456
+ log(`No screenshot found with ID: ${filterId}`);
361
457
  return { total: 0, success: 0, failed: 0, results: [] };
362
458
  }
363
- console.log(`Syncing ${screenshots.length} screenshot(s)...`);
364
- console.log("");
459
+ log.verbose(`Syncing ${screenshots.length} screenshot(s)...`);
365
460
  const configDirectory = path3.dirname(configPath);
366
461
  const outputDirectory = path3.resolve(configDirectory, config.outputDirectory);
367
462
  const viewport = config.browser?.viewport ?? { width: 1280, height: 800 };
@@ -370,19 +465,30 @@ async function sync(options = {}) {
370
465
  viewport
371
466
  });
372
467
  const page = await context.newPage();
468
+ const colorSchemeSetting = config.browser?.colorScheme;
469
+ const schemes = getColorSchemes(colorSchemeSetting);
470
+ const captureOptions = {
471
+ format: config.outputFormat ?? "png",
472
+ quality: config.jpegQuality
473
+ };
373
474
  const results = [];
374
475
  for (const screenshot of screenshots) {
375
- const result = await captureScreenshot(page, screenshot, outputDirectory);
376
- results.push({
377
- id: screenshot.id,
378
- name: screenshot.name,
379
- success: result.success,
380
- error: result.error
381
- });
382
- if (result.success) {
383
- console.log(` Saved: ${screenshot.filename}`);
476
+ if (schemes.length === 0) {
477
+ const result = await captureAndLog(page, screenshot, outputDirectory, captureOptions, "");
478
+ results.push(result);
384
479
  } else {
385
- console.log(` Failed: ${result.error ?? "Unknown error"}`);
480
+ for (const scheme of schemes) {
481
+ await page.emulateMedia({ colorScheme: scheme });
482
+ const suffix = schemes.length > 1 ? `-${scheme}` : "";
483
+ const result = await captureAndLog(
484
+ page,
485
+ screenshot,
486
+ outputDirectory,
487
+ captureOptions,
488
+ suffix
489
+ );
490
+ results.push(result);
491
+ }
386
492
  }
387
493
  }
388
494
  await context.close();
@@ -390,8 +496,11 @@ async function sync(options = {}) {
390
496
  const successfulResults = results.filter(({ success }) => success);
391
497
  const { length: successCount } = successfulResults;
392
498
  const failedCount = totalCount - successCount;
393
- console.log("");
394
- console.log(`Done: ${successCount} succeeded, ${failedCount} failed`);
499
+ if (failedCount > 0) {
500
+ log(`Done: ${successCount}/${totalCount} screenshots (${failedCount} failed)`);
501
+ } else {
502
+ log(`Done: ${successCount} screenshot${successCount === 1 ? "" : "s"} captured`);
503
+ }
395
504
  return {
396
505
  total: totalCount,
397
506
  success: successCount,
@@ -402,30 +511,43 @@ async function sync(options = {}) {
402
511
 
403
512
  // src/cli.ts
404
513
  var program = new Command();
405
- program.name("heroshot").description("Define your screenshots once, update them forever with one command").version("0.0.2-alpha.1");
406
- program.command("setup", { isDefault: true }).description("Open browser to log in to sites you want to screenshot").option("--reset", "Clear existing browser profile and start fresh").action(async (options) => {
514
+ program.name("heroshot").description("Define your screenshots once, update them forever with one command").version("0.0.2-alpha.2").option("-v, --verbose", "Show detailed output").hook("preAction", () => {
515
+ const options = program.opts();
516
+ setVerbose(options.verbose ?? false);
517
+ });
518
+ program.command("run", { isDefault: true, hidden: true }).description("Run heroshot (setup if no config, otherwise sync)").action(async () => {
519
+ const configPath = getConfigPath();
520
+ if (existsSync3(configPath)) {
521
+ const result = await sync({});
522
+ if (result.failed > 0) {
523
+ process.exitCode = 1;
524
+ }
525
+ } else {
526
+ const { hasScreenshots } = await setup();
527
+ if (hasScreenshots) {
528
+ log("");
529
+ const result = await sync({});
530
+ if (result.failed > 0) {
531
+ process.exitCode = 1;
532
+ }
533
+ }
534
+ }
535
+ });
536
+ program.command("config").description("Open browser to add/edit screenshot definitions").option("--reset", "Clear existing browser profile and start fresh").option("--only", "Only run config, skip sync afterwards").action(async (options) => {
407
537
  if (options.reset) {
408
538
  const profilePath = getProfilePath();
409
539
  if (existsSync3(profilePath)) {
410
540
  rmSync(profilePath, { recursive: true });
411
- console.log("Browser profile cleared.");
541
+ log.verbose("Browser profile cleared.");
412
542
  }
413
543
  }
414
- await setup();
415
- });
416
- program.command("capture <url> <output>").description("Capture a screenshot of a URL").action(async (url, output) => {
417
- await captureUrl(url, output);
418
- });
419
- program.command("init").description("Create a heroshot.json config file").action(() => {
420
- console.log("TODO: Create heroshot.json");
421
- });
422
- program.command("sync").description("Capture all screenshots defined in config").option("--id <id>", "Only capture a specific screenshot by ID").action(async (options) => {
423
- const result = await sync(options);
424
- if (result.failed > 0) {
425
- process.exitCode = 1;
544
+ const { hasScreenshots } = await setup();
545
+ if (hasScreenshots && !options.only) {
546
+ log("");
547
+ const result = await sync({});
548
+ if (result.failed > 0) {
549
+ process.exitCode = 1;
550
+ }
426
551
  }
427
552
  });
428
- program.command("check").description("Check if screenshots are up-to-date (for CI)").action(() => {
429
- console.log("TODO: Check screenshots");
430
- });
431
553
  program.parse();
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "heroshot",
3
- "version": "0.0.2-alpha.1",
3
+ "version": "0.0.2-alpha.3",
4
4
  "description": "Define your screenshots once, update them forever with one command",
5
5
  "type": "module",
6
6
  "author": "Ondrej Machala",
7
- "license": "AGPL-3.0-or-later",
7
+ "license": "MIT",
8
8
  "bin": {
9
9
  "heroshot": "dist/cli.js"
10
10
  },
11
+ "files": [
12
+ "dist",
13
+ "toolbar/dist"
14
+ ],
11
15
  "packageManager": "pnpm@9.15.0",
12
16
  "scripts": {
13
17
  "dev": "tsx src/cli.ts",
@@ -17,6 +21,7 @@
17
21
  "test:run": "vitest run",
18
22
  "test:toolbar": "vitest run --config toolbar/vite.config.ts",
19
23
  "test:toolbar:coverage": "vitest run --config toolbar/vite.config.ts --coverage",
24
+ "test:toolbar:e2e": "playwright test --config toolbar/playwright.config.ts",
20
25
  "typecheck": "tsc --noEmit --incremental --tsBuildInfoFile node_modules/.cache/tsbuildinfo",
21
26
  "typecheck:toolbar": "tsc --noEmit -p toolbar/tsconfig.json --incremental --tsBuildInfoFile node_modules/.cache/tsbuildinfo-toolbar",
22
27
  "lint": "eslint --cache --cache-location node_modules/.cache/eslint src/",
@@ -43,7 +48,9 @@
43
48
  "devDependencies": {
44
49
  "@eslint/js": "^9.16.0",
45
50
  "@sveltejs/vite-plugin-svelte": "^6.2.4",
51
+ "@tailwindcss/vite": "^4.1.18",
46
52
  "@types/node": "^22.10.0",
53
+ "@vitest/browser": "^4.0.16",
47
54
  "@vitest/coverage-v8": "^4.0.16",
48
55
  "eslint": "^9.16.0",
49
56
  "eslint-config-prettier": "^9.1.0",
@@ -60,10 +67,12 @@
60
67
  "eslint-plugin-unused-imports": "^4.3.0",
61
68
  "jsdom": "^26.0.0",
62
69
  "knip": "^5.80.0",
70
+ "postcss": "^8.5.6",
63
71
  "prettier": "^3.7.4",
64
72
  "simple-git-hooks": "^2.11.1",
65
73
  "svelte": "^5.46.1",
66
74
  "svelte-eslint-parser": "^1.4.1",
75
+ "tailwindcss": "^4.1.18",
67
76
  "tsup": "^8.3.0",
68
77
  "tsx": "^4.19.0",
69
78
  "typescript": "^5.7.0",