simple-photo-gallery 2.0.17 → 2.1.0

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/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- var process3 = require('process');
4
+ var process4 = require('process');
5
5
  var commander = require('commander');
6
6
  var consola = require('consola');
7
7
  var child_process = require('child_process');
@@ -11,16 +11,19 @@ var buffer = require('buffer');
11
11
  var sharp2 = require('sharp');
12
12
  var blurhash = require('blurhash');
13
13
  var common = require('@simple-photo-gallery/common');
14
+ var theme = require('@simple-photo-gallery/common/theme');
14
15
  var ExifReader = require('exifreader');
15
16
  var ffprobe = require('node-ffprobe');
17
+ var url = require('url');
16
18
  var os = require('os');
17
19
  var Conf = require('conf');
18
20
  var axios = require('axios');
19
21
  var semverParser = require('semver-parser');
20
22
 
23
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
21
24
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
22
25
 
23
- var process3__default = /*#__PURE__*/_interopDefault(process3);
26
+ var process4__default = /*#__PURE__*/_interopDefault(process4);
24
27
  var fs8__default = /*#__PURE__*/_interopDefault(fs8);
25
28
  var path7__default = /*#__PURE__*/_interopDefault(path7);
26
29
  var sharp2__default = /*#__PURE__*/_interopDefault(sharp2);
@@ -31,7 +34,6 @@ var Conf__default = /*#__PURE__*/_interopDefault(Conf);
31
34
  var axios__default = /*#__PURE__*/_interopDefault(axios);
32
35
 
33
36
  // src/config/index.ts
34
- var DEFAULT_THUMBNAIL_SIZE = 300;
35
37
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".tiff", ".tif", ".svg", ".avif"]);
36
38
  var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".avi", ".mov", ".wmv", ".flv", ".webm", ".mkv", ".m4v", ".3gp"]);
37
39
  var HEADER_IMAGE_LANDSCAPE_WIDTHS = [3840, 2560, 1920, 1280, 960, 640];
@@ -60,7 +62,7 @@ async function cropAndResizeImage(image, outputPath, width, height, format = "av
60
62
  withoutEnlargement: true
61
63
  }).toFormat(format).toFile(outputPath);
62
64
  }
63
- async function createImageThumbnails(image, metadata, outputPath, outputPathRetina, size) {
65
+ async function createImageThumbnails(image, metadata, outputPath, outputPathRetina, size, sizeDimension = "auto") {
64
66
  const originalWidth = metadata.width || 0;
65
67
  const originalHeight = metadata.height || 0;
66
68
  if (originalWidth === 0 || originalHeight === 0) {
@@ -69,12 +71,20 @@ async function createImageThumbnails(image, metadata, outputPath, outputPathReti
69
71
  const aspectRatio = originalWidth / originalHeight;
70
72
  let width;
71
73
  let height;
72
- if (originalWidth > originalHeight) {
74
+ if (sizeDimension === "width") {
73
75
  width = size;
74
76
  height = Math.round(size / aspectRatio);
75
- } else {
77
+ } else if (sizeDimension === "height") {
76
78
  width = Math.round(size * aspectRatio);
77
79
  height = size;
80
+ } else {
81
+ if (originalWidth > originalHeight) {
82
+ width = size;
83
+ height = Math.round(size / aspectRatio);
84
+ } else {
85
+ width = Math.round(size * aspectRatio);
86
+ height = size;
87
+ }
78
88
  }
79
89
  await resizeImage(image, outputPath, width, height);
80
90
  await resizeImage(image, outputPathRetina, width * 2, height * 2);
@@ -90,6 +100,30 @@ async function generateBlurHash(imagePath, componentX = 4, componentY = 3) {
90
100
  }
91
101
 
92
102
  // src/modules/build/utils/index.ts
103
+ function wrapText(text, maxCharsPerLine) {
104
+ const words = text.split(" ");
105
+ const lines = [];
106
+ let currentLine = "";
107
+ for (const word of words) {
108
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
109
+ if (word.length > maxCharsPerLine) {
110
+ if (currentLine) {
111
+ lines.push(currentLine);
112
+ currentLine = "";
113
+ }
114
+ lines.push(word);
115
+ } else if (testLine.length > maxCharsPerLine && currentLine) {
116
+ lines.push(currentLine);
117
+ currentLine = word;
118
+ } else {
119
+ currentLine = testLine;
120
+ }
121
+ }
122
+ if (currentLine) {
123
+ lines.push(currentLine);
124
+ }
125
+ return lines;
126
+ }
93
127
  async function createGallerySocialMediaCardImage(headerPhotoPath, title, ouputPath, ui) {
94
128
  ui?.start(`Creating social media card image`);
95
129
  const headerBasename = path7__default.default.basename(headerPhotoPath, path7__default.default.extname(headerPhotoPath));
@@ -101,14 +135,38 @@ async function createGallerySocialMediaCardImage(headerPhotoPath, title, ouputPa
101
135
  const resizedImageBuffer = await image.resize(1200, 631, { fit: "cover" }).jpeg({ quality: 90 }).toBuffer();
102
136
  const outputPath = ouputPath;
103
137
  await sharp2__default.default(resizedImageBuffer).toFile(outputPath);
138
+ const CANVAS_WIDTH = 1200;
139
+ const CANVAS_HEIGHT = 631;
140
+ const FONT_SIZE = 72;
141
+ const MARGIN = 50;
142
+ const CHAR_WIDTH_RATIO = 0.6;
143
+ const usableWidth = CANVAS_WIDTH - 2 * MARGIN;
144
+ const maxCharsPerLine = Math.floor(usableWidth / (FONT_SIZE * CHAR_WIDTH_RATIO));
145
+ const lines = wrapText(title, maxCharsPerLine);
146
+ const lineHeight = FONT_SIZE * 1.2;
147
+ const totalTextHeight = FONT_SIZE + (lines.length - 1) * lineHeight;
148
+ const startY = CANVAS_HEIGHT - MARGIN - totalTextHeight + FONT_SIZE;
149
+ const leftX = MARGIN;
150
+ const tspanElements = lines.map((line, index) => {
151
+ const yPosition = startY + index * lineHeight;
152
+ const escapedLine = line.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
153
+ return `<tspan x="${leftX}" y="${yPosition}">${escapedLine}</tspan>`;
154
+ }).join("\n ");
104
155
  const svgText = `
105
- <svg width="1200" height="631" xmlns="http://www.w3.org/2000/svg">
156
+ <svg width="${CANVAS_WIDTH}" height="${CANVAS_HEIGHT}" xmlns="http://www.w3.org/2000/svg">
106
157
  <defs>
158
+ <linearGradient id="darkGradient" x1="0%" y1="0%" x2="0%" y2="100%">
159
+ <stop offset="0%" style="stop-color:rgb(0,0,0);stop-opacity:0" />
160
+ <stop offset="100%" style="stop-color:rgb(0,0,0);stop-opacity:0.65" />
161
+ </linearGradient>
107
162
  <style>
108
- .title { font-family: 'Arial, sans-serif'; font-size: 96px; font-weight: bold; fill: white; stroke: black; stroke-width: 5; paint-order: stroke; text-anchor: middle; }
163
+ .title { font-family: 'Arial, sans-serif'; font-size: ${FONT_SIZE}px; font-weight: bold; fill: white; text-anchor: start; }
109
164
  </style>
110
165
  </defs>
111
- <text x="600" y="250" class="title">${title}</text>
166
+ <rect x="0" y="0" width="${CANVAS_WIDTH}" height="${CANVAS_HEIGHT}" fill="url(#darkGradient)" />
167
+ <text x="${leftX}" class="title">
168
+ ${tspanElements}
169
+ </text>
112
170
  </svg>
113
171
  `;
114
172
  const finalImageBuffer = await sharp2__default.default(resizedImageBuffer).composite([{ input: buffer.Buffer.from(svgText), top: 0, left: 0 }]).jpeg({ quality: 90 }).toBuffer();
@@ -292,11 +350,15 @@ function migrateGalleryJson(deprecatedGalleryData, galleryJsonPath, ui) {
292
350
  filename: path7__default.default.basename(image.path)
293
351
  }))
294
352
  }));
353
+ const thumbnails2 = deprecatedGalleryData.thumbnailSize === void 0 ? void 0 : { size: deprecatedGalleryData.thumbnailSize };
295
354
  const galleryData = {
296
355
  ...deprecatedGalleryData,
356
+ thumbnailSize: void 0,
357
+ // Remove old field
297
358
  headerImage: path7__default.default.basename(deprecatedGalleryData.headerImage),
298
359
  sections,
299
- mediaBasePath
360
+ mediaBasePath,
361
+ thumbnails: thumbnails2
300
362
  };
301
363
  ui.debug("Backing up old gallery.json file");
302
364
  fs8__default.default.copyFileSync(galleryJsonPath, `${galleryJsonPath}.old`);
@@ -367,9 +429,18 @@ async function getGallerySettingsFromUser(galleryName, defaultImage, ui) {
367
429
  default: defaultImage,
368
430
  placeholder: defaultImage
369
431
  });
370
- return { title, description, url, headerImage };
432
+ return {
433
+ title,
434
+ description,
435
+ url,
436
+ headerImage,
437
+ thumbnails: {
438
+ size: "",
439
+ edge: void 0
440
+ }
441
+ };
371
442
  }
372
- async function createGalleryJson(mediaFiles, galleryJsonPath, scanPath, subGalleries = [], useDefaultSettings, ctaBanner, ui) {
443
+ async function createGalleryJson(mediaFiles, galleryJsonPath, scanPath, subGalleries = [], useDefaultSettings, ctaBanner, configOptions, ui) {
373
444
  const galleryDir = path7__default.default.dirname(galleryJsonPath);
374
445
  const isSameLocation = path7__default.default.relative(scanPath, path7__default.default.join(galleryDir, "..")) === "";
375
446
  const mediaBasePath = isSameLocation ? void 0 : scanPath;
@@ -377,12 +448,23 @@ async function createGalleryJson(mediaFiles, galleryJsonPath, scanPath, subGalle
377
448
  ...subGallery,
378
449
  headerImage: subGallery.headerImage ? path7__default.default.relative(galleryDir, subGallery.headerImage) : ""
379
450
  }));
451
+ const thumbnailsConfig = {};
452
+ if (configOptions.thumbnailSize !== void 0) {
453
+ thumbnailsConfig.size = configOptions.thumbnailSize;
454
+ }
455
+ if (configOptions.thumbnailEdge !== void 0) {
456
+ thumbnailsConfig.edge = configOptions.thumbnailEdge;
457
+ }
380
458
  let galleryData = {
381
459
  title: "My Gallery",
382
460
  description: "My gallery with fantastic photos.",
383
461
  headerImage: mediaFiles[0]?.filename || "",
384
462
  mediaBasePath,
385
463
  metadata: {},
464
+ // Include theme if provided via CLI
465
+ ...configOptions.theme && { theme: configOptions.theme },
466
+ // Include thumbnails if any values were set via CLI
467
+ ...Object.keys(thumbnailsConfig).length > 0 && { thumbnails: thumbnailsConfig },
386
468
  sections: [
387
469
  {
388
470
  images: mediaFiles
@@ -395,13 +477,15 @@ async function createGalleryJson(mediaFiles, galleryJsonPath, scanPath, subGalle
395
477
  ...ctaBanner !== void 0 && { ctaBanner }
396
478
  };
397
479
  if (!useDefaultSettings) {
480
+ const userSettings = await getGallerySettingsFromUser(
481
+ path7__default.default.basename(path7__default.default.join(galleryDir, "..")),
482
+ path7__default.default.basename(mediaFiles[0]?.filename || ""),
483
+ ui
484
+ );
485
+ const { thumbnails: thumbnails2, ...otherSettings } = userSettings;
398
486
  galleryData = {
399
487
  ...galleryData,
400
- ...await getGallerySettingsFromUser(
401
- path7__default.default.basename(path7__default.default.join(galleryDir, "..")),
402
- path7__default.default.basename(mediaFiles[0]?.filename || ""),
403
- ui
404
- )
488
+ ...otherSettings
405
489
  };
406
490
  }
407
491
  await fs8.promises.writeFile(galleryJsonPath, JSON.stringify(galleryData, null, 2));
@@ -416,7 +500,7 @@ async function galleryExists(outputPath) {
416
500
  return false;
417
501
  }
418
502
  }
419
- async function processDirectory(scanPath, outputPath, recursive, useDefaultSettings, force, ctaBanner, ui) {
503
+ async function processDirectory(scanPath, outputPath, recursive, useDefaultSettings, force, ctaBanner, configOptions, ui) {
420
504
  ui.start(`Scanning ${scanPath}`);
421
505
  let totalFiles = 0;
422
506
  let totalGalleries = 1;
@@ -432,6 +516,7 @@ async function processDirectory(scanPath, outputPath, recursive, useDefaultSetti
432
516
  useDefaultSettings,
433
517
  force,
434
518
  ctaBanner,
519
+ configOptions,
435
520
  ui
436
521
  );
437
522
  totalFiles += result2.totalFiles;
@@ -457,7 +542,16 @@ async function processDirectory(scanPath, outputPath, recursive, useDefaultSetti
457
542
  }
458
543
  try {
459
544
  await fs8.promises.mkdir(galleryPath, { recursive: true });
460
- await createGalleryJson(mediaFiles, galleryJsonPath, scanPath, subGalleries, useDefaultSettings, ctaBanner, ui);
545
+ await createGalleryJson(
546
+ mediaFiles,
547
+ galleryJsonPath,
548
+ scanPath,
549
+ subGalleries,
550
+ useDefaultSettings,
551
+ ctaBanner,
552
+ configOptions,
553
+ ui
554
+ );
461
555
  ui.success(
462
556
  `Create gallery with ${mediaFiles.length} files and ${subGalleries.length} subgalleries at: ${galleryJsonPath}`
463
557
  );
@@ -481,6 +575,11 @@ async function init(options, ui) {
481
575
  try {
482
576
  const scanPath = path7__default.default.resolve(options.photos);
483
577
  const outputPath = options.gallery ? path7__default.default.resolve(options.gallery) : scanPath;
578
+ const configOptions = {
579
+ theme: options.theme,
580
+ thumbnailSize: options.thumbnailSize,
581
+ thumbnailEdge: options.thumbnailEdge
582
+ };
484
583
  const result = await processDirectory(
485
584
  scanPath,
486
585
  outputPath,
@@ -488,6 +587,7 @@ async function init(options, ui) {
488
587
  options.default,
489
588
  options.force,
490
589
  options.ctaBanner,
590
+ configOptions,
491
591
  ui
492
592
  );
493
593
  ui.box(
@@ -537,9 +637,25 @@ async function getVideoDimensions(filePath) {
537
637
  }
538
638
  return dimensions;
539
639
  }
540
- async function createVideoThumbnails(inputPath, videoDimensions, outputPath, outputPathRetina, height, verbose = false) {
640
+ async function createVideoThumbnails(inputPath, videoDimensions, outputPath, outputPathRetina, size, sizeDimension = "auto", verbose = false) {
541
641
  const aspectRatio = videoDimensions.width / videoDimensions.height;
542
- const width = Math.round(height * aspectRatio);
642
+ let width;
643
+ let height;
644
+ if (sizeDimension === "width") {
645
+ width = size;
646
+ height = Math.round(size / aspectRatio);
647
+ } else if (sizeDimension === "height") {
648
+ width = Math.round(size * aspectRatio);
649
+ height = size;
650
+ } else {
651
+ if (videoDimensions.width > videoDimensions.height) {
652
+ width = size;
653
+ height = Math.round(size / aspectRatio);
654
+ } else {
655
+ width = Math.round(size * aspectRatio);
656
+ height = size;
657
+ }
658
+ }
543
659
  const tempFramePath = `${outputPath}.temp.png`;
544
660
  return new Promise((resolve, reject) => {
545
661
  const ffmpeg = child_process.spawn("ffmpeg", [
@@ -580,7 +696,7 @@ async function createVideoThumbnails(inputPath, videoDimensions, outputPath, out
580
696
  }
581
697
 
582
698
  // src/modules/thumbnails/index.ts
583
- async function processImage(imagePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, lastMediaTimestamp) {
699
+ async function processImage(imagePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, thumbnailSizeDimension = "auto", lastMediaTimestamp) {
584
700
  const fileMtime = await getFileMtime(imagePath);
585
701
  if (lastMediaTimestamp && fileMtime <= lastMediaTimestamp && fs8__default.default.existsSync(thumbnailPath)) {
586
702
  return void 0;
@@ -599,7 +715,8 @@ async function processImage(imagePath, thumbnailPath, thumbnailPathRetina, thumb
599
715
  metadata,
600
716
  thumbnailPath,
601
717
  thumbnailPathRetina,
602
- thumbnailSize
718
+ thumbnailSize,
719
+ thumbnailSizeDimension
603
720
  );
604
721
  const blurHash = await generateBlurHash(thumbnailPath);
605
722
  return {
@@ -618,7 +735,7 @@ async function processImage(imagePath, thumbnailPath, thumbnailPathRetina, thumb
618
735
  lastMediaTimestamp: fileMtime.toISOString()
619
736
  };
620
737
  }
621
- async function processVideo(videoPath, thumbnailPath, thumbnailPathRetina, thumbnailSize, verbose, lastMediaTimestamp) {
738
+ async function processVideo(videoPath, thumbnailPath, thumbnailPathRetina, thumbnailSize, thumbnailSizeDimension = "auto", verbose, lastMediaTimestamp) {
622
739
  const fileMtime = await getFileMtime(videoPath);
623
740
  if (lastMediaTimestamp && fileMtime <= lastMediaTimestamp && fs8__default.default.existsSync(thumbnailPath)) {
624
741
  return void 0;
@@ -630,6 +747,7 @@ async function processVideo(videoPath, thumbnailPath, thumbnailPathRetina, thumb
630
747
  thumbnailPath,
631
748
  thumbnailPathRetina,
632
749
  thumbnailSize,
750
+ thumbnailSizeDimension,
633
751
  verbose
634
752
  );
635
753
  const blurHash = await generateBlurHash(thumbnailPath);
@@ -649,7 +767,7 @@ async function processVideo(videoPath, thumbnailPath, thumbnailPathRetina, thumb
649
767
  lastMediaTimestamp: fileMtime.toISOString()
650
768
  };
651
769
  }
652
- async function processMediaFile(mediaFile, mediaBasePath, thumbnailsPath, thumbnailSize, ui) {
770
+ async function processMediaFile(mediaFile, mediaBasePath, thumbnailsPath, thumbnailConfig, ui) {
653
771
  try {
654
772
  const filePath = path7__default.default.resolve(path7__default.default.join(mediaBasePath, mediaFile.filename));
655
773
  const fileName = mediaFile.filename;
@@ -660,7 +778,22 @@ async function processMediaFile(mediaFile, mediaBasePath, thumbnailsPath, thumbn
660
778
  const lastMediaTimestamp = mediaFile.lastMediaTimestamp ? new Date(mediaFile.lastMediaTimestamp) : void 0;
661
779
  const verbose = ui.level === consola.LogLevels.debug;
662
780
  ui.debug(` Processing ${mediaFile.type}: ${fileName}`);
663
- const updatedMediaFile = await (mediaFile.type === "image" ? processImage(filePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, lastMediaTimestamp) : processVideo(filePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, verbose, lastMediaTimestamp));
781
+ const updatedMediaFile = await (mediaFile.type === "image" ? processImage(
782
+ filePath,
783
+ thumbnailPath,
784
+ thumbnailPathRetina,
785
+ thumbnailConfig.size,
786
+ thumbnailConfig.edge,
787
+ lastMediaTimestamp
788
+ ) : processVideo(
789
+ filePath,
790
+ thumbnailPath,
791
+ thumbnailPathRetina,
792
+ thumbnailConfig.size,
793
+ thumbnailConfig.edge,
794
+ verbose,
795
+ lastMediaTimestamp
796
+ ));
664
797
  if (!updatedMediaFile) {
665
798
  ui.debug(` Skipping ${fileName} because it has already been processed`);
666
799
  if (mediaFile.thumbnail && !mediaFile.thumbnail.blurHash && fs8__default.default.existsSync(thumbnailPath)) {
@@ -696,19 +829,30 @@ async function processMediaFile(mediaFile, mediaBasePath, thumbnailsPath, thumbn
696
829
  return { ...mediaFile, thumbnail: void 0 };
697
830
  }
698
831
  }
699
- async function processGalleryThumbnails(galleryDir, ui) {
832
+ async function processGalleryThumbnails(galleryDir, ui, cliThumbnailConfig) {
700
833
  const galleryJsonPath = path7__default.default.join(galleryDir, "gallery", "gallery.json");
701
834
  const thumbnailsPath = path7__default.default.join(galleryDir, "gallery", "images");
702
835
  ui.start(`Creating thumbnails: ${galleryDir}`);
703
836
  try {
704
837
  fs8__default.default.mkdirSync(thumbnailsPath, { recursive: true });
705
838
  const galleryData = parseGalleryJson(galleryJsonPath, ui);
706
- const thumbnailSize = galleryData.thumbnailSize || DEFAULT_THUMBNAIL_SIZE;
839
+ const galleryThumbnailConfig = theme.extractThumbnailConfigFromGallery(galleryData);
840
+ let themeConfig;
841
+ if (galleryData.theme) {
842
+ try {
843
+ const themeDir = await resolveThemeDir(galleryData.theme, ui);
844
+ themeConfig = theme.loadThemeConfig(themeDir);
845
+ } catch {
846
+ ui.debug(`Could not load theme config from ${galleryData.theme}, using defaults`);
847
+ }
848
+ }
849
+ const thumbnailConfig = theme.mergeThumbnailConfig(cliThumbnailConfig, galleryThumbnailConfig, themeConfig);
850
+ ui.debug(`Thumbnail config: size=${thumbnailConfig.size}, edge=${thumbnailConfig.edge}`);
707
851
  const mediaBasePath = galleryData.mediaBasePath ?? path7__default.default.join(galleryDir);
708
852
  let processedCount = 0;
709
853
  for (const section of galleryData.sections) {
710
854
  for (const [index, mediaFile] of section.images.entries()) {
711
- section.images[index] = await processMediaFile(mediaFile, mediaBasePath, thumbnailsPath, thumbnailSize, ui);
855
+ section.images[index] = await processMediaFile(mediaFile, mediaBasePath, thumbnailsPath, thumbnailConfig, ui);
712
856
  }
713
857
  processedCount += section.images.length;
714
858
  }
@@ -727,10 +871,11 @@ async function thumbnails(options, ui) {
727
871
  ui.error("No galleries found.");
728
872
  return { processedGalleryCount: 0, processedMediaCount: 0 };
729
873
  }
874
+ const cliThumbnailConfig = options.thumbnailSize !== void 0 || options.thumbnailEdge !== void 0 ? { size: options.thumbnailSize, edge: options.thumbnailEdge } : void 0;
730
875
  let totalGalleries = 0;
731
876
  let totalProcessed = 0;
732
877
  for (const galleryDir of galleryDirs) {
733
- const processed = await processGalleryThumbnails(galleryDir, ui);
878
+ const processed = await processGalleryThumbnails(galleryDir, ui, cliThumbnailConfig);
734
879
  if (processed > 0) {
735
880
  ++totalGalleries;
736
881
  totalProcessed += processed;
@@ -789,7 +934,7 @@ async function scanAndAppendNewFiles(galleryDir, galleryJsonPath, galleryData, u
789
934
  }
790
935
  return galleryData;
791
936
  }
792
- async function buildGallery(galleryDir, templateDir, scan, shouldCreateThumbnails, ui, baseUrl, thumbsBaseUrl) {
937
+ async function buildGallery(galleryDir, templateDir, scan, shouldCreateThumbnails, ui, baseUrl, thumbsBaseUrl, cliThumbnailConfig, cliTheme) {
793
938
  ui.start(`Building gallery ${galleryDir}`);
794
939
  const galleryJsonPath = path7__default.default.join(galleryDir, "gallery", "gallery.json");
795
940
  let galleryData = parseGalleryJson(galleryJsonPath, ui);
@@ -842,18 +987,35 @@ async function buildGallery(galleryDir, templateDir, scan, shouldCreateThumbnail
842
987
  galleryData.thumbsBaseUrl = thumbsBaseUrl;
843
988
  fs8__default.default.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
844
989
  }
990
+ if (cliTheme && galleryData.theme !== cliTheme) {
991
+ ui.debug("Updating gallery.json with theme");
992
+ galleryData.theme = cliTheme;
993
+ fs8__default.default.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
994
+ }
995
+ if (cliThumbnailConfig) {
996
+ const needsUpdate = cliThumbnailConfig.size !== void 0 && galleryData.thumbnails?.size !== cliThumbnailConfig.size || cliThumbnailConfig.edge !== void 0 && galleryData.thumbnails?.edge !== cliThumbnailConfig.edge;
997
+ if (needsUpdate) {
998
+ ui.debug("Updating gallery.json with thumbnail settings");
999
+ galleryData.thumbnails = {
1000
+ ...galleryData.thumbnails,
1001
+ ...cliThumbnailConfig.size !== void 0 && { size: cliThumbnailConfig.size },
1002
+ ...cliThumbnailConfig.edge !== void 0 && { edge: cliThumbnailConfig.edge }
1003
+ };
1004
+ fs8__default.default.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
1005
+ }
1006
+ }
845
1007
  if (!galleryData.metadata.image) {
846
1008
  ui.debug("Updating gallery.json with social media card URL");
847
1009
  galleryData.metadata.image = thumbsBaseUrl ? `${thumbsBaseUrl}/${path7__default.default.basename(socialMediaCardImagePath)}` : `${galleryData.url || ""}/${path7__default.default.relative(galleryDir, socialMediaCardImagePath)}`;
848
1010
  fs8__default.default.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
849
1011
  }
850
1012
  if (shouldCreateThumbnails) {
851
- await processGalleryThumbnails(galleryDir, ui);
1013
+ await processGalleryThumbnails(galleryDir, ui, cliThumbnailConfig);
852
1014
  }
853
1015
  ui.debug("Building gallery from template");
854
1016
  try {
855
- process3__default.default.env.GALLERY_JSON_PATH = galleryJsonPath;
856
- process3__default.default.env.GALLERY_OUTPUT_DIR = path7__default.default.join(galleryDir, "gallery");
1017
+ process4__default.default.env.GALLERY_JSON_PATH = galleryJsonPath;
1018
+ process4__default.default.env.GALLERY_OUTPUT_DIR = path7__default.default.join(galleryDir, "gallery");
857
1019
  child_process.execSync("npx astro build", { cwd: templateDir, stdio: ui.level === consola.LogLevels.debug ? "inherit" : "ignore" });
858
1020
  } catch (error) {
859
1021
  ui.error(`Build failed for ${galleryDir}`);
@@ -870,6 +1032,25 @@ async function buildGallery(galleryDir, templateDir, scan, shouldCreateThumbnail
870
1032
  fs8__default.default.rmSync(buildDir, { recursive: true, force: true });
871
1033
  ui.success(`Gallery built successfully`);
872
1034
  }
1035
+ function isLocalThemePath(theme) {
1036
+ return theme.startsWith("./") || theme.startsWith("../") || theme.startsWith("/");
1037
+ }
1038
+ async function resolveThemeDir(theme, ui) {
1039
+ if (isLocalThemePath(theme)) {
1040
+ const themeDir = path7__default.default.resolve(theme);
1041
+ const packageJsonPath = path7__default.default.join(themeDir, "package.json");
1042
+ if (!fs8__default.default.existsSync(packageJsonPath)) {
1043
+ throw new Error(`Theme directory not found or invalid: ${themeDir}. package.json not found.`);
1044
+ }
1045
+ ui.debug(`Using local theme: ${themeDir}`);
1046
+ return themeDir;
1047
+ } else {
1048
+ const themePath = await undefined(`${theme}/package.json`);
1049
+ const themeDir = path7__default.default.dirname(new URL(themePath).pathname);
1050
+ ui.debug(`Using npm theme package: ${theme} (${themeDir})`);
1051
+ return themeDir;
1052
+ }
1053
+ }
873
1054
  async function build(options, ui) {
874
1055
  try {
875
1056
  const galleryDirs = findGalleries(options.gallery, options.recursive);
@@ -877,20 +1058,41 @@ async function build(options, ui) {
877
1058
  ui.error("No galleries found.");
878
1059
  return { processedGalleryCount: 0 };
879
1060
  }
880
- const themePath = await undefined("@simple-photo-gallery/theme-modern/package.json");
881
- const themeDir = path7__default.default.dirname(new URL(themePath).pathname);
1061
+ const cliThumbnailConfig = options.thumbnailSize !== void 0 || options.thumbnailEdge !== void 0 ? { size: options.thumbnailSize, edge: options.thumbnailEdge } : void 0;
882
1062
  let totalGalleries = 0;
883
1063
  for (const dir of galleryDirs) {
1064
+ const galleryJsonPath = path7__default.default.join(dir, "gallery", "gallery.json");
1065
+ const galleryData = parseGalleryJson(galleryJsonPath, ui);
1066
+ const themeIdentifier = options.theme || galleryData.theme || "@simple-photo-gallery/theme-modern";
1067
+ const themeDir = await resolveThemeDir(themeIdentifier, ui);
884
1068
  const baseUrl = options.baseUrl ? `${options.baseUrl}${path7__default.default.relative(options.gallery, dir)}` : void 0;
885
1069
  const thumbsBaseUrl = options.thumbsBaseUrl ? `${options.thumbsBaseUrl}${path7__default.default.relative(options.gallery, dir)}` : void 0;
886
- await buildGallery(path7__default.default.resolve(dir), themeDir, options.scan, options.thumbnails, ui, baseUrl, thumbsBaseUrl);
1070
+ await buildGallery(
1071
+ path7__default.default.resolve(dir),
1072
+ themeDir,
1073
+ options.scan,
1074
+ options.thumbnails,
1075
+ ui,
1076
+ baseUrl,
1077
+ thumbsBaseUrl,
1078
+ cliThumbnailConfig,
1079
+ options.theme
1080
+ );
887
1081
  ++totalGalleries;
888
1082
  }
889
1083
  ui.box(`Built ${totalGalleries} ${totalGalleries === 1 ? "gallery" : "galleries"} successfully`);
890
1084
  return { processedGalleryCount: totalGalleries };
891
1085
  } catch (error) {
892
- if (error instanceof Error && error.message.includes("Cannot find package")) {
893
- ui.error("Theme package not found: @simple-photo-gallery/theme-modern/package.json");
1086
+ if (error instanceof Error) {
1087
+ if (error.message.includes("Cannot find package")) {
1088
+ ui.error(
1089
+ `Theme package not found: ${options.theme || "@simple-photo-gallery/theme-modern"}. Make sure it's installed.`
1090
+ );
1091
+ } else if (error.message.includes("Theme directory not found") || error.message.includes("package.json not found")) {
1092
+ ui.error(error.message);
1093
+ } else {
1094
+ ui.error("Error building gallery");
1095
+ }
894
1096
  } else {
895
1097
  ui.error("Error building gallery");
896
1098
  }
@@ -948,6 +1150,162 @@ async function clean(options, ui) {
948
1150
  throw error;
949
1151
  }
950
1152
  }
1153
+ function findMonorepoRoot(startDir) {
1154
+ let dir = path7__default.default.resolve(startDir);
1155
+ while (true) {
1156
+ const pkgPath = path7__default.default.join(dir, "package.json");
1157
+ if (fs8__default.default.existsSync(pkgPath)) {
1158
+ try {
1159
+ const pkg = JSON.parse(fs8__default.default.readFileSync(pkgPath, "utf8"));
1160
+ if (pkg && typeof pkg === "object" && "workspaces" in pkg) {
1161
+ return dir;
1162
+ }
1163
+ } catch {
1164
+ }
1165
+ }
1166
+ const parent = path7__default.default.dirname(dir);
1167
+ if (parent === dir) {
1168
+ return void 0;
1169
+ }
1170
+ dir = parent;
1171
+ }
1172
+ }
1173
+ function validateThemeName(name) {
1174
+ if (!name || name.trim().length === 0) {
1175
+ throw new Error("Theme name cannot be empty");
1176
+ }
1177
+ if (!/^[a-z0-9-]+$/i.test(name)) {
1178
+ throw new Error("Theme name can only contain letters, numbers, and hyphens");
1179
+ }
1180
+ return true;
1181
+ }
1182
+ async function ensureDirectory(dirPath, ui) {
1183
+ try {
1184
+ await fs8__default.default.promises.mkdir(dirPath, { recursive: true });
1185
+ ui.debug(`Created directory: ${dirPath}`);
1186
+ } catch (error) {
1187
+ if (error instanceof Error && "code" in error && error.code !== "EEXIST") {
1188
+ throw new Error(`Failed to create directory ${dirPath}: ${error.message}`);
1189
+ }
1190
+ }
1191
+ }
1192
+ var EXCLUDE_PATTERNS = ["node_modules", ".astro", "dist", "_build", ".git", "*.log", ".DS_Store"];
1193
+ function shouldExclude(name) {
1194
+ if (name === "README.md" || name === "README_BASE.md") {
1195
+ return true;
1196
+ }
1197
+ return EXCLUDE_PATTERNS.some((pattern) => {
1198
+ if (pattern.includes("*")) {
1199
+ const regexPattern = pattern.split("*").join(".*");
1200
+ const regex = new RegExp(regexPattern);
1201
+ return regex.test(name);
1202
+ }
1203
+ return name === pattern;
1204
+ });
1205
+ }
1206
+ async function copyDirectory(src, dest, ui) {
1207
+ await fs8__default.default.promises.mkdir(dest, { recursive: true });
1208
+ const entries = await fs8__default.default.promises.readdir(src, { withFileTypes: true });
1209
+ for (const entry of entries) {
1210
+ if (shouldExclude(entry.name)) {
1211
+ ui.debug(`Skipping excluded file/directory: ${entry.name}`);
1212
+ continue;
1213
+ }
1214
+ const srcPath = path7__default.default.join(src, entry.name);
1215
+ const destPath = path7__default.default.join(dest, entry.name);
1216
+ if (entry.isDirectory()) {
1217
+ await copyDirectory(srcPath, destPath, ui);
1218
+ } else {
1219
+ await fs8__default.default.promises.copyFile(srcPath, destPath);
1220
+ ui.debug(`Copied file: ${destPath}`);
1221
+ }
1222
+ }
1223
+ }
1224
+ function findBaseThemePath() {
1225
+ const moduleDir = path7__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
1226
+ const bundledTemplatePath = path7__default.default.resolve(moduleDir, "../../../src/modules/create-theme/templates/base");
1227
+ if (fs8__default.default.existsSync(bundledTemplatePath)) {
1228
+ return bundledTemplatePath;
1229
+ }
1230
+ const monorepoRoot = findMonorepoRoot(process4__default.default.cwd());
1231
+ const workspaceRoot = monorepoRoot ?? process4__default.default.cwd();
1232
+ const workspaceBaseThemePath = path7__default.default.join(workspaceRoot, "themes", "base");
1233
+ if (fs8__default.default.existsSync(workspaceBaseThemePath)) {
1234
+ return workspaceBaseThemePath;
1235
+ }
1236
+ throw new Error(
1237
+ `Base theme template not found. Tried:
1238
+ - ${bundledTemplatePath}
1239
+ - ${workspaceBaseThemePath}
1240
+
1241
+ Please ensure the templates are included in the package or themes/base exists in the workspace.`
1242
+ );
1243
+ }
1244
+ async function updatePackageJson(themeDir, themeName, ui) {
1245
+ const packageJsonPath = path7__default.default.join(themeDir, "package.json");
1246
+ const packageJsonContent = await fs8__default.default.promises.readFile(packageJsonPath, "utf8");
1247
+ const packageJson = JSON.parse(packageJsonContent);
1248
+ packageJson.name = themeName;
1249
+ await fs8__default.default.promises.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", "utf8");
1250
+ ui.debug(`Updated package.json with theme name: ${themeName}`);
1251
+ }
1252
+ async function createReadmeFromBase(baseThemePath, themeDir, themeName, ui) {
1253
+ const readmeBasePath = path7__default.default.join(baseThemePath, "README_BASE.md");
1254
+ const readmePath = path7__default.default.join(themeDir, "README.md");
1255
+ if (!fs8__default.default.existsSync(readmeBasePath)) {
1256
+ throw new Error(`README_BASE.md not found in template: ${readmeBasePath}`);
1257
+ }
1258
+ let readme = await fs8__default.default.promises.readFile(readmeBasePath, "utf8");
1259
+ const displayName = themeName.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
1260
+ readme = readme.replaceAll("{THEME_NAME}", displayName);
1261
+ readme = readme.replaceAll("{THEME_NAME_LOWER}", displayName.toLowerCase());
1262
+ await fs8__default.default.promises.writeFile(readmePath, readme, "utf8");
1263
+ ui.debug(`Created README.md from README_BASE.md for theme: ${themeName}`);
1264
+ }
1265
+ async function createTheme(options, ui) {
1266
+ try {
1267
+ validateThemeName(options.name);
1268
+ let themeDir;
1269
+ if (options.path) {
1270
+ themeDir = path7__default.default.resolve(options.path);
1271
+ } else {
1272
+ const monorepoRoot = findMonorepoRoot(process4__default.default.cwd());
1273
+ const baseDir = monorepoRoot ?? process4__default.default.cwd();
1274
+ const themesBaseDir = path7__default.default.resolve(baseDir, "themes");
1275
+ themeDir = path7__default.default.join(themesBaseDir, options.name);
1276
+ if (!fs8__default.default.existsSync(themesBaseDir)) {
1277
+ await ensureDirectory(themesBaseDir, ui);
1278
+ }
1279
+ }
1280
+ if (fs8__default.default.existsSync(themeDir)) {
1281
+ throw new Error(`Theme directory already exists: ${themeDir}. Cannot overwrite existing theme.`);
1282
+ }
1283
+ ui.start(`Creating theme: ${options.name}`);
1284
+ const baseThemePath = findBaseThemePath();
1285
+ ui.debug(`Using base theme from: ${baseThemePath}`);
1286
+ ui.debug("Copying base theme files...");
1287
+ await copyDirectory(baseThemePath, themeDir, ui);
1288
+ ui.debug("Updating theme-specific files...");
1289
+ await updatePackageJson(themeDir, options.name, ui);
1290
+ await createReadmeFromBase(baseThemePath, themeDir, options.name, ui);
1291
+ ui.success(`Theme created successfully at: ${themeDir}`);
1292
+ ui.info(`
1293
+ Next steps:`);
1294
+ ui.info(`1. cd ${themeDir}`);
1295
+ ui.info(`2. yarn install`);
1296
+ ui.info(`3. Customize your theme in src/pages/index.astro`);
1297
+ ui.info(`4. Initialize a gallery (run from directory with your images): spg init -p <images-folder>`);
1298
+ ui.info(`5. Build a gallery with your theme: spg build --theme ${themeDir} -g <gallery-folder>`);
1299
+ return { processedGalleryCount: 0 };
1300
+ } catch (error) {
1301
+ if (error instanceof Error) {
1302
+ ui.error(error.message);
1303
+ } else {
1304
+ ui.error("Failed to create theme");
1305
+ }
1306
+ throw error;
1307
+ }
1308
+ }
951
1309
 
952
1310
  // src/modules/telemetry/index.ts
953
1311
  async function telemetry(options, ui, telemetryService2) {
@@ -977,7 +1335,7 @@ var ApiTelemetryClient = class {
977
1335
  axios__default.default.post(this.endpoint, event, {
978
1336
  headers: {
979
1337
  "content-type": "application/json",
980
- "user-agent": `simple-photo-gallery/${event.packageVersion} (${process3__default.default.platform}; ${process3__default.default.arch})`
1338
+ "user-agent": `simple-photo-gallery/${event.packageVersion} (${process4__default.default.platform}; ${process4__default.default.arch})`
981
1339
  }
982
1340
  });
983
1341
  } catch {
@@ -987,7 +1345,7 @@ var ApiTelemetryClient = class {
987
1345
  var ConsoleTelemetryClient = class {
988
1346
  async record(event) {
989
1347
  const serialized = JSON.stringify(event, null, 2);
990
- process3.stdout.write(`TELEMETRY EVENT: ${serialized}
1348
+ process4.stdout.write(`TELEMETRY EVENT: ${serialized}
991
1349
  `);
992
1350
  }
993
1351
  };
@@ -1014,11 +1372,11 @@ var TelemetryService = class {
1014
1372
  if (override) {
1015
1373
  return override === "1";
1016
1374
  }
1017
- if (process3__default.default.env.CI || process3__default.default.env.DO_NOT_TRACK) {
1375
+ if (process4__default.default.env.CI || process4__default.default.env.DO_NOT_TRACK) {
1018
1376
  return false;
1019
1377
  }
1020
- if (process3__default.default.env.SPG_TELEMETRY) {
1021
- return process3__default.default.env.SPG_TELEMETRY === "1";
1378
+ if (process4__default.default.env.SPG_TELEMETRY) {
1379
+ return process4__default.default.env.SPG_TELEMETRY === "1";
1022
1380
  }
1023
1381
  const stored = this.getStoredPreference();
1024
1382
  if (stored === void 0) {
@@ -1060,7 +1418,7 @@ var TelemetryService = class {
1060
1418
  durationMs: now - startedAt,
1061
1419
  packageName: this.packageName,
1062
1420
  packageVersion: this.packageVersion,
1063
- nodeVersion: process3__default.default.version,
1421
+ nodeVersion: process4__default.default.version,
1064
1422
  osPlatform: os__default.default.platform(),
1065
1423
  osRelease: os__default.default.release(),
1066
1424
  osArch: os__default.default.arch(),
@@ -1094,7 +1452,7 @@ var TelemetryService = class {
1094
1452
  /** Returns the telemetry client. */
1095
1453
  getClient() {
1096
1454
  if (!this.client) {
1097
- switch (process3__default.default.env.SPG_TELEMETRY_PROVIDER) {
1455
+ switch (process4__default.default.env.SPG_TELEMETRY_PROVIDER) {
1098
1456
  case "none": {
1099
1457
  this.client = void 0;
1100
1458
  break;
@@ -1186,7 +1544,7 @@ async function waitForUpdateCheck(checkPromise) {
1186
1544
  // package.json
1187
1545
  var package_default = {
1188
1546
  name: "simple-photo-gallery",
1189
- version: "2.0.17"};
1547
+ version: "2.1.0"};
1190
1548
 
1191
1549
  // src/index.ts
1192
1550
  var program = new commander.Command();
@@ -1228,7 +1586,7 @@ function withCommandContext(handler) {
1228
1586
  } catch (error) {
1229
1587
  ui.debug(error);
1230
1588
  errorInfo = error instanceof Error ? { name: error.name, message: error.message } : { name: "UnknownError", message: String(error) };
1231
- process3__default.default.exitCode = 1;
1589
+ process4__default.default.exitCode = 1;
1232
1590
  }
1233
1591
  const updateInfo = await waitForUpdateCheck(updateCheckPromise);
1234
1592
  if (updateInfo) {
@@ -1250,14 +1608,21 @@ function withCommandContext(handler) {
1250
1608
  program.command("init").description("Initialize a gallery by scaning a folder for images and videos").option(
1251
1609
  "-p, --photos <path>",
1252
1610
  "Path to the folder where the photos are stored. Default: current working directory",
1253
- process3__default.default.cwd()
1611
+ process4__default.default.cwd()
1254
1612
  ).option(
1255
1613
  "-g, --gallery <path>",
1256
1614
  "Path to the directory where the gallery will be initialized. Default: same directory as the photos folder"
1257
- ).option("-r, --recursive", "Recursively create galleries from all photos subdirectories", false).option("-d, --default", "Use default gallery settings instead of asking the user", false).option("-f, --force", "Force override existing galleries without asking", false).option("--cta-banner", "Add a Simple Photo Gallery call-to-action banner to the end of the gallery", false).action(withCommandContext((options, ui) => init(options, ui)));
1258
- program.command("thumbnails").description("Create thumbnails for all media files in the gallery").option("-g, --gallery <path>", "Path to the directory of the gallery. Default: current working directory", process3__default.default.cwd()).option("-r, --recursive", "Scan subdirectories recursively", false).action(withCommandContext((options, ui) => thumbnails(options, ui)));
1259
- program.command("build").description("Build the HTML gallery in the specified directory").option("-g, --gallery <path>", "Path to the directory of the gallery. Default: current working directory", process3__default.default.cwd()).option("-r, --recursive", "Scan subdirectories recursively", false).option("-b, --base-url <url>", "Base URL where the photos are hosted").option("-t, --thumbs-base-url <url>", "Base URL where the thumbnails are hosted").option("--no-thumbnails", "Skip creating thumbnails when building the gallery", true).option("--no-scan", "Do not scan for new photos when building the gallery", true).action(withCommandContext((options, ui) => build(options, ui)));
1260
- program.command("clean").description("Remove all gallery files and folders (index.html, gallery/)").option("-g, --gallery <path>", "Path to the directory of the gallery. Default: current working directory", process3__default.default.cwd()).option("-r, --recursive", "Clean subdirectories recursively", false).action(withCommandContext((options, ui) => clean(options, ui)));
1615
+ ).option("-r, --recursive", "Recursively create galleries from all photos subdirectories", false).option("-d, --default", "Use default gallery settings instead of asking the user", false).option("-f, --force", "Force override existing galleries without asking", false).option("--cta-banner", "Add a Simple Photo Gallery call-to-action banner to the end of the gallery", false).option("--theme <package|path>", "Theme package name or local path to store in gallery.json").option("--thumbnail-size <pixels>", "Thumbnail size in pixels to store in gallery.json", Number.parseInt).option("--thumbnail-edge <mode>", "How thumbnail size is applied: auto, width, or height").action(withCommandContext((options, ui) => init(options, ui)));
1616
+ program.command("thumbnails").description("Create thumbnails for all media files in the gallery").option("-g, --gallery <path>", "Path to the directory of the gallery. Default: current working directory", process4__default.default.cwd()).option("-r, --recursive", "Scan subdirectories recursively", false).option("--thumbnail-size <pixels>", "Override thumbnail size in pixels", Number.parseInt).option("--thumbnail-edge <mode>", "Override how thumbnail size is applied: auto, width, or height").action(withCommandContext((options, ui) => thumbnails(options, ui)));
1617
+ program.command("build").description("Build the HTML gallery in the specified directory").option("-g, --gallery <path>", "Path to the directory of the gallery. Default: current working directory", process4__default.default.cwd()).option("-r, --recursive", "Scan subdirectories recursively", false).option("-b, --base-url <url>", "Base URL where the photos are hosted").option("-t, --thumbs-base-url <url>", "Base URL where the thumbnails are hosted").option("--no-thumbnails", "Skip creating thumbnails when building the gallery", true).option("--no-scan", "Do not scan for new photos when building the gallery", true).option(
1618
+ "--theme <package|path>",
1619
+ "Theme package name (e.g., @simple-photo-gallery/theme-modern) or local path (e.g., ./themes/my-theme)"
1620
+ ).option("--thumbnail-size <pixels>", "Override thumbnail size in pixels", Number.parseInt).option("--thumbnail-edge <mode>", "Override how thumbnail size is applied: auto, width, or height").action(withCommandContext((options, ui) => build(options, ui)));
1621
+ program.command("clean").description("Remove all gallery files and folders (index.html, gallery/)").option("-g, --gallery <path>", "Path to the directory of the gallery. Default: current working directory", process4__default.default.cwd()).option("-r, --recursive", "Clean subdirectories recursively", false).action(withCommandContext((options, ui) => clean(options, ui)));
1622
+ program.command("create-theme").description("Create a new theme template").argument("<name>", "Name of the theme to create").option("-p, --path <path>", "Path where the theme should be created. Default: ./themes/<name>").action(async (name, options, command) => {
1623
+ const handler = withCommandContext((opts, ui) => createTheme({ name, path: opts.path }, ui));
1624
+ await handler(options, command);
1625
+ });
1261
1626
  program.command("telemetry").description("Manage anonymous telemetry preferences. Use 1 to enable, 0 to disable, or no argument to check status").option("-s, --state <state>", "Enable (1) or disable (0) telemetry", parseTelemetryOption).action(withCommandContext((options, ui) => telemetry(options, ui, telemetryService)));
1262
1627
  program.parse();
1263
1628
  //# sourceMappingURL=index.cjs.map