@officexapp/catalogs-cli 0.2.9 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1082 -64
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { readFileSync as readFileSync5 } from "fs";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
|
-
import { dirname as dirname3, join as
|
|
7
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
8
8
|
|
|
9
9
|
// src/config.ts
|
|
10
10
|
var DEFAULT_API_URL = "https://api.catalogkit.cc";
|
|
@@ -233,7 +233,7 @@ Check status later: catalogs video status ${videoId}
|
|
|
233
233
|
`);
|
|
234
234
|
}
|
|
235
235
|
function sleep(ms) {
|
|
236
|
-
return new Promise((
|
|
236
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
// src/commands/video-status.ts
|
|
@@ -265,9 +265,7 @@ async function videoStatus(videoId) {
|
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
// src/commands/catalog-push.ts
|
|
268
|
-
import {
|
|
269
|
-
import { resolve as resolve2, extname as extname2, dirname } from "path";
|
|
270
|
-
import { pathToFileURL } from "url";
|
|
268
|
+
import { resolve as resolve3, dirname } from "path";
|
|
271
269
|
import ora3 from "ora";
|
|
272
270
|
|
|
273
271
|
// src/lib/serialize.ts
|
|
@@ -315,6 +313,97 @@ function validateCatalog(schema) {
|
|
|
315
313
|
}
|
|
316
314
|
return errors;
|
|
317
315
|
}
|
|
316
|
+
function deepValidateCatalog(schema) {
|
|
317
|
+
const errors = [];
|
|
318
|
+
const warnings = [];
|
|
319
|
+
const pages = schema.pages || {};
|
|
320
|
+
const pageIds = new Set(Object.keys(pages));
|
|
321
|
+
const routing = schema.routing || {};
|
|
322
|
+
const edges = routing.edges || [];
|
|
323
|
+
const entry = routing.entry;
|
|
324
|
+
const KNOWN_TYPES = /* @__PURE__ */ new Set([
|
|
325
|
+
"heading",
|
|
326
|
+
"paragraph",
|
|
327
|
+
"image",
|
|
328
|
+
"video",
|
|
329
|
+
"html",
|
|
330
|
+
"banner",
|
|
331
|
+
"callout",
|
|
332
|
+
"divider",
|
|
333
|
+
"pricing_card",
|
|
334
|
+
"testimonial",
|
|
335
|
+
"faq",
|
|
336
|
+
"timeline",
|
|
337
|
+
"file_download",
|
|
338
|
+
"iframe",
|
|
339
|
+
"short_text",
|
|
340
|
+
"email",
|
|
341
|
+
"phone",
|
|
342
|
+
"url",
|
|
343
|
+
"number",
|
|
344
|
+
"password",
|
|
345
|
+
"long_text",
|
|
346
|
+
"multiple_choice",
|
|
347
|
+
"checkboxes",
|
|
348
|
+
"dropdown",
|
|
349
|
+
"slider",
|
|
350
|
+
"star_rating",
|
|
351
|
+
"switch",
|
|
352
|
+
"checkbox",
|
|
353
|
+
"payment"
|
|
354
|
+
]);
|
|
355
|
+
if (entry && !pageIds.has(entry)) {
|
|
356
|
+
errors.push(`routing.entry "${entry}" does not exist in pages`);
|
|
357
|
+
}
|
|
358
|
+
for (const edge of edges) {
|
|
359
|
+
if (!pageIds.has(edge.from)) {
|
|
360
|
+
errors.push(`routing edge from "${edge.from}" references non-existent page`);
|
|
361
|
+
}
|
|
362
|
+
if (edge.to != null && !pageIds.has(edge.to)) {
|
|
363
|
+
errors.push(`routing edge to "${edge.to}" references non-existent page`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
for (const [pageId, page] of Object.entries(pages)) {
|
|
367
|
+
const components = page.components || [];
|
|
368
|
+
const compIds = /* @__PURE__ */ new Set();
|
|
369
|
+
for (const comp of components) {
|
|
370
|
+
if (comp.id && compIds.has(comp.id)) {
|
|
371
|
+
errors.push(`page "${pageId}": duplicate component ID "${comp.id}"`);
|
|
372
|
+
}
|
|
373
|
+
if (comp.id) compIds.add(comp.id);
|
|
374
|
+
if (comp.type && !KNOWN_TYPES.has(comp.type)) {
|
|
375
|
+
warnings.push(`page "${pageId}": unknown component type "${comp.type}"`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (page.offer?.accept_field) {
|
|
379
|
+
if (!compIds.has(page.offer.accept_field)) {
|
|
380
|
+
errors.push(
|
|
381
|
+
`page "${pageId}": offer.accept_field "${page.offer.accept_field}" does not match any component ID on this page`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (entry && pageIds.has(entry)) {
|
|
387
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
388
|
+
const queue = [entry];
|
|
389
|
+
reachable.add(entry);
|
|
390
|
+
while (queue.length > 0) {
|
|
391
|
+
const id = queue.shift();
|
|
392
|
+
for (const edge of edges) {
|
|
393
|
+
if (edge.from === id && !reachable.has(edge.to)) {
|
|
394
|
+
reachable.add(edge.to);
|
|
395
|
+
queue.push(edge.to);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
for (const id of pageIds) {
|
|
400
|
+
if (!reachable.has(id)) {
|
|
401
|
+
warnings.push(`page "${id}" is unreachable from entry "${entry}"`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return { errors, warnings };
|
|
406
|
+
}
|
|
318
407
|
|
|
319
408
|
// src/lib/resolve-assets.ts
|
|
320
409
|
import { existsSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
@@ -557,29 +646,33 @@ async function uploadFile(localPath, filename, api) {
|
|
|
557
646
|
return downloadUrl;
|
|
558
647
|
}
|
|
559
648
|
|
|
560
|
-
// src/
|
|
561
|
-
|
|
649
|
+
// src/lib/load-file.ts
|
|
650
|
+
import { resolve as resolve2, extname as extname2 } from "path";
|
|
651
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
652
|
+
async function loadCatalogFile(file) {
|
|
562
653
|
const abs = resolve2(file);
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
654
|
+
const ext = extname2(file).toLowerCase();
|
|
655
|
+
const isTs = ext === ".ts" || ext === ".mts";
|
|
656
|
+
if (isTs) {
|
|
657
|
+
const { tsImport } = await import("tsx/esm/api");
|
|
658
|
+
const mod = await tsImport(abs, import.meta.url);
|
|
659
|
+
let raw = mod.default ?? mod;
|
|
660
|
+
if (raw.__esModule && raw.default) raw = raw.default;
|
|
661
|
+
return serializeCatalog(raw);
|
|
662
|
+
} else {
|
|
663
|
+
const raw = readFileSync3(abs, "utf-8");
|
|
664
|
+
return JSON.parse(raw);
|
|
665
|
+
}
|
|
567
666
|
}
|
|
667
|
+
|
|
668
|
+
// src/commands/catalog-push.ts
|
|
568
669
|
async function catalogPush(file, opts) {
|
|
569
670
|
const config = requireConfig();
|
|
570
671
|
const api = new ApiClient(config);
|
|
571
672
|
await printIdentity(api);
|
|
572
|
-
const ext = extname2(file).toLowerCase();
|
|
573
|
-
const isTs = ext === ".ts" || ext === ".mts";
|
|
574
673
|
let schema;
|
|
575
674
|
try {
|
|
576
|
-
|
|
577
|
-
const rawCatalog = await loadTsFile(file);
|
|
578
|
-
schema = serializeCatalog(rawCatalog);
|
|
579
|
-
} else {
|
|
580
|
-
const raw = readFileSync3(file, "utf-8");
|
|
581
|
-
schema = JSON.parse(raw);
|
|
582
|
-
}
|
|
675
|
+
schema = await loadCatalogFile(file);
|
|
583
676
|
} catch (err) {
|
|
584
677
|
console.error(`Failed to read ${file}: ${err.message}`);
|
|
585
678
|
process.exit(1);
|
|
@@ -591,7 +684,7 @@ async function catalogPush(file, opts) {
|
|
|
591
684
|
}
|
|
592
685
|
process.exit(1);
|
|
593
686
|
}
|
|
594
|
-
const catalogDir = dirname(
|
|
687
|
+
const catalogDir = dirname(resolve3(file));
|
|
595
688
|
const assetSpinner = ora3("Checking for local assets...").start();
|
|
596
689
|
try {
|
|
597
690
|
const result = await uploadLocalAssets(schema, catalogDir, api, (msg) => {
|
|
@@ -680,27 +773,11 @@ async function catalogList() {
|
|
|
680
773
|
}
|
|
681
774
|
|
|
682
775
|
// src/commands/catalog-dev.ts
|
|
683
|
-
import { resolve as
|
|
776
|
+
import { resolve as resolve4, dirname as dirname2, extname as extname3, join as join2 } from "path";
|
|
684
777
|
import { existsSync as existsSync2, readFileSync as readFileSync4, statSync as statSync3, watch } from "fs";
|
|
685
|
-
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
686
778
|
import { createServer } from "http";
|
|
687
779
|
import ora5 from "ora";
|
|
688
780
|
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
781
|
var MIME_TYPES = {
|
|
705
782
|
".html": "text/html",
|
|
706
783
|
".js": "application/javascript",
|
|
@@ -730,7 +807,7 @@ function getMime(filepath) {
|
|
|
730
807
|
const ext = extname3(filepath).toLowerCase();
|
|
731
808
|
return MIME_TYPES[ext] || "application/octet-stream";
|
|
732
809
|
}
|
|
733
|
-
function buildPreviewHtml(schema, port) {
|
|
810
|
+
function buildPreviewHtml(schema, port, validation, devConfig) {
|
|
734
811
|
const schemaJson = JSON.stringify(schema).replace(/<\/script/gi, "<\\/script");
|
|
735
812
|
const themeColor = schema.settings?.theme?.primary_color || "#6366f1";
|
|
736
813
|
return `<!DOCTYPE html>
|
|
@@ -927,6 +1004,55 @@ function buildPreviewHtml(schema, port) {
|
|
|
927
1004
|
padding: 32px; text-align: center;
|
|
928
1005
|
box-shadow: 0 2px 12px rgba(0,0,0,0.04);
|
|
929
1006
|
}
|
|
1007
|
+
|
|
1008
|
+
/* Validation banner */
|
|
1009
|
+
.validation-banner {
|
|
1010
|
+
position: fixed; bottom: 0; left: 0; right: 0; z-index: 99999;
|
|
1011
|
+
background: #7f1d1d; color: #fecaca; font-family: monospace; font-size: 12px;
|
|
1012
|
+
padding: 10px 16px; max-height: 200px; overflow-y: auto;
|
|
1013
|
+
border-top: 2px solid #ef4444;
|
|
1014
|
+
}
|
|
1015
|
+
.validation-banner .vb-header {
|
|
1016
|
+
display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px;
|
|
1017
|
+
}
|
|
1018
|
+
.validation-banner .vb-header strong { color: #fca5a5; }
|
|
1019
|
+
.validation-banner .vb-dismiss {
|
|
1020
|
+
background: rgba(255,255,255,0.1); border: none; color: #fca5a5; border-radius: 4px;
|
|
1021
|
+
padding: 2px 8px; cursor: pointer; font-size: 11px;
|
|
1022
|
+
}
|
|
1023
|
+
.validation-banner .vb-dismiss:hover { background: rgba(255,255,255,0.2); }
|
|
1024
|
+
.validation-banner .vb-error { color: #fca5a5; }
|
|
1025
|
+
.validation-banner .vb-warn { color: #fde68a; }
|
|
1026
|
+
|
|
1027
|
+
/* Debug panel */
|
|
1028
|
+
.debug-panel {
|
|
1029
|
+
position: fixed; bottom: 0; right: 0; z-index: 99998;
|
|
1030
|
+
width: 380px; max-height: 60vh; background: #1e1b4b; color: #e0e7ff;
|
|
1031
|
+
font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px;
|
|
1032
|
+
border-top-left-radius: 12px; overflow: hidden;
|
|
1033
|
+
box-shadow: -4px -4px 24px rgba(0,0,0,0.3); display: none;
|
|
1034
|
+
}
|
|
1035
|
+
.debug-panel.open { display: flex; flex-direction: column; }
|
|
1036
|
+
.debug-panel .dp-header {
|
|
1037
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
1038
|
+
padding: 8px 12px; background: #312e81; border-bottom: 1px solid #4338ca;
|
|
1039
|
+
font-weight: 600; font-size: 12px;
|
|
1040
|
+
}
|
|
1041
|
+
.debug-panel .dp-close {
|
|
1042
|
+
background: none; border: none; color: #a5b4fc; cursor: pointer; font-size: 14px;
|
|
1043
|
+
}
|
|
1044
|
+
.debug-panel .dp-body {
|
|
1045
|
+
padding: 10px 12px; overflow-y: auto; flex: 1;
|
|
1046
|
+
}
|
|
1047
|
+
.debug-panel .dp-section { margin-bottom: 10px; }
|
|
1048
|
+
.debug-panel .dp-label { color: #818cf8; font-weight: 600; font-size: 10px; text-transform: uppercase; margin-bottom: 4px; }
|
|
1049
|
+
.debug-panel .dp-badge {
|
|
1050
|
+
display: inline-block; background: #4338ca; color: #c7d2fe; padding: 1px 8px;
|
|
1051
|
+
border-radius: 4px; font-size: 11px; font-weight: 600;
|
|
1052
|
+
}
|
|
1053
|
+
.debug-panel pre { margin: 0; white-space: pre-wrap; word-break: break-all; color: #c7d2fe; line-height: 1.5; }
|
|
1054
|
+
.dev-banner .stub-tag.debug-btn { cursor: pointer; transition: all 0.15s ease; }
|
|
1055
|
+
.dev-banner .stub-tag.debug-btn:hover { background: rgba(255,255,255,0.2); color: #a5b4fc; }
|
|
930
1056
|
</style>
|
|
931
1057
|
</head>
|
|
932
1058
|
<body>
|
|
@@ -935,9 +1061,10 @@ function buildPreviewHtml(schema, port) {
|
|
|
935
1061
|
<span class="label">LOCAL DEV</span>
|
|
936
1062
|
<span class="slug">${schema.slug || "catalog"}</span>
|
|
937
1063
|
<span class="stub-tags">
|
|
938
|
-
<span class="stub-tag"
|
|
939
|
-
<span class="stub-tag">
|
|
1064
|
+
<span class="stub-tag" id="checkout-tag">${devConfig?.stripeEnabled ? "Checkout: live (test)" : "Checkout: stubbed"}</span>
|
|
1065
|
+
<span class="stub-tag">Events: local</span>
|
|
940
1066
|
<span class="stub-tag clickable" id="pages-btn">Pages</span>
|
|
1067
|
+
<span class="stub-tag debug-btn" id="debug-btn">Debug</span>
|
|
941
1068
|
</span>
|
|
942
1069
|
</div>
|
|
943
1070
|
<div class="pages-overlay" id="pages-overlay">
|
|
@@ -948,8 +1075,15 @@ function buildPreviewHtml(schema, port) {
|
|
|
948
1075
|
<div class="inspector-tooltip" id="inspector-tooltip" style="display:none"></div>
|
|
949
1076
|
<div class="inspector-active-banner" id="inspector-banner" style="display:none">Inspector active — hover elements, click to copy</div>
|
|
950
1077
|
<div id="catalog-root"></div>
|
|
1078
|
+
<div class="debug-panel" id="debug-panel">
|
|
1079
|
+
<div class="dp-header"><span>Debug Panel</span><button class="dp-close" id="debug-close">×</button></div>
|
|
1080
|
+
<div class="dp-body" id="debug-body"></div>
|
|
1081
|
+
</div>
|
|
1082
|
+
<div class="validation-banner" id="validation-banner" style="display:none"></div>
|
|
951
1083
|
|
|
952
1084
|
<script id="__catalog_data" type="application/json">${schemaJson}</script>
|
|
1085
|
+
<script id="__validation_data" type="application/json">${JSON.stringify(validation || { errors: [], warnings: [] })}</script>
|
|
1086
|
+
<script id="__dev_config" type="application/json">${JSON.stringify(devConfig || { stripeEnabled: false, port })}</script>
|
|
953
1087
|
|
|
954
1088
|
<script type="module">
|
|
955
1089
|
import React from 'https://esm.sh/react@18';
|
|
@@ -959,6 +1093,61 @@ function buildPreviewHtml(schema, port) {
|
|
|
959
1093
|
const schema = JSON.parse(document.getElementById('__catalog_data').textContent);
|
|
960
1094
|
const themeColor = '${themeColor}';
|
|
961
1095
|
|
|
1096
|
+
// --- Visibility condition engine (ported from shared/engine/conditions.ts) ---
|
|
1097
|
+
const devContext = (() => {
|
|
1098
|
+
const params = {};
|
|
1099
|
+
new URLSearchParams(window.location.search).forEach((v, k) => { params[k] = v; });
|
|
1100
|
+
return { url_params: params, hints: {}, quiz_scores: null, video_state: null };
|
|
1101
|
+
})();
|
|
1102
|
+
|
|
1103
|
+
function resolveConditionValue(rule, formState, ctx) {
|
|
1104
|
+
const source = rule.source || 'field';
|
|
1105
|
+
if (source === 'field') return rule.field != null ? formState[rule.field] : undefined;
|
|
1106
|
+
if (source === 'url_param') return rule.param != null ? ctx.url_params[rule.param] : undefined;
|
|
1107
|
+
if (source === 'hint') return rule.param != null ? ctx.hints[rule.param] : undefined;
|
|
1108
|
+
if (source === 'score') return 0;
|
|
1109
|
+
if (source === 'video') return undefined;
|
|
1110
|
+
return undefined;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function applyConditionOperator(op, actual, expected) {
|
|
1114
|
+
switch (op) {
|
|
1115
|
+
case 'equals': return String(actual ?? '') === String(expected ?? '');
|
|
1116
|
+
case 'not_equals': return String(actual ?? '') !== String(expected ?? '');
|
|
1117
|
+
case 'contains':
|
|
1118
|
+
if (typeof actual === 'string') return actual.includes(String(expected));
|
|
1119
|
+
if (Array.isArray(actual)) return actual.includes(expected);
|
|
1120
|
+
return false;
|
|
1121
|
+
case 'not_contains':
|
|
1122
|
+
if (typeof actual === 'string') return !actual.includes(String(expected));
|
|
1123
|
+
if (Array.isArray(actual)) return !actual.includes(expected);
|
|
1124
|
+
return true;
|
|
1125
|
+
case 'greater_than': return Number(actual) > Number(expected);
|
|
1126
|
+
case 'greater_than_or_equal': return Number(actual) >= Number(expected);
|
|
1127
|
+
case 'less_than': return Number(actual) < Number(expected);
|
|
1128
|
+
case 'less_than_or_equal': return Number(actual) <= Number(expected);
|
|
1129
|
+
case 'is_empty': return actual == null || actual === '' || (Array.isArray(actual) && actual.length === 0);
|
|
1130
|
+
case 'is_not_empty': return !(actual == null || actual === '' || (Array.isArray(actual) && actual.length === 0));
|
|
1131
|
+
case 'matches_regex':
|
|
1132
|
+
try { const p = String(expected); if (p.length > 200) return false; return new RegExp(p).test(String(actual ?? '')); } catch { return false; }
|
|
1133
|
+
case 'in':
|
|
1134
|
+
if (Array.isArray(expected)) return expected.includes(actual);
|
|
1135
|
+
if (typeof expected === 'string') return expected.split(',').map(s => s.trim()).includes(String(actual));
|
|
1136
|
+
return false;
|
|
1137
|
+
default: return false;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function evaluateConditionGroup(group, formState, ctx) {
|
|
1142
|
+
if (!group || !group.rules) return true;
|
|
1143
|
+
const evaluate = (item) => {
|
|
1144
|
+
if (item.match && item.rules) return evaluateConditionGroup(item, formState, ctx);
|
|
1145
|
+
const actual = resolveConditionValue(item, formState, ctx);
|
|
1146
|
+
return applyConditionOperator(item.operator, actual, item.value);
|
|
1147
|
+
};
|
|
1148
|
+
return group.match === 'all' ? group.rules.every(evaluate) : group.rules.some(evaluate);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
962
1151
|
// --- Markdown-ish text rendering ---
|
|
963
1152
|
function inlineMarkdown(text) {
|
|
964
1153
|
return text
|
|
@@ -1260,13 +1449,7 @@ function buildPreviewHtml(schema, port) {
|
|
|
1260
1449
|
return h(SwitchInput, { comp, type, formState, onFieldChange, isCover, compClass, compStyle });
|
|
1261
1450
|
|
|
1262
1451
|
case 'payment':
|
|
1263
|
-
return h(
|
|
1264
|
-
h('h3', null, 'Stripe Checkout (Dev Stub)'),
|
|
1265
|
-
h('p', null, 'Payment processing is disabled in local dev mode.'),
|
|
1266
|
-
props.amount ? h('p', { className: 'mt-2 font-bold text-lg' },
|
|
1267
|
-
(props.currency || 'USD').toUpperCase() + ' ' + (props.amount / 100).toFixed(2)
|
|
1268
|
-
) : null
|
|
1269
|
-
);
|
|
1452
|
+
return h(PaymentComponent, { comp, formState, isCover, compClass, compStyle });
|
|
1270
1453
|
|
|
1271
1454
|
default:
|
|
1272
1455
|
return h('div', {
|
|
@@ -1427,6 +1610,102 @@ function buildPreviewHtml(schema, port) {
|
|
|
1427
1610
|
);
|
|
1428
1611
|
}
|
|
1429
1612
|
|
|
1613
|
+
// --- Dev Config ---
|
|
1614
|
+
const devConfig = JSON.parse(document.getElementById('__dev_config').textContent);
|
|
1615
|
+
|
|
1616
|
+
// --- Local Event Emitter ---
|
|
1617
|
+
const devEvents = {
|
|
1618
|
+
_sse: null,
|
|
1619
|
+
init() {
|
|
1620
|
+
this._sse = new EventSource('/__dev_events_stream');
|
|
1621
|
+
this._sse.onerror = () => {};
|
|
1622
|
+
},
|
|
1623
|
+
emit(type, data) {
|
|
1624
|
+
const event = { type, timestamp: new Date().toISOString(), data };
|
|
1625
|
+
// Fire to local SSE listeners (agents can listen via /__dev_events_stream)
|
|
1626
|
+
fetch('/__dev_event', {
|
|
1627
|
+
method: 'POST',
|
|
1628
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1629
|
+
body: JSON.stringify(event),
|
|
1630
|
+
}).catch(() => {});
|
|
1631
|
+
// Also dispatch as browser event for debug panel
|
|
1632
|
+
window.dispatchEvent(new CustomEvent('devEvent', { detail: event }));
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
devEvents.init();
|
|
1636
|
+
|
|
1637
|
+
// --- Payment Component ---
|
|
1638
|
+
function PaymentComponent({ comp, formState, isCover, compClass, compStyle }) {
|
|
1639
|
+
const props = comp.props || {};
|
|
1640
|
+
const [loading, setLoading] = React.useState(false);
|
|
1641
|
+
const [error, setError] = React.useState(null);
|
|
1642
|
+
|
|
1643
|
+
const handleCheckout = React.useCallback(async () => {
|
|
1644
|
+
devEvents.emit('checkout_started', { component_id: comp.id, amount: props.amount, currency: props.currency });
|
|
1645
|
+
setLoading(true);
|
|
1646
|
+
setError(null);
|
|
1647
|
+
try {
|
|
1648
|
+
const res = await fetch('/__dev_checkout', {
|
|
1649
|
+
method: 'POST',
|
|
1650
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1651
|
+
body: JSON.stringify({
|
|
1652
|
+
line_items: [{
|
|
1653
|
+
title: props.title || comp.id,
|
|
1654
|
+
amount_cents: props.amount,
|
|
1655
|
+
currency: (props.currency || 'usd').toLowerCase(),
|
|
1656
|
+
quantity: 1,
|
|
1657
|
+
stripe_price_id: props.stripe_price_id,
|
|
1658
|
+
payment_type: props.checkout_type === 'redirect' ? 'one_time' : (schema.settings?.checkout?.payment_type || 'one_time'),
|
|
1659
|
+
}],
|
|
1660
|
+
form_state: formState,
|
|
1661
|
+
catalog_slug: schema.slug,
|
|
1662
|
+
}),
|
|
1663
|
+
});
|
|
1664
|
+
const data = await res.json();
|
|
1665
|
+
if (data.session_url) {
|
|
1666
|
+
devEvents.emit('checkout_redirect', { session_id: data.session_id });
|
|
1667
|
+
window.location.href = data.session_url;
|
|
1668
|
+
} else if (data.error) {
|
|
1669
|
+
setError(data.error);
|
|
1670
|
+
}
|
|
1671
|
+
} catch (err) {
|
|
1672
|
+
setError(err.message || 'Checkout failed');
|
|
1673
|
+
} finally {
|
|
1674
|
+
setLoading(false);
|
|
1675
|
+
}
|
|
1676
|
+
}, [comp.id, props, formState]);
|
|
1677
|
+
|
|
1678
|
+
// No Stripe key \u2014 show informative stub
|
|
1679
|
+
if (!devConfig.stripeEnabled) {
|
|
1680
|
+
return h('div', { className: 'checkout-stub ' + compClass, style: compStyle },
|
|
1681
|
+
h('h3', null, 'Stripe Checkout'),
|
|
1682
|
+
h('p', null, 'Add STRIPE_SECRET_KEY to your .env to enable real checkout in dev.'),
|
|
1683
|
+
props.amount ? h('p', { className: 'mt-2 font-bold text-lg' },
|
|
1684
|
+
(props.currency || 'USD').toUpperCase() + ' ' + (props.amount / 100).toFixed(2)
|
|
1685
|
+
) : null,
|
|
1686
|
+
h('details', { className: 'mt-3 text-left', style: { fontSize: '11px', color: '#92400e' } },
|
|
1687
|
+
h('summary', { style: { cursor: 'pointer' } }, 'Checkout payload'),
|
|
1688
|
+
h('pre', { style: { whiteSpace: 'pre-wrap', marginTop: '4px' } },
|
|
1689
|
+
JSON.stringify({ amount: props.amount, currency: props.currency, stripe_price_id: props.stripe_price_id, payment_type: schema.settings?.checkout?.payment_type }, null, 2)
|
|
1690
|
+
)
|
|
1691
|
+
)
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// Real Stripe checkout
|
|
1696
|
+
return h('div', { className: compClass, style: compStyle },
|
|
1697
|
+
h('button', {
|
|
1698
|
+
className: 'cf-btn-primary w-full text-white',
|
|
1699
|
+
style: { backgroundColor: themeColor, opacity: loading ? 0.7 : 1 },
|
|
1700
|
+
onClick: handleCheckout,
|
|
1701
|
+
disabled: loading,
|
|
1702
|
+
},
|
|
1703
|
+
loading ? 'Redirecting to Stripe...' : (props.button_text || (props.amount ? ((props.currency || 'USD').toUpperCase() + ' ' + (props.amount / 100).toFixed(2) + ' \u2014 Pay Now') : 'Checkout'))
|
|
1704
|
+
),
|
|
1705
|
+
error ? h('p', { className: 'text-red-500 text-sm mt-2 text-center' }, error) : null
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1430
1709
|
// --- Cart Components ---
|
|
1431
1710
|
const cartIconPath = 'M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 00-16.536-1.84M7.5 14.25L5.106 5.272M6 20.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm12.75 0a.75.75 0 11-1.5 0 .75.75 0 011.5 0z';
|
|
1432
1711
|
|
|
@@ -1522,15 +1801,76 @@ function buildPreviewHtml(schema, port) {
|
|
|
1522
1801
|
'Added to order'
|
|
1523
1802
|
)
|
|
1524
1803
|
),
|
|
1525
|
-
h(
|
|
1526
|
-
h('h3', null, 'Checkout (Dev Stub)'),
|
|
1527
|
-
h('p', null, 'Payment processing is disabled in local dev mode.')
|
|
1528
|
-
)
|
|
1804
|
+
h(CartCheckoutButton, { items, themeColor })
|
|
1529
1805
|
) : null
|
|
1530
1806
|
)
|
|
1531
1807
|
);
|
|
1532
1808
|
}
|
|
1533
1809
|
|
|
1810
|
+
function CartCheckoutButton({ items, themeColor }) {
|
|
1811
|
+
const [loading, setLoading] = React.useState(false);
|
|
1812
|
+
const [error, setError] = React.useState(null);
|
|
1813
|
+
|
|
1814
|
+
const handleCheckout = React.useCallback(async () => {
|
|
1815
|
+
const state = window.__devDebugState;
|
|
1816
|
+
devEvents.emit('cart_checkout_started', { items: items.map(i => ({ offer_id: i.offer_id, title: i.title })) });
|
|
1817
|
+
setLoading(true);
|
|
1818
|
+
setError(null);
|
|
1819
|
+
try {
|
|
1820
|
+
const res = await fetch('/__dev_checkout', {
|
|
1821
|
+
method: 'POST',
|
|
1822
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1823
|
+
body: JSON.stringify({
|
|
1824
|
+
line_items: items.map(item => ({
|
|
1825
|
+
title: item.title,
|
|
1826
|
+
amount_cents: item.amount_cents,
|
|
1827
|
+
currency: item.currency || 'usd',
|
|
1828
|
+
quantity: 1,
|
|
1829
|
+
stripe_price_id: item.stripe_price_id,
|
|
1830
|
+
payment_type: schema.settings?.checkout?.payment_type || 'one_time',
|
|
1831
|
+
})),
|
|
1832
|
+
form_state: state?.formState || {},
|
|
1833
|
+
catalog_slug: schema.slug,
|
|
1834
|
+
}),
|
|
1835
|
+
});
|
|
1836
|
+
const data = await res.json();
|
|
1837
|
+
if (data.session_url) {
|
|
1838
|
+
devEvents.emit('checkout_redirect', { session_id: data.session_id });
|
|
1839
|
+
window.location.href = data.session_url;
|
|
1840
|
+
} else if (data.error) {
|
|
1841
|
+
setError(data.error);
|
|
1842
|
+
}
|
|
1843
|
+
} catch (err) {
|
|
1844
|
+
setError(err.message || 'Checkout failed');
|
|
1845
|
+
} finally {
|
|
1846
|
+
setLoading(false);
|
|
1847
|
+
}
|
|
1848
|
+
}, [items]);
|
|
1849
|
+
|
|
1850
|
+
if (!devConfig.stripeEnabled) {
|
|
1851
|
+
return h('div', { className: 'checkout-stub' },
|
|
1852
|
+
h('h3', null, 'Checkout'),
|
|
1853
|
+
h('p', null, 'Add STRIPE_SECRET_KEY to .env for real checkout.'),
|
|
1854
|
+
h('details', { className: 'mt-2 text-left', style: { fontSize: '11px', color: '#92400e' } },
|
|
1855
|
+
h('summary', { style: { cursor: 'pointer' } }, 'Cart payload'),
|
|
1856
|
+
h('pre', { style: { whiteSpace: 'pre-wrap', marginTop: '4px' } },
|
|
1857
|
+
JSON.stringify(items.map(i => ({ offer_id: i.offer_id, title: i.title, price: i.price_display })), null, 2)
|
|
1858
|
+
)
|
|
1859
|
+
)
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
return h('div', { className: 'space-y-2' },
|
|
1864
|
+
h('button', {
|
|
1865
|
+
className: 'cf-btn-primary w-full text-white',
|
|
1866
|
+
style: { backgroundColor: themeColor, opacity: loading ? 0.7 : 1 },
|
|
1867
|
+
onClick: handleCheckout,
|
|
1868
|
+
disabled: loading,
|
|
1869
|
+
}, loading ? 'Redirecting to Stripe...' : 'Proceed to Checkout'),
|
|
1870
|
+
error ? h('p', { className: 'text-red-500 text-sm text-center' }, error) : null
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1534
1874
|
// --- Main App ---
|
|
1535
1875
|
function CatalogPreview({ catalog }) {
|
|
1536
1876
|
const pages = catalog.pages || {};
|
|
@@ -1587,12 +1927,20 @@ function buildPreviewHtml(schema, port) {
|
|
|
1587
1927
|
}
|
|
1588
1928
|
}, [formState, pages, cartItems, addToCart, removeFromCart]);
|
|
1589
1929
|
|
|
1590
|
-
// Expose navigation for mindmap
|
|
1930
|
+
// Expose navigation for mindmap + emit page_view
|
|
1591
1931
|
React.useEffect(() => {
|
|
1592
1932
|
window.__devNavigateTo = (id) => { setCurrentPageId(id); setHistory([]); window.scrollTo({ top: 0, behavior: 'smooth' }); };
|
|
1593
1933
|
window.__devSetCurrentPage && window.__devSetCurrentPage(currentPageId);
|
|
1934
|
+
devEvents.emit('page_view', { page_id: currentPageId, catalog_slug: catalog.slug });
|
|
1594
1935
|
}, [currentPageId]);
|
|
1595
1936
|
|
|
1937
|
+
// Expose debug state
|
|
1938
|
+
React.useEffect(() => {
|
|
1939
|
+
const edges = (routing.edges || []).filter(e => e.from === currentPageId);
|
|
1940
|
+
window.__devDebugState = { currentPageId, formState, cartItems, edges };
|
|
1941
|
+
window.dispatchEvent(new CustomEvent('devStateUpdate'));
|
|
1942
|
+
}, [currentPageId, formState, cartItems, routing]);
|
|
1943
|
+
|
|
1596
1944
|
const page = currentPageId ? pages[currentPageId] : null;
|
|
1597
1945
|
const isCover = page?.layout === 'cover';
|
|
1598
1946
|
const isLastPage = (() => {
|
|
@@ -1602,7 +1950,8 @@ function buildPreviewHtml(schema, port) {
|
|
|
1602
1950
|
|
|
1603
1951
|
const onFieldChange = React.useCallback((id, value) => {
|
|
1604
1952
|
setFormState(prev => ({ ...prev, [id]: value }));
|
|
1605
|
-
|
|
1953
|
+
devEvents.emit('field_change', { field_id: id, value, page_id: currentPageId });
|
|
1954
|
+
}, [currentPageId]);
|
|
1606
1955
|
|
|
1607
1956
|
const handleNext = React.useCallback(() => {
|
|
1608
1957
|
// Check if page has an offer \u2014 treat "Next" as an accept action
|
|
@@ -1637,7 +1986,11 @@ function buildPreviewHtml(schema, port) {
|
|
|
1637
1986
|
);
|
|
1638
1987
|
}
|
|
1639
1988
|
|
|
1640
|
-
const components = (page.components || []).filter(c =>
|
|
1989
|
+
const components = (page.components || []).filter(c => {
|
|
1990
|
+
if (c.hidden || c.props?.hidden) return false;
|
|
1991
|
+
if (c.visibility) return evaluateConditionGroup(c.visibility, formState, devContext);
|
|
1992
|
+
return true;
|
|
1993
|
+
});
|
|
1641
1994
|
const bgImage = page.background_image || catalog.settings?.theme?.background_image;
|
|
1642
1995
|
|
|
1643
1996
|
// Cart UI (shared between cover and standard)
|
|
@@ -1987,18 +2340,108 @@ function buildPreviewHtml(schema, port) {
|
|
|
1987
2340
|
});
|
|
1988
2341
|
}, true);
|
|
1989
2342
|
})();
|
|
2343
|
+
|
|
2344
|
+
// --- Validation banner ---
|
|
2345
|
+
(function initValidationBanner() {
|
|
2346
|
+
const banner = document.getElementById('validation-banner');
|
|
2347
|
+
const validation = JSON.parse(document.getElementById('__validation_data').textContent);
|
|
2348
|
+
const errors = validation.errors || [];
|
|
2349
|
+
const warnings = validation.warnings || [];
|
|
2350
|
+
if (errors.length === 0 && warnings.length === 0) return;
|
|
2351
|
+
let html = '<div class="vb-header"><strong>' + errors.length + ' error(s), ' + warnings.length + ' warning(s)</strong><button class="vb-dismiss" id="vb-dismiss">Dismiss</button></div>';
|
|
2352
|
+
for (const e of errors) html += '<div class="vb-error">ERROR: ' + e + '</div>';
|
|
2353
|
+
for (const w of warnings) html += '<div class="vb-warn">WARN: ' + w + '</div>';
|
|
2354
|
+
banner.innerHTML = html;
|
|
2355
|
+
banner.style.display = 'block';
|
|
2356
|
+
document.getElementById('vb-dismiss').addEventListener('click', () => { banner.style.display = 'none'; });
|
|
2357
|
+
})();
|
|
2358
|
+
|
|
2359
|
+
// --- Debug panel ---
|
|
2360
|
+
(function initDebugPanel() {
|
|
2361
|
+
const panel = document.getElementById('debug-panel');
|
|
2362
|
+
const body = document.getElementById('debug-body');
|
|
2363
|
+
const btn = document.getElementById('debug-btn');
|
|
2364
|
+
const closeBtn = document.getElementById('debug-close');
|
|
2365
|
+
let isOpen = false;
|
|
2366
|
+
|
|
2367
|
+
function toggle() {
|
|
2368
|
+
isOpen = !isOpen;
|
|
2369
|
+
panel.classList.toggle('open', isOpen);
|
|
2370
|
+
if (isOpen) render();
|
|
2371
|
+
}
|
|
2372
|
+
btn.addEventListener('click', toggle);
|
|
2373
|
+
closeBtn.addEventListener('click', toggle);
|
|
2374
|
+
|
|
2375
|
+
document.addEventListener('keydown', (e) => {
|
|
2376
|
+
if (e.ctrlKey && e.key === 'd') { e.preventDefault(); toggle(); }
|
|
2377
|
+
});
|
|
2378
|
+
|
|
2379
|
+
const recentEvents = [];
|
|
2380
|
+
window.addEventListener('devEvent', (e) => {
|
|
2381
|
+
recentEvents.push(e.detail);
|
|
2382
|
+
if (recentEvents.length > 20) recentEvents.shift();
|
|
2383
|
+
if (isOpen) render();
|
|
2384
|
+
});
|
|
2385
|
+
|
|
2386
|
+
function render() {
|
|
2387
|
+
const state = window.__devDebugState;
|
|
2388
|
+
if (!state) { body.innerHTML = '<p>Waiting for state...</p>'; return; }
|
|
2389
|
+
let html = '<div class="dp-section"><div class="dp-label">Current Page</div><span class="dp-badge">' + (state.currentPageId || 'none') + '</span></div>';
|
|
2390
|
+
html += '<div class="dp-section"><div class="dp-label">Form State</div><pre>' + JSON.stringify(state.formState || {}, null, 2) + '</pre></div>';
|
|
2391
|
+
html += '<div class="dp-section"><div class="dp-label">Cart (' + (state.cartItems?.length || 0) + ')</div>';
|
|
2392
|
+
if (state.cartItems && state.cartItems.length > 0) {
|
|
2393
|
+
html += '<pre>' + JSON.stringify(state.cartItems.map(i => i.title || i.offer_id), null, 2) + '</pre>';
|
|
2394
|
+
} else {
|
|
2395
|
+
html += '<pre>empty</pre>';
|
|
2396
|
+
}
|
|
2397
|
+
html += '</div>';
|
|
2398
|
+
html += '<div class="dp-section"><div class="dp-label">Edges from here</div>';
|
|
2399
|
+
if (state.edges && state.edges.length > 0) {
|
|
2400
|
+
html += '<pre>' + state.edges.map(e => e.from + ' \u2192 ' + e.to + (e.conditions?.length ? ' (conditional)' : '')).join('\\n') + '</pre>';
|
|
2401
|
+
} else {
|
|
2402
|
+
html += '<pre>none (terminal page)</pre>';
|
|
2403
|
+
}
|
|
2404
|
+
html += '</div>';
|
|
2405
|
+
html += '<div class="dp-section"><div class="dp-label">Recent Events (' + recentEvents.length + ')</div>';
|
|
2406
|
+
if (recentEvents.length > 0) {
|
|
2407
|
+
html += '<pre>' + recentEvents.slice(-8).map(e => e.type + ' ' + JSON.stringify(e.data || {})).join('\\n') + '</pre>';
|
|
2408
|
+
} else {
|
|
2409
|
+
html += '<pre>none yet</pre>';
|
|
2410
|
+
}
|
|
2411
|
+
html += '</div>';
|
|
2412
|
+
body.innerHTML = html;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
window.addEventListener('devStateUpdate', () => { if (isOpen) render(); });
|
|
2416
|
+
})();
|
|
1990
2417
|
</script>
|
|
1991
2418
|
</body>
|
|
1992
2419
|
</html>`;
|
|
1993
2420
|
}
|
|
1994
2421
|
async function catalogDev(file, opts) {
|
|
1995
|
-
const abs =
|
|
2422
|
+
const abs = resolve4(file);
|
|
1996
2423
|
const catalogDir = dirname2(abs);
|
|
1997
2424
|
const port = parseInt(opts.port || String(DEFAULT_PORT), 10);
|
|
1998
2425
|
if (!existsSync2(abs)) {
|
|
1999
2426
|
console.error(`File not found: ${file}`);
|
|
2000
2427
|
process.exit(1);
|
|
2001
2428
|
}
|
|
2429
|
+
let stripeSecretKey = process.env.STRIPE_SECRET_KEY || "";
|
|
2430
|
+
if (!stripeSecretKey) {
|
|
2431
|
+
for (const envFile of [".env", ".env.local", ".env.development"]) {
|
|
2432
|
+
const envPath = join2(catalogDir, envFile);
|
|
2433
|
+
if (existsSync2(envPath)) {
|
|
2434
|
+
const envContent = readFileSync4(envPath, "utf-8");
|
|
2435
|
+
const match = envContent.match(/^STRIPE_SECRET_KEY\s*=\s*"?([^"\n]+)"?/m);
|
|
2436
|
+
if (match) {
|
|
2437
|
+
stripeSecretKey = match[1].trim();
|
|
2438
|
+
break;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
const stripeEnabled = stripeSecretKey.length > 0;
|
|
2444
|
+
const devConfig = { stripeEnabled, port };
|
|
2002
2445
|
const spinner = ora5("Loading catalog schema...").start();
|
|
2003
2446
|
let schema;
|
|
2004
2447
|
try {
|
|
@@ -2007,14 +2450,15 @@ async function catalogDev(file, opts) {
|
|
|
2007
2450
|
spinner.fail(`Failed to load catalog: ${err.message}`);
|
|
2008
2451
|
process.exit(1);
|
|
2009
2452
|
}
|
|
2010
|
-
const
|
|
2011
|
-
if (
|
|
2453
|
+
const basicErrors = validateCatalog(schema);
|
|
2454
|
+
if (basicErrors.length > 0) {
|
|
2012
2455
|
spinner.fail("Schema validation errors:");
|
|
2013
|
-
for (const err of
|
|
2456
|
+
for (const err of basicErrors) {
|
|
2014
2457
|
console.error(` - ${err}`);
|
|
2015
2458
|
}
|
|
2016
2459
|
process.exit(1);
|
|
2017
2460
|
}
|
|
2461
|
+
let validation = deepValidateCatalog(schema);
|
|
2018
2462
|
const localBaseUrl = `http://localhost:${port}/assets`;
|
|
2019
2463
|
schema = resolveLocalAssets(schema, catalogDir, localBaseUrl);
|
|
2020
2464
|
spinner.succeed(`Loaded: ${schema.slug || file}`);
|
|
@@ -2022,6 +2466,8 @@ async function catalogDev(file, opts) {
|
|
|
2022
2466
|
console.log(` Entry: ${schema.routing?.entry || "first page"}`);
|
|
2023
2467
|
console.log();
|
|
2024
2468
|
const sseClients = /* @__PURE__ */ new Set();
|
|
2469
|
+
const eventSseClients = /* @__PURE__ */ new Set();
|
|
2470
|
+
const eventLog = [];
|
|
2025
2471
|
function notifyReload() {
|
|
2026
2472
|
sseClients.forEach((client) => {
|
|
2027
2473
|
try {
|
|
@@ -2031,6 +2477,22 @@ async function catalogDev(file, opts) {
|
|
|
2031
2477
|
}
|
|
2032
2478
|
});
|
|
2033
2479
|
}
|
|
2480
|
+
function emitDevEvent(event) {
|
|
2481
|
+
eventLog.push(event);
|
|
2482
|
+
if (eventLog.length > 500) eventLog.shift();
|
|
2483
|
+
const data = JSON.stringify(event);
|
|
2484
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
|
|
2485
|
+
console.log(` \x1B[36m[event]\x1B[0m ${ts} ${event.type} ${JSON.stringify(event.data || {})}`);
|
|
2486
|
+
eventSseClients.forEach((client) => {
|
|
2487
|
+
try {
|
|
2488
|
+
client.write(`data: ${data}
|
|
2489
|
+
|
|
2490
|
+
`);
|
|
2491
|
+
} catch {
|
|
2492
|
+
eventSseClients.delete(client);
|
|
2493
|
+
}
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2034
2496
|
const server = createServer(async (req, res) => {
|
|
2035
2497
|
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
2036
2498
|
if (url.pathname === "/__dev_sse") {
|
|
@@ -2045,11 +2507,127 @@ async function catalogDev(file, opts) {
|
|
|
2045
2507
|
req.on("close", () => sseClients.delete(res));
|
|
2046
2508
|
return;
|
|
2047
2509
|
}
|
|
2510
|
+
if (url.pathname === "/__dev_events_stream") {
|
|
2511
|
+
res.writeHead(200, {
|
|
2512
|
+
"Content-Type": "text/event-stream",
|
|
2513
|
+
"Cache-Control": "no-cache",
|
|
2514
|
+
"Connection": "keep-alive",
|
|
2515
|
+
"Access-Control-Allow-Origin": "*"
|
|
2516
|
+
});
|
|
2517
|
+
res.write("data: connected\n\n");
|
|
2518
|
+
eventSseClients.add(res);
|
|
2519
|
+
req.on("close", () => eventSseClients.delete(res));
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
if (url.pathname === "/__dev_events" && req.method === "GET") {
|
|
2523
|
+
res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
|
|
2524
|
+
const limit = parseInt(url.searchParams.get("limit") || "50", 10);
|
|
2525
|
+
res.end(JSON.stringify(eventLog.slice(-limit)));
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
if (url.pathname === "/__dev_event" && req.method === "POST") {
|
|
2529
|
+
let body = "";
|
|
2530
|
+
req.on("data", (chunk) => {
|
|
2531
|
+
body += chunk;
|
|
2532
|
+
});
|
|
2533
|
+
req.on("end", () => {
|
|
2534
|
+
try {
|
|
2535
|
+
const event = JSON.parse(body);
|
|
2536
|
+
emitDevEvent(event);
|
|
2537
|
+
res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
|
|
2538
|
+
res.end('{"ok":true}');
|
|
2539
|
+
} catch {
|
|
2540
|
+
res.writeHead(400);
|
|
2541
|
+
res.end('{"error":"invalid json"}');
|
|
2542
|
+
}
|
|
2543
|
+
});
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
if (url.pathname === "/__dev_checkout" && req.method === "POST") {
|
|
2547
|
+
let body = "";
|
|
2548
|
+
req.on("data", (chunk) => {
|
|
2549
|
+
body += chunk;
|
|
2550
|
+
});
|
|
2551
|
+
req.on("end", async () => {
|
|
2552
|
+
res.setHeader("Content-Type", "application/json");
|
|
2553
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
2554
|
+
if (!stripeEnabled) {
|
|
2555
|
+
res.writeHead(400);
|
|
2556
|
+
res.end(JSON.stringify({ error: "No STRIPE_SECRET_KEY found. Add it to .env in your catalog directory." }));
|
|
2557
|
+
return;
|
|
2558
|
+
}
|
|
2559
|
+
try {
|
|
2560
|
+
const { line_items, form_state, catalog_slug } = JSON.parse(body);
|
|
2561
|
+
const checkoutSettings = schema.settings?.checkout || {};
|
|
2562
|
+
const params = new URLSearchParams();
|
|
2563
|
+
params.set("mode", checkoutSettings.payment_type === "subscription" ? "subscription" : "payment");
|
|
2564
|
+
params.set("success_url", `http://localhost:${port}/?checkout=success&session_id={CHECKOUT_SESSION_ID}`);
|
|
2565
|
+
params.set("cancel_url", `http://localhost:${port}/?checkout=cancel`);
|
|
2566
|
+
const emailField = checkoutSettings.prefill_fields?.customer_email;
|
|
2567
|
+
if (emailField && form_state?.[emailField]) {
|
|
2568
|
+
params.set("customer_email", form_state[emailField]);
|
|
2569
|
+
}
|
|
2570
|
+
const methods = checkoutSettings.payment_methods || ["card"];
|
|
2571
|
+
methods.forEach((m, i) => params.set(`payment_method_types[${i}]`, m));
|
|
2572
|
+
if (checkoutSettings.allow_discount_codes) {
|
|
2573
|
+
params.set("allow_promotion_codes", "true");
|
|
2574
|
+
}
|
|
2575
|
+
line_items.forEach((item, i) => {
|
|
2576
|
+
if (item.stripe_price_id) {
|
|
2577
|
+
params.set(`line_items[${i}][price]`, item.stripe_price_id);
|
|
2578
|
+
params.set(`line_items[${i}][quantity]`, String(item.quantity || 1));
|
|
2579
|
+
} else if (item.amount_cents) {
|
|
2580
|
+
params.set(`line_items[${i}][price_data][currency]`, item.currency || "usd");
|
|
2581
|
+
params.set(`line_items[${i}][price_data][unit_amount]`, String(item.amount_cents));
|
|
2582
|
+
params.set(`line_items[${i}][price_data][product_data][name]`, item.title || "Item");
|
|
2583
|
+
if (checkoutSettings.payment_type === "subscription") {
|
|
2584
|
+
params.set(`line_items[${i}][price_data][recurring][interval]`, "month");
|
|
2585
|
+
}
|
|
2586
|
+
params.set(`line_items[${i}][quantity]`, String(item.quantity || 1));
|
|
2587
|
+
}
|
|
2588
|
+
});
|
|
2589
|
+
if (checkoutSettings.payment_type === "subscription" && checkoutSettings.free_trial?.enabled && checkoutSettings.free_trial.days) {
|
|
2590
|
+
params.set("subscription_data[trial_period_days]", String(checkoutSettings.free_trial.days));
|
|
2591
|
+
}
|
|
2592
|
+
const stripeRes = await fetch("https://api.stripe.com/v1/checkout/sessions", {
|
|
2593
|
+
method: "POST",
|
|
2594
|
+
headers: {
|
|
2595
|
+
"Authorization": `Basic ${Buffer.from(stripeSecretKey + ":").toString("base64")}`,
|
|
2596
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
2597
|
+
},
|
|
2598
|
+
body: params.toString()
|
|
2599
|
+
});
|
|
2600
|
+
const stripeData = await stripeRes.json();
|
|
2601
|
+
if (!stripeRes.ok) {
|
|
2602
|
+
emitDevEvent({ type: "checkout_error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), data: { error: stripeData.error?.message || "Stripe error" } });
|
|
2603
|
+
res.writeHead(stripeRes.status);
|
|
2604
|
+
res.end(JSON.stringify({ error: stripeData.error?.message || "Stripe session creation failed" }));
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
emitDevEvent({ type: "checkout_session_created", timestamp: (/* @__PURE__ */ new Date()).toISOString(), data: { session_id: stripeData.id, url: stripeData.url } });
|
|
2608
|
+
res.writeHead(200);
|
|
2609
|
+
res.end(JSON.stringify({ session_id: stripeData.id, session_url: stripeData.url }));
|
|
2610
|
+
} catch (err) {
|
|
2611
|
+
res.writeHead(500);
|
|
2612
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2613
|
+
}
|
|
2614
|
+
});
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
if (req.method === "OPTIONS") {
|
|
2618
|
+
res.writeHead(204, {
|
|
2619
|
+
"Access-Control-Allow-Origin": "*",
|
|
2620
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
2621
|
+
"Access-Control-Allow-Headers": "Content-Type"
|
|
2622
|
+
});
|
|
2623
|
+
res.end();
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
2048
2626
|
if (url.pathname.startsWith("/assets/")) {
|
|
2049
2627
|
const relativePath = decodeURIComponent(url.pathname.slice("/assets/".length));
|
|
2050
2628
|
const filePath = join2(catalogDir, relativePath);
|
|
2051
|
-
const resolved =
|
|
2052
|
-
if (!resolved.startsWith(
|
|
2629
|
+
const resolved = resolve4(filePath);
|
|
2630
|
+
if (!resolved.startsWith(resolve4(catalogDir))) {
|
|
2053
2631
|
res.writeHead(403);
|
|
2054
2632
|
res.end("Forbidden");
|
|
2055
2633
|
return;
|
|
@@ -2077,11 +2655,13 @@ async function catalogDev(file, opts) {
|
|
|
2077
2655
|
"Content-Type": "text/html; charset=utf-8",
|
|
2078
2656
|
"Cache-Control": "no-cache"
|
|
2079
2657
|
});
|
|
2080
|
-
res.end(buildPreviewHtml(schema, port));
|
|
2658
|
+
res.end(buildPreviewHtml(schema, port, validation, devConfig));
|
|
2081
2659
|
});
|
|
2082
2660
|
server.listen(port, () => {
|
|
2083
2661
|
console.log(` Local preview: http://localhost:${port}`);
|
|
2084
2662
|
console.log(` Assets served from: ${catalogDir}`);
|
|
2663
|
+
console.log(` Checkout: ${stripeEnabled ? `\x1B[32menabled\x1B[0m (${stripeSecretKey.startsWith("sk_test_") ? "test mode" : "live mode"})` : "\x1B[33mstubbed\x1B[0m \u2014 add STRIPE_SECRET_KEY to .env"}`);
|
|
2664
|
+
console.log(` Events: \x1B[32mlocal\x1B[0m \u2014 stream at http://localhost:${port}/__dev_events_stream`);
|
|
2085
2665
|
console.log(` Watching for changes...
|
|
2086
2666
|
`);
|
|
2087
2667
|
});
|
|
@@ -2098,6 +2678,7 @@ async function catalogDev(file, opts) {
|
|
|
2098
2678
|
for (const e of errs) console.error(` - ${e}`);
|
|
2099
2679
|
return;
|
|
2100
2680
|
}
|
|
2681
|
+
validation = deepValidateCatalog(schema);
|
|
2101
2682
|
schema = resolveLocalAssets(schema, catalogDir, localBaseUrl);
|
|
2102
2683
|
reloadSpinner.succeed(`Reloaded \u2014 auto-refreshing browser`);
|
|
2103
2684
|
notifyReload();
|
|
@@ -2108,7 +2689,7 @@ async function catalogDev(file, opts) {
|
|
|
2108
2689
|
});
|
|
2109
2690
|
try {
|
|
2110
2691
|
watch(catalogDir, { recursive: true }, (event, filename) => {
|
|
2111
|
-
if (!filename || filename.startsWith(".") ||
|
|
2692
|
+
if (!filename || filename.startsWith(".") || resolve4(join2(catalogDir, filename)) === abs) return;
|
|
2112
2693
|
});
|
|
2113
2694
|
} catch {
|
|
2114
2695
|
}
|
|
@@ -2119,6 +2700,439 @@ async function catalogDev(file, opts) {
|
|
|
2119
2700
|
});
|
|
2120
2701
|
}
|
|
2121
2702
|
|
|
2703
|
+
// src/commands/catalog-validate.ts
|
|
2704
|
+
import { resolve as resolve5 } from "path";
|
|
2705
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2706
|
+
import ora6 from "ora";
|
|
2707
|
+
async function catalogValidate(file) {
|
|
2708
|
+
const abs = resolve5(file);
|
|
2709
|
+
if (!existsSync3(abs)) {
|
|
2710
|
+
console.error(`File not found: ${file}`);
|
|
2711
|
+
process.exit(1);
|
|
2712
|
+
}
|
|
2713
|
+
const spinner = ora6("Loading catalog schema...").start();
|
|
2714
|
+
let schema;
|
|
2715
|
+
try {
|
|
2716
|
+
schema = await loadCatalogFile(abs);
|
|
2717
|
+
} catch (err) {
|
|
2718
|
+
spinner.fail(`Failed to load catalog: ${err.message}`);
|
|
2719
|
+
process.exit(1);
|
|
2720
|
+
}
|
|
2721
|
+
spinner.succeed(`Loaded: ${schema.slug || file}`);
|
|
2722
|
+
const basicErrors = validateCatalog(schema);
|
|
2723
|
+
const { errors: deepErrors, warnings } = deepValidateCatalog(schema);
|
|
2724
|
+
const allErrors = [...basicErrors, ...deepErrors];
|
|
2725
|
+
const pages = schema.pages || {};
|
|
2726
|
+
const pageCount = Object.keys(pages).length;
|
|
2727
|
+
const componentCount = Object.values(pages).reduce(
|
|
2728
|
+
(sum, p) => sum + (p.components?.length || 0),
|
|
2729
|
+
0
|
|
2730
|
+
);
|
|
2731
|
+
const edgeCount = schema.routing?.edges?.length || 0;
|
|
2732
|
+
const offerCount = Object.values(pages).filter((p) => p.offer).length;
|
|
2733
|
+
console.log();
|
|
2734
|
+
console.log(` Slug: ${schema.slug || "(none)"}`);
|
|
2735
|
+
console.log(` Version: ${schema.schema_version || "(none)"}`);
|
|
2736
|
+
console.log(` Pages: ${pageCount}`);
|
|
2737
|
+
console.log(` Components: ${componentCount}`);
|
|
2738
|
+
console.log(` Edges: ${edgeCount}`);
|
|
2739
|
+
console.log(` Offers: ${offerCount}`);
|
|
2740
|
+
console.log();
|
|
2741
|
+
if (allErrors.length > 0) {
|
|
2742
|
+
for (const err of allErrors) {
|
|
2743
|
+
console.error(` \x1B[31mERROR\x1B[0m ${err}`);
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
if (warnings.length > 0) {
|
|
2747
|
+
for (const warn of warnings) {
|
|
2748
|
+
console.warn(` \x1B[33mWARN\x1B[0m ${warn}`);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
if (allErrors.length === 0 && warnings.length === 0) {
|
|
2752
|
+
console.log(" \x1B[32m\u2713 No issues found\x1B[0m");
|
|
2753
|
+
} else {
|
|
2754
|
+
console.log();
|
|
2755
|
+
console.log(
|
|
2756
|
+
` ${allErrors.length} error(s), ${warnings.length} warning(s)`
|
|
2757
|
+
);
|
|
2758
|
+
}
|
|
2759
|
+
process.exit(allErrors.length > 0 ? 1 : 0);
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
// src/commands/catalog-init.ts
|
|
2763
|
+
import { mkdirSync, writeFileSync, existsSync as existsSync4 } from "fs";
|
|
2764
|
+
import { join as join3 } from "path";
|
|
2765
|
+
import { createInterface } from "readline";
|
|
2766
|
+
function ask(rl, question) {
|
|
2767
|
+
return new Promise((resolve7) => rl.question(question, resolve7));
|
|
2768
|
+
}
|
|
2769
|
+
var TEMPLATES = {
|
|
2770
|
+
"quiz-funnel": {
|
|
2771
|
+
description: "3-page quiz funnel: welcome cover, quiz question, result with offer",
|
|
2772
|
+
content: (slug) => `import type { CatalogSchema } from "@shared/types";
|
|
2773
|
+
|
|
2774
|
+
const catalog = {
|
|
2775
|
+
schema_version: "1.0",
|
|
2776
|
+
slug: "${slug}",
|
|
2777
|
+
settings: {
|
|
2778
|
+
theme: { primary_color: "#6366f1" },
|
|
2779
|
+
progress_steps: [
|
|
2780
|
+
{ id: "quiz", label: "Quiz", pages: ["welcome", "question"] },
|
|
2781
|
+
{ id: "result", label: "Result", pages: ["result"] },
|
|
2782
|
+
],
|
|
2783
|
+
},
|
|
2784
|
+
routing: {
|
|
2785
|
+
entry: "welcome",
|
|
2786
|
+
edges: [
|
|
2787
|
+
{ from: "welcome", to: "question" },
|
|
2788
|
+
{ from: "question", to: "result" },
|
|
2789
|
+
],
|
|
2790
|
+
},
|
|
2791
|
+
pages: {
|
|
2792
|
+
welcome: {
|
|
2793
|
+
layout: "cover",
|
|
2794
|
+
components: [
|
|
2795
|
+
{ id: "heading", type: "heading", props: { level: 1, text: "Find Your Perfect Plan", subtitle: "Answer a few quick questions to get a personalized recommendation." } },
|
|
2796
|
+
],
|
|
2797
|
+
},
|
|
2798
|
+
question: {
|
|
2799
|
+
title: "About You",
|
|
2800
|
+
components: [
|
|
2801
|
+
{ id: "role", type: "multiple_choice", props: { label: "What best describes you?", options: ["Solopreneur", "Agency", "E-commerce", "SaaS"], required: true } },
|
|
2802
|
+
],
|
|
2803
|
+
},
|
|
2804
|
+
result: {
|
|
2805
|
+
title: "Your Result",
|
|
2806
|
+
components: [
|
|
2807
|
+
{ id: "result_heading", type: "heading", props: { level: 2, text: "Here's our recommendation" } },
|
|
2808
|
+
{ id: "result_text", type: "paragraph", props: { text: "Based on your answers, we think this plan is perfect for you." } },
|
|
2809
|
+
],
|
|
2810
|
+
offer: {
|
|
2811
|
+
id: "main-offer",
|
|
2812
|
+
title: "Starter Plan",
|
|
2813
|
+
price_display: "$29/mo",
|
|
2814
|
+
},
|
|
2815
|
+
},
|
|
2816
|
+
},
|
|
2817
|
+
} satisfies CatalogSchema;
|
|
2818
|
+
|
|
2819
|
+
export default catalog;
|
|
2820
|
+
`
|
|
2821
|
+
},
|
|
2822
|
+
"lead-capture": {
|
|
2823
|
+
description: "2-page lead capture: info form + thank you",
|
|
2824
|
+
content: (slug) => `import type { CatalogSchema } from "@shared/types";
|
|
2825
|
+
|
|
2826
|
+
const catalog = {
|
|
2827
|
+
schema_version: "1.0",
|
|
2828
|
+
slug: "${slug}",
|
|
2829
|
+
settings: {
|
|
2830
|
+
theme: { primary_color: "#10b981" },
|
|
2831
|
+
},
|
|
2832
|
+
routing: {
|
|
2833
|
+
entry: "form",
|
|
2834
|
+
edges: [
|
|
2835
|
+
{ from: "form", to: "thank_you" },
|
|
2836
|
+
],
|
|
2837
|
+
},
|
|
2838
|
+
pages: {
|
|
2839
|
+
form: {
|
|
2840
|
+
title: "Get Started",
|
|
2841
|
+
components: [
|
|
2842
|
+
{ id: "heading", type: "heading", props: { level: 2, text: "Tell us about yourself" } },
|
|
2843
|
+
{ id: "name", type: "short_text", props: { label: "Full Name", placeholder: "Jane Doe", required: true } },
|
|
2844
|
+
{ id: "email", type: "email", props: { label: "Email", placeholder: "you@example.com", required: true } },
|
|
2845
|
+
{ id: "company", type: "short_text", props: { label: "Company", placeholder: "Acme Inc" } },
|
|
2846
|
+
],
|
|
2847
|
+
},
|
|
2848
|
+
thank_you: {
|
|
2849
|
+
title: "Thank You!",
|
|
2850
|
+
hide_navigation: true,
|
|
2851
|
+
components: [
|
|
2852
|
+
{ id: "thanks", type: "heading", props: { level: 2, text: "Thanks! We'll be in touch soon." } },
|
|
2853
|
+
{ id: "note", type: "paragraph", props: { text: "Check your inbox for a confirmation email." } },
|
|
2854
|
+
],
|
|
2855
|
+
},
|
|
2856
|
+
},
|
|
2857
|
+
} satisfies CatalogSchema;
|
|
2858
|
+
|
|
2859
|
+
export default catalog;
|
|
2860
|
+
`
|
|
2861
|
+
},
|
|
2862
|
+
"product-catalog": {
|
|
2863
|
+
description: "2-page product showcase: pricing cards + checkout",
|
|
2864
|
+
content: (slug) => `import type { CatalogSchema } from "@shared/types";
|
|
2865
|
+
|
|
2866
|
+
const catalog = {
|
|
2867
|
+
schema_version: "1.0",
|
|
2868
|
+
slug: "${slug}",
|
|
2869
|
+
settings: {
|
|
2870
|
+
theme: { primary_color: "#f59e0b" },
|
|
2871
|
+
},
|
|
2872
|
+
routing: {
|
|
2873
|
+
entry: "pricing",
|
|
2874
|
+
edges: [
|
|
2875
|
+
{ from: "pricing", to: "checkout" },
|
|
2876
|
+
],
|
|
2877
|
+
},
|
|
2878
|
+
pages: {
|
|
2879
|
+
pricing: {
|
|
2880
|
+
title: "Choose a Plan",
|
|
2881
|
+
components: [
|
|
2882
|
+
{ id: "heading", type: "heading", props: { level: 1, text: "Simple, transparent pricing" } },
|
|
2883
|
+
{ id: "starter", type: "pricing_card", props: { title: "Starter", price: "$19", period: "mo", features: ["5 projects", "Basic analytics", "Email support"], cta_text: "Get Started" } },
|
|
2884
|
+
{ id: "pro", type: "pricing_card", props: { title: "Pro", badge: "Popular", price: "$49", period: "mo", features: ["Unlimited projects", "Advanced analytics", "Priority support", "Custom domain"], cta_text: "Go Pro" } },
|
|
2885
|
+
],
|
|
2886
|
+
},
|
|
2887
|
+
checkout: {
|
|
2888
|
+
title: "Complete Your Order",
|
|
2889
|
+
components: [
|
|
2890
|
+
{ id: "checkout_pay", type: "payment", props: { amount: 4900, currency: "usd" } },
|
|
2891
|
+
],
|
|
2892
|
+
},
|
|
2893
|
+
},
|
|
2894
|
+
} satisfies CatalogSchema;
|
|
2895
|
+
|
|
2896
|
+
export default catalog;
|
|
2897
|
+
`
|
|
2898
|
+
},
|
|
2899
|
+
blank: {
|
|
2900
|
+
description: "Minimal 1-page skeleton",
|
|
2901
|
+
content: (slug) => `import type { CatalogSchema } from "@shared/types";
|
|
2902
|
+
|
|
2903
|
+
const catalog = {
|
|
2904
|
+
schema_version: "1.0",
|
|
2905
|
+
slug: "${slug}",
|
|
2906
|
+
settings: {
|
|
2907
|
+
theme: { primary_color: "#6366f1" },
|
|
2908
|
+
},
|
|
2909
|
+
routing: {
|
|
2910
|
+
entry: "main",
|
|
2911
|
+
edges: [],
|
|
2912
|
+
},
|
|
2913
|
+
pages: {
|
|
2914
|
+
main: {
|
|
2915
|
+
title: "Welcome",
|
|
2916
|
+
components: [
|
|
2917
|
+
{ id: "heading", type: "heading", props: { level: 1, text: "Hello, World!" } },
|
|
2918
|
+
{ id: "text", type: "paragraph", props: { text: "Start building your catalog here." } },
|
|
2919
|
+
],
|
|
2920
|
+
},
|
|
2921
|
+
},
|
|
2922
|
+
} satisfies CatalogSchema;
|
|
2923
|
+
|
|
2924
|
+
export default catalog;
|
|
2925
|
+
`
|
|
2926
|
+
}
|
|
2927
|
+
};
|
|
2928
|
+
async function catalogInit() {
|
|
2929
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2930
|
+
try {
|
|
2931
|
+
console.log("\n Catalog Kit \u2014 New Catalog Scaffold\n");
|
|
2932
|
+
const slug = (await ask(rl, " Catalog slug (e.g. my-quiz): ")).trim();
|
|
2933
|
+
if (!slug || !/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
|
|
2934
|
+
console.error("\n Invalid slug. Use lowercase letters, numbers, and hyphens.");
|
|
2935
|
+
process.exit(1);
|
|
2936
|
+
}
|
|
2937
|
+
if (existsSync4(slug)) {
|
|
2938
|
+
console.error(`
|
|
2939
|
+
Directory "${slug}" already exists.`);
|
|
2940
|
+
process.exit(1);
|
|
2941
|
+
}
|
|
2942
|
+
console.log("\n Templates:");
|
|
2943
|
+
const templateKeys = Object.keys(TEMPLATES);
|
|
2944
|
+
for (let i = 0; i < templateKeys.length; i++) {
|
|
2945
|
+
const key = templateKeys[i];
|
|
2946
|
+
console.log(` ${i + 1}. ${key} \u2014 ${TEMPLATES[key].description}`);
|
|
2947
|
+
}
|
|
2948
|
+
const choice = (await ask(rl, `
|
|
2949
|
+
Pick a template (1-${templateKeys.length}): `)).trim();
|
|
2950
|
+
const idx = parseInt(choice, 10) - 1;
|
|
2951
|
+
if (isNaN(idx) || idx < 0 || idx >= templateKeys.length) {
|
|
2952
|
+
console.error("\n Invalid choice.");
|
|
2953
|
+
process.exit(1);
|
|
2954
|
+
}
|
|
2955
|
+
const templateKey = templateKeys[idx];
|
|
2956
|
+
const template = TEMPLATES[templateKey];
|
|
2957
|
+
mkdirSync(join3(slug, "images"), { recursive: true });
|
|
2958
|
+
writeFileSync(join3(slug, `${slug}.ts`), template.content(slug));
|
|
2959
|
+
writeFileSync(
|
|
2960
|
+
join3(slug, ".env.example"),
|
|
2961
|
+
`CATALOG_KIT_TOKEN=cfk_your_token_here
|
|
2962
|
+
`
|
|
2963
|
+
);
|
|
2964
|
+
console.log(`
|
|
2965
|
+
\x1B[32m\u2713 Created ${slug}/\x1B[0m`);
|
|
2966
|
+
console.log(` ${slug}/${slug}.ts`);
|
|
2967
|
+
console.log(` ${slug}/images/`);
|
|
2968
|
+
console.log(` ${slug}/.env.example`);
|
|
2969
|
+
console.log(`
|
|
2970
|
+
Next steps:`);
|
|
2971
|
+
console.log(` cd ${slug}`);
|
|
2972
|
+
console.log(` catalogs catalog dev ${slug}.ts`);
|
|
2973
|
+
console.log(` catalogs catalog validate ${slug}.ts`);
|
|
2974
|
+
console.log(` catalogs catalog push ${slug}.ts --publish
|
|
2975
|
+
`);
|
|
2976
|
+
} finally {
|
|
2977
|
+
rl.close();
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
// src/commands/catalog-diff.ts
|
|
2982
|
+
import { resolve as resolve6 } from "path";
|
|
2983
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2984
|
+
import ora7 from "ora";
|
|
2985
|
+
async function catalogDiff(file) {
|
|
2986
|
+
const config = requireConfig();
|
|
2987
|
+
const api = new ApiClient(config);
|
|
2988
|
+
await printIdentity(api);
|
|
2989
|
+
const abs = resolve6(file);
|
|
2990
|
+
if (!existsSync5(abs)) {
|
|
2991
|
+
console.error(`File not found: ${file}`);
|
|
2992
|
+
process.exit(1);
|
|
2993
|
+
}
|
|
2994
|
+
const spinner = ora7("Loading local catalog...").start();
|
|
2995
|
+
let local;
|
|
2996
|
+
try {
|
|
2997
|
+
local = await loadCatalogFile(abs);
|
|
2998
|
+
} catch (err) {
|
|
2999
|
+
spinner.fail(`Failed to load catalog: ${err.message}`);
|
|
3000
|
+
process.exit(1);
|
|
3001
|
+
}
|
|
3002
|
+
spinner.succeed(`Local: ${local.slug || file}`);
|
|
3003
|
+
const fetchSpinner = ora7("Fetching remote catalog...").start();
|
|
3004
|
+
let remote = null;
|
|
3005
|
+
try {
|
|
3006
|
+
const listRes = await api.get("/api/v1/catalogs");
|
|
3007
|
+
const catalogs = listRes.data || [];
|
|
3008
|
+
const match = catalogs.find((c) => c.slug === local.slug);
|
|
3009
|
+
if (match) {
|
|
3010
|
+
remote = match.schema;
|
|
3011
|
+
}
|
|
3012
|
+
} catch (err) {
|
|
3013
|
+
fetchSpinner.fail(`Failed to fetch remote: ${err.message}`);
|
|
3014
|
+
process.exit(1);
|
|
3015
|
+
}
|
|
3016
|
+
if (!remote) {
|
|
3017
|
+
fetchSpinner.info("This would be a new catalog (slug not found remotely).");
|
|
3018
|
+
return;
|
|
3019
|
+
}
|
|
3020
|
+
fetchSpinner.succeed(`Remote: ${remote.slug || local.slug}`);
|
|
3021
|
+
console.log();
|
|
3022
|
+
const localPages = new Set(Object.keys(local.pages || {}));
|
|
3023
|
+
const remotePages = new Set(Object.keys(remote.pages || {}));
|
|
3024
|
+
const addedPages = [];
|
|
3025
|
+
const removedPages = [];
|
|
3026
|
+
const modifiedPages = [];
|
|
3027
|
+
for (const id of localPages) {
|
|
3028
|
+
if (!remotePages.has(id)) {
|
|
3029
|
+
addedPages.push(id);
|
|
3030
|
+
} else {
|
|
3031
|
+
const localComps = (local.pages[id].components || []).map((c) => `${c.id}:${c.type}`).join(",");
|
|
3032
|
+
const remoteComps = (remote.pages[id].components || []).map((c) => `${c.id}:${c.type}`).join(",");
|
|
3033
|
+
if (localComps !== remoteComps) {
|
|
3034
|
+
modifiedPages.push(id);
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
for (const id of remotePages) {
|
|
3039
|
+
if (!localPages.has(id)) {
|
|
3040
|
+
removedPages.push(id);
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
if (addedPages.length + removedPages.length + modifiedPages.length === 0) {
|
|
3044
|
+
console.log(" Pages: no changes");
|
|
3045
|
+
} else {
|
|
3046
|
+
console.log(" Pages:");
|
|
3047
|
+
for (const id of addedPages) {
|
|
3048
|
+
console.log(` \x1B[32m+ ${id}\x1B[0m`);
|
|
3049
|
+
}
|
|
3050
|
+
for (const id of removedPages) {
|
|
3051
|
+
console.log(` \x1B[31m- ${id}\x1B[0m`);
|
|
3052
|
+
}
|
|
3053
|
+
for (const id of modifiedPages) {
|
|
3054
|
+
console.log(` \x1B[33m~ ${id}\x1B[0m`);
|
|
3055
|
+
const localCompIds = new Set((local.pages[id].components || []).map((c) => c.id));
|
|
3056
|
+
const remoteCompIds = new Set((remote.pages[id].components || []).map((c) => c.id));
|
|
3057
|
+
for (const cid of localCompIds) {
|
|
3058
|
+
if (!remoteCompIds.has(cid)) console.log(` \x1B[32m+ component: ${cid}\x1B[0m`);
|
|
3059
|
+
}
|
|
3060
|
+
for (const cid of remoteCompIds) {
|
|
3061
|
+
if (!localCompIds.has(cid)) console.log(` \x1B[31m- component: ${cid}\x1B[0m`);
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
const localEdges = (local.routing?.edges || []).map((e) => `${e.from}->${e.to}`);
|
|
3066
|
+
const remoteEdges = (remote.routing?.edges || []).map((e) => `${e.from}->${e.to}`);
|
|
3067
|
+
const localEdgeSet = new Set(localEdges);
|
|
3068
|
+
const remoteEdgeSet = new Set(remoteEdges);
|
|
3069
|
+
const addedEdges = localEdges.filter((e) => !remoteEdgeSet.has(e));
|
|
3070
|
+
const removedEdges = remoteEdges.filter((e) => !localEdgeSet.has(e));
|
|
3071
|
+
if (addedEdges.length + removedEdges.length === 0) {
|
|
3072
|
+
console.log(" Edges: no changes");
|
|
3073
|
+
} else {
|
|
3074
|
+
console.log(" Edges:");
|
|
3075
|
+
for (const e of addedEdges) console.log(` \x1B[32m+ ${e}\x1B[0m`);
|
|
3076
|
+
for (const e of removedEdges) console.log(` \x1B[31m- ${e}\x1B[0m`);
|
|
3077
|
+
}
|
|
3078
|
+
const totalChanges = addedPages.length + removedPages.length + modifiedPages.length + addedEdges.length + removedEdges.length;
|
|
3079
|
+
console.log();
|
|
3080
|
+
console.log(
|
|
3081
|
+
` Summary: ${addedPages.length} page(s) added, ${removedPages.length} removed, ${modifiedPages.length} modified | ${addedEdges.length} edge(s) added, ${removedEdges.length} removed`
|
|
3082
|
+
);
|
|
3083
|
+
if (totalChanges === 0) {
|
|
3084
|
+
console.log(" \x1B[32m\u2713 Local matches remote\x1B[0m");
|
|
3085
|
+
}
|
|
3086
|
+
console.log();
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
// src/commands/catalog-open.ts
|
|
3090
|
+
import { exec } from "child_process";
|
|
3091
|
+
import { platform } from "os";
|
|
3092
|
+
import ora8 from "ora";
|
|
3093
|
+
async function catalogOpen(slug) {
|
|
3094
|
+
const config = requireConfig();
|
|
3095
|
+
const api = new ApiClient(config);
|
|
3096
|
+
await printIdentity(api);
|
|
3097
|
+
const spinner = ora8(`Looking up catalog "${slug}"...`).start();
|
|
3098
|
+
try {
|
|
3099
|
+
const listRes = await api.get("/api/v1/catalogs");
|
|
3100
|
+
const catalogs = listRes.data || [];
|
|
3101
|
+
const catalog2 = catalogs.find((c) => c.slug === slug);
|
|
3102
|
+
if (!catalog2) {
|
|
3103
|
+
spinner.fail(`Catalog "${slug}" not found.`);
|
|
3104
|
+
process.exit(1);
|
|
3105
|
+
}
|
|
3106
|
+
let url = catalog2.url;
|
|
3107
|
+
if (!url) {
|
|
3108
|
+
try {
|
|
3109
|
+
const me = await api.get("/api/v1/me");
|
|
3110
|
+
const subdomain = me.data?.subdomain || me.data?.app_slug;
|
|
3111
|
+
if (subdomain) {
|
|
3112
|
+
url = `https://${subdomain}.catalogkit.cc/${slug}`;
|
|
3113
|
+
}
|
|
3114
|
+
} catch {
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
if (!url) {
|
|
3118
|
+
url = `https://catalogkit.cc/c/${catalog2.catalog_id}`;
|
|
3119
|
+
}
|
|
3120
|
+
spinner.succeed(`Opening: ${url}`);
|
|
3121
|
+
const os = platform();
|
|
3122
|
+
const cmd = os === "darwin" ? "open" : os === "win32" ? "start" : "xdg-open";
|
|
3123
|
+
exec(`${cmd} "${url}"`, (err) => {
|
|
3124
|
+
if (err) {
|
|
3125
|
+
console.log(`
|
|
3126
|
+
Could not open browser. Visit: ${url}
|
|
3127
|
+
`);
|
|
3128
|
+
}
|
|
3129
|
+
});
|
|
3130
|
+
} catch (err) {
|
|
3131
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
3132
|
+
process.exit(1);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
|
|
2122
3136
|
// src/commands/whoami.ts
|
|
2123
3137
|
async function whoami() {
|
|
2124
3138
|
const config = getConfig();
|
|
@@ -2177,7 +3191,7 @@ async function whoami() {
|
|
|
2177
3191
|
|
|
2178
3192
|
// src/index.ts
|
|
2179
3193
|
var __dirname = dirname3(fileURLToPath(import.meta.url));
|
|
2180
|
-
var { version } = JSON.parse(readFileSync5(
|
|
3194
|
+
var { version } = JSON.parse(readFileSync5(join4(__dirname, "../package.json"), "utf-8"));
|
|
2181
3195
|
var program = new Command();
|
|
2182
3196
|
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) => {
|
|
2183
3197
|
const opts = thisCommand.opts();
|
|
@@ -2192,5 +3206,9 @@ var catalog = program.command("catalog").description("Catalog schema management"
|
|
|
2192
3206
|
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);
|
|
2193
3207
|
catalog.command("list").description("List all catalogs").action(catalogList);
|
|
2194
3208
|
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);
|
|
3209
|
+
catalog.command("validate <file>").description("Validate a catalog schema (no token required)").action(catalogValidate);
|
|
3210
|
+
catalog.command("init").description("Scaffold a new catalog from a template").action(catalogInit);
|
|
3211
|
+
catalog.command("diff <file>").description("Compare local catalog schema against remote").action(catalogDiff);
|
|
3212
|
+
catalog.command("open <slug>").description("Open a published catalog in the browser").action(catalogOpen);
|
|
2195
3213
|
program.command("whoami").description("Show current authentication info").action(whoami);
|
|
2196
3214
|
program.parse();
|