@officexapp/catalogs-cli 0.2.0 → 0.2.2

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