@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/cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command, InvalidArgumentError } from "commander";
|
|
5
5
|
import { createRequire } from "module";
|
|
6
|
+
import path7 from "path";
|
|
6
7
|
|
|
7
8
|
// src/config.ts
|
|
8
9
|
import { access, readFile } from "fs/promises";
|
|
@@ -10,6 +11,7 @@ import path from "path";
|
|
|
10
11
|
var CONFIG_FILE = "botlr.config.json";
|
|
11
12
|
var DEFAULT_FRAMEWORK = "react-next";
|
|
12
13
|
var DEFAULT_SECTIONS_DIR = "components/sections";
|
|
14
|
+
var DEFAULT_REGISTRY_FILE = "publisher-section-registry.ts";
|
|
13
15
|
var ConfigError = class extends Error {
|
|
14
16
|
constructor(message) {
|
|
15
17
|
super(message);
|
|
@@ -67,6 +69,8 @@ async function loadConfig(cwd, overrides2 = {}, env = process.env) {
|
|
|
67
69
|
appId,
|
|
68
70
|
framework: overrides2.framework ?? env.BOTLR_FRAMEWORK ?? file.framework ?? DEFAULT_FRAMEWORK,
|
|
69
71
|
sectionsDir,
|
|
72
|
+
registryFile: path.resolve(sectionsDir, file.registryFile ?? DEFAULT_REGISTRY_FILE),
|
|
73
|
+
types: file.types !== false,
|
|
70
74
|
apiKey: env.BOTLR_API_KEY || void 0,
|
|
71
75
|
personalToken: env.BOTLR_PAT || void 0
|
|
72
76
|
};
|
|
@@ -104,6 +108,12 @@ import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
|
|
|
104
108
|
import path2 from "path";
|
|
105
109
|
var LOCK_DIR = ".botlr";
|
|
106
110
|
var LOCK_FILE = "sections.lock.json";
|
|
111
|
+
function nextLockEntry(previous, fresh) {
|
|
112
|
+
if (previous && previous.scope === fresh.scope && previous.framework === fresh.framework && previous.version === fresh.version && previous.contentHash === fresh.contentHash) {
|
|
113
|
+
return { ...fresh, pulledAt: previous.pulledAt };
|
|
114
|
+
}
|
|
115
|
+
return fresh;
|
|
116
|
+
}
|
|
107
117
|
function lockPath(root) {
|
|
108
118
|
return path2.join(root, LOCK_DIR, LOCK_FILE);
|
|
109
119
|
}
|
|
@@ -127,7 +137,7 @@ async function writeLock(root, lock) {
|
|
|
127
137
|
}
|
|
128
138
|
async function updateLock(root, name, entry) {
|
|
129
139
|
const lock = await readLock(root);
|
|
130
|
-
lock[name] = entry;
|
|
140
|
+
lock[name] = nextLockEntry(lock[name], entry);
|
|
131
141
|
await writeLock(root, lock);
|
|
132
142
|
return lock;
|
|
133
143
|
}
|
|
@@ -198,13 +208,80 @@ function validateManifest(value, where = "manifest") {
|
|
|
198
208
|
}
|
|
199
209
|
return m;
|
|
200
210
|
}
|
|
211
|
+
var MANIFEST_KEY_ORDER = [
|
|
212
|
+
"section",
|
|
213
|
+
"framework",
|
|
214
|
+
"entry",
|
|
215
|
+
"exportName",
|
|
216
|
+
"files",
|
|
217
|
+
"title",
|
|
218
|
+
"description",
|
|
219
|
+
"category",
|
|
220
|
+
"icon",
|
|
221
|
+
"dependencies",
|
|
222
|
+
"schema"
|
|
223
|
+
];
|
|
224
|
+
var DEPENDENCIES_KEY_ORDER = ["npm", "internal"];
|
|
225
|
+
function orderKeys(value, first) {
|
|
226
|
+
const out = {};
|
|
227
|
+
for (const key of first) {
|
|
228
|
+
if (key in value && value[key] !== void 0) out[key] = value[key];
|
|
229
|
+
}
|
|
230
|
+
for (const key of Object.keys(value).sort()) {
|
|
231
|
+
if (first.includes(key) || value[key] === void 0) continue;
|
|
232
|
+
out[key] = value[key];
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
function normalizeNested(value) {
|
|
237
|
+
if (Array.isArray(value)) return value.map(normalizeNested);
|
|
238
|
+
if (value && typeof value === "object") {
|
|
239
|
+
const record = value;
|
|
240
|
+
const ordered = orderKeys(record, []);
|
|
241
|
+
for (const key of Object.keys(ordered)) ordered[key] = normalizeNested(ordered[key]);
|
|
242
|
+
return ordered;
|
|
243
|
+
}
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
function normalizeManifest(manifest) {
|
|
247
|
+
const stripped = stripManifestState(manifest);
|
|
248
|
+
const ordered = orderKeys(stripped, MANIFEST_KEY_ORDER);
|
|
249
|
+
for (const key of Object.keys(ordered)) {
|
|
250
|
+
if (key === "dependencies" && ordered[key] && typeof ordered[key] === "object" && !Array.isArray(ordered[key])) {
|
|
251
|
+
const deps = orderKeys(ordered[key], DEPENDENCIES_KEY_ORDER);
|
|
252
|
+
for (const depKey of Object.keys(deps)) deps[depKey] = normalizeNested(deps[depKey]);
|
|
253
|
+
ordered[key] = deps;
|
|
254
|
+
} else {
|
|
255
|
+
ordered[key] = normalizeNested(ordered[key]);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return ordered;
|
|
259
|
+
}
|
|
260
|
+
function formatManifest(manifest) {
|
|
261
|
+
return `${JSON.stringify(normalizeManifest(manifest), null, 2)}
|
|
262
|
+
`;
|
|
263
|
+
}
|
|
264
|
+
function manifestsEqual(a, b) {
|
|
265
|
+
return formatManifest(a) === formatManifest(b);
|
|
266
|
+
}
|
|
201
267
|
async function writeManifest(sectionsDir, name, manifest) {
|
|
202
268
|
const file = manifestPath(sectionsDir, name);
|
|
203
269
|
await mkdir2(sectionsDir, { recursive: true });
|
|
204
|
-
await writeFile2(file,
|
|
205
|
-
`, "utf8");
|
|
270
|
+
await writeFile2(file, formatManifest(manifest), "utf8");
|
|
206
271
|
return file;
|
|
207
272
|
}
|
|
273
|
+
async function writeManifestIfChanged(sectionsDir, name, manifest) {
|
|
274
|
+
const file = manifestPath(sectionsDir, name);
|
|
275
|
+
try {
|
|
276
|
+
const existing = JSON.parse(await readFile3(file, "utf8"));
|
|
277
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
278
|
+
if (manifestsEqual(existing, manifest)) return { file, changed: false };
|
|
279
|
+
}
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
await writeManifest(sectionsDir, name, manifest);
|
|
283
|
+
return { file, changed: true };
|
|
284
|
+
}
|
|
208
285
|
async function readSectionFiles(sectionsDir, manifest) {
|
|
209
286
|
return Promise.all(
|
|
210
287
|
manifest.files.map(async (relative) => {
|
|
@@ -240,6 +317,512 @@ function safeJoin(dir, relative) {
|
|
|
240
317
|
return target;
|
|
241
318
|
}
|
|
242
319
|
|
|
320
|
+
// src/registry.ts
|
|
321
|
+
import { readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
|
|
322
|
+
import path5 from "path";
|
|
323
|
+
|
|
324
|
+
// src/init.ts
|
|
325
|
+
import { readFile as readFile4, readdir as readdir2 } from "fs/promises";
|
|
326
|
+
import path4 from "path";
|
|
327
|
+
function parseRegistry(source) {
|
|
328
|
+
const importedFrom = /* @__PURE__ */ new Map();
|
|
329
|
+
const importRe = /import\s*(?:type\s*)?\{([^}]*)\}\s*from\s*["']([^"']+)["']/g;
|
|
330
|
+
for (const match of source.matchAll(importRe)) {
|
|
331
|
+
const names = match[1];
|
|
332
|
+
const spec = match[2];
|
|
333
|
+
for (const raw of names.split(",")) {
|
|
334
|
+
const name = raw.trim().split(/\s+as\s+/).pop()?.trim();
|
|
335
|
+
if (name) importedFrom.set(name, spec);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const bodyMatch = source.match(/=\s*\{([\s\S]*?)\n\}\s*;?/);
|
|
339
|
+
if (!bodyMatch) return [];
|
|
340
|
+
const entries = [];
|
|
341
|
+
const entryRe = /^\s*(?:"([^"]+)"|'([^']+)'|([A-Za-z0-9_$-]+))\s*:\s*([A-Za-z0-9_$]+)\s*,?\s*(?:\/\/.*)?$/gm;
|
|
342
|
+
for (const match of bodyMatch[1].matchAll(entryRe)) {
|
|
343
|
+
const slug = match[1] ?? match[2] ?? match[3];
|
|
344
|
+
const exportName = match[4];
|
|
345
|
+
const importPath = importedFrom.get(exportName);
|
|
346
|
+
if (!importPath || !importPath.startsWith(".")) continue;
|
|
347
|
+
entries.push({ slug, exportName, importPath });
|
|
348
|
+
}
|
|
349
|
+
return entries;
|
|
350
|
+
}
|
|
351
|
+
function parseDefinition(source) {
|
|
352
|
+
const match = source.match(/export\s+const\s+\w+Definition\s*=\s*\{([\s\S]*?)\n\}/);
|
|
353
|
+
if (!match) return null;
|
|
354
|
+
const body = match[1];
|
|
355
|
+
const field = (key) => {
|
|
356
|
+
const m = body.match(new RegExp(`(?:^|\\n)\\s*${key}\\s*:\\s*(?:"([^"]*)"|'([^']*)'|\`([^\`]*)\`)`));
|
|
357
|
+
return m ? m[1] ?? m[2] ?? m[3] : void 0;
|
|
358
|
+
};
|
|
359
|
+
const schemaName = body.match(/(?:^|\n)\s*schema\s*:\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*,?/)?.[1];
|
|
360
|
+
return {
|
|
361
|
+
name: field("name"),
|
|
362
|
+
heading: field("heading"),
|
|
363
|
+
category: field("category"),
|
|
364
|
+
description: field("description"),
|
|
365
|
+
icon: field("icon"),
|
|
366
|
+
...schemaName ? { schemaName } : {}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function splitTopLevel(body) {
|
|
370
|
+
const parts = [];
|
|
371
|
+
let depth = 0;
|
|
372
|
+
let quote = null;
|
|
373
|
+
let current = "";
|
|
374
|
+
for (let i = 0; i < body.length; i++) {
|
|
375
|
+
const ch = body[i];
|
|
376
|
+
if (quote) {
|
|
377
|
+
current += ch;
|
|
378
|
+
if (ch === "\\") current += body[++i] ?? "";
|
|
379
|
+
else if (ch === quote) quote = null;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (ch === "/" && body[i + 1] === "/") {
|
|
383
|
+
while (i < body.length && body[i] !== "\n") i++;
|
|
384
|
+
current += "\n";
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
388
|
+
else if ("([{".includes(ch)) depth++;
|
|
389
|
+
else if (")]}".includes(ch)) depth--;
|
|
390
|
+
if (ch === "," && depth === 0) {
|
|
391
|
+
parts.push(current);
|
|
392
|
+
current = "";
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
current += ch;
|
|
396
|
+
}
|
|
397
|
+
parts.push(current);
|
|
398
|
+
return parts.map((p) => p.trim()).filter(Boolean);
|
|
399
|
+
}
|
|
400
|
+
function enumValues(expr) {
|
|
401
|
+
const m = expr.match(/^z\.enum\(\s*\[([\s\S]*?)\]/);
|
|
402
|
+
if (!m) return null;
|
|
403
|
+
const values = [];
|
|
404
|
+
for (const raw of splitTopLevel(m[1])) {
|
|
405
|
+
const lit = raw.match(/^(?:"([^"]*)"|'([^']*)'|`([^`]*)`)$/);
|
|
406
|
+
if (!lit) return null;
|
|
407
|
+
values.push(lit[1] ?? lit[2] ?? lit[3]);
|
|
408
|
+
}
|
|
409
|
+
return values;
|
|
410
|
+
}
|
|
411
|
+
function zodFieldToSchemaField(expr) {
|
|
412
|
+
const source = expr.trim();
|
|
413
|
+
const optional = /\.(optional|nullable|nullish|default)\(/.test(source);
|
|
414
|
+
let field = null;
|
|
415
|
+
if (/^z\.string\(/.test(source)) field = { type: /\.(email)\(/.test(source) ? "email" : /\.(url)\(/.test(source) ? "url" : "text" };
|
|
416
|
+
else if (/^z\.number\(/.test(source)) field = { type: /\.int\(/.test(source) ? "integer" : "number" };
|
|
417
|
+
else if (/^z\.boolean\(/.test(source)) field = { type: "boolean" };
|
|
418
|
+
else if (/^z\.enum\(/.test(source)) {
|
|
419
|
+
const options = enumValues(source);
|
|
420
|
+
field = options ? { type: "select", options } : { type: "select" };
|
|
421
|
+
} else if (/^z\.array\(/.test(source)) field = { type: "array" };
|
|
422
|
+
else if (/^z\.(object|record)\(/.test(source)) field = { type: "json" };
|
|
423
|
+
else if (/^z\.union\(\s*\[\s*z\.string\(\)\s*,\s*z\.(any|object|record)\(/.test(source)) field = { type: "asset" };
|
|
424
|
+
if (!field) return null;
|
|
425
|
+
if (!optional) field.required = true;
|
|
426
|
+
const describe = source.match(/\.describe\(\s*(?:"([^"]*)"|'([^']*)'|`([^`]*)`)\s*\)/);
|
|
427
|
+
const label = describe ? describe[1] ?? describe[2] ?? describe[3] : void 0;
|
|
428
|
+
if (label) field.label = label;
|
|
429
|
+
return field;
|
|
430
|
+
}
|
|
431
|
+
function parseZodSchema(source, schemaName) {
|
|
432
|
+
const start = source.match(new RegExp(`(?:export\\s+)?const\\s+${schemaName}\\s*=\\s*z\\.object\\(\\s*\\{`));
|
|
433
|
+
if (!start || start.index === void 0) return null;
|
|
434
|
+
const open = start.index + start[0].length;
|
|
435
|
+
let depth = 1;
|
|
436
|
+
let end = -1;
|
|
437
|
+
for (let i = open; i < source.length; i++) {
|
|
438
|
+
const ch = source[i];
|
|
439
|
+
if (ch === "{" || ch === "(" || ch === "[") depth++;
|
|
440
|
+
else if (ch === "}" || ch === ")" || ch === "]") {
|
|
441
|
+
depth--;
|
|
442
|
+
if (depth === 0) {
|
|
443
|
+
end = i;
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (end < 0) return null;
|
|
449
|
+
const schema2 = {};
|
|
450
|
+
for (const entry of splitTopLevel(source.slice(open, end))) {
|
|
451
|
+
const m = entry.match(/^(?:"([^"]+)"|'([^']+)'|([A-Za-z_$][A-Za-z0-9_$]*))\s*:\s*([\s\S]+)$/);
|
|
452
|
+
if (!m) continue;
|
|
453
|
+
const field = zodFieldToSchemaField(m[4]);
|
|
454
|
+
if (field) schema2[m[1] ?? m[2] ?? m[3]] = field;
|
|
455
|
+
}
|
|
456
|
+
return Object.keys(schema2).length ? schema2 : null;
|
|
457
|
+
}
|
|
458
|
+
function parseImports(source) {
|
|
459
|
+
const internal = /* @__PURE__ */ new Set();
|
|
460
|
+
const npm = /* @__PURE__ */ new Set();
|
|
461
|
+
const re = /(?:import|export)\s+(?:[^"';]*?\s+from\s+)?["']([^"']+)["']|import\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
462
|
+
for (const match of source.matchAll(re)) {
|
|
463
|
+
const spec = match[1] ?? match[2];
|
|
464
|
+
if (!spec) continue;
|
|
465
|
+
if (spec.startsWith(".") || spec.startsWith("@/") || spec.startsWith("~/")) internal.add(spec);
|
|
466
|
+
else npm.add(spec.startsWith("@") ? spec.split("/").slice(0, 2).join("/") : spec.split("/")[0]);
|
|
467
|
+
}
|
|
468
|
+
return { internal: [...internal].sort(), npm: [...npm].sort() };
|
|
469
|
+
}
|
|
470
|
+
var SOURCE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
|
|
471
|
+
async function resolveImport(sectionsDir, spec) {
|
|
472
|
+
const base = path4.resolve(sectionsDir, spec);
|
|
473
|
+
for (const ext of SOURCE_EXTENSIONS) {
|
|
474
|
+
try {
|
|
475
|
+
await readFile4(base + ext);
|
|
476
|
+
return base + ext;
|
|
477
|
+
} catch {
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
async function deriveManifests(sectionsDir, options) {
|
|
483
|
+
const registrySource = await readFile4(options.registryFile ?? path4.join(sectionsDir, DEFAULT_REGISTRY_FILE), "utf8");
|
|
484
|
+
const entries = parseRegistry(registrySource);
|
|
485
|
+
const out = [];
|
|
486
|
+
for (const entry of entries) {
|
|
487
|
+
const entryFile = await resolveImport(sectionsDir, entry.importPath);
|
|
488
|
+
if (!entryFile) continue;
|
|
489
|
+
const source = await readFile4(entryFile, "utf8");
|
|
490
|
+
const meta = parseDefinition(source);
|
|
491
|
+
const schema2 = meta?.schemaName ? parseZodSchema(source, meta.schemaName) : null;
|
|
492
|
+
const imports = parseImports(source);
|
|
493
|
+
const relative = path4.relative(sectionsDir, entryFile).split(path4.sep).join("/");
|
|
494
|
+
const npm = {};
|
|
495
|
+
for (const pkg of imports.npm) {
|
|
496
|
+
if (pkg === "react" || pkg === "next") continue;
|
|
497
|
+
npm[pkg] = options.npmVersions?.[pkg] ?? "*";
|
|
498
|
+
}
|
|
499
|
+
const manifest = {
|
|
500
|
+
section: entry.slug,
|
|
501
|
+
framework: options.framework,
|
|
502
|
+
entry: relative,
|
|
503
|
+
exportName: entry.exportName,
|
|
504
|
+
files: [relative],
|
|
505
|
+
...meta?.heading ? { title: meta.heading } : {},
|
|
506
|
+
...meta?.description ? { description: meta.description } : {},
|
|
507
|
+
...meta?.category ? { category: meta.category } : {},
|
|
508
|
+
...meta?.icon ? { icon: meta.icon } : {},
|
|
509
|
+
dependencies: {
|
|
510
|
+
...Object.keys(npm).length ? { npm } : {},
|
|
511
|
+
...imports.internal.length ? { internal: imports.internal } : {}
|
|
512
|
+
},
|
|
513
|
+
...schema2 ? { schema: schema2 } : {}
|
|
514
|
+
};
|
|
515
|
+
out.push({ name: entry.slug, manifest, entryFile });
|
|
516
|
+
}
|
|
517
|
+
return out;
|
|
518
|
+
}
|
|
519
|
+
async function readNpmVersions(dir) {
|
|
520
|
+
let current = path4.resolve(dir);
|
|
521
|
+
for (; ; ) {
|
|
522
|
+
try {
|
|
523
|
+
const raw = await readFile4(path4.join(current, "package.json"), "utf8");
|
|
524
|
+
const pkg = JSON.parse(raw);
|
|
525
|
+
return { ...pkg.devDependencies, ...pkg.dependencies };
|
|
526
|
+
} catch {
|
|
527
|
+
const parent = path4.dirname(current);
|
|
528
|
+
if (parent === current) return {};
|
|
529
|
+
current = parent;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
async function hasRegistry(sectionsDir, registryFile) {
|
|
534
|
+
const file = registryFile ?? path4.join(sectionsDir, DEFAULT_REGISTRY_FILE);
|
|
535
|
+
try {
|
|
536
|
+
return (await readdir2(path4.dirname(file))).includes(path4.basename(file));
|
|
537
|
+
} catch {
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// src/registry.ts
|
|
543
|
+
var SOURCE_EXTENSIONS2 = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
|
|
544
|
+
function importPathFor(registryFile, entryFile) {
|
|
545
|
+
const relative = path5.relative(path5.dirname(registryFile), entryFile).split(path5.sep).join("/");
|
|
546
|
+
const ext = path5.extname(relative);
|
|
547
|
+
const bare = SOURCE_EXTENSIONS2.includes(ext) ? relative.slice(0, -ext.length) : relative;
|
|
548
|
+
return bare.startsWith(".") ? bare : `./${bare}`;
|
|
549
|
+
}
|
|
550
|
+
var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
551
|
+
function addRegistryEntry(source, addition) {
|
|
552
|
+
const { slug, exportName, importPath } = addition;
|
|
553
|
+
if (!IDENTIFIER.test(exportName)) throw new Error(`'${exportName}' is not a valid export name`);
|
|
554
|
+
const eol = source.includes("\r\n") ? "\r\n" : "\n";
|
|
555
|
+
let out = source;
|
|
556
|
+
let addedImport = false;
|
|
557
|
+
let addedEntry = false;
|
|
558
|
+
const importRe = /^[ \t]*import\s*(?:type\s*)?\{([^}]*)\}\s*from\s*(["'])([^"']+)\2[ \t]*;?[ \t]*$/gm;
|
|
559
|
+
const imports = [...out.matchAll(importRe)];
|
|
560
|
+
const alreadyImported = imports.some(
|
|
561
|
+
(m) => m[1].split(",").some((raw) => raw.trim().split(/\s+as\s+/).pop()?.trim() === exportName)
|
|
562
|
+
);
|
|
563
|
+
if (!alreadyImported) {
|
|
564
|
+
const last = imports.at(-1);
|
|
565
|
+
const quote = last?.[2] ?? '"';
|
|
566
|
+
const semicolon = last ? last[0].trimEnd().endsWith(";") : true;
|
|
567
|
+
const line = `import { ${exportName} } from ${quote}${importPath}${quote}${semicolon ? ";" : ""}`;
|
|
568
|
+
if (last) {
|
|
569
|
+
const end = last.index + last[0].length;
|
|
570
|
+
out = `${out.slice(0, end)}${eol}${line}${out.slice(end)}`;
|
|
571
|
+
} else {
|
|
572
|
+
out = `${line}${eol}${eol}${out}`;
|
|
573
|
+
}
|
|
574
|
+
addedImport = true;
|
|
575
|
+
}
|
|
576
|
+
const alreadyMapped = parseRegistry(out).some((e) => e.slug === slug) || hasEntry(out, slug);
|
|
577
|
+
if (!alreadyMapped) {
|
|
578
|
+
const object = findRegistryObject(out);
|
|
579
|
+
if (!object) throw new Error("could not find the registry object (`export const ... = { ... }`)");
|
|
580
|
+
const body = out.slice(object.bodyStart, object.bodyEnd);
|
|
581
|
+
const entryLines = body.split(/\r?\n/).filter((l) => /^\s*(?:"[^"]+"|'[^']+'|[A-Za-z0-9_$-]+)\s*:/.test(l));
|
|
582
|
+
const lastEntry = entryLines.at(-1);
|
|
583
|
+
const indent = lastEntry?.match(/^\s*/)?.[0] ?? " ";
|
|
584
|
+
const key = IDENTIFIER.test(slug) ? slug : `"${slug}"`;
|
|
585
|
+
const entryLine = `${indent}${key}: ${exportName},`;
|
|
586
|
+
let newBody = body;
|
|
587
|
+
if (lastEntry !== void 0) {
|
|
588
|
+
const stripped = lastEntry.replace(/\/\/.*$/, "").trimEnd();
|
|
589
|
+
if (!stripped.endsWith(",")) {
|
|
590
|
+
const at = newBody.lastIndexOf(lastEntry);
|
|
591
|
+
const fixed = lastEntry.replace(/^(\s*(?:"[^"]+"|'[^']+'|[A-Za-z0-9_$-]+)\s*:\s*[A-Za-z0-9_$]+)/, "$1,");
|
|
592
|
+
newBody = `${newBody.slice(0, at)}${fixed}${newBody.slice(at + lastEntry.length)}`;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
newBody = `${newBody.replace(/\s*$/, "")}${eol}${entryLine}`;
|
|
596
|
+
out = `${out.slice(0, object.bodyStart)}${newBody}${out.slice(object.bodyEnd)}`;
|
|
597
|
+
addedEntry = true;
|
|
598
|
+
}
|
|
599
|
+
return { source: out, changed: addedImport || addedEntry, addedImport, addedEntry };
|
|
600
|
+
}
|
|
601
|
+
function hasEntry(source, slug) {
|
|
602
|
+
const object = findRegistryObject(source);
|
|
603
|
+
if (!object) return false;
|
|
604
|
+
const body = source.slice(object.bodyStart, object.bodyEnd);
|
|
605
|
+
const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
606
|
+
return new RegExp(`^\\s*(?:"${escaped}"|'${escaped}'|${escaped})\\s*:`, "m").test(body);
|
|
607
|
+
}
|
|
608
|
+
function findRegistryObject(source) {
|
|
609
|
+
const open = source.match(/export\s+const\s+\w+\s*(?::[^=]*)?=\s*\{/) ?? source.match(/=\s*\{/);
|
|
610
|
+
if (!open || open.index === void 0) return null;
|
|
611
|
+
const bodyStart = open.index + open[0].length;
|
|
612
|
+
const close = /\r?\n\}/g;
|
|
613
|
+
close.lastIndex = bodyStart;
|
|
614
|
+
const closeMatch = close.exec(source);
|
|
615
|
+
if (!closeMatch) return null;
|
|
616
|
+
return { bodyStart, bodyEnd: closeMatch.index };
|
|
617
|
+
}
|
|
618
|
+
async function registerSection(sectionsDir, registryFile, manifest) {
|
|
619
|
+
const name = path5.basename(registryFile);
|
|
620
|
+
let source;
|
|
621
|
+
try {
|
|
622
|
+
source = await readFile5(registryFile, "utf8");
|
|
623
|
+
} catch (error) {
|
|
624
|
+
if (error.code === "ENOENT") {
|
|
625
|
+
return { file: registryFile, changed: false, notice: `no ${name} in ${path5.dirname(registryFile)}, register the section yourself` };
|
|
626
|
+
}
|
|
627
|
+
throw error;
|
|
628
|
+
}
|
|
629
|
+
if (!manifest.exportName) {
|
|
630
|
+
return { file: registryFile, changed: false, notice: `manifest has no exportName, ${name} not updated` };
|
|
631
|
+
}
|
|
632
|
+
const importPath = importPathFor(registryFile, path5.resolve(sectionsDir, manifest.entry));
|
|
633
|
+
let edit;
|
|
634
|
+
try {
|
|
635
|
+
edit = addRegistryEntry(source, { slug: manifest.section, exportName: manifest.exportName, importPath });
|
|
636
|
+
} catch (error) {
|
|
637
|
+
return { file: registryFile, changed: false, notice: `${name} not updated: ${error.message}` };
|
|
638
|
+
}
|
|
639
|
+
if (edit.changed) await writeFile3(registryFile, edit.source, "utf8");
|
|
640
|
+
return { file: registryFile, changed: edit.changed, addedImport: edit.addedImport, addedEntry: edit.addedEntry };
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/typegen.ts
|
|
644
|
+
import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
|
|
645
|
+
import path6 from "path";
|
|
646
|
+
import {
|
|
647
|
+
isFieldSchema,
|
|
648
|
+
isFormBuilderSchema
|
|
649
|
+
} from "@botlr/sdk";
|
|
650
|
+
var PROPS_SUFFIX = ".props.ts";
|
|
651
|
+
var STRING_TYPES = /* @__PURE__ */ new Set([
|
|
652
|
+
"text",
|
|
653
|
+
"textarea",
|
|
654
|
+
"markdown",
|
|
655
|
+
"richtext",
|
|
656
|
+
"string",
|
|
657
|
+
"email",
|
|
658
|
+
"tel",
|
|
659
|
+
"url",
|
|
660
|
+
"icon",
|
|
661
|
+
"color",
|
|
662
|
+
"date",
|
|
663
|
+
"datetime",
|
|
664
|
+
"timestamp",
|
|
665
|
+
"timestamptz",
|
|
666
|
+
"uuid"
|
|
667
|
+
]);
|
|
668
|
+
var NUMBER_TYPES = /* @__PURE__ */ new Set(["number", "integer", "currency"]);
|
|
669
|
+
var BOOLEAN_TYPES = /* @__PURE__ */ new Set(["boolean"]);
|
|
670
|
+
var STRING_LIST_TYPES = /* @__PURE__ */ new Set(["multiselect", "tags"]);
|
|
671
|
+
var LIST_TYPES = /* @__PURE__ */ new Set(["array", "list", "repeater"]);
|
|
672
|
+
var OBJECT_TYPES = /* @__PURE__ */ new Set(["json", "jsonb", "object"]);
|
|
673
|
+
var REFERENCE_TYPES = /* @__PURE__ */ new Set([
|
|
674
|
+
"asset",
|
|
675
|
+
"asset-picker-image",
|
|
676
|
+
"asset-picker-file",
|
|
677
|
+
"asset-picker-video",
|
|
678
|
+
"media",
|
|
679
|
+
"document",
|
|
680
|
+
"page",
|
|
681
|
+
"post",
|
|
682
|
+
"reference",
|
|
683
|
+
"relation",
|
|
684
|
+
"entity_reference",
|
|
685
|
+
"app_instance",
|
|
686
|
+
"forum_thread"
|
|
687
|
+
]);
|
|
688
|
+
var REFERENCE_TYPE = "string | Record<string, unknown>";
|
|
689
|
+
var OBJECT_TYPE = "Record<string, unknown>";
|
|
690
|
+
var GROUP_NODE_TYPES = /* @__PURE__ */ new Set(["section", "fieldset", "composite", "group"]);
|
|
691
|
+
function literal(value) {
|
|
692
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
693
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
function optionUnion(options) {
|
|
697
|
+
if (!Array.isArray(options) || options.length === 0) return null;
|
|
698
|
+
const literals = [];
|
|
699
|
+
for (const option of options) {
|
|
700
|
+
const raw = option && typeof option === "object" ? option.value : option;
|
|
701
|
+
const lit = literal(raw);
|
|
702
|
+
if (lit === null) return null;
|
|
703
|
+
if (!literals.includes(lit)) literals.push(lit);
|
|
704
|
+
}
|
|
705
|
+
return literals.join(" | ");
|
|
706
|
+
}
|
|
707
|
+
function wrapArray(type) {
|
|
708
|
+
return /^[A-Za-z0-9_<>, ]+$/.test(type) ? `${type}[]` : `Array<${type}>`;
|
|
709
|
+
}
|
|
710
|
+
function tsTypeForField(field) {
|
|
711
|
+
const kind = typeof field.type === "string" ? field.type : "";
|
|
712
|
+
const multiple = field.multiple === true;
|
|
713
|
+
if (kind === "select") {
|
|
714
|
+
const union = optionUnion(field.options) ?? "string";
|
|
715
|
+
return multiple ? wrapArray(union) : union;
|
|
716
|
+
}
|
|
717
|
+
if (STRING_TYPES.has(kind)) return "string";
|
|
718
|
+
if (NUMBER_TYPES.has(kind)) return "number";
|
|
719
|
+
if (BOOLEAN_TYPES.has(kind)) return "boolean";
|
|
720
|
+
if (STRING_LIST_TYPES.has(kind)) return "string[]";
|
|
721
|
+
if (kind === "repeater" && Array.isArray(field.fields)) {
|
|
722
|
+
const items = collectFormBuilderFields(field.fields);
|
|
723
|
+
return items.length ? `Array<${inlineObject(items)}>` : "unknown[]";
|
|
724
|
+
}
|
|
725
|
+
if (LIST_TYPES.has(kind)) return "unknown[]";
|
|
726
|
+
if (OBJECT_TYPES.has(kind)) return OBJECT_TYPE;
|
|
727
|
+
if (REFERENCE_TYPES.has(kind)) return multiple ? `Array<${REFERENCE_TYPE}>` : REFERENCE_TYPE;
|
|
728
|
+
return "unknown";
|
|
729
|
+
}
|
|
730
|
+
function docFor(field) {
|
|
731
|
+
const doc = [];
|
|
732
|
+
for (const value of [field.label, field.helpText ?? field.description]) {
|
|
733
|
+
if (typeof value === "string" && value.trim()) doc.push(value.trim());
|
|
734
|
+
}
|
|
735
|
+
return doc;
|
|
736
|
+
}
|
|
737
|
+
function collectDslFields(schema2) {
|
|
738
|
+
const out = [];
|
|
739
|
+
for (const [name, value] of Object.entries(schema2)) {
|
|
740
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
741
|
+
const field = value;
|
|
742
|
+
out.push({ name, type: tsTypeForField(field), required: field.required === true, doc: docFor(field) });
|
|
743
|
+
}
|
|
744
|
+
return out;
|
|
745
|
+
}
|
|
746
|
+
function collectFormBuilderFields(nodes) {
|
|
747
|
+
const out = [];
|
|
748
|
+
for (const node of nodes) {
|
|
749
|
+
if (!node || typeof node !== "object") continue;
|
|
750
|
+
if (GROUP_NODE_TYPES.has(node.type) && Array.isArray(node.children)) {
|
|
751
|
+
out.push(...collectFormBuilderFields(node.children));
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
const name = typeof node.name === "string" && node.name ? node.name : typeof node.key === "string" ? node.key : "";
|
|
755
|
+
if (!name) continue;
|
|
756
|
+
const kind = node.type === "field" ? node.fieldType ?? "" : node.fieldType ?? node.type;
|
|
757
|
+
const type = tsTypeForField({ type: kind, options: node.options, multiple: node.multiple, fields: node.fields });
|
|
758
|
+
out.push({ name, type, required: node.required === true, doc: docFor(node) });
|
|
759
|
+
}
|
|
760
|
+
return out;
|
|
761
|
+
}
|
|
762
|
+
function collectFields(schema2) {
|
|
763
|
+
if (isFormBuilderSchema(schema2)) return collectFormBuilderFields(schema2.nodes);
|
|
764
|
+
if (isFieldSchema(schema2)) return collectDslFields(schema2);
|
|
765
|
+
return [];
|
|
766
|
+
}
|
|
767
|
+
function schemaFieldNames(schema2) {
|
|
768
|
+
return collectFields(schema2).map((f) => f.name);
|
|
769
|
+
}
|
|
770
|
+
var IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
771
|
+
function propertyKey(name) {
|
|
772
|
+
return IDENTIFIER_RE.test(name) ? name : JSON.stringify(name);
|
|
773
|
+
}
|
|
774
|
+
function inlineObject(fields) {
|
|
775
|
+
return `{ ${fields.map((f) => `${propertyKey(f.name)}${f.required ? "" : "?"}: ${f.type}`).join("; ")} }`;
|
|
776
|
+
}
|
|
777
|
+
function pascalCase(slug) {
|
|
778
|
+
const words = slug.split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
779
|
+
const name = words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
780
|
+
return /^[0-9]/.test(name) ? `Section${name}` : name || "Section";
|
|
781
|
+
}
|
|
782
|
+
function propsInterfaceName(manifest) {
|
|
783
|
+
const base = manifest.exportName && IDENTIFIER_RE.test(manifest.exportName) ? manifest.exportName : pascalCase(manifest.section);
|
|
784
|
+
return `${base}Props`;
|
|
785
|
+
}
|
|
786
|
+
function generatePropsSource(schema2, options) {
|
|
787
|
+
const fields = collectFields(schema2);
|
|
788
|
+
const lines = [
|
|
789
|
+
`// Generated by @botlr/cli from the Botlr section definition "${options.section}".`,
|
|
790
|
+
"// Do not edit: the props schema lives in the dashboard, and `botlr pull` rewrites this file.",
|
|
791
|
+
""
|
|
792
|
+
];
|
|
793
|
+
if (fields.length === 0) {
|
|
794
|
+
lines.push("/** The definition has no props yet; define them in the dashboard and pull again. */");
|
|
795
|
+
lines.push(`export type ${options.interfaceName} = Record<string, unknown>;`);
|
|
796
|
+
return `${lines.join("\n")}
|
|
797
|
+
`;
|
|
798
|
+
}
|
|
799
|
+
lines.push(`export interface ${options.interfaceName} {`);
|
|
800
|
+
fields.forEach((field, index) => {
|
|
801
|
+
if (index > 0 && field.doc.length) lines.push("");
|
|
802
|
+
if (field.doc.length === 1) lines.push(` /** ${field.doc[0]} */`);
|
|
803
|
+
else if (field.doc.length > 1) lines.push(" /**", ...field.doc.map((d) => ` * ${d}`), " */");
|
|
804
|
+
lines.push(` ${propertyKey(field.name)}${field.required ? "" : "?"}: ${field.type};`);
|
|
805
|
+
});
|
|
806
|
+
lines.push("}");
|
|
807
|
+
return `${lines.join("\n")}
|
|
808
|
+
`;
|
|
809
|
+
}
|
|
810
|
+
function propsPath(sectionsDir, name) {
|
|
811
|
+
return path6.join(sectionsDir, `${name}${PROPS_SUFFIX}`);
|
|
812
|
+
}
|
|
813
|
+
async function writePropsTypes(sectionsDir, manifest, schema2) {
|
|
814
|
+
const interfaceName = propsInterfaceName(manifest);
|
|
815
|
+
const file = propsPath(sectionsDir, manifest.section);
|
|
816
|
+
const text = generatePropsSource(schema2, { section: manifest.section, interfaceName });
|
|
817
|
+
try {
|
|
818
|
+
if (await readFile6(file, "utf8") === text) return { file, interfaceName, changed: false };
|
|
819
|
+
} catch {
|
|
820
|
+
}
|
|
821
|
+
await mkdir3(sectionsDir, { recursive: true });
|
|
822
|
+
await writeFile4(file, text, "utf8");
|
|
823
|
+
return { file, interfaceName, changed: true };
|
|
824
|
+
}
|
|
825
|
+
|
|
243
826
|
// src/commands/pull.ts
|
|
244
827
|
async function localHash(sectionsDir, name) {
|
|
245
828
|
try {
|
|
@@ -276,16 +859,35 @@ async function pullSections(config, options) {
|
|
|
276
859
|
});
|
|
277
860
|
const { source } = response;
|
|
278
861
|
const files = await writeSectionFiles(config.sectionsDir, source.files);
|
|
279
|
-
await
|
|
280
|
-
const
|
|
862
|
+
const manifest = await writeManifestIfChanged(config.sectionsDir, name, source.manifest);
|
|
863
|
+
const registry = options.noRegistry ? void 0 : await registerSection(config.sectionsDir, config.registryFile, source.manifest);
|
|
864
|
+
let types;
|
|
865
|
+
let typesNotice;
|
|
866
|
+
if (config.types && !options.noTypes) {
|
|
867
|
+
if (response.definition) {
|
|
868
|
+
types = await writePropsTypes(config.sectionsDir, source.manifest, response.definition.schema);
|
|
869
|
+
} else {
|
|
870
|
+
typesNotice = "no section definition resolves for this section, no prop types written";
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
const fresh = {
|
|
281
874
|
scope: source.scope,
|
|
282
875
|
framework: source.framework,
|
|
283
876
|
version: source.version,
|
|
284
877
|
contentHash: source.contentHash,
|
|
285
878
|
pulledAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
286
879
|
};
|
|
287
|
-
lock[name] =
|
|
288
|
-
results.push({
|
|
880
|
+
lock[name] = nextLockEntry(entry, fresh);
|
|
881
|
+
results.push({
|
|
882
|
+
name,
|
|
883
|
+
scope: source.scope,
|
|
884
|
+
version: source.version,
|
|
885
|
+
files,
|
|
886
|
+
manifestChanged: manifest.changed,
|
|
887
|
+
registry,
|
|
888
|
+
types,
|
|
889
|
+
typesNotice
|
|
890
|
+
});
|
|
289
891
|
}
|
|
290
892
|
await writeLock(config.root, lock);
|
|
291
893
|
return results;
|
|
@@ -296,6 +898,38 @@ import {
|
|
|
296
898
|
BotlrConflictError,
|
|
297
899
|
contentHash as contentHash2
|
|
298
900
|
} from "@botlr/sdk";
|
|
901
|
+
|
|
902
|
+
// src/drift.ts
|
|
903
|
+
import { schemaFormat } from "@botlr/sdk";
|
|
904
|
+
function manifestSchema(manifest) {
|
|
905
|
+
const schema2 = manifest.schema;
|
|
906
|
+
return schemaFormat(schema2) ? schema2 : null;
|
|
907
|
+
}
|
|
908
|
+
function compareSchemas(manifest, definitionSchema) {
|
|
909
|
+
const code = manifestSchema(manifest);
|
|
910
|
+
if (!code) return null;
|
|
911
|
+
const codeKeys = new Set(schemaFieldNames(code));
|
|
912
|
+
const dashboardKeys = new Set(schemaFieldNames(definitionSchema));
|
|
913
|
+
const missingInDashboard = [...codeKeys].filter((k) => !dashboardKeys.has(k)).sort();
|
|
914
|
+
const missingInCode = [...dashboardKeys].filter((k) => !codeKeys.has(k)).sort();
|
|
915
|
+
if (missingInDashboard.length === 0 && missingInCode.length === 0) return null;
|
|
916
|
+
return { missingInDashboard, missingInCode };
|
|
917
|
+
}
|
|
918
|
+
function formatDriftWarning(name, drift) {
|
|
919
|
+
const lines = [`warning: props schema of '${name}' differs between the code and the dashboard`];
|
|
920
|
+
if (drift.missingInDashboard.length) {
|
|
921
|
+
lines.push(` the code expects props the dashboard does not define: ${drift.missingInDashboard.join(", ")}`);
|
|
922
|
+
}
|
|
923
|
+
if (drift.missingInCode.length) {
|
|
924
|
+
lines.push(` the dashboard defines props the code does not list: ${drift.missingInCode.join(", ")}`);
|
|
925
|
+
}
|
|
926
|
+
lines.push(` the dashboard owns the schema; a code push never changes it. To resolve:`);
|
|
927
|
+
lines.push(` botlr schema pull ${name} takes the dashboard's schema into ${name}.botlr.json`);
|
|
928
|
+
lines.push(` botlr schema push ${name} sends the code's schema to the dashboard`);
|
|
929
|
+
return lines.join("\n");
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// src/commands/push.ts
|
|
299
933
|
var PushConflict = class extends Error {
|
|
300
934
|
constructor(message, latest) {
|
|
301
935
|
super(message);
|
|
@@ -358,12 +992,14 @@ async function pushSection(config, options) {
|
|
|
358
992
|
contentHash: response.contentHash,
|
|
359
993
|
pulledAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
360
994
|
});
|
|
995
|
+
const drift = compareSchemas(manifest, response.definition?.schema) ?? void 0;
|
|
361
996
|
return {
|
|
362
997
|
name,
|
|
363
998
|
created: response.created,
|
|
364
999
|
scope: response.scope,
|
|
365
1000
|
version: response.version,
|
|
366
|
-
contentHash: response.contentHash
|
|
1001
|
+
contentHash: response.contentHash,
|
|
1002
|
+
drift
|
|
367
1003
|
};
|
|
368
1004
|
}
|
|
369
1005
|
|
|
@@ -391,10 +1027,20 @@ async function sectionStatuses(config, options = {}) {
|
|
|
391
1027
|
const names = options.names?.length ? options.names : await listManifests(config.sectionsDir);
|
|
392
1028
|
const lock = await readLock(config.root);
|
|
393
1029
|
let summaries = /* @__PURE__ */ new Map();
|
|
394
|
-
|
|
395
|
-
|
|
1030
|
+
const client = options.offline ? null : clientFor(config);
|
|
1031
|
+
if (client) {
|
|
1032
|
+
const listing = await client.sections.list({ framework: config.framework });
|
|
396
1033
|
summaries = new Map(listing.sections.map((s) => [s.name, s]));
|
|
397
1034
|
}
|
|
1035
|
+
const driftFor = async (manifest) => {
|
|
1036
|
+
if (!client || !manifestSchema(manifest) || !summaries.has(manifest.section)) return void 0;
|
|
1037
|
+
try {
|
|
1038
|
+
const pulled = await client.sections.pull(manifest.section, { framework: config.framework });
|
|
1039
|
+
return compareSchemas(manifest, pulled.definition?.schema) ?? void 0;
|
|
1040
|
+
} catch {
|
|
1041
|
+
return void 0;
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
398
1044
|
const rows = [];
|
|
399
1045
|
for (const name of names) {
|
|
400
1046
|
const entry = lock[name];
|
|
@@ -408,7 +1054,8 @@ async function sectionStatuses(config, options = {}) {
|
|
|
408
1054
|
status: computeStatus({ localHash: localHash2, lock: entry, remote }),
|
|
409
1055
|
local: entry?.version ?? null,
|
|
410
1056
|
remote: remote?.version ?? null,
|
|
411
|
-
scope: entry?.scope ?? summary?.effective ?? "-"
|
|
1057
|
+
scope: entry?.scope ?? summary?.effective ?? "-",
|
|
1058
|
+
drift: await driftFor(manifest)
|
|
412
1059
|
});
|
|
413
1060
|
} catch (error) {
|
|
414
1061
|
rows.push({
|
|
@@ -424,149 +1071,171 @@ async function sectionStatuses(config, options = {}) {
|
|
|
424
1071
|
return rows;
|
|
425
1072
|
}
|
|
426
1073
|
|
|
427
|
-
// src/init.ts
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
function parseRegistry(source) {
|
|
432
|
-
const importedFrom = /* @__PURE__ */ new Map();
|
|
433
|
-
const importRe = /import\s*(?:type\s*)?\{([^}]*)\}\s*from\s*["']([^"']+)["']/g;
|
|
434
|
-
for (const match of source.matchAll(importRe)) {
|
|
435
|
-
const names = match[1];
|
|
436
|
-
const spec = match[2];
|
|
437
|
-
for (const raw of names.split(",")) {
|
|
438
|
-
const name = raw.trim().split(/\s+as\s+/).pop()?.trim();
|
|
439
|
-
if (name) importedFrom.set(name, spec);
|
|
440
|
-
}
|
|
1074
|
+
// src/commands/init.ts
|
|
1075
|
+
async function initSections(config, options = {}) {
|
|
1076
|
+
if (!await hasRegistry(config.sectionsDir, config.registryFile)) {
|
|
1077
|
+
throw new Error(`No ${config.registryFile}; sections init reads the registry map to find components`);
|
|
441
1078
|
}
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
1079
|
+
const existing = new Set(await listManifests(config.sectionsDir));
|
|
1080
|
+
const npmVersions = await readNpmVersions(config.sectionsDir);
|
|
1081
|
+
const derived = await deriveManifests(config.sectionsDir, {
|
|
1082
|
+
framework: config.framework,
|
|
1083
|
+
npmVersions,
|
|
1084
|
+
registryFile: config.registryFile
|
|
1085
|
+
});
|
|
1086
|
+
const results = [];
|
|
1087
|
+
for (const { name, manifest, entryFile } of derived) {
|
|
1088
|
+
const skip = existing.has(name) && !options.force;
|
|
1089
|
+
let file = `${name}.botlr.json`;
|
|
1090
|
+
if (!skip && !options.dryRun) {
|
|
1091
|
+
file = await writeManifest(config.sectionsDir, name, manifest);
|
|
1092
|
+
}
|
|
1093
|
+
results.push({ name, file, entry: entryFile, written: !skip && !options.dryRun });
|
|
452
1094
|
}
|
|
453
|
-
return
|
|
1095
|
+
return results;
|
|
454
1096
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
1097
|
+
|
|
1098
|
+
// src/commands/schema.ts
|
|
1099
|
+
import { BotlrError, BotlrSchemaFormatConflictError, botlrErrorMessage } from "@botlr/sdk";
|
|
1100
|
+
|
|
1101
|
+
// src/schema-diff.ts
|
|
1102
|
+
import { schemaFormat as schemaFormat2 } from "@botlr/sdk";
|
|
1103
|
+
function schemaKeys(schema2) {
|
|
1104
|
+
return [...new Set(schemaFieldNames(schema2))];
|
|
1105
|
+
}
|
|
1106
|
+
function diffSchemaKeys(current, next) {
|
|
1107
|
+
const currentKeys = new Set(schemaKeys(current));
|
|
1108
|
+
const nextKeys = new Set(schemaKeys(next));
|
|
1109
|
+
return {
|
|
1110
|
+
added: [...nextKeys].filter((k) => !currentKeys.has(k)).sort(),
|
|
1111
|
+
removed: [...currentKeys].filter((k) => !nextKeys.has(k)).sort(),
|
|
1112
|
+
kept: [...nextKeys].filter((k) => currentKeys.has(k)).sort()
|
|
462
1113
|
};
|
|
1114
|
+
}
|
|
1115
|
+
function previewSchemaPush(name, current, next, formats = {}) {
|
|
463
1116
|
return {
|
|
464
|
-
name
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
1117
|
+
name,
|
|
1118
|
+
current,
|
|
1119
|
+
currentFormat: formats.currentFormat ?? schemaFormat2(current),
|
|
1120
|
+
next,
|
|
1121
|
+
nextFormat: formats.nextFormat ?? schemaFormat2(next),
|
|
1122
|
+
diff: diffSchemaKeys(current, next)
|
|
469
1123
|
};
|
|
470
1124
|
}
|
|
471
|
-
function
|
|
472
|
-
|
|
473
|
-
const npm = /* @__PURE__ */ new Set();
|
|
474
|
-
const re = /(?:import|export)\s+(?:[^"';]*?\s+from\s+)?["']([^"']+)["']|import\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
475
|
-
for (const match of source.matchAll(re)) {
|
|
476
|
-
const spec = match[1] ?? match[2];
|
|
477
|
-
if (!spec) continue;
|
|
478
|
-
if (spec.startsWith(".") || spec.startsWith("@/") || spec.startsWith("~/")) internal.add(spec);
|
|
479
|
-
else npm.add(spec.startsWith("@") ? spec.split("/").slice(0, 2).join("/") : spec.split("/")[0]);
|
|
480
|
-
}
|
|
481
|
-
return { internal: [...internal].sort(), npm: [...npm].sort() };
|
|
1125
|
+
function plural(n, word) {
|
|
1126
|
+
return `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
482
1127
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const base = path4.resolve(sectionsDir, spec);
|
|
486
|
-
for (const ext of SOURCE_EXTENSIONS) {
|
|
487
|
-
try {
|
|
488
|
-
await readFile4(base + ext);
|
|
489
|
-
return base + ext;
|
|
490
|
-
} catch {
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
return null;
|
|
1128
|
+
function formatName(format) {
|
|
1129
|
+
return format ?? "unknown";
|
|
494
1130
|
}
|
|
495
|
-
|
|
496
|
-
const
|
|
497
|
-
const
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const imports = parseImports(source);
|
|
505
|
-
const relative = path4.relative(sectionsDir, entryFile).split(path4.sep).join("/");
|
|
506
|
-
const npm = {};
|
|
507
|
-
for (const pkg of imports.npm) {
|
|
508
|
-
if (pkg === "react" || pkg === "next") continue;
|
|
509
|
-
npm[pkg] = options.npmVersions?.[pkg] ?? "*";
|
|
510
|
-
}
|
|
511
|
-
const manifest = {
|
|
512
|
-
section: entry.slug,
|
|
513
|
-
framework: options.framework,
|
|
514
|
-
entry: relative,
|
|
515
|
-
exportName: entry.exportName,
|
|
516
|
-
files: [relative],
|
|
517
|
-
...meta?.heading ? { title: meta.heading } : {},
|
|
518
|
-
...meta?.description ? { description: meta.description } : {},
|
|
519
|
-
...meta?.category ? { category: meta.category } : {},
|
|
520
|
-
...meta?.icon ? { icon: meta.icon } : {},
|
|
521
|
-
dependencies: {
|
|
522
|
-
...Object.keys(npm).length ? { npm } : {},
|
|
523
|
-
...imports.internal.length ? { internal: imports.internal } : {}
|
|
524
|
-
}
|
|
525
|
-
};
|
|
526
|
-
out.push({ name: entry.slug, manifest, entryFile });
|
|
1131
|
+
function formatSchemaDiff(preview) {
|
|
1132
|
+
const { diff } = preview;
|
|
1133
|
+
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")})`;
|
|
1134
|
+
const lines = [header];
|
|
1135
|
+
if (diff.added.length) lines.push(` + added ${diff.added.join(", ")}`);
|
|
1136
|
+
if (diff.removed.length) lines.push(` - removed ${diff.removed.join(", ")}`);
|
|
1137
|
+
lines.push(` = kept ${diff.kept.length ? diff.kept.join(", ") : "none"}`);
|
|
1138
|
+
if (preview.currentFormat && preview.nextFormat && preview.currentFormat !== preview.nextFormat) {
|
|
1139
|
+
lines.push(` the formats differ: the push needs --force to replace ${preview.currentFormat} with ${preview.nextFormat}`);
|
|
527
1140
|
}
|
|
528
|
-
|
|
1141
|
+
if (!diff.added.length && !diff.removed.length) lines.push(" the field keys match; only field settings may change");
|
|
1142
|
+
return lines.join("\n");
|
|
529
1143
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
1144
|
+
|
|
1145
|
+
// src/commands/schema.ts
|
|
1146
|
+
function isNotFound(error) {
|
|
1147
|
+
return error instanceof BotlrError && error.status === 404;
|
|
1148
|
+
}
|
|
1149
|
+
async function pullSchemas(config, options) {
|
|
1150
|
+
const client = clientFor(config);
|
|
1151
|
+
const names = options.all ? await listManifests(config.sectionsDir) : options.names;
|
|
1152
|
+
const results = [];
|
|
1153
|
+
for (const name of names) {
|
|
1154
|
+
let manifest;
|
|
533
1155
|
try {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
const parent = path4.dirname(current);
|
|
539
|
-
if (parent === current) return {};
|
|
540
|
-
current = parent;
|
|
1156
|
+
manifest = await readManifest(config.sectionsDir, name);
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
results.push({ name, changed: false, file: "", error: error.message });
|
|
1159
|
+
continue;
|
|
541
1160
|
}
|
|
1161
|
+
let definition;
|
|
1162
|
+
try {
|
|
1163
|
+
definition = (await client.sections.getSchema(name)).definition;
|
|
1164
|
+
} catch (error) {
|
|
1165
|
+
if (!isNotFound(error)) throw error;
|
|
1166
|
+
results.push({ name, changed: false, file: "", error: `the app has no section type '${name}' (botlr push ${name} --create adds one)` });
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
const updated = { ...manifest, schema: definition.schema };
|
|
1170
|
+
const written = await writeManifestIfChanged(config.sectionsDir, name, updated);
|
|
1171
|
+
const types = config.types && !options.noTypes ? await writePropsTypes(config.sectionsDir, manifest, definition.schema) : void 0;
|
|
1172
|
+
results.push({ name, changed: written.changed, file: written.file, format: definition.format, types });
|
|
542
1173
|
}
|
|
1174
|
+
return results;
|
|
543
1175
|
}
|
|
544
|
-
|
|
1176
|
+
var SchemaFormatConflict = class extends Error {
|
|
1177
|
+
constructor(sectionName, currentFormat, newFormat, message) {
|
|
1178
|
+
super(message ?? `the schema of '${sectionName}' is stored as ${currentFormat}, the manifest's is ${newFormat}`);
|
|
1179
|
+
this.sectionName = sectionName;
|
|
1180
|
+
this.currentFormat = currentFormat;
|
|
1181
|
+
this.newFormat = newFormat;
|
|
1182
|
+
this.name = "SchemaFormatConflict";
|
|
1183
|
+
}
|
|
1184
|
+
sectionName;
|
|
1185
|
+
currentFormat;
|
|
1186
|
+
newFormat;
|
|
1187
|
+
};
|
|
1188
|
+
var SchemaRejected = class extends Error {
|
|
1189
|
+
constructor(sectionName, message) {
|
|
1190
|
+
super(message);
|
|
1191
|
+
this.sectionName = sectionName;
|
|
1192
|
+
this.name = "SchemaRejected";
|
|
1193
|
+
}
|
|
1194
|
+
sectionName;
|
|
1195
|
+
};
|
|
1196
|
+
async function pushSchema(config, options) {
|
|
1197
|
+
const client = clientFor(config, { needPersonalToken: true });
|
|
1198
|
+
const { name } = options;
|
|
1199
|
+
const manifest = await readManifest(config.sectionsDir, name);
|
|
1200
|
+
const next = manifest.schema;
|
|
1201
|
+
if (!next || typeof next !== "object" || Array.isArray(next)) {
|
|
1202
|
+
throw new ManifestError(
|
|
1203
|
+
`${name}.botlr.json has no 'schema' object to push; add one, or run 'botlr schema pull ${name}' to take the dashboard's`
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
let current;
|
|
545
1207
|
try {
|
|
546
|
-
|
|
547
|
-
} catch {
|
|
548
|
-
|
|
1208
|
+
current = (await client.sections.getSchema(name)).definition;
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
if (!isNotFound(error)) throw error;
|
|
1211
|
+
throw new ManifestError(`the app has no section type '${name}'; push the code first with 'botlr push ${name} --create'`);
|
|
549
1212
|
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
async function initSections(config, options = {}) {
|
|
554
|
-
if (!await hasRegistry(config.sectionsDir)) {
|
|
555
|
-
throw new Error(`No ${REGISTRY_FILE} in ${config.sectionsDir}; sections init reads the registry map to find components`);
|
|
1213
|
+
const preview = previewSchemaPush(name, current.schema, next, { currentFormat: current.format });
|
|
1214
|
+
if (options.confirm && !await options.confirm(preview)) {
|
|
1215
|
+
return { name, preview, pushed: false };
|
|
556
1216
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
let file = `${name}.botlr.json`;
|
|
564
|
-
if (!skip && !options.dryRun) {
|
|
565
|
-
file = await writeManifest(config.sectionsDir, name, manifest);
|
|
1217
|
+
let response;
|
|
1218
|
+
try {
|
|
1219
|
+
response = await client.sections.putSchema(name, { schema: next, force: options.force ?? false });
|
|
1220
|
+
} catch (error) {
|
|
1221
|
+
if (error instanceof BotlrSchemaFormatConflictError) {
|
|
1222
|
+
throw new SchemaFormatConflict(name, error.currentFormat, error.newFormat, botlrErrorMessage(error));
|
|
566
1223
|
}
|
|
567
|
-
|
|
1224
|
+
if (error instanceof BotlrError && error.status === 422) {
|
|
1225
|
+
throw new SchemaRejected(name, botlrErrorMessage(error) ?? "the server rejected the schema (422)");
|
|
1226
|
+
}
|
|
1227
|
+
throw error;
|
|
568
1228
|
}
|
|
569
|
-
|
|
1229
|
+
const types = config.types && !options.noTypes ? await writePropsTypes(config.sectionsDir, manifest, response.definition.schema) : void 0;
|
|
1230
|
+
return {
|
|
1231
|
+
name,
|
|
1232
|
+
preview,
|
|
1233
|
+
pushed: true,
|
|
1234
|
+
changed: response.changed,
|
|
1235
|
+
previousFormat: response.previousFormat,
|
|
1236
|
+
format: response.definition.format,
|
|
1237
|
+
types
|
|
1238
|
+
};
|
|
570
1239
|
}
|
|
571
1240
|
|
|
572
1241
|
// src/output.ts
|
|
@@ -586,6 +1255,7 @@ function fail(message) {
|
|
|
586
1255
|
}
|
|
587
1256
|
|
|
588
1257
|
// src/cli.ts
|
|
1258
|
+
import { createInterface } from "readline/promises";
|
|
589
1259
|
var require2 = createRequire(import.meta.url);
|
|
590
1260
|
var { version } = require2("../package.json");
|
|
591
1261
|
function parseVersion(value) {
|
|
@@ -609,12 +1279,23 @@ async function run(action) {
|
|
|
609
1279
|
latest on server: ${latest}
|
|
610
1280
|
pull first, or push --force to overwrite`);
|
|
611
1281
|
}
|
|
1282
|
+
if (error instanceof SchemaFormatConflict) {
|
|
1283
|
+
fail(
|
|
1284
|
+
`${error.message}
|
|
1285
|
+
dashboard: ${error.currentFormat}
|
|
1286
|
+
code: ${error.newFormat}
|
|
1287
|
+
rerun with --force to replace the ${error.currentFormat} schema: botlr schema push ${error.sectionName} --force`
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
if (error instanceof SchemaRejected) {
|
|
1291
|
+
fail(`the server rejected the schema of '${error.sectionName}': ${error.message}`);
|
|
1292
|
+
}
|
|
612
1293
|
fail(error.message);
|
|
613
1294
|
}
|
|
614
1295
|
}
|
|
615
1296
|
var program = new Command("botlr").description("Pull and push section source between a Botlr site and this app").version(version).configureHelp({ sortSubcommands: true });
|
|
616
1297
|
withCommon(
|
|
617
|
-
program.command("pull [names...]").description("pull sections into the sections directory; writes <name>.botlr.json and the lock file").option("--all", "every section the server lists for the framework").option("--version <n>", "a specific version instead of the latest", parseVersion).option("--shared", "the shared row (per section definition) even when the app has its own").option("--force", "overwrite files that changed locally since the last pull")
|
|
1298
|
+
program.command("pull [names...]").description("pull sections into the sections directory; writes <name>.botlr.json and the lock file").option("--all", "every section the server lists for the framework").option("--version <n>", "a specific version instead of the latest", parseVersion).option("--shared", "the shared row (per section definition) even when the app has its own").option("--force", "overwrite files that changed locally since the last pull").option("--no-registry", "do not add the section to the registry file").option("--no-types", "do not write <name>.props.ts from the definition's props schema")
|
|
618
1299
|
).action(
|
|
619
1300
|
(names, opts) => run(async () => {
|
|
620
1301
|
if (names.length === 0 && !opts.all) fail("name a section or pass --all");
|
|
@@ -625,15 +1306,30 @@ withCommon(
|
|
|
625
1306
|
all: opts.all,
|
|
626
1307
|
version: opts.version,
|
|
627
1308
|
shared: opts.shared,
|
|
628
|
-
force: opts.force
|
|
1309
|
+
force: opts.force,
|
|
1310
|
+
noRegistry: opts.registry === false,
|
|
1311
|
+
noTypes: opts.types === false
|
|
629
1312
|
});
|
|
630
1313
|
if (results.length === 0) {
|
|
631
1314
|
console.log("nothing to pull");
|
|
632
1315
|
return;
|
|
633
1316
|
}
|
|
634
1317
|
for (const r of results) {
|
|
635
|
-
if (r.skipped)
|
|
636
|
-
|
|
1318
|
+
if (r.skipped) {
|
|
1319
|
+
console.log(`skip ${r.name}: ${r.skipped}`);
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1322
|
+
console.log(`pull ${r.name} ${r.scope} v${r.version} (${r.files.length} file${r.files.length === 1 ? "" : "s"})`);
|
|
1323
|
+
if (r.types?.changed) console.log(` types: wrote ${r.types.interfaceName} to ${path7.relative(process.cwd(), r.types.file)}`);
|
|
1324
|
+
if (r.typesNotice) console.log(` types: ${r.typesNotice}`);
|
|
1325
|
+
const registry = r.registry;
|
|
1326
|
+
if (!registry) continue;
|
|
1327
|
+
const file = path7.relative(process.cwd(), registry.file);
|
|
1328
|
+
if (registry.notice) console.log(` registry: ${registry.notice}`);
|
|
1329
|
+
else if (registry.changed) {
|
|
1330
|
+
const what = [registry.addedImport ? "import" : "", registry.addedEntry ? `"${r.name}" entry` : ""].filter(Boolean).join(" and ");
|
|
1331
|
+
console.log(` registry: added ${what} to ${file}`);
|
|
1332
|
+
}
|
|
637
1333
|
}
|
|
638
1334
|
})
|
|
639
1335
|
);
|
|
@@ -651,6 +1347,8 @@ withCommon(
|
|
|
651
1347
|
});
|
|
652
1348
|
if (result.created) console.log(`pushed ${result.name} ${result.scope} v${result.version}`);
|
|
653
1349
|
else console.log(`${result.name} ${result.scope} v${result.version} already matches, nothing pushed`);
|
|
1350
|
+
if (result.drift) process.stderr.write(`${formatDriftWarning(result.name, result.drift)}
|
|
1351
|
+
`);
|
|
654
1352
|
})
|
|
655
1353
|
);
|
|
656
1354
|
var sections = program.command("sections").description("list, inspect and bootstrap sections");
|
|
@@ -694,6 +1392,10 @@ withCommon(
|
|
|
694
1392
|
["name", "status", "local", "remote", "scope", ""]
|
|
695
1393
|
)
|
|
696
1394
|
);
|
|
1395
|
+
for (const r of rows) {
|
|
1396
|
+
if (r.drift) process.stderr.write(`${formatDriftWarning(r.name, r.drift)}
|
|
1397
|
+
`);
|
|
1398
|
+
}
|
|
697
1399
|
if (rows.some((r) => r.status === "diverged" || r.status === "error")) process.exitCode = 1;
|
|
698
1400
|
})
|
|
699
1401
|
);
|
|
@@ -711,5 +1413,67 @@ withCommon(
|
|
|
711
1413
|
review the manifests, then push each section: botlr push <name> --create`);
|
|
712
1414
|
})
|
|
713
1415
|
);
|
|
1416
|
+
var schema = program.command("schema").description("copy a section's props schema between the dashboard and <name>.botlr.json");
|
|
1417
|
+
withCommon(
|
|
1418
|
+
schema.command("pull [names...]").description("write the dashboard's props schema into <name>.botlr.json as 'schema', then regenerate <name>.props.ts").option("--all", "every section with a manifest in the sections directory").option("--no-types", "do not write <name>.props.ts afterwards")
|
|
1419
|
+
).action(
|
|
1420
|
+
(names, opts) => run(async () => {
|
|
1421
|
+
if (names.length === 0 && !opts.all) fail("name a section or pass --all");
|
|
1422
|
+
const config = await loadConfig(process.cwd(), overrides(opts));
|
|
1423
|
+
const results = await pullSchemas(config, { names, all: opts.all, noTypes: opts.types === false });
|
|
1424
|
+
if (results.length === 0) {
|
|
1425
|
+
console.log(`no *.botlr.json manifests in ${config.sectionsDir}`);
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
for (const r of results) {
|
|
1429
|
+
if (r.error) {
|
|
1430
|
+
console.log(`skip ${r.name}: ${r.error}`);
|
|
1431
|
+
process.exitCode = 1;
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
const format = r.format ? ` (${r.format})` : "";
|
|
1435
|
+
if (r.changed) console.log(`pull ${r.name}: wrote schema${format} to ${path7.relative(process.cwd(), r.file)}`);
|
|
1436
|
+
else console.log(`pull ${r.name}: schema unchanged${format}`);
|
|
1437
|
+
if (r.types?.changed) console.log(` types: wrote ${r.types.interfaceName} to ${path7.relative(process.cwd(), r.types.file)}`);
|
|
1438
|
+
}
|
|
1439
|
+
})
|
|
1440
|
+
);
|
|
1441
|
+
async function confirmOnTerminal(preview) {
|
|
1442
|
+
console.log(formatSchemaDiff(preview));
|
|
1443
|
+
if (!process.stdin.isTTY) {
|
|
1444
|
+
fail(`not a terminal: rerun with --yes to push the schema of '${preview.name}' without asking`);
|
|
1445
|
+
}
|
|
1446
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1447
|
+
try {
|
|
1448
|
+
const answer = await rl.question(`replace the dashboard's schema of '${preview.name}' with the code's? [y/N] `);
|
|
1449
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
1450
|
+
} finally {
|
|
1451
|
+
rl.close();
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
withCommon(
|
|
1455
|
+
schema.command("push <name>").description("send the manifest's 'schema' to the dashboard as the definition's props schema (needs BOTLR_PAT)").option("--force", "replace the schema even when the dashboard's is in the other format").option("-y, --yes", "push without asking").option("--no-types", "do not write <name>.props.ts afterwards")
|
|
1456
|
+
).action(
|
|
1457
|
+
(name, opts) => run(async () => {
|
|
1458
|
+
const config = await loadConfig(process.cwd(), overrides(opts));
|
|
1459
|
+
const result = await pushSchema(config, {
|
|
1460
|
+
name,
|
|
1461
|
+
force: opts.force,
|
|
1462
|
+
noTypes: opts.types === false,
|
|
1463
|
+
confirm: opts.yes ? (preview) => {
|
|
1464
|
+
console.log(formatSchemaDiff(preview));
|
|
1465
|
+
return true;
|
|
1466
|
+
} : confirmOnTerminal
|
|
1467
|
+
});
|
|
1468
|
+
if (!result.pushed) {
|
|
1469
|
+
console.log(`nothing pushed`);
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
const formats = result.previousFormat && result.format && result.previousFormat !== result.format ? ` (${result.previousFormat} -> ${result.format})` : result.format ? ` (${result.format})` : "";
|
|
1473
|
+
if (result.changed) console.log(`pushed schema of ${result.name}${formats}`);
|
|
1474
|
+
else console.log(`schema of ${result.name} already matches, nothing changed${formats}`);
|
|
1475
|
+
if (result.types?.changed) console.log(` types: wrote ${result.types.interfaceName} to ${path7.relative(process.cwd(), result.types.file)}`);
|
|
1476
|
+
})
|
|
1477
|
+
);
|
|
714
1478
|
program.parseAsync(process.argv).catch((error) => fail(error.message));
|
|
715
1479
|
//# sourceMappingURL=cli.js.map
|