@officexapp/catalogs-cli 0.2.0 → 0.2.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.
Files changed (2) hide show
  1. package/dist/index.js +666 -7
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
+ import { createRequire } from "module";
5
6
 
6
7
  // src/config.ts
7
8
  var DEFAULT_API_URL = "https://api.catalogkit.cc";
@@ -230,7 +231,7 @@ Check status later: catalogs video status ${videoId}
230
231
  `);
231
232
  }
232
233
  function sleep(ms) {
233
- return new Promise((resolve2) => setTimeout(resolve2, ms));
234
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
234
235
  }
235
236
 
236
237
  // src/commands/video-status.ts
@@ -262,8 +263,8 @@ async function videoStatus(videoId) {
262
263
  }
263
264
 
264
265
  // src/commands/catalog-push.ts
265
- import { readFileSync as readFileSync2 } from "fs";
266
- import { resolve, extname } from "path";
266
+ import { readFileSync as readFileSync3 } from "fs";
267
+ import { resolve as resolve2, extname as extname2, dirname } from "path";
267
268
  import { pathToFileURL } from "url";
268
269
  import ora3 from "ora";
269
270
 
@@ -313,9 +314,250 @@ function validateCatalog(schema) {
313
314
  return errors;
314
315
  }
315
316
 
317
+ // src/lib/resolve-assets.ts
318
+ import { existsSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
319
+ import { resolve, join, extname, basename as basename2 } from "path";
320
+ var FILE_PROPS = /* @__PURE__ */ new Set(["src", "poster", "hls_url", "href", "link"]);
321
+ var IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".avif", ".ico"]);
322
+ var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".webm", ".avi", ".mkv", ".m4v", ".wmv", ".flv"]);
323
+ function isLocalRef(value) {
324
+ if (!value || typeof value !== "string") return false;
325
+ if (value.startsWith("http://") || value.startsWith("https://") || value.startsWith("//")) return false;
326
+ if (value.startsWith("data:")) return false;
327
+ if (value.startsWith("{{")) return false;
328
+ return value.startsWith("./") || value.startsWith("../") || !value.startsWith("/");
329
+ }
330
+ function rewriteHtmlLocalRefs(html, catalogDir, baseUrl) {
331
+ return html.replace(
332
+ /(\bsrc\s*=\s*)(["'])(\.\.?\/[^"']+)\2/gi,
333
+ (match, prefix, quote, path) => {
334
+ if (isLocalRef(path)) {
335
+ const fullPath = resolve(join(catalogDir, path));
336
+ if (existsSync(fullPath)) {
337
+ const relativePath = fullPath.slice(resolve(catalogDir).length + 1);
338
+ return `${prefix}${quote}${baseUrl}/${relativePath}${quote}`;
339
+ }
340
+ }
341
+ return match;
342
+ }
343
+ );
344
+ }
345
+ function resolveLocalAssets(schema, catalogDir, baseUrl) {
346
+ return walkAndRewrite(schema, catalogDir, (localPath) => {
347
+ const relativePath = localPath.slice(resolve(catalogDir).length + 1);
348
+ return `${baseUrl}/${relativePath}`;
349
+ }, baseUrl);
350
+ }
351
+ async function uploadLocalAssets(schema, catalogDir, api, onProgress) {
352
+ const localRefs = /* @__PURE__ */ new Map();
353
+ collectLocalRefs(schema, catalogDir, localRefs);
354
+ collectHtmlLocalRefs(schema, catalogDir, localRefs);
355
+ if (localRefs.size === 0) {
356
+ return { schema, uploaded: 0, errors: [] };
357
+ }
358
+ const uploadMap = /* @__PURE__ */ new Map();
359
+ const errors = [];
360
+ let uploaded = 0;
361
+ for (const localPath of localRefs.keys()) {
362
+ const ext = extname(localPath).toLowerCase();
363
+ const filename = basename2(localPath);
364
+ try {
365
+ if (IMAGE_EXTS.has(ext)) {
366
+ onProgress?.(`Uploading image: ${filename}`);
367
+ const url = await uploadImage(localPath, filename, api);
368
+ uploadMap.set(localPath, url);
369
+ uploaded++;
370
+ } else if (VIDEO_EXTS.has(ext)) {
371
+ onProgress?.(`Uploading video: ${filename}`);
372
+ const url = await uploadVideo(localPath, filename, api);
373
+ uploadMap.set(localPath, url);
374
+ uploaded++;
375
+ } else {
376
+ onProgress?.(`Uploading file: ${filename}`);
377
+ const url = await uploadFile(localPath, filename, api);
378
+ uploadMap.set(localPath, url);
379
+ uploaded++;
380
+ }
381
+ } catch (err) {
382
+ errors.push(`${filename}: ${err.message}`);
383
+ }
384
+ }
385
+ const resolved = walkAndRewrite(schema, catalogDir, (localPath) => {
386
+ return uploadMap.get(localPath) || localPath;
387
+ });
388
+ return { schema: resolved, uploaded, errors };
389
+ }
390
+ function walkAndRewrite(obj, catalogDir, resolver, htmlBaseUrl) {
391
+ if (obj === null || obj === void 0) return obj;
392
+ if (typeof obj === "string") return obj;
393
+ if (Array.isArray(obj)) {
394
+ return obj.map((item) => walkAndRewrite(item, catalogDir, resolver, htmlBaseUrl));
395
+ }
396
+ if (typeof obj === "object") {
397
+ const result = {};
398
+ for (const [key, value] of Object.entries(obj)) {
399
+ if (typeof value === "string" && FILE_PROPS.has(key) && isLocalRef(value)) {
400
+ const localPath = resolve(join(catalogDir, value));
401
+ if (existsSync(localPath)) {
402
+ result[key] = resolver(localPath);
403
+ } else {
404
+ result[key] = value;
405
+ }
406
+ } else if (key === "content" && typeof value === "string" && value.includes("<")) {
407
+ if (htmlBaseUrl) {
408
+ result[key] = rewriteHtmlLocalRefs(value, catalogDir, htmlBaseUrl);
409
+ } else {
410
+ result[key] = rewriteHtmlForUpload(value, catalogDir, resolver);
411
+ }
412
+ } else {
413
+ result[key] = walkAndRewrite(value, catalogDir, resolver, htmlBaseUrl);
414
+ }
415
+ }
416
+ return result;
417
+ }
418
+ return obj;
419
+ }
420
+ function rewriteHtmlForUpload(html, catalogDir, resolver) {
421
+ return html.replace(
422
+ /(\bsrc\s*=\s*)(["'])(\.\.?\/[^"']+)\2/gi,
423
+ (match, prefix, quote, path) => {
424
+ if (isLocalRef(path)) {
425
+ const fullPath = resolve(join(catalogDir, path));
426
+ if (existsSync(fullPath)) {
427
+ const resolved = resolver(fullPath);
428
+ return `${prefix}${quote}${resolved}${quote}`;
429
+ }
430
+ }
431
+ return match;
432
+ }
433
+ );
434
+ }
435
+ function collectLocalRefs(obj, catalogDir, refs) {
436
+ if (obj === null || obj === void 0 || typeof obj !== "object") return;
437
+ if (Array.isArray(obj)) {
438
+ for (const item of obj) collectLocalRefs(item, catalogDir, refs);
439
+ return;
440
+ }
441
+ for (const [key, value] of Object.entries(obj)) {
442
+ if (typeof value === "string" && FILE_PROPS.has(key) && isLocalRef(value)) {
443
+ const localPath = resolve(join(catalogDir, value));
444
+ if (existsSync(localPath)) {
445
+ refs.set(localPath, "");
446
+ }
447
+ } else if (typeof value === "object") {
448
+ collectLocalRefs(value, catalogDir, refs);
449
+ }
450
+ }
451
+ }
452
+ function collectHtmlLocalRefs(obj, catalogDir, refs) {
453
+ if (obj === null || obj === void 0 || typeof obj !== "object") return;
454
+ if (Array.isArray(obj)) {
455
+ for (const item of obj) collectHtmlLocalRefs(item, catalogDir, refs);
456
+ return;
457
+ }
458
+ for (const [key, value] of Object.entries(obj)) {
459
+ if (key === "content" && typeof value === "string" && value.includes("<")) {
460
+ const matches = value.matchAll(/\bsrc\s*=\s*["'](\.\.?\/[^"']+)["']/gi);
461
+ for (const m of matches) {
462
+ const path = m[1];
463
+ if (isLocalRef(path)) {
464
+ const localPath = resolve(join(catalogDir, path));
465
+ if (existsSync(localPath)) {
466
+ refs.set(localPath, "");
467
+ }
468
+ }
469
+ }
470
+ } else if (typeof value === "object") {
471
+ collectHtmlLocalRefs(value, catalogDir, refs);
472
+ }
473
+ }
474
+ }
475
+ async function uploadImage(localPath, filename, api) {
476
+ const ext = extname(filename).toLowerCase();
477
+ const contentType = {
478
+ ".png": "image/png",
479
+ ".jpg": "image/jpeg",
480
+ ".jpeg": "image/jpeg",
481
+ ".gif": "image/gif",
482
+ ".svg": "image/svg+xml",
483
+ ".webp": "image/webp",
484
+ ".avif": "image/avif",
485
+ ".ico": "image/x-icon"
486
+ }[ext] || "image/png";
487
+ const sizeBytes = statSync2(localPath).size;
488
+ const res = await api.post("/api/v1/images/upload", {
489
+ filename,
490
+ content_type: contentType,
491
+ size_bytes: sizeBytes
492
+ });
493
+ const uploadUrl = res.data.upload_url;
494
+ const compressedUrl = res.data.compressed_url;
495
+ const originalUrl = res.data.original_url;
496
+ const fileBuffer = readFileSync2(localPath);
497
+ const putRes = await fetch(uploadUrl, {
498
+ method: "PUT",
499
+ headers: { "Content-Type": contentType },
500
+ body: fileBuffer
501
+ });
502
+ if (!putRes.ok) throw new Error(`S3 upload failed: ${putRes.status}`);
503
+ return compressedUrl || originalUrl;
504
+ }
505
+ async function uploadVideo(localPath, filename, api) {
506
+ const ext = extname(filename).toLowerCase();
507
+ const contentType = {
508
+ ".mp4": "video/mp4",
509
+ ".mov": "video/quicktime",
510
+ ".webm": "video/webm",
511
+ ".avi": "video/x-msvideo",
512
+ ".mkv": "video/x-matroska",
513
+ ".m4v": "video/x-m4v",
514
+ ".wmv": "video/x-ms-wmv",
515
+ ".flv": "video/x-flv"
516
+ }[ext] || "video/mp4";
517
+ const sizeBytes = statSync2(localPath).size;
518
+ const res = await api.post("/api/v1/videos/upload", {
519
+ filename,
520
+ content_type: contentType,
521
+ size_bytes: sizeBytes
522
+ });
523
+ const uploadUrl = res.data.upload_url;
524
+ const videoId = res.data.video_id;
525
+ const fileBuffer = readFileSync2(localPath);
526
+ const putRes = await fetch(uploadUrl, {
527
+ method: "PUT",
528
+ headers: { "Content-Type": contentType },
529
+ body: fileBuffer
530
+ });
531
+ if (!putRes.ok) throw new Error(`S3 upload failed: ${putRes.status}`);
532
+ try {
533
+ const transcodeRes = await api.post(`/api/v1/videos/${videoId}/transcode`);
534
+ if (transcodeRes.data?.hls_url) {
535
+ return transcodeRes.data.hls_url;
536
+ }
537
+ } catch {
538
+ }
539
+ return res.data.video_url || res.data.original_url || `video:${videoId}`;
540
+ }
541
+ async function uploadFile(localPath, filename, api) {
542
+ const sizeBytes = statSync2(localPath).size;
543
+ const res = await api.post("/api/v1/files/upload", {
544
+ filename,
545
+ size_bytes: sizeBytes
546
+ });
547
+ const uploadUrl = res.data.upload_url;
548
+ const downloadUrl = res.data.download_url || res.data.url;
549
+ const fileBuffer = readFileSync2(localPath);
550
+ const putRes = await fetch(uploadUrl, {
551
+ method: "PUT",
552
+ body: fileBuffer
553
+ });
554
+ if (!putRes.ok) throw new Error(`S3 upload failed: ${putRes.status}`);
555
+ return downloadUrl;
556
+ }
557
+
316
558
  // src/commands/catalog-push.ts
317
559
  async function loadTsFile(file) {
318
- const abs = resolve(file);
560
+ const abs = resolve2(file);
319
561
  const { register } = await import("module");
320
562
  register("tsx/esm", pathToFileURL("./"));
321
563
  const mod = await import(pathToFileURL(abs).href);
@@ -325,7 +567,7 @@ async function catalogPush(file, opts) {
325
567
  const config = requireConfig();
326
568
  const api = new ApiClient(config);
327
569
  await printIdentity(api);
328
- const ext = extname(file).toLowerCase();
570
+ const ext = extname2(file).toLowerCase();
329
571
  const isTs = ext === ".ts" || ext === ".mts";
330
572
  let schema;
331
573
  try {
@@ -333,7 +575,7 @@ async function catalogPush(file, opts) {
333
575
  const rawCatalog = await loadTsFile(file);
334
576
  schema = serializeCatalog(rawCatalog);
335
577
  } else {
336
- const raw = readFileSync2(file, "utf-8");
578
+ const raw = readFileSync3(file, "utf-8");
337
579
  schema = JSON.parse(raw);
338
580
  }
339
581
  } catch (err) {
@@ -347,6 +589,26 @@ async function catalogPush(file, opts) {
347
589
  }
348
590
  process.exit(1);
349
591
  }
592
+ const catalogDir = dirname(resolve2(file));
593
+ const assetSpinner = ora3("Checking for local assets...").start();
594
+ try {
595
+ const result = await uploadLocalAssets(schema, catalogDir, api, (msg) => {
596
+ assetSpinner.text = msg;
597
+ });
598
+ schema = result.schema;
599
+ if (result.uploaded > 0) {
600
+ assetSpinner.succeed(`Uploaded ${result.uploaded} local asset(s) to CDN`);
601
+ } else {
602
+ assetSpinner.succeed("No local assets to upload");
603
+ }
604
+ if (result.errors.length > 0) {
605
+ for (const err of result.errors) {
606
+ console.error(` Warning: ${err}`);
607
+ }
608
+ }
609
+ } catch (err) {
610
+ assetSpinner.warn(`Asset upload failed: ${err.message} (continuing with push)`);
611
+ }
350
612
  const slug = schema.slug;
351
613
  const name = schema.catalog_id || schema.slug || file;
352
614
  const status = opts.publish ? "published" : "draft";
@@ -415,6 +677,400 @@ async function catalogList() {
415
677
  }
416
678
  }
417
679
 
680
+ // src/commands/catalog-dev.ts
681
+ import { resolve as resolve3, dirname as dirname2, extname as extname3, join as join2 } from "path";
682
+ import { existsSync as existsSync2, readFileSync as readFileSync4, statSync as statSync3, watch } from "fs";
683
+ import { pathToFileURL as pathToFileURL2 } from "url";
684
+ import { createServer } from "http";
685
+ import ora5 from "ora";
686
+ var DEFAULT_PORT = 3456;
687
+ async function loadCatalogFile(file) {
688
+ const abs = resolve3(file);
689
+ const ext = extname3(file).toLowerCase();
690
+ const isTs = ext === ".ts" || ext === ".mts";
691
+ if (isTs) {
692
+ const { register } = await import("module");
693
+ register("tsx/esm", pathToFileURL2("./"));
694
+ const url = pathToFileURL2(abs).href + `?t=${Date.now()}`;
695
+ const mod = await import(url);
696
+ return serializeCatalog(mod.default ?? mod);
697
+ } else {
698
+ const raw = readFileSync4(abs, "utf-8");
699
+ return JSON.parse(raw);
700
+ }
701
+ }
702
+ var MIME_TYPES = {
703
+ ".html": "text/html",
704
+ ".js": "application/javascript",
705
+ ".css": "text/css",
706
+ ".json": "application/json",
707
+ ".png": "image/png",
708
+ ".jpg": "image/jpeg",
709
+ ".jpeg": "image/jpeg",
710
+ ".gif": "image/gif",
711
+ ".svg": "image/svg+xml",
712
+ ".webp": "image/webp",
713
+ ".avif": "image/avif",
714
+ ".ico": "image/x-icon",
715
+ ".mp4": "video/mp4",
716
+ ".webm": "video/webm",
717
+ ".mov": "video/quicktime",
718
+ ".mp3": "audio/mpeg",
719
+ ".wav": "audio/wav",
720
+ ".pdf": "application/pdf",
721
+ ".zip": "application/zip",
722
+ ".woff": "font/woff",
723
+ ".woff2": "font/woff2",
724
+ ".ttf": "font/ttf",
725
+ ".otf": "font/otf"
726
+ };
727
+ function getMime(filepath) {
728
+ const ext = extname3(filepath).toLowerCase();
729
+ return MIME_TYPES[ext] || "application/octet-stream";
730
+ }
731
+ function buildPreviewHtml(schema, port) {
732
+ const schemaJson = JSON.stringify(schema);
733
+ return `<!DOCTYPE html>
734
+ <html lang="en">
735
+ <head>
736
+ <meta charset="UTF-8" />
737
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
738
+ <title>${schema.slug || "Catalog"} \u2014 Local Preview</title>
739
+ <style>
740
+ *, *::before, *::after { box-sizing: border-box; }
741
+ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
742
+ .dev-banner {
743
+ position: fixed; top: 0; left: 0; right: 0; z-index: 99999;
744
+ background: #1a1a2e; color: #e0e0ff; font-size: 12px;
745
+ padding: 4px 12px; display: flex; align-items: center; gap: 8px;
746
+ font-family: monospace; border-bottom: 2px solid #6c63ff;
747
+ }
748
+ .dev-banner .dot { width: 8px; height: 8px; border-radius: 50%; background: #4ade80; }
749
+ .dev-banner .label { opacity: 0.7; }
750
+ .dev-banner .slug { font-weight: bold; color: #a5b4fc; }
751
+ .dev-banner .stub-tags { margin-left: auto; display: flex; gap: 6px; }
752
+ .dev-banner .stub-tag {
753
+ background: rgba(255,255,255,0.1); border-radius: 3px;
754
+ padding: 1px 6px; font-size: 11px; color: #fbbf24;
755
+ }
756
+ #catalog-root { padding-top: 28px; }
757
+ .checkout-stub {
758
+ background: #fef3c7; border: 2px dashed #f59e0b; border-radius: 8px;
759
+ padding: 20px; text-align: center; margin: 16px;
760
+ }
761
+ .checkout-stub h3 { margin: 0 0 8px; color: #92400e; }
762
+ .checkout-stub p { margin: 0; color: #a16207; font-size: 14px; }
763
+ </style>
764
+ <script src="https://cdn.tailwindcss.com"></script>
765
+ </head>
766
+ <body>
767
+ <div class="dev-banner">
768
+ <span class="dot"></span>
769
+ <span class="label">LOCAL DEV</span>
770
+ <span class="slug">${schema.slug || "catalog"}</span>
771
+ <span class="stub-tags">
772
+ <span class="stub-tag">Checkout: stubbed</span>
773
+ <span class="stub-tag">Analytics: off</span>
774
+ </span>
775
+ </div>
776
+ <div id="catalog-root"></div>
777
+
778
+ <script type="module">
779
+ import React from 'https://esm.sh/react@18?bundle';
780
+ import ReactDOM from 'https://esm.sh/react-dom@18/client?bundle';
781
+
782
+ const schema = ${schemaJson};
783
+
784
+ // Render catalog schema as a formatted preview
785
+ function CatalogPreview({ catalog }) {
786
+ const pages = catalog.pages || {};
787
+ const pageKeys = Object.keys(pages);
788
+ const [currentPage, setCurrentPage] = React.useState(catalog.routing?.entry || pageKeys[0] || null);
789
+
790
+ const page = currentPage ? pages[currentPage] : null;
791
+
792
+ function renderComponent(comp, i) {
793
+ const props = comp.props || {};
794
+ const type = comp.type;
795
+
796
+ switch (type) {
797
+ case 'heading':
798
+ const Tag = props.level === 2 ? 'h2' : props.level === 3 ? 'h3' : 'h1';
799
+ return React.createElement(Tag, { key: i, className: 'text-2xl font-bold mb-4 px-4', dangerouslySetInnerHTML: { __html: props.text || '' } });
800
+
801
+ case 'paragraph':
802
+ return React.createElement('p', { key: i, className: 'text-gray-700 mb-4 px-4 leading-relaxed', dangerouslySetInnerHTML: { __html: props.text || '' } });
803
+
804
+ case 'image':
805
+ return React.createElement('div', { key: i, className: 'px-4 mb-4' },
806
+ React.createElement('img', {
807
+ src: props.src || '',
808
+ alt: props.alt || '',
809
+ className: 'max-w-full rounded-lg',
810
+ style: { maxHeight: '400px', objectFit: 'contain' }
811
+ })
812
+ );
813
+
814
+ case 'video':
815
+ const videoSrc = props.src || props.hls_url || '';
816
+ return React.createElement('div', { key: i, className: 'px-4 mb-4' },
817
+ React.createElement('video', {
818
+ src: videoSrc,
819
+ controls: true,
820
+ className: 'max-w-full rounded-lg',
821
+ poster: props.poster || undefined
822
+ })
823
+ );
824
+
825
+ case 'html':
826
+ return React.createElement('div', {
827
+ key: i,
828
+ className: 'px-4 mb-4',
829
+ dangerouslySetInnerHTML: { __html: props.content || '' }
830
+ });
831
+
832
+ case 'short_text':
833
+ case 'email':
834
+ case 'phone':
835
+ case 'url':
836
+ return React.createElement('div', { key: i, className: 'px-4 mb-4' },
837
+ props.label ? React.createElement('label', { className: 'block text-sm font-medium text-gray-700 mb-1' }, props.label) : null,
838
+ React.createElement('input', {
839
+ type: type === 'email' ? 'email' : type === 'phone' ? 'tel' : type === 'url' ? 'url' : 'text',
840
+ placeholder: props.placeholder || '',
841
+ className: 'w-full border border-gray-300 rounded-lg px-3 py-2 text-sm'
842
+ })
843
+ );
844
+
845
+ case 'long_text':
846
+ return React.createElement('div', { key: i, className: 'px-4 mb-4' },
847
+ props.label ? React.createElement('label', { className: 'block text-sm font-medium text-gray-700 mb-1' }, props.label) : null,
848
+ React.createElement('textarea', {
849
+ placeholder: props.placeholder || '',
850
+ className: 'w-full border border-gray-300 rounded-lg px-3 py-2 text-sm',
851
+ rows: 4
852
+ })
853
+ );
854
+
855
+ case 'multiple_choice':
856
+ return React.createElement('div', { key: i, className: 'px-4 mb-4' },
857
+ props.label ? React.createElement('label', { className: 'block text-sm font-medium text-gray-700 mb-2' }, props.label) : null,
858
+ React.createElement('div', { className: 'space-y-2' },
859
+ ...(props.options || []).map((opt, j) =>
860
+ React.createElement('button', {
861
+ key: j,
862
+ className: 'block w-full text-left border border-gray-300 rounded-lg px-4 py-3 text-sm hover:border-blue-500 hover:bg-blue-50 transition'
863
+ }, typeof opt === 'string' ? opt : opt.label || opt.value || '')
864
+ )
865
+ )
866
+ );
867
+
868
+ case 'dropdown':
869
+ return React.createElement('div', { key: i, className: 'px-4 mb-4' },
870
+ props.label ? React.createElement('label', { className: 'block text-sm font-medium text-gray-700 mb-1' }, props.label) : null,
871
+ React.createElement('select', { className: 'w-full border border-gray-300 rounded-lg px-3 py-2 text-sm' },
872
+ React.createElement('option', { value: '' }, props.placeholder || 'Select...'),
873
+ ...(props.options || []).map((opt, j) =>
874
+ React.createElement('option', { key: j, value: typeof opt === 'string' ? opt : opt.value },
875
+ typeof opt === 'string' ? opt : opt.label || opt.value || ''
876
+ )
877
+ )
878
+ )
879
+ );
880
+
881
+ case 'payment':
882
+ return React.createElement('div', { key: i, className: 'checkout-stub' },
883
+ React.createElement('h3', null, 'Stripe Checkout'),
884
+ React.createElement('p', null, 'Payment would trigger here in production.'),
885
+ props.amount ? React.createElement('p', { className: 'mt-2 font-bold' },
886
+ (props.currency || 'USD').toUpperCase() + ' ' + (props.amount / 100).toFixed(2)
887
+ ) : null
888
+ );
889
+
890
+ case 'banner':
891
+ const bannerColors = {
892
+ info: 'bg-blue-50 border-blue-200 text-blue-800',
893
+ success: 'bg-green-50 border-green-200 text-green-800',
894
+ warning: 'bg-yellow-50 border-yellow-200 text-yellow-800',
895
+ error: 'bg-red-50 border-red-200 text-red-800',
896
+ };
897
+ return React.createElement('div', {
898
+ key: i,
899
+ className: 'px-4 mb-4'
900
+ },
901
+ React.createElement('div', {
902
+ className: 'border rounded-lg px-4 py-3 text-sm ' + (bannerColors[props.style] || bannerColors.info),
903
+ dangerouslySetInnerHTML: { __html: props.text || '' }
904
+ })
905
+ );
906
+
907
+ case 'divider':
908
+ return React.createElement('hr', { key: i, className: 'my-6 mx-4 border-gray-200' });
909
+
910
+ case 'file_download':
911
+ return React.createElement('div', { key: i, className: 'px-4 mb-4' },
912
+ React.createElement('a', {
913
+ href: props.src || '#',
914
+ download: props.filename || 'file',
915
+ className: 'inline-flex items-center gap-2 border border-gray-300 rounded-lg px-4 py-2 text-sm hover:bg-gray-50'
916
+ }, '\u{1F4E5} ' + (props.filename || props.label || 'Download File'))
917
+ );
918
+
919
+ default:
920
+ return React.createElement('div', {
921
+ key: i,
922
+ className: 'px-4 mb-4 bg-gray-50 border border-dashed border-gray-300 rounded-lg p-3 text-xs text-gray-500'
923
+ }, '[' + type + '] ' + (props.label || props.text || comp.id || ''));
924
+ }
925
+ }
926
+
927
+ if (!page) {
928
+ return React.createElement('div', { className: 'p-8 text-center text-gray-500' }, 'No pages found in catalog.');
929
+ }
930
+
931
+ const components = page.components || [];
932
+ const pageTitle = page.title;
933
+
934
+ return React.createElement('div', { className: 'max-w-2xl mx-auto py-8' },
935
+ // Page navigation
936
+ React.createElement('div', { className: 'flex gap-1 px-4 mb-6 overflow-x-auto' },
937
+ ...pageKeys.map(key =>
938
+ React.createElement('button', {
939
+ key: key,
940
+ onClick: () => setCurrentPage(key),
941
+ className: 'px-3 py-1 text-xs rounded-full whitespace-nowrap ' +
942
+ (key === currentPage ? 'bg-indigo-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200')
943
+ }, pages[key].title || key)
944
+ )
945
+ ),
946
+ // Page title
947
+ pageTitle ? React.createElement('h1', { className: 'text-xl font-bold px-4 mb-4' }, pageTitle) : null,
948
+ // Components
949
+ ...components.map(renderComponent),
950
+ // Navigation buttons
951
+ React.createElement('div', { className: 'px-4 mt-6 flex gap-3' },
952
+ React.createElement('button', {
953
+ className: 'px-6 py-2 bg-indigo-600 text-white rounded-lg text-sm font-medium hover:bg-indigo-700',
954
+ onClick: () => {
955
+ const idx = pageKeys.indexOf(currentPage);
956
+ if (idx < pageKeys.length - 1) setCurrentPage(pageKeys[idx + 1]);
957
+ }
958
+ }, page.cta_text || 'Continue'),
959
+ )
960
+ );
961
+ }
962
+
963
+ const root = ReactDOM.createRoot(document.getElementById('catalog-root'));
964
+ root.render(React.createElement(CatalogPreview, { catalog: schema }));
965
+ </script>
966
+ </body>
967
+ </html>`;
968
+ }
969
+ async function catalogDev(file, opts) {
970
+ const abs = resolve3(file);
971
+ const catalogDir = dirname2(abs);
972
+ const port = parseInt(opts.port || String(DEFAULT_PORT), 10);
973
+ if (!existsSync2(abs)) {
974
+ console.error(`File not found: ${file}`);
975
+ process.exit(1);
976
+ }
977
+ const spinner = ora5("Loading catalog schema...").start();
978
+ let schema;
979
+ try {
980
+ schema = await loadCatalogFile(abs);
981
+ } catch (err) {
982
+ spinner.fail(`Failed to load catalog: ${err.message}`);
983
+ process.exit(1);
984
+ }
985
+ const errors = validateCatalog(schema);
986
+ if (errors.length > 0) {
987
+ spinner.fail("Schema validation errors:");
988
+ for (const err of errors) {
989
+ console.error(` - ${err}`);
990
+ }
991
+ process.exit(1);
992
+ }
993
+ const localBaseUrl = `http://localhost:${port}/assets`;
994
+ schema = resolveLocalAssets(schema, catalogDir, localBaseUrl);
995
+ spinner.succeed(`Loaded: ${schema.slug || file}`);
996
+ console.log(` Pages: ${Object.keys(schema.pages || {}).length}`);
997
+ console.log(` Entry: ${schema.routing?.entry || "first page"}`);
998
+ console.log();
999
+ const server = createServer(async (req, res) => {
1000
+ const url = new URL(req.url || "/", `http://localhost:${port}`);
1001
+ if (url.pathname.startsWith("/assets/")) {
1002
+ const relativePath = decodeURIComponent(url.pathname.slice("/assets/".length));
1003
+ const filePath = join2(catalogDir, relativePath);
1004
+ const resolved = resolve3(filePath);
1005
+ if (!resolved.startsWith(resolve3(catalogDir))) {
1006
+ res.writeHead(403);
1007
+ res.end("Forbidden");
1008
+ return;
1009
+ }
1010
+ try {
1011
+ if (!existsSync2(resolved) || !statSync3(resolved).isFile()) {
1012
+ res.writeHead(404);
1013
+ res.end("Not found");
1014
+ return;
1015
+ }
1016
+ const content = readFileSync4(resolved);
1017
+ res.writeHead(200, {
1018
+ "Content-Type": getMime(resolved),
1019
+ "Cache-Control": "no-cache",
1020
+ "Access-Control-Allow-Origin": "*"
1021
+ });
1022
+ res.end(content);
1023
+ } catch {
1024
+ res.writeHead(500);
1025
+ res.end("Internal error");
1026
+ }
1027
+ return;
1028
+ }
1029
+ res.writeHead(200, {
1030
+ "Content-Type": "text/html; charset=utf-8",
1031
+ "Cache-Control": "no-cache"
1032
+ });
1033
+ res.end(buildPreviewHtml(schema, port));
1034
+ });
1035
+ server.listen(port, () => {
1036
+ console.log(` Local preview: http://localhost:${port}`);
1037
+ console.log(` Assets served from: ${catalogDir}`);
1038
+ console.log(` Watching for changes...
1039
+ `);
1040
+ });
1041
+ let debounce = null;
1042
+ watch(abs, () => {
1043
+ if (debounce) clearTimeout(debounce);
1044
+ debounce = setTimeout(async () => {
1045
+ const reloadSpinner = ora5("Reloading catalog...").start();
1046
+ try {
1047
+ schema = await loadCatalogFile(abs);
1048
+ const errs = validateCatalog(schema);
1049
+ if (errs.length > 0) {
1050
+ reloadSpinner.warn("Schema has validation errors:");
1051
+ for (const e of errs) console.error(` - ${e}`);
1052
+ return;
1053
+ }
1054
+ schema = resolveLocalAssets(schema, catalogDir, localBaseUrl);
1055
+ reloadSpinner.succeed(`Reloaded \u2014 refresh browser to see changes`);
1056
+ } catch (err) {
1057
+ reloadSpinner.warn(`Reload failed: ${err.message}`);
1058
+ }
1059
+ }, 300);
1060
+ });
1061
+ try {
1062
+ watch(catalogDir, { recursive: true }, (event, filename) => {
1063
+ if (!filename || filename.startsWith(".") || resolve3(join2(catalogDir, filename)) === abs) return;
1064
+ });
1065
+ } catch {
1066
+ }
1067
+ process.on("SIGINT", () => {
1068
+ console.log("\nStopping dev server...");
1069
+ server.close();
1070
+ process.exit(0);
1071
+ });
1072
+ }
1073
+
418
1074
  // src/commands/whoami.ts
419
1075
  async function whoami() {
420
1076
  const config = getConfig();
@@ -472,8 +1128,10 @@ async function whoami() {
472
1128
  }
473
1129
 
474
1130
  // src/index.ts
1131
+ var require2 = createRequire(import.meta.url);
1132
+ var { version } = require2("../../package.json");
475
1133
  var program = new Command();
476
- program.name("catalogs").description("CLI for Catalog Kit \u2014 upload videos, push catalogs, manage assets").version("0.1.0").option("--token <token>", "Auth token (overrides CATALOG_KIT_TOKEN env var)").hook("preAction", (thisCommand) => {
1134
+ program.name("catalogs").description("CLI for Catalog Kit \u2014 upload videos, push catalogs, manage assets").version(version).option("--token <token>", "Auth token (overrides CATALOG_KIT_TOKEN env var)").hook("preAction", (thisCommand) => {
477
1135
  const opts = thisCommand.opts();
478
1136
  if (opts.token) {
479
1137
  setGlobalToken(opts.token);
@@ -485,5 +1143,6 @@ video.command("status <videoId>").description("Check transcode status for a vide
485
1143
  var catalog = program.command("catalog").description("Catalog schema management");
486
1144
  catalog.command("push <file>").description("Create or update a catalog from a JSON or TypeScript schema file").option("--publish", "Set status to published (default: draft)").action(catalogPush);
487
1145
  catalog.command("list").description("List all catalogs").action(catalogList);
1146
+ catalog.command("dev <file>").description("Preview a catalog locally with hot reload and local asset serving").option("--port <port>", "Port to serve on (default: 3456)").action(catalogDev);
488
1147
  program.command("whoami").description("Show current authentication info").action(whoami);
489
1148
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@officexapp/catalogs-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "CLI for Catalog Kit — upload videos, push catalogs, manage assets",
5
5
  "type": "module",
6
6
  "bin": {