@botlr/cli 0.1.2 → 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/CHANGELOG.md +39 -0
- package/README.md +154 -8
- package/dist/cli.js +902 -138
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +303 -6
- package/dist/index.js +719 -21
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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,
|
|
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 =
|
|
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,
|
|
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,375 @@ 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(
|
|
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 { schemaFormat } from "@botlr/sdk";
|
|
809
|
+
function manifestSchema(manifest) {
|
|
810
|
+
const schema = manifest.schema;
|
|
811
|
+
return schemaFormat(schema) ? schema : null;
|
|
812
|
+
}
|
|
813
|
+
function compareSchemas(manifest, definitionSchema) {
|
|
814
|
+
const code = manifestSchema(manifest);
|
|
815
|
+
if (!code) return null;
|
|
816
|
+
const codeKeys = new Set(schemaFieldNames(code));
|
|
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; a code push never changes it. To resolve:`);
|
|
832
|
+
lines.push(` botlr schema pull ${name} takes the dashboard's schema into ${name}.botlr.json`);
|
|
833
|
+
lines.push(` botlr schema push ${name} sends the code's schema to the dashboard`);
|
|
834
|
+
return lines.join("\n");
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// src/schema-diff.ts
|
|
838
|
+
import { schemaFormat as schemaFormat2 } from "@botlr/sdk";
|
|
839
|
+
function schemaKeys(schema) {
|
|
840
|
+
return [...new Set(schemaFieldNames(schema))];
|
|
841
|
+
}
|
|
842
|
+
function diffSchemaKeys(current, next) {
|
|
843
|
+
const currentKeys = new Set(schemaKeys(current));
|
|
844
|
+
const nextKeys = new Set(schemaKeys(next));
|
|
845
|
+
return {
|
|
846
|
+
added: [...nextKeys].filter((k) => !currentKeys.has(k)).sort(),
|
|
847
|
+
removed: [...currentKeys].filter((k) => !nextKeys.has(k)).sort(),
|
|
848
|
+
kept: [...nextKeys].filter((k) => currentKeys.has(k)).sort()
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
function previewSchemaPush(name, current, next, formats = {}) {
|
|
852
|
+
return {
|
|
853
|
+
name,
|
|
854
|
+
current,
|
|
855
|
+
currentFormat: formats.currentFormat ?? schemaFormat2(current),
|
|
856
|
+
next,
|
|
857
|
+
nextFormat: formats.nextFormat ?? schemaFormat2(next),
|
|
858
|
+
diff: diffSchemaKeys(current, next)
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
function plural(n, word) {
|
|
862
|
+
return `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
863
|
+
}
|
|
864
|
+
function formatName(format) {
|
|
865
|
+
return format ?? "unknown";
|
|
866
|
+
}
|
|
867
|
+
function formatSchemaDiff(preview) {
|
|
868
|
+
const { diff } = preview;
|
|
869
|
+
const header = `schema of '${preview.name}': dashboard has ${formatName(preview.currentFormat)} (${plural(schemaKeys(preview.current).length, "field")}), code has ${formatName(preview.nextFormat)} (${plural(schemaKeys(preview.next).length, "field")})`;
|
|
870
|
+
const lines = [header];
|
|
871
|
+
if (diff.added.length) lines.push(` + added ${diff.added.join(", ")}`);
|
|
872
|
+
if (diff.removed.length) lines.push(` - removed ${diff.removed.join(", ")}`);
|
|
873
|
+
lines.push(` = kept ${diff.kept.length ? diff.kept.join(", ") : "none"}`);
|
|
874
|
+
if (preview.currentFormat && preview.nextFormat && preview.currentFormat !== preview.nextFormat) {
|
|
875
|
+
lines.push(` the formats differ: the push needs --force to replace ${preview.currentFormat} with ${preview.nextFormat}`);
|
|
876
|
+
}
|
|
877
|
+
if (!diff.added.length && !diff.removed.length) lines.push(" the field keys match; only field settings may change");
|
|
878
|
+
return lines.join("\n");
|
|
879
|
+
}
|
|
880
|
+
|
|
351
881
|
// src/client.ts
|
|
352
882
|
import { createBotlrClient } from "@botlr/sdk";
|
|
353
883
|
function clientFor(config, options = {}) {
|
|
@@ -402,16 +932,35 @@ async function pullSections(config, options) {
|
|
|
402
932
|
});
|
|
403
933
|
const { source } = response;
|
|
404
934
|
const files = await writeSectionFiles(config.sectionsDir, source.files);
|
|
405
|
-
await
|
|
406
|
-
const
|
|
935
|
+
const manifest = await writeManifestIfChanged(config.sectionsDir, name, source.manifest);
|
|
936
|
+
const registry = options.noRegistry ? void 0 : await registerSection(config.sectionsDir, config.registryFile, source.manifest);
|
|
937
|
+
let types;
|
|
938
|
+
let typesNotice;
|
|
939
|
+
if (config.types && !options.noTypes) {
|
|
940
|
+
if (response.definition) {
|
|
941
|
+
types = await writePropsTypes(config.sectionsDir, source.manifest, response.definition.schema);
|
|
942
|
+
} else {
|
|
943
|
+
typesNotice = "no section definition resolves for this section, no prop types written";
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
const fresh = {
|
|
407
947
|
scope: source.scope,
|
|
408
948
|
framework: source.framework,
|
|
409
949
|
version: source.version,
|
|
410
950
|
contentHash: source.contentHash,
|
|
411
951
|
pulledAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
412
952
|
};
|
|
413
|
-
lock[name] =
|
|
414
|
-
results.push({
|
|
953
|
+
lock[name] = nextLockEntry(entry, fresh);
|
|
954
|
+
results.push({
|
|
955
|
+
name,
|
|
956
|
+
scope: source.scope,
|
|
957
|
+
version: source.version,
|
|
958
|
+
files,
|
|
959
|
+
manifestChanged: manifest.changed,
|
|
960
|
+
registry,
|
|
961
|
+
types,
|
|
962
|
+
typesNotice
|
|
963
|
+
});
|
|
415
964
|
}
|
|
416
965
|
await writeLock(config.root, lock);
|
|
417
966
|
return results;
|
|
@@ -484,12 +1033,14 @@ async function pushSection(config, options) {
|
|
|
484
1033
|
contentHash: response.contentHash,
|
|
485
1034
|
pulledAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
486
1035
|
});
|
|
1036
|
+
const drift = compareSchemas(manifest, response.definition?.schema) ?? void 0;
|
|
487
1037
|
return {
|
|
488
1038
|
name,
|
|
489
1039
|
created: response.created,
|
|
490
1040
|
scope: response.scope,
|
|
491
1041
|
version: response.version,
|
|
492
|
-
contentHash: response.contentHash
|
|
1042
|
+
contentHash: response.contentHash,
|
|
1043
|
+
drift
|
|
493
1044
|
};
|
|
494
1045
|
}
|
|
495
1046
|
|
|
@@ -506,10 +1057,20 @@ async function sectionStatuses(config, options = {}) {
|
|
|
506
1057
|
const names = options.names?.length ? options.names : await listManifests(config.sectionsDir);
|
|
507
1058
|
const lock = await readLock(config.root);
|
|
508
1059
|
let summaries = /* @__PURE__ */ new Map();
|
|
509
|
-
|
|
510
|
-
|
|
1060
|
+
const client = options.offline ? null : clientFor(config);
|
|
1061
|
+
if (client) {
|
|
1062
|
+
const listing = await client.sections.list({ framework: config.framework });
|
|
511
1063
|
summaries = new Map(listing.sections.map((s) => [s.name, s]));
|
|
512
1064
|
}
|
|
1065
|
+
const driftFor = async (manifest) => {
|
|
1066
|
+
if (!client || !manifestSchema(manifest) || !summaries.has(manifest.section)) return void 0;
|
|
1067
|
+
try {
|
|
1068
|
+
const pulled = await client.sections.pull(manifest.section, { framework: config.framework });
|
|
1069
|
+
return compareSchemas(manifest, pulled.definition?.schema) ?? void 0;
|
|
1070
|
+
} catch {
|
|
1071
|
+
return void 0;
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
513
1074
|
const rows = [];
|
|
514
1075
|
for (const name of names) {
|
|
515
1076
|
const entry = lock[name];
|
|
@@ -523,7 +1084,8 @@ async function sectionStatuses(config, options = {}) {
|
|
|
523
1084
|
status: computeStatus({ localHash: localHash2, lock: entry, remote }),
|
|
524
1085
|
local: entry?.version ?? null,
|
|
525
1086
|
remote: remote?.version ?? null,
|
|
526
|
-
scope: entry?.scope ?? summary?.effective ?? "-"
|
|
1087
|
+
scope: entry?.scope ?? summary?.effective ?? "-",
|
|
1088
|
+
drift: await driftFor(manifest)
|
|
527
1089
|
});
|
|
528
1090
|
} catch (error) {
|
|
529
1091
|
rows.push({
|
|
@@ -541,12 +1103,16 @@ async function sectionStatuses(config, options = {}) {
|
|
|
541
1103
|
|
|
542
1104
|
// src/commands/init.ts
|
|
543
1105
|
async function initSections(config, options = {}) {
|
|
544
|
-
if (!await hasRegistry(config.sectionsDir)) {
|
|
545
|
-
throw new Error(`No ${
|
|
1106
|
+
if (!await hasRegistry(config.sectionsDir, config.registryFile)) {
|
|
1107
|
+
throw new Error(`No ${config.registryFile}; sections init reads the registry map to find components`);
|
|
546
1108
|
}
|
|
547
1109
|
const existing = new Set(await listManifests(config.sectionsDir));
|
|
548
1110
|
const npmVersions = await readNpmVersions(config.sectionsDir);
|
|
549
|
-
const derived = await deriveManifests(config.sectionsDir, {
|
|
1111
|
+
const derived = await deriveManifests(config.sectionsDir, {
|
|
1112
|
+
framework: config.framework,
|
|
1113
|
+
npmVersions,
|
|
1114
|
+
registryFile: config.registryFile
|
|
1115
|
+
});
|
|
550
1116
|
const results = [];
|
|
551
1117
|
for (const { name, manifest, entryFile } of derived) {
|
|
552
1118
|
const skip = existing.has(name) && !options.force;
|
|
@@ -558,44 +1124,176 @@ async function initSections(config, options = {}) {
|
|
|
558
1124
|
}
|
|
559
1125
|
return results;
|
|
560
1126
|
}
|
|
1127
|
+
|
|
1128
|
+
// src/commands/schema.ts
|
|
1129
|
+
import { BotlrError, BotlrSchemaFormatConflictError, botlrErrorMessage } from "@botlr/sdk";
|
|
1130
|
+
function isNotFound(error) {
|
|
1131
|
+
return error instanceof BotlrError && error.status === 404;
|
|
1132
|
+
}
|
|
1133
|
+
async function pullSchemas(config, options) {
|
|
1134
|
+
const client = clientFor(config);
|
|
1135
|
+
const names = options.all ? await listManifests(config.sectionsDir) : options.names;
|
|
1136
|
+
const results = [];
|
|
1137
|
+
for (const name of names) {
|
|
1138
|
+
let manifest;
|
|
1139
|
+
try {
|
|
1140
|
+
manifest = await readManifest(config.sectionsDir, name);
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
results.push({ name, changed: false, file: "", error: error.message });
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
let definition;
|
|
1146
|
+
try {
|
|
1147
|
+
definition = (await client.sections.getSchema(name)).definition;
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
if (!isNotFound(error)) throw error;
|
|
1150
|
+
results.push({ name, changed: false, file: "", error: `the app has no section type '${name}' (botlr push ${name} --create adds one)` });
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
const updated = { ...manifest, schema: definition.schema };
|
|
1154
|
+
const written = await writeManifestIfChanged(config.sectionsDir, name, updated);
|
|
1155
|
+
const types = config.types && !options.noTypes ? await writePropsTypes(config.sectionsDir, manifest, definition.schema) : void 0;
|
|
1156
|
+
results.push({ name, changed: written.changed, file: written.file, format: definition.format, types });
|
|
1157
|
+
}
|
|
1158
|
+
return results;
|
|
1159
|
+
}
|
|
1160
|
+
var SchemaFormatConflict = class extends Error {
|
|
1161
|
+
constructor(sectionName, currentFormat, newFormat, message) {
|
|
1162
|
+
super(message ?? `the schema of '${sectionName}' is stored as ${currentFormat}, the manifest's is ${newFormat}`);
|
|
1163
|
+
this.sectionName = sectionName;
|
|
1164
|
+
this.currentFormat = currentFormat;
|
|
1165
|
+
this.newFormat = newFormat;
|
|
1166
|
+
this.name = "SchemaFormatConflict";
|
|
1167
|
+
}
|
|
1168
|
+
sectionName;
|
|
1169
|
+
currentFormat;
|
|
1170
|
+
newFormat;
|
|
1171
|
+
};
|
|
1172
|
+
var SchemaRejected = class extends Error {
|
|
1173
|
+
constructor(sectionName, message) {
|
|
1174
|
+
super(message);
|
|
1175
|
+
this.sectionName = sectionName;
|
|
1176
|
+
this.name = "SchemaRejected";
|
|
1177
|
+
}
|
|
1178
|
+
sectionName;
|
|
1179
|
+
};
|
|
1180
|
+
async function pushSchema(config, options) {
|
|
1181
|
+
const client = clientFor(config, { needPersonalToken: true });
|
|
1182
|
+
const { name } = options;
|
|
1183
|
+
const manifest = await readManifest(config.sectionsDir, name);
|
|
1184
|
+
const next = manifest.schema;
|
|
1185
|
+
if (!next || typeof next !== "object" || Array.isArray(next)) {
|
|
1186
|
+
throw new ManifestError(
|
|
1187
|
+
`${name}.botlr.json has no 'schema' object to push; add one, or run 'botlr schema pull ${name}' to take the dashboard's`
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
let current;
|
|
1191
|
+
try {
|
|
1192
|
+
current = (await client.sections.getSchema(name)).definition;
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
if (!isNotFound(error)) throw error;
|
|
1195
|
+
throw new ManifestError(`the app has no section type '${name}'; push the code first with 'botlr push ${name} --create'`);
|
|
1196
|
+
}
|
|
1197
|
+
const preview = previewSchemaPush(name, current.schema, next, { currentFormat: current.format });
|
|
1198
|
+
if (options.confirm && !await options.confirm(preview)) {
|
|
1199
|
+
return { name, preview, pushed: false };
|
|
1200
|
+
}
|
|
1201
|
+
let response;
|
|
1202
|
+
try {
|
|
1203
|
+
response = await client.sections.putSchema(name, { schema: next, force: options.force ?? false });
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
if (error instanceof BotlrSchemaFormatConflictError) {
|
|
1206
|
+
throw new SchemaFormatConflict(name, error.currentFormat, error.newFormat, botlrErrorMessage(error));
|
|
1207
|
+
}
|
|
1208
|
+
if (error instanceof BotlrError && error.status === 422) {
|
|
1209
|
+
throw new SchemaRejected(name, botlrErrorMessage(error) ?? "the server rejected the schema (422)");
|
|
1210
|
+
}
|
|
1211
|
+
throw error;
|
|
1212
|
+
}
|
|
1213
|
+
const types = config.types && !options.noTypes ? await writePropsTypes(config.sectionsDir, manifest, response.definition.schema) : void 0;
|
|
1214
|
+
return {
|
|
1215
|
+
name,
|
|
1216
|
+
preview,
|
|
1217
|
+
pushed: true,
|
|
1218
|
+
changed: response.changed,
|
|
1219
|
+
previousFormat: response.previousFormat,
|
|
1220
|
+
format: response.definition.format,
|
|
1221
|
+
types
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
561
1224
|
export {
|
|
562
1225
|
CONFIG_FILE,
|
|
563
1226
|
ConfigError,
|
|
564
1227
|
DEFAULT_FRAMEWORK,
|
|
1228
|
+
DEFAULT_REGISTRY_FILE,
|
|
565
1229
|
DEFAULT_SECTIONS_DIR,
|
|
566
1230
|
LOCK_DIR,
|
|
567
1231
|
LOCK_FILE,
|
|
1232
|
+
MANIFEST_KEY_ORDER,
|
|
568
1233
|
MANIFEST_SUFFIX,
|
|
569
1234
|
ManifestError,
|
|
1235
|
+
PROPS_SUFFIX,
|
|
570
1236
|
REGISTRY_FILE,
|
|
1237
|
+
SchemaFormatConflict,
|
|
1238
|
+
SchemaRejected,
|
|
1239
|
+
addRegistryEntry,
|
|
571
1240
|
clientFor,
|
|
1241
|
+
collectDslFields,
|
|
1242
|
+
collectFields,
|
|
1243
|
+
collectFormBuilderFields,
|
|
1244
|
+
compareSchemas,
|
|
572
1245
|
computeStatus,
|
|
573
1246
|
deriveManifests,
|
|
1247
|
+
diffSchemaKeys,
|
|
574
1248
|
findConfigFile,
|
|
1249
|
+
formatDriftWarning,
|
|
1250
|
+
formatManifest,
|
|
1251
|
+
formatSchemaDiff,
|
|
1252
|
+
generatePropsSource,
|
|
575
1253
|
hasRegistry,
|
|
1254
|
+
importPathFor,
|
|
576
1255
|
initSections,
|
|
577
1256
|
listManifests,
|
|
578
1257
|
listSections,
|
|
579
1258
|
loadConfig,
|
|
580
1259
|
lockPath,
|
|
581
1260
|
manifestPath,
|
|
1261
|
+
manifestSchema,
|
|
1262
|
+
manifestsEqual,
|
|
1263
|
+
nextLockEntry,
|
|
1264
|
+
normalizeManifest,
|
|
1265
|
+
optionUnion,
|
|
582
1266
|
parseDefinition,
|
|
583
1267
|
parseImports,
|
|
584
1268
|
parseRegistry,
|
|
1269
|
+
parseZodSchema,
|
|
1270
|
+
pascalCase,
|
|
1271
|
+
previewSchemaPush,
|
|
1272
|
+
propsInterfaceName,
|
|
1273
|
+
propsPath,
|
|
1274
|
+
pullSchemas,
|
|
585
1275
|
pullSections,
|
|
1276
|
+
pushSchema,
|
|
586
1277
|
pushSection,
|
|
587
1278
|
readConfigFile,
|
|
588
1279
|
readLock,
|
|
589
1280
|
readManifest,
|
|
590
1281
|
readNpmVersions,
|
|
591
1282
|
readSectionFiles,
|
|
1283
|
+
registerSection,
|
|
592
1284
|
remoteForLock,
|
|
593
1285
|
safeJoin,
|
|
1286
|
+
schemaFieldNames,
|
|
1287
|
+
schemaKeys,
|
|
594
1288
|
sectionStatuses,
|
|
1289
|
+
tsTypeForField,
|
|
595
1290
|
updateLock,
|
|
596
1291
|
validateManifest,
|
|
597
1292
|
writeLock,
|
|
598
1293
|
writeManifest,
|
|
599
|
-
|
|
1294
|
+
writeManifestIfChanged,
|
|
1295
|
+
writePropsTypes,
|
|
1296
|
+
writeSectionFiles,
|
|
1297
|
+
zodFieldToSchemaField
|
|
600
1298
|
};
|
|
601
1299
|
//# sourceMappingURL=index.js.map
|