@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.
- package/dist/index.js +668 -7
- 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((
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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();
|