@botlr/cli 0.1.2 → 0.2.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 CHANGED
@@ -4,6 +4,7 @@ import path from "path";
4
4
  var CONFIG_FILE = "botlr.config.json";
5
5
  var DEFAULT_FRAMEWORK = "react-next";
6
6
  var DEFAULT_SECTIONS_DIR = "components/sections";
7
+ var DEFAULT_REGISTRY_FILE = "publisher-section-registry.ts";
7
8
  var ConfigError = class extends Error {
8
9
  constructor(message) {
9
10
  super(message);
@@ -61,6 +62,8 @@ async function loadConfig(cwd, overrides = {}, env = process.env) {
61
62
  appId,
62
63
  framework: overrides.framework ?? env.BOTLR_FRAMEWORK ?? file.framework ?? DEFAULT_FRAMEWORK,
63
64
  sectionsDir,
65
+ registryFile: path.resolve(sectionsDir, file.registryFile ?? DEFAULT_REGISTRY_FILE),
66
+ types: file.types !== false,
64
67
  apiKey: env.BOTLR_API_KEY || void 0,
65
68
  personalToken: env.BOTLR_PAT || void 0
66
69
  };
@@ -71,6 +74,12 @@ import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
71
74
  import path2 from "path";
72
75
  var LOCK_DIR = ".botlr";
73
76
  var LOCK_FILE = "sections.lock.json";
77
+ function nextLockEntry(previous, fresh) {
78
+ if (previous && previous.scope === fresh.scope && previous.framework === fresh.framework && previous.version === fresh.version && previous.contentHash === fresh.contentHash) {
79
+ return { ...fresh, pulledAt: previous.pulledAt };
80
+ }
81
+ return fresh;
82
+ }
74
83
  function lockPath(root) {
75
84
  return path2.join(root, LOCK_DIR, LOCK_FILE);
76
85
  }
@@ -94,7 +103,7 @@ async function writeLock(root, lock) {
94
103
  }
95
104
  async function updateLock(root, name, entry) {
96
105
  const lock = await readLock(root);
97
- lock[name] = entry;
106
+ lock[name] = nextLockEntry(lock[name], entry);
98
107
  await writeLock(root, lock);
99
108
  return lock;
100
109
  }
@@ -165,13 +174,80 @@ function validateManifest(value, where = "manifest") {
165
174
  }
166
175
  return m;
167
176
  }
177
+ var MANIFEST_KEY_ORDER = [
178
+ "section",
179
+ "framework",
180
+ "entry",
181
+ "exportName",
182
+ "files",
183
+ "title",
184
+ "description",
185
+ "category",
186
+ "icon",
187
+ "dependencies",
188
+ "schema"
189
+ ];
190
+ var DEPENDENCIES_KEY_ORDER = ["npm", "internal"];
191
+ function orderKeys(value, first) {
192
+ const out = {};
193
+ for (const key of first) {
194
+ if (key in value && value[key] !== void 0) out[key] = value[key];
195
+ }
196
+ for (const key of Object.keys(value).sort()) {
197
+ if (first.includes(key) || value[key] === void 0) continue;
198
+ out[key] = value[key];
199
+ }
200
+ return out;
201
+ }
202
+ function normalizeNested(value) {
203
+ if (Array.isArray(value)) return value.map(normalizeNested);
204
+ if (value && typeof value === "object") {
205
+ const record = value;
206
+ const ordered = orderKeys(record, []);
207
+ for (const key of Object.keys(ordered)) ordered[key] = normalizeNested(ordered[key]);
208
+ return ordered;
209
+ }
210
+ return value;
211
+ }
212
+ function normalizeManifest(manifest) {
213
+ const stripped = stripManifestState(manifest);
214
+ const ordered = orderKeys(stripped, MANIFEST_KEY_ORDER);
215
+ for (const key of Object.keys(ordered)) {
216
+ if (key === "dependencies" && ordered[key] && typeof ordered[key] === "object" && !Array.isArray(ordered[key])) {
217
+ const deps = orderKeys(ordered[key], DEPENDENCIES_KEY_ORDER);
218
+ for (const depKey of Object.keys(deps)) deps[depKey] = normalizeNested(deps[depKey]);
219
+ ordered[key] = deps;
220
+ } else {
221
+ ordered[key] = normalizeNested(ordered[key]);
222
+ }
223
+ }
224
+ return ordered;
225
+ }
226
+ function formatManifest(manifest) {
227
+ return `${JSON.stringify(normalizeManifest(manifest), null, 2)}
228
+ `;
229
+ }
230
+ function manifestsEqual(a, b) {
231
+ return formatManifest(a) === formatManifest(b);
232
+ }
168
233
  async function writeManifest(sectionsDir, name, manifest) {
169
234
  const file = manifestPath(sectionsDir, name);
170
235
  await mkdir2(sectionsDir, { recursive: true });
171
- await writeFile2(file, `${JSON.stringify(stripManifestState(manifest), null, 2)}
172
- `, "utf8");
236
+ await writeFile2(file, formatManifest(manifest), "utf8");
173
237
  return file;
174
238
  }
239
+ async function writeManifestIfChanged(sectionsDir, name, manifest) {
240
+ const file = manifestPath(sectionsDir, name);
241
+ try {
242
+ const existing = JSON.parse(await readFile3(file, "utf8"));
243
+ if (existing && typeof existing === "object" && !Array.isArray(existing)) {
244
+ if (manifestsEqual(existing, manifest)) return { file, changed: false };
245
+ }
246
+ } catch {
247
+ }
248
+ await writeManifest(sectionsDir, name, manifest);
249
+ return { file, changed: true };
250
+ }
175
251
  async function readSectionFiles(sectionsDir, manifest) {
176
252
  return Promise.all(
177
253
  manifest.files.map(async (relative) => {
@@ -226,7 +302,7 @@ function remoteForLock(summary, lock) {
226
302
  // src/init.ts
227
303
  import { readFile as readFile4, readdir as readdir2 } from "fs/promises";
228
304
  import path4 from "path";
229
- var REGISTRY_FILE = "publisher-section-registry.ts";
305
+ var REGISTRY_FILE = DEFAULT_REGISTRY_FILE;
230
306
  function parseRegistry(source) {
231
307
  const importedFrom = /* @__PURE__ */ new Map();
232
308
  const importRe = /import\s*(?:type\s*)?\{([^}]*)\}\s*from\s*["']([^"']+)["']/g;
@@ -259,14 +335,105 @@ function parseDefinition(source) {
259
335
  const m = body.match(new RegExp(`(?:^|\\n)\\s*${key}\\s*:\\s*(?:"([^"]*)"|'([^']*)'|\`([^\`]*)\`)`));
260
336
  return m ? m[1] ?? m[2] ?? m[3] : void 0;
261
337
  };
338
+ const schemaName = body.match(/(?:^|\n)\s*schema\s*:\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*,?/)?.[1];
262
339
  return {
263
340
  name: field("name"),
264
341
  heading: field("heading"),
265
342
  category: field("category"),
266
343
  description: field("description"),
267
- icon: field("icon")
344
+ icon: field("icon"),
345
+ ...schemaName ? { schemaName } : {}
268
346
  };
269
347
  }
348
+ function splitTopLevel(body) {
349
+ const parts = [];
350
+ let depth = 0;
351
+ let quote = null;
352
+ let current = "";
353
+ for (let i = 0; i < body.length; i++) {
354
+ const ch = body[i];
355
+ if (quote) {
356
+ current += ch;
357
+ if (ch === "\\") current += body[++i] ?? "";
358
+ else if (ch === quote) quote = null;
359
+ continue;
360
+ }
361
+ if (ch === "/" && body[i + 1] === "/") {
362
+ while (i < body.length && body[i] !== "\n") i++;
363
+ current += "\n";
364
+ continue;
365
+ }
366
+ if (ch === '"' || ch === "'" || ch === "`") quote = ch;
367
+ else if ("([{".includes(ch)) depth++;
368
+ else if (")]}".includes(ch)) depth--;
369
+ if (ch === "," && depth === 0) {
370
+ parts.push(current);
371
+ current = "";
372
+ continue;
373
+ }
374
+ current += ch;
375
+ }
376
+ parts.push(current);
377
+ return parts.map((p) => p.trim()).filter(Boolean);
378
+ }
379
+ function enumValues(expr) {
380
+ const m = expr.match(/^z\.enum\(\s*\[([\s\S]*?)\]/);
381
+ if (!m) return null;
382
+ const values = [];
383
+ for (const raw of splitTopLevel(m[1])) {
384
+ const lit = raw.match(/^(?:"([^"]*)"|'([^']*)'|`([^`]*)`)$/);
385
+ if (!lit) return null;
386
+ values.push(lit[1] ?? lit[2] ?? lit[3]);
387
+ }
388
+ return values;
389
+ }
390
+ function zodFieldToSchemaField(expr) {
391
+ const source = expr.trim();
392
+ const optional = /\.(optional|nullable|nullish|default)\(/.test(source);
393
+ let field = null;
394
+ if (/^z\.string\(/.test(source)) field = { type: /\.(email)\(/.test(source) ? "email" : /\.(url)\(/.test(source) ? "url" : "text" };
395
+ else if (/^z\.number\(/.test(source)) field = { type: /\.int\(/.test(source) ? "integer" : "number" };
396
+ else if (/^z\.boolean\(/.test(source)) field = { type: "boolean" };
397
+ else if (/^z\.enum\(/.test(source)) {
398
+ const options = enumValues(source);
399
+ field = options ? { type: "select", options } : { type: "select" };
400
+ } else if (/^z\.array\(/.test(source)) field = { type: "array" };
401
+ else if (/^z\.(object|record)\(/.test(source)) field = { type: "json" };
402
+ else if (/^z\.union\(\s*\[\s*z\.string\(\)\s*,\s*z\.(any|object|record)\(/.test(source)) field = { type: "asset" };
403
+ if (!field) return null;
404
+ if (!optional) field.required = true;
405
+ const describe = source.match(/\.describe\(\s*(?:"([^"]*)"|'([^']*)'|`([^`]*)`)\s*\)/);
406
+ const label = describe ? describe[1] ?? describe[2] ?? describe[3] : void 0;
407
+ if (label) field.label = label;
408
+ return field;
409
+ }
410
+ function parseZodSchema(source, schemaName) {
411
+ const start = source.match(new RegExp(`(?:export\\s+)?const\\s+${schemaName}\\s*=\\s*z\\.object\\(\\s*\\{`));
412
+ if (!start || start.index === void 0) return null;
413
+ const open = start.index + start[0].length;
414
+ let depth = 1;
415
+ let end = -1;
416
+ for (let i = open; i < source.length; i++) {
417
+ const ch = source[i];
418
+ if (ch === "{" || ch === "(" || ch === "[") depth++;
419
+ else if (ch === "}" || ch === ")" || ch === "]") {
420
+ depth--;
421
+ if (depth === 0) {
422
+ end = i;
423
+ break;
424
+ }
425
+ }
426
+ }
427
+ if (end < 0) return null;
428
+ const schema = {};
429
+ for (const entry of splitTopLevel(source.slice(open, end))) {
430
+ const m = entry.match(/^(?:"([^"]+)"|'([^']+)'|([A-Za-z_$][A-Za-z0-9_$]*))\s*:\s*([\s\S]+)$/);
431
+ if (!m) continue;
432
+ const field = zodFieldToSchemaField(m[4]);
433
+ if (field) schema[m[1] ?? m[2] ?? m[3]] = field;
434
+ }
435
+ return Object.keys(schema).length ? schema : null;
436
+ }
270
437
  function parseImports(source) {
271
438
  const internal = /* @__PURE__ */ new Set();
272
439
  const npm = /* @__PURE__ */ new Set();
@@ -292,7 +459,7 @@ async function resolveImport(sectionsDir, spec) {
292
459
  return null;
293
460
  }
294
461
  async function deriveManifests(sectionsDir, options) {
295
- const registrySource = await readFile4(path4.join(sectionsDir, REGISTRY_FILE), "utf8");
462
+ const registrySource = await readFile4(options.registryFile ?? path4.join(sectionsDir, DEFAULT_REGISTRY_FILE), "utf8");
296
463
  const entries = parseRegistry(registrySource);
297
464
  const out = [];
298
465
  for (const entry of entries) {
@@ -300,6 +467,7 @@ async function deriveManifests(sectionsDir, options) {
300
467
  if (!entryFile) continue;
301
468
  const source = await readFile4(entryFile, "utf8");
302
469
  const meta = parseDefinition(source);
470
+ const schema = meta?.schemaName ? parseZodSchema(source, meta.schemaName) : null;
303
471
  const imports = parseImports(source);
304
472
  const relative = path4.relative(sectionsDir, entryFile).split(path4.sep).join("/");
305
473
  const npm = {};
@@ -320,7 +488,8 @@ async function deriveManifests(sectionsDir, options) {
320
488
  dependencies: {
321
489
  ...Object.keys(npm).length ? { npm } : {},
322
490
  ...imports.internal.length ? { internal: imports.internal } : {}
323
- }
491
+ },
492
+ ...schema ? { schema } : {}
324
493
  };
325
494
  out.push({ name: entry.slug, manifest, entryFile });
326
495
  }
@@ -340,14 +509,329 @@ async function readNpmVersions(dir) {
340
509
  }
341
510
  }
342
511
  }
343
- async function hasRegistry(sectionsDir) {
512
+ async function hasRegistry(sectionsDir, registryFile) {
513
+ const file = registryFile ?? path4.join(sectionsDir, DEFAULT_REGISTRY_FILE);
344
514
  try {
345
- return (await readdir2(sectionsDir)).includes(REGISTRY_FILE);
515
+ return (await readdir2(path4.dirname(file))).includes(path4.basename(file));
346
516
  } catch {
347
517
  return false;
348
518
  }
349
519
  }
350
520
 
521
+ // src/registry.ts
522
+ import { readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
523
+ import path5 from "path";
524
+ var SOURCE_EXTENSIONS2 = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
525
+ function importPathFor(registryFile, entryFile) {
526
+ const relative = path5.relative(path5.dirname(registryFile), entryFile).split(path5.sep).join("/");
527
+ const ext = path5.extname(relative);
528
+ const bare = SOURCE_EXTENSIONS2.includes(ext) ? relative.slice(0, -ext.length) : relative;
529
+ return bare.startsWith(".") ? bare : `./${bare}`;
530
+ }
531
+ var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
532
+ function addRegistryEntry(source, addition) {
533
+ const { slug, exportName, importPath } = addition;
534
+ if (!IDENTIFIER.test(exportName)) throw new Error(`'${exportName}' is not a valid export name`);
535
+ const eol = source.includes("\r\n") ? "\r\n" : "\n";
536
+ let out = source;
537
+ let addedImport = false;
538
+ let addedEntry = false;
539
+ const importRe = /^[ \t]*import\s*(?:type\s*)?\{([^}]*)\}\s*from\s*(["'])([^"']+)\2[ \t]*;?[ \t]*$/gm;
540
+ const imports = [...out.matchAll(importRe)];
541
+ const alreadyImported = imports.some(
542
+ (m) => m[1].split(",").some((raw) => raw.trim().split(/\s+as\s+/).pop()?.trim() === exportName)
543
+ );
544
+ if (!alreadyImported) {
545
+ const last = imports.at(-1);
546
+ const quote = last?.[2] ?? '"';
547
+ const semicolon = last ? last[0].trimEnd().endsWith(";") : true;
548
+ const line = `import { ${exportName} } from ${quote}${importPath}${quote}${semicolon ? ";" : ""}`;
549
+ if (last) {
550
+ const end = last.index + last[0].length;
551
+ out = `${out.slice(0, end)}${eol}${line}${out.slice(end)}`;
552
+ } else {
553
+ out = `${line}${eol}${eol}${out}`;
554
+ }
555
+ addedImport = true;
556
+ }
557
+ const alreadyMapped = parseRegistry(out).some((e) => e.slug === slug) || hasEntry(out, slug);
558
+ if (!alreadyMapped) {
559
+ const object = findRegistryObject(out);
560
+ if (!object) throw new Error("could not find the registry object (`export const ... = { ... }`)");
561
+ const body = out.slice(object.bodyStart, object.bodyEnd);
562
+ const entryLines = body.split(/\r?\n/).filter((l) => /^\s*(?:"[^"]+"|'[^']+'|[A-Za-z0-9_$-]+)\s*:/.test(l));
563
+ const lastEntry = entryLines.at(-1);
564
+ const indent = lastEntry?.match(/^\s*/)?.[0] ?? " ";
565
+ const key = IDENTIFIER.test(slug) ? slug : `"${slug}"`;
566
+ const entryLine = `${indent}${key}: ${exportName},`;
567
+ let newBody = body;
568
+ if (lastEntry !== void 0) {
569
+ const stripped = lastEntry.replace(/\/\/.*$/, "").trimEnd();
570
+ if (!stripped.endsWith(",")) {
571
+ const at = newBody.lastIndexOf(lastEntry);
572
+ const fixed = lastEntry.replace(/^(\s*(?:"[^"]+"|'[^']+'|[A-Za-z0-9_$-]+)\s*:\s*[A-Za-z0-9_$]+)/, "$1,");
573
+ newBody = `${newBody.slice(0, at)}${fixed}${newBody.slice(at + lastEntry.length)}`;
574
+ }
575
+ }
576
+ newBody = `${newBody.replace(/\s*$/, "")}${eol}${entryLine}`;
577
+ out = `${out.slice(0, object.bodyStart)}${newBody}${out.slice(object.bodyEnd)}`;
578
+ addedEntry = true;
579
+ }
580
+ return { source: out, changed: addedImport || addedEntry, addedImport, addedEntry };
581
+ }
582
+ function hasEntry(source, slug) {
583
+ const object = findRegistryObject(source);
584
+ if (!object) return false;
585
+ const body = source.slice(object.bodyStart, object.bodyEnd);
586
+ const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
587
+ return new RegExp(`^\\s*(?:"${escaped}"|'${escaped}'|${escaped})\\s*:`, "m").test(body);
588
+ }
589
+ function findRegistryObject(source) {
590
+ const open = source.match(/export\s+const\s+\w+\s*(?::[^=]*)?=\s*\{/) ?? source.match(/=\s*\{/);
591
+ if (!open || open.index === void 0) return null;
592
+ const bodyStart = open.index + open[0].length;
593
+ const close = /\r?\n\}/g;
594
+ close.lastIndex = bodyStart;
595
+ const closeMatch = close.exec(source);
596
+ if (!closeMatch) return null;
597
+ return { bodyStart, bodyEnd: closeMatch.index };
598
+ }
599
+ async function registerSection(sectionsDir, registryFile, manifest) {
600
+ const name = path5.basename(registryFile);
601
+ let source;
602
+ try {
603
+ source = await readFile5(registryFile, "utf8");
604
+ } catch (error) {
605
+ if (error.code === "ENOENT") {
606
+ return { file: registryFile, changed: false, notice: `no ${name} in ${path5.dirname(registryFile)}, register the section yourself` };
607
+ }
608
+ throw error;
609
+ }
610
+ if (!manifest.exportName) {
611
+ return { file: registryFile, changed: false, notice: `manifest has no exportName, ${name} not updated` };
612
+ }
613
+ const importPath = importPathFor(registryFile, path5.resolve(sectionsDir, manifest.entry));
614
+ let edit;
615
+ try {
616
+ edit = addRegistryEntry(source, { slug: manifest.section, exportName: manifest.exportName, importPath });
617
+ } catch (error) {
618
+ return { file: registryFile, changed: false, notice: `${name} not updated: ${error.message}` };
619
+ }
620
+ if (edit.changed) await writeFile3(registryFile, edit.source, "utf8");
621
+ return { file: registryFile, changed: edit.changed, addedImport: edit.addedImport, addedEntry: edit.addedEntry };
622
+ }
623
+
624
+ // src/typegen.ts
625
+ import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
626
+ import path6 from "path";
627
+ import {
628
+ isFieldSchema,
629
+ isFormBuilderSchema
630
+ } from "@botlr/sdk";
631
+ var PROPS_SUFFIX = ".props.ts";
632
+ var STRING_TYPES = /* @__PURE__ */ new Set([
633
+ "text",
634
+ "textarea",
635
+ "markdown",
636
+ "richtext",
637
+ "string",
638
+ "email",
639
+ "tel",
640
+ "url",
641
+ "icon",
642
+ "color",
643
+ "date",
644
+ "datetime",
645
+ "timestamp",
646
+ "timestamptz",
647
+ "uuid"
648
+ ]);
649
+ var NUMBER_TYPES = /* @__PURE__ */ new Set(["number", "integer", "currency"]);
650
+ var BOOLEAN_TYPES = /* @__PURE__ */ new Set(["boolean"]);
651
+ var STRING_LIST_TYPES = /* @__PURE__ */ new Set(["multiselect", "tags"]);
652
+ var LIST_TYPES = /* @__PURE__ */ new Set(["array", "list", "repeater"]);
653
+ var OBJECT_TYPES = /* @__PURE__ */ new Set(["json", "jsonb", "object"]);
654
+ var REFERENCE_TYPES = /* @__PURE__ */ new Set([
655
+ "asset",
656
+ "asset-picker-image",
657
+ "asset-picker-file",
658
+ "asset-picker-video",
659
+ "media",
660
+ "document",
661
+ "page",
662
+ "post",
663
+ "reference",
664
+ "relation",
665
+ "entity_reference",
666
+ "app_instance",
667
+ "forum_thread"
668
+ ]);
669
+ var REFERENCE_TYPE = "string | Record<string, unknown>";
670
+ var OBJECT_TYPE = "Record<string, unknown>";
671
+ var GROUP_NODE_TYPES = /* @__PURE__ */ new Set(["section", "fieldset", "composite", "group"]);
672
+ function literal(value) {
673
+ if (typeof value === "string") return JSON.stringify(value);
674
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
675
+ return null;
676
+ }
677
+ function optionUnion(options) {
678
+ if (!Array.isArray(options) || options.length === 0) return null;
679
+ const literals = [];
680
+ for (const option of options) {
681
+ const raw = option && typeof option === "object" ? option.value : option;
682
+ const lit = literal(raw);
683
+ if (lit === null) return null;
684
+ if (!literals.includes(lit)) literals.push(lit);
685
+ }
686
+ return literals.join(" | ");
687
+ }
688
+ function wrapArray(type) {
689
+ return /^[A-Za-z0-9_<>, ]+$/.test(type) ? `${type}[]` : `Array<${type}>`;
690
+ }
691
+ function tsTypeForField(field) {
692
+ const kind = typeof field.type === "string" ? field.type : "";
693
+ const multiple = field.multiple === true;
694
+ if (kind === "select") {
695
+ const union = optionUnion(field.options) ?? "string";
696
+ return multiple ? wrapArray(union) : union;
697
+ }
698
+ if (STRING_TYPES.has(kind)) return "string";
699
+ if (NUMBER_TYPES.has(kind)) return "number";
700
+ if (BOOLEAN_TYPES.has(kind)) return "boolean";
701
+ if (STRING_LIST_TYPES.has(kind)) return "string[]";
702
+ if (kind === "repeater" && Array.isArray(field.fields)) {
703
+ const items = collectFormBuilderFields(field.fields);
704
+ return items.length ? `Array<${inlineObject(items)}>` : "unknown[]";
705
+ }
706
+ if (LIST_TYPES.has(kind)) return "unknown[]";
707
+ if (OBJECT_TYPES.has(kind)) return OBJECT_TYPE;
708
+ if (REFERENCE_TYPES.has(kind)) return multiple ? `Array<${REFERENCE_TYPE}>` : REFERENCE_TYPE;
709
+ return "unknown";
710
+ }
711
+ function docFor(field) {
712
+ const doc = [];
713
+ for (const value of [field.label, field.helpText ?? field.description]) {
714
+ if (typeof value === "string" && value.trim()) doc.push(value.trim());
715
+ }
716
+ return doc;
717
+ }
718
+ function collectDslFields(schema) {
719
+ const out = [];
720
+ for (const [name, value] of Object.entries(schema)) {
721
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
722
+ const field = value;
723
+ out.push({ name, type: tsTypeForField(field), required: field.required === true, doc: docFor(field) });
724
+ }
725
+ return out;
726
+ }
727
+ function collectFormBuilderFields(nodes) {
728
+ const out = [];
729
+ for (const node of nodes) {
730
+ if (!node || typeof node !== "object") continue;
731
+ if (GROUP_NODE_TYPES.has(node.type) && Array.isArray(node.children)) {
732
+ out.push(...collectFormBuilderFields(node.children));
733
+ continue;
734
+ }
735
+ const name = typeof node.name === "string" && node.name ? node.name : typeof node.key === "string" ? node.key : "";
736
+ if (!name) continue;
737
+ const kind = node.type === "field" ? node.fieldType ?? "" : node.fieldType ?? node.type;
738
+ const type = tsTypeForField({ type: kind, options: node.options, multiple: node.multiple, fields: node.fields });
739
+ out.push({ name, type, required: node.required === true, doc: docFor(node) });
740
+ }
741
+ return out;
742
+ }
743
+ function collectFields(schema) {
744
+ if (isFormBuilderSchema(schema)) return collectFormBuilderFields(schema.nodes);
745
+ if (isFieldSchema(schema)) return collectDslFields(schema);
746
+ return [];
747
+ }
748
+ function schemaFieldNames(schema) {
749
+ return collectFields(schema).map((f) => f.name);
750
+ }
751
+ var IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
752
+ function propertyKey(name) {
753
+ return IDENTIFIER_RE.test(name) ? name : JSON.stringify(name);
754
+ }
755
+ function inlineObject(fields) {
756
+ return `{ ${fields.map((f) => `${propertyKey(f.name)}${f.required ? "" : "?"}: ${f.type}`).join("; ")} }`;
757
+ }
758
+ function pascalCase(slug) {
759
+ const words = slug.split(/[^A-Za-z0-9]+/).filter(Boolean);
760
+ const name = words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
761
+ return /^[0-9]/.test(name) ? `Section${name}` : name || "Section";
762
+ }
763
+ function propsInterfaceName(manifest) {
764
+ const base = manifest.exportName && IDENTIFIER_RE.test(manifest.exportName) ? manifest.exportName : pascalCase(manifest.section);
765
+ return `${base}Props`;
766
+ }
767
+ function generatePropsSource(schema, options) {
768
+ const fields = collectFields(schema);
769
+ const lines = [
770
+ `// Generated by @botlr/cli from the Botlr section definition "${options.section}".`,
771
+ "// Do not edit: the props schema lives in the dashboard, and `botlr pull` rewrites this file.",
772
+ ""
773
+ ];
774
+ if (fields.length === 0) {
775
+ lines.push("/** The definition has no props yet; define them in the dashboard and pull again. */");
776
+ lines.push(`export type ${options.interfaceName} = Record<string, unknown>;`);
777
+ return `${lines.join("\n")}
778
+ `;
779
+ }
780
+ lines.push(`export interface ${options.interfaceName} {`);
781
+ fields.forEach((field, index) => {
782
+ if (index > 0 && field.doc.length) lines.push("");
783
+ if (field.doc.length === 1) lines.push(` /** ${field.doc[0]} */`);
784
+ else if (field.doc.length > 1) lines.push(" /**", ...field.doc.map((d) => ` * ${d}`), " */");
785
+ lines.push(` ${propertyKey(field.name)}${field.required ? "" : "?"}: ${field.type};`);
786
+ });
787
+ lines.push("}");
788
+ return `${lines.join("\n")}
789
+ `;
790
+ }
791
+ function propsPath(sectionsDir, name) {
792
+ return path6.join(sectionsDir, `${name}${PROPS_SUFFIX}`);
793
+ }
794
+ async function writePropsTypes(sectionsDir, manifest, schema) {
795
+ const interfaceName = propsInterfaceName(manifest);
796
+ const file = propsPath(sectionsDir, manifest.section);
797
+ const text = generatePropsSource(schema, { section: manifest.section, interfaceName });
798
+ try {
799
+ if (await readFile6(file, "utf8") === text) return { file, interfaceName, changed: false };
800
+ } catch {
801
+ }
802
+ await mkdir3(sectionsDir, { recursive: true });
803
+ await writeFile4(file, text, "utf8");
804
+ return { file, interfaceName, changed: true };
805
+ }
806
+
807
+ // src/drift.ts
808
+ import { isFieldSchema as isFieldSchema2 } from "@botlr/sdk";
809
+ function manifestSchema(manifest) {
810
+ const schema = manifest.schema;
811
+ return isFieldSchema2(schema) ? schema : null;
812
+ }
813
+ function compareSchemas(manifest, definitionSchema) {
814
+ const code = manifestSchema(manifest);
815
+ if (!code) return null;
816
+ const codeKeys = new Set(Object.keys(code).filter((k) => code[k] && typeof code[k] === "object"));
817
+ const dashboardKeys = new Set(schemaFieldNames(definitionSchema));
818
+ const missingInDashboard = [...codeKeys].filter((k) => !dashboardKeys.has(k)).sort();
819
+ const missingInCode = [...dashboardKeys].filter((k) => !codeKeys.has(k)).sort();
820
+ if (missingInDashboard.length === 0 && missingInCode.length === 0) return null;
821
+ return { missingInDashboard, missingInCode };
822
+ }
823
+ function formatDriftWarning(name, drift) {
824
+ const lines = [`warning: props schema of '${name}' differs between the code and the dashboard`];
825
+ if (drift.missingInDashboard.length) {
826
+ lines.push(` the code expects props the dashboard does not define: ${drift.missingInDashboard.join(", ")}`);
827
+ }
828
+ if (drift.missingInCode.length) {
829
+ lines.push(` the dashboard defines props the code does not list: ${drift.missingInCode.join(", ")}`);
830
+ }
831
+ lines.push(` the dashboard owns the schema: edit it there, or update 'schema' in ${name}.botlr.json`);
832
+ return lines.join("\n");
833
+ }
834
+
351
835
  // src/client.ts
352
836
  import { createBotlrClient } from "@botlr/sdk";
353
837
  function clientFor(config, options = {}) {
@@ -402,16 +886,35 @@ async function pullSections(config, options) {
402
886
  });
403
887
  const { source } = response;
404
888
  const files = await writeSectionFiles(config.sectionsDir, source.files);
405
- await writeManifest(config.sectionsDir, name, source.manifest);
406
- const newEntry = {
889
+ const manifest = await writeManifestIfChanged(config.sectionsDir, name, source.manifest);
890
+ const registry = options.noRegistry ? void 0 : await registerSection(config.sectionsDir, config.registryFile, source.manifest);
891
+ let types;
892
+ let typesNotice;
893
+ if (config.types && !options.noTypes) {
894
+ if (response.definition) {
895
+ types = await writePropsTypes(config.sectionsDir, source.manifest, response.definition.schema);
896
+ } else {
897
+ typesNotice = "no section definition resolves for this section, no prop types written";
898
+ }
899
+ }
900
+ const fresh = {
407
901
  scope: source.scope,
408
902
  framework: source.framework,
409
903
  version: source.version,
410
904
  contentHash: source.contentHash,
411
905
  pulledAt: (/* @__PURE__ */ new Date()).toISOString()
412
906
  };
413
- lock[name] = newEntry;
414
- results.push({ name, scope: source.scope, version: source.version, files });
907
+ lock[name] = nextLockEntry(entry, fresh);
908
+ results.push({
909
+ name,
910
+ scope: source.scope,
911
+ version: source.version,
912
+ files,
913
+ manifestChanged: manifest.changed,
914
+ registry,
915
+ types,
916
+ typesNotice
917
+ });
415
918
  }
416
919
  await writeLock(config.root, lock);
417
920
  return results;
@@ -484,12 +987,14 @@ async function pushSection(config, options) {
484
987
  contentHash: response.contentHash,
485
988
  pulledAt: (/* @__PURE__ */ new Date()).toISOString()
486
989
  });
990
+ const drift = compareSchemas(manifest, response.definition?.schema) ?? void 0;
487
991
  return {
488
992
  name,
489
993
  created: response.created,
490
994
  scope: response.scope,
491
995
  version: response.version,
492
- contentHash: response.contentHash
996
+ contentHash: response.contentHash,
997
+ drift
493
998
  };
494
999
  }
495
1000
 
@@ -506,10 +1011,20 @@ async function sectionStatuses(config, options = {}) {
506
1011
  const names = options.names?.length ? options.names : await listManifests(config.sectionsDir);
507
1012
  const lock = await readLock(config.root);
508
1013
  let summaries = /* @__PURE__ */ new Map();
509
- if (!options.offline) {
510
- const listing = await clientFor(config).sections.list({ framework: config.framework });
1014
+ const client = options.offline ? null : clientFor(config);
1015
+ if (client) {
1016
+ const listing = await client.sections.list({ framework: config.framework });
511
1017
  summaries = new Map(listing.sections.map((s) => [s.name, s]));
512
1018
  }
1019
+ const driftFor = async (manifest) => {
1020
+ if (!client || !manifestSchema(manifest) || !summaries.has(manifest.section)) return void 0;
1021
+ try {
1022
+ const pulled = await client.sections.pull(manifest.section, { framework: config.framework });
1023
+ return compareSchemas(manifest, pulled.definition?.schema) ?? void 0;
1024
+ } catch {
1025
+ return void 0;
1026
+ }
1027
+ };
513
1028
  const rows = [];
514
1029
  for (const name of names) {
515
1030
  const entry = lock[name];
@@ -523,7 +1038,8 @@ async function sectionStatuses(config, options = {}) {
523
1038
  status: computeStatus({ localHash: localHash2, lock: entry, remote }),
524
1039
  local: entry?.version ?? null,
525
1040
  remote: remote?.version ?? null,
526
- scope: entry?.scope ?? summary?.effective ?? "-"
1041
+ scope: entry?.scope ?? summary?.effective ?? "-",
1042
+ drift: await driftFor(manifest)
527
1043
  });
528
1044
  } catch (error) {
529
1045
  rows.push({
@@ -541,12 +1057,16 @@ async function sectionStatuses(config, options = {}) {
541
1057
 
542
1058
  // src/commands/init.ts
543
1059
  async function initSections(config, options = {}) {
544
- if (!await hasRegistry(config.sectionsDir)) {
545
- throw new Error(`No ${REGISTRY_FILE} in ${config.sectionsDir}; sections init reads the registry map to find components`);
1060
+ if (!await hasRegistry(config.sectionsDir, config.registryFile)) {
1061
+ throw new Error(`No ${config.registryFile}; sections init reads the registry map to find components`);
546
1062
  }
547
1063
  const existing = new Set(await listManifests(config.sectionsDir));
548
1064
  const npmVersions = await readNpmVersions(config.sectionsDir);
549
- const derived = await deriveManifests(config.sectionsDir, { framework: config.framework, npmVersions });
1065
+ const derived = await deriveManifests(config.sectionsDir, {
1066
+ framework: config.framework,
1067
+ npmVersions,
1068
+ registryFile: config.registryFile
1069
+ });
550
1070
  const results = [];
551
1071
  for (const { name, manifest, entryFile } of derived) {
552
1072
  const skip = existing.has(name) && !options.force;
@@ -562,26 +1082,47 @@ export {
562
1082
  CONFIG_FILE,
563
1083
  ConfigError,
564
1084
  DEFAULT_FRAMEWORK,
1085
+ DEFAULT_REGISTRY_FILE,
565
1086
  DEFAULT_SECTIONS_DIR,
566
1087
  LOCK_DIR,
567
1088
  LOCK_FILE,
1089
+ MANIFEST_KEY_ORDER,
568
1090
  MANIFEST_SUFFIX,
569
1091
  ManifestError,
1092
+ PROPS_SUFFIX,
570
1093
  REGISTRY_FILE,
1094
+ addRegistryEntry,
571
1095
  clientFor,
1096
+ collectDslFields,
1097
+ collectFields,
1098
+ collectFormBuilderFields,
1099
+ compareSchemas,
572
1100
  computeStatus,
573
1101
  deriveManifests,
574
1102
  findConfigFile,
1103
+ formatDriftWarning,
1104
+ formatManifest,
1105
+ generatePropsSource,
575
1106
  hasRegistry,
1107
+ importPathFor,
576
1108
  initSections,
577
1109
  listManifests,
578
1110
  listSections,
579
1111
  loadConfig,
580
1112
  lockPath,
581
1113
  manifestPath,
1114
+ manifestSchema,
1115
+ manifestsEqual,
1116
+ nextLockEntry,
1117
+ normalizeManifest,
1118
+ optionUnion,
582
1119
  parseDefinition,
583
1120
  parseImports,
584
1121
  parseRegistry,
1122
+ parseZodSchema,
1123
+ pascalCase,
1124
+ propsInterfaceName,
1125
+ propsPath,
585
1126
  pullSections,
586
1127
  pushSection,
587
1128
  readConfigFile,
@@ -589,13 +1130,19 @@ export {
589
1130
  readManifest,
590
1131
  readNpmVersions,
591
1132
  readSectionFiles,
1133
+ registerSection,
592
1134
  remoteForLock,
593
1135
  safeJoin,
1136
+ schemaFieldNames,
594
1137
  sectionStatuses,
1138
+ tsTypeForField,
595
1139
  updateLock,
596
1140
  validateManifest,
597
1141
  writeLock,
598
1142
  writeManifest,
599
- writeSectionFiles
1143
+ writeManifestIfChanged,
1144
+ writePropsTypes,
1145
+ writeSectionFiles,
1146
+ zodFieldToSchemaField
600
1147
  };
601
1148
  //# sourceMappingURL=index.js.map