@botlr/cli 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,715 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command, InvalidArgumentError } from "commander";
5
+ import { createRequire } from "module";
6
+
7
+ // src/config.ts
8
+ import { access, readFile } from "fs/promises";
9
+ import path from "path";
10
+ var CONFIG_FILE = "botlr.config.json";
11
+ var DEFAULT_FRAMEWORK = "react-next";
12
+ var DEFAULT_SECTIONS_DIR = "components/sections";
13
+ var ConfigError = class extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = "ConfigError";
17
+ }
18
+ };
19
+ async function exists(file) {
20
+ try {
21
+ await access(file);
22
+ return true;
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+ async function findConfigFile(startDir) {
28
+ let dir = path.resolve(startDir);
29
+ for (; ; ) {
30
+ const candidate = path.join(dir, CONFIG_FILE);
31
+ if (await exists(candidate)) return candidate;
32
+ const parent = path.dirname(dir);
33
+ if (parent === dir) return null;
34
+ dir = parent;
35
+ }
36
+ }
37
+ async function readConfigFile(file) {
38
+ const raw = await readFile(file, "utf8");
39
+ try {
40
+ const parsed = JSON.parse(raw);
41
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
42
+ throw new ConfigError(`${file} must hold a JSON object`);
43
+ }
44
+ return parsed;
45
+ } catch (error) {
46
+ if (error instanceof ConfigError) throw error;
47
+ throw new ConfigError(`${file} is not valid JSON: ${error.message}`);
48
+ }
49
+ }
50
+ async function loadConfig(cwd, overrides2 = {}, env = process.env) {
51
+ const configPath = await findConfigFile(cwd);
52
+ const file = configPath ? await readConfigFile(configPath) : {};
53
+ const root = configPath ? path.dirname(configPath) : path.resolve(cwd);
54
+ const baseUrl = overrides2.baseUrl ?? env.BOTLR_BASE_URL ?? file.baseUrl;
55
+ const appId = overrides2.appId ?? env.BOTLR_APP_ID ?? file.appId;
56
+ if (!baseUrl) {
57
+ throw new ConfigError(`No baseUrl: set it in ${CONFIG_FILE} or BOTLR_BASE_URL`);
58
+ }
59
+ if (!appId) {
60
+ throw new ConfigError(`No appId: set it in ${CONFIG_FILE} or BOTLR_APP_ID`);
61
+ }
62
+ const sectionsDir = overrides2.dir ? path.resolve(cwd, overrides2.dir) : path.resolve(root, env.BOTLR_SECTIONS_DIR ?? file.sectionsDir ?? DEFAULT_SECTIONS_DIR);
63
+ return {
64
+ root,
65
+ configPath,
66
+ baseUrl: baseUrl.replace(/\/+$/, ""),
67
+ appId,
68
+ framework: overrides2.framework ?? env.BOTLR_FRAMEWORK ?? file.framework ?? DEFAULT_FRAMEWORK,
69
+ sectionsDir,
70
+ apiKey: env.BOTLR_API_KEY || void 0,
71
+ personalToken: env.BOTLR_PAT || void 0
72
+ };
73
+ }
74
+
75
+ // src/client.ts
76
+ import { createBotlrClient } from "@botlr/sdk";
77
+ function clientFor(config, options = {}) {
78
+ if (!config.apiKey && !options.needPersonalToken) {
79
+ process.stderr.write("botlr: BOTLR_API_KEY is not set, requests go out without a site token\n");
80
+ }
81
+ if (options.needPersonalToken && !config.personalToken) {
82
+ throw new ConfigError("Pushing needs BOTLR_PAT: a personal access token with the write:sections scope");
83
+ }
84
+ return createBotlrClient({
85
+ baseUrl: config.baseUrl,
86
+ appId: config.appId,
87
+ apiKey: config.apiKey,
88
+ personalToken: config.personalToken
89
+ });
90
+ }
91
+
92
+ // src/commands/list.ts
93
+ async function listSections(config) {
94
+ const client = clientFor(config);
95
+ const listing = await client.sections.list({ framework: config.framework });
96
+ return listing.sections;
97
+ }
98
+
99
+ // src/commands/pull.ts
100
+ import { contentHash } from "@botlr/sdk";
101
+
102
+ // src/lock.ts
103
+ import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
104
+ import path2 from "path";
105
+ var LOCK_DIR = ".botlr";
106
+ var LOCK_FILE = "sections.lock.json";
107
+ function lockPath(root) {
108
+ return path2.join(root, LOCK_DIR, LOCK_FILE);
109
+ }
110
+ async function readLock(root) {
111
+ try {
112
+ const raw = await readFile2(lockPath(root), "utf8");
113
+ const parsed = JSON.parse(raw);
114
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
115
+ return parsed;
116
+ } catch (error) {
117
+ if (error.code === "ENOENT") return {};
118
+ throw error;
119
+ }
120
+ }
121
+ async function writeLock(root, lock) {
122
+ const file = lockPath(root);
123
+ await mkdir(path2.dirname(file), { recursive: true });
124
+ const sorted = Object.fromEntries(Object.entries(lock).sort(([a], [b]) => a.localeCompare(b)));
125
+ await writeFile(file, `${JSON.stringify(sorted, null, 2)}
126
+ `, "utf8");
127
+ }
128
+ async function updateLock(root, name, entry) {
129
+ const lock = await readLock(root);
130
+ lock[name] = entry;
131
+ await writeLock(root, lock);
132
+ return lock;
133
+ }
134
+
135
+ // src/manifest.ts
136
+ import { mkdir as mkdir2, readFile as readFile3, readdir, writeFile as writeFile2 } from "fs/promises";
137
+ import path3 from "path";
138
+ import {
139
+ isValidSectionPath,
140
+ stripManifestState
141
+ } from "@botlr/sdk";
142
+ var MANIFEST_SUFFIX = ".botlr.json";
143
+ var ManifestError = class extends Error {
144
+ constructor(message) {
145
+ super(message);
146
+ this.name = "ManifestError";
147
+ }
148
+ };
149
+ function manifestPath(sectionsDir, name) {
150
+ return path3.join(sectionsDir, `${name}${MANIFEST_SUFFIX}`);
151
+ }
152
+ async function listManifests(sectionsDir) {
153
+ let entries;
154
+ try {
155
+ entries = await readdir(sectionsDir);
156
+ } catch (error) {
157
+ if (error.code === "ENOENT") return [];
158
+ throw error;
159
+ }
160
+ return entries.filter((entry) => entry.endsWith(MANIFEST_SUFFIX)).map((entry) => entry.slice(0, -MANIFEST_SUFFIX.length)).sort();
161
+ }
162
+ async function readManifest(sectionsDir, name) {
163
+ const file = manifestPath(sectionsDir, name);
164
+ let raw;
165
+ try {
166
+ raw = await readFile3(file, "utf8");
167
+ } catch (error) {
168
+ if (error.code === "ENOENT") {
169
+ throw new ManifestError(`No manifest for '${name}': expected ${file}`);
170
+ }
171
+ throw error;
172
+ }
173
+ let parsed;
174
+ try {
175
+ parsed = JSON.parse(raw);
176
+ } catch (error) {
177
+ throw new ManifestError(`${file} is not valid JSON: ${error.message}`);
178
+ }
179
+ return validateManifest(parsed, file);
180
+ }
181
+ function validateManifest(value, where = "manifest") {
182
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
183
+ throw new ManifestError(`${where} must be a JSON object`);
184
+ }
185
+ const m = value;
186
+ if (typeof m.section !== "string" || !m.section) throw new ManifestError(`${where}: 'section' is required`);
187
+ if (typeof m.entry !== "string" || !m.entry) throw new ManifestError(`${where}: 'entry' is required`);
188
+ if (!Array.isArray(m.files) || m.files.length === 0 || !m.files.every((f) => typeof f === "string")) {
189
+ throw new ManifestError(`${where}: 'files' must list at least one path`);
190
+ }
191
+ const files = m.files;
192
+ for (const file of files) {
193
+ if (!isValidSectionPath(file)) throw new ManifestError(`${where}: invalid file path '${file}'`);
194
+ }
195
+ if (!files.includes(m.entry)) throw new ManifestError(`${where}: 'entry' (${m.entry}) must be listed in 'files'`);
196
+ if (m.framework !== void 0 && typeof m.framework !== "string") {
197
+ throw new ManifestError(`${where}: 'framework' must be a string`);
198
+ }
199
+ return m;
200
+ }
201
+ async function writeManifest(sectionsDir, name, manifest) {
202
+ const file = manifestPath(sectionsDir, name);
203
+ await mkdir2(sectionsDir, { recursive: true });
204
+ await writeFile2(file, `${JSON.stringify(stripManifestState(manifest), null, 2)}
205
+ `, "utf8");
206
+ return file;
207
+ }
208
+ async function readSectionFiles(sectionsDir, manifest) {
209
+ return Promise.all(
210
+ manifest.files.map(async (relative) => {
211
+ const file = safeJoin(sectionsDir, relative);
212
+ try {
213
+ return { path: relative, content: await readFile3(file, "utf8") };
214
+ } catch (error) {
215
+ if (error.code === "ENOENT") {
216
+ throw new ManifestError(`Missing file '${relative}' listed in ${manifest.section}${MANIFEST_SUFFIX}`);
217
+ }
218
+ throw error;
219
+ }
220
+ })
221
+ );
222
+ }
223
+ async function writeSectionFiles(sectionsDir, files) {
224
+ const written = [];
225
+ for (const file of files) {
226
+ const target = safeJoin(sectionsDir, file.path);
227
+ await mkdir2(path3.dirname(target), { recursive: true });
228
+ await writeFile2(target, file.content, "utf8");
229
+ written.push(target);
230
+ }
231
+ return written;
232
+ }
233
+ function safeJoin(dir, relative) {
234
+ if (!isValidSectionPath(relative)) throw new ManifestError(`Invalid file path '${relative}'`);
235
+ const target = path3.resolve(dir, relative);
236
+ const base = path3.resolve(dir);
237
+ if (target !== base && !target.startsWith(base + path3.sep)) {
238
+ throw new ManifestError(`File path '${relative}' escapes ${dir}`);
239
+ }
240
+ return target;
241
+ }
242
+
243
+ // src/commands/pull.ts
244
+ async function localHash(sectionsDir, name) {
245
+ try {
246
+ const manifest = await readManifest(sectionsDir, name);
247
+ return await contentHash(await readSectionFiles(sectionsDir, manifest));
248
+ } catch {
249
+ return null;
250
+ }
251
+ }
252
+ async function pullSections(config, options) {
253
+ const client = clientFor(config);
254
+ const scope = options.shared ? "shared" : void 0;
255
+ let names = options.names;
256
+ if (options.all) {
257
+ const listing = await client.sections.list({ framework: config.framework });
258
+ names = listing.sections.filter((s) => options.shared ? s.shared : s.instance || s.shared).map((s) => s.name);
259
+ }
260
+ if (names.length === 0) return [];
261
+ const lock = await readLock(config.root);
262
+ const results = [];
263
+ for (const name of names) {
264
+ const entry = lock[name];
265
+ if (entry && !options.force) {
266
+ const current = await localHash(config.sectionsDir, name);
267
+ if (current !== null && current !== entry.contentHash) {
268
+ results.push({ name, scope: entry.scope, version: entry.version, files: [], skipped: "modified locally, use --force to overwrite" });
269
+ continue;
270
+ }
271
+ }
272
+ const response = await client.sections.pull(name, {
273
+ framework: config.framework,
274
+ version: options.version,
275
+ scope
276
+ });
277
+ const { source } = response;
278
+ const files = await writeSectionFiles(config.sectionsDir, source.files);
279
+ await writeManifest(config.sectionsDir, name, source.manifest);
280
+ const newEntry = {
281
+ scope: source.scope,
282
+ framework: source.framework,
283
+ version: source.version,
284
+ contentHash: source.contentHash,
285
+ pulledAt: (/* @__PURE__ */ new Date()).toISOString()
286
+ };
287
+ lock[name] = newEntry;
288
+ results.push({ name, scope: source.scope, version: source.version, files });
289
+ }
290
+ await writeLock(config.root, lock);
291
+ return results;
292
+ }
293
+
294
+ // src/commands/push.ts
295
+ import {
296
+ BotlrConflictError,
297
+ contentHash as contentHash2
298
+ } from "@botlr/sdk";
299
+ var PushConflict = class extends Error {
300
+ constructor(message, latest) {
301
+ super(message);
302
+ this.latest = latest;
303
+ this.name = "PushConflict";
304
+ }
305
+ latest;
306
+ };
307
+ async function pushSection(config, options) {
308
+ const client = clientFor(config, { needPersonalToken: true });
309
+ const { name } = options;
310
+ const manifest = await readManifest(config.sectionsDir, name);
311
+ if (manifest.section !== name) {
312
+ throw new ManifestError(`${name}.botlr.json says section '${manifest.section}', expected '${name}'`);
313
+ }
314
+ if (manifest.framework && manifest.framework !== config.framework) {
315
+ throw new ManifestError(
316
+ `${name}.botlr.json is for framework '${manifest.framework}', the config says '${config.framework}'`
317
+ );
318
+ }
319
+ manifest.framework = config.framework;
320
+ const files = await readSectionFiles(config.sectionsDir, manifest);
321
+ const localHash2 = await contentHash2(files);
322
+ const targetScope = options.shared ? "shared" : "instance";
323
+ let baseVersion;
324
+ if (!options.force) {
325
+ const lock = (await readLock(config.root))[name];
326
+ if (lock && lock.scope === targetScope) {
327
+ baseVersion = lock.version;
328
+ } else {
329
+ const listing = await client.sections.list({ framework: config.framework });
330
+ const summary = listing.sections.find((s) => s.name === name);
331
+ const remote = summary ? targetScope === "shared" ? summary.shared : summary.instance : null;
332
+ baseVersion = remote?.version ?? 0;
333
+ if (remote && lock === void 0 && remote.contentHash === localHash2) {
334
+ }
335
+ }
336
+ }
337
+ let response;
338
+ try {
339
+ response = await client.sections.push(name, {
340
+ framework: config.framework,
341
+ manifest,
342
+ files,
343
+ shared: options.shared ?? false,
344
+ baseVersion,
345
+ message: options.message,
346
+ create: options.create ?? false
347
+ });
348
+ } catch (error) {
349
+ if (error instanceof BotlrConflictError) {
350
+ throw new PushConflict(error.message, error.latest);
351
+ }
352
+ throw error;
353
+ }
354
+ await updateLock(config.root, name, {
355
+ scope: response.scope,
356
+ framework: config.framework,
357
+ version: response.version,
358
+ contentHash: response.contentHash,
359
+ pulledAt: (/* @__PURE__ */ new Date()).toISOString()
360
+ });
361
+ return {
362
+ name,
363
+ created: response.created,
364
+ scope: response.scope,
365
+ version: response.version,
366
+ contentHash: response.contentHash
367
+ };
368
+ }
369
+
370
+ // src/commands/status.ts
371
+ import { contentHash as contentHash3 } from "@botlr/sdk";
372
+
373
+ // src/status.ts
374
+ function computeStatus({ localHash: localHash2, lock, remote }) {
375
+ if (!lock) return "untracked";
376
+ const localChanged = localHash2 !== lock.contentHash;
377
+ const remoteChanged = remote != null && remote.contentHash !== lock.contentHash;
378
+ if (localChanged && remoteChanged) return "diverged";
379
+ if (localChanged) return "modified";
380
+ if (remoteChanged) return "behind";
381
+ return "clean";
382
+ }
383
+ function remoteForLock(summary, lock) {
384
+ if (!summary) return void 0;
385
+ if (!lock) return summary.effective === "shared" ? summary.shared : summary.instance;
386
+ return lock.scope === "shared" ? summary.shared : summary.instance;
387
+ }
388
+
389
+ // src/commands/status.ts
390
+ async function sectionStatuses(config, options = {}) {
391
+ const names = options.names?.length ? options.names : await listManifests(config.sectionsDir);
392
+ const lock = await readLock(config.root);
393
+ let summaries = /* @__PURE__ */ new Map();
394
+ if (!options.offline) {
395
+ const listing = await clientFor(config).sections.list({ framework: config.framework });
396
+ summaries = new Map(listing.sections.map((s) => [s.name, s]));
397
+ }
398
+ const rows = [];
399
+ for (const name of names) {
400
+ const entry = lock[name];
401
+ const summary = summaries.get(name);
402
+ const remote = remoteForLock(summary, entry);
403
+ try {
404
+ const manifest = await readManifest(config.sectionsDir, name);
405
+ const localHash2 = await contentHash3(await readSectionFiles(config.sectionsDir, manifest));
406
+ rows.push({
407
+ name,
408
+ status: computeStatus({ localHash: localHash2, lock: entry, remote }),
409
+ local: entry?.version ?? null,
410
+ remote: remote?.version ?? null,
411
+ scope: entry?.scope ?? summary?.effective ?? "-"
412
+ });
413
+ } catch (error) {
414
+ rows.push({
415
+ name,
416
+ status: "error",
417
+ local: entry?.version ?? null,
418
+ remote: remote?.version ?? null,
419
+ scope: entry?.scope ?? "-",
420
+ detail: error.message
421
+ });
422
+ }
423
+ }
424
+ return rows;
425
+ }
426
+
427
+ // src/init.ts
428
+ import { readFile as readFile4, readdir as readdir2 } from "fs/promises";
429
+ import path4 from "path";
430
+ var REGISTRY_FILE = "publisher-section-registry.ts";
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
+ }
441
+ }
442
+ const bodyMatch = source.match(/=\s*\{([\s\S]*?)\n\}\s*;?/);
443
+ if (!bodyMatch) return [];
444
+ const entries = [];
445
+ const entryRe = /^\s*(?:"([^"]+)"|'([^']+)'|([A-Za-z0-9_$-]+))\s*:\s*([A-Za-z0-9_$]+)\s*,?\s*(?:\/\/.*)?$/gm;
446
+ for (const match of bodyMatch[1].matchAll(entryRe)) {
447
+ const slug = match[1] ?? match[2] ?? match[3];
448
+ const exportName = match[4];
449
+ const importPath = importedFrom.get(exportName);
450
+ if (!importPath || !importPath.startsWith(".")) continue;
451
+ entries.push({ slug, exportName, importPath });
452
+ }
453
+ return entries;
454
+ }
455
+ function parseDefinition(source) {
456
+ const match = source.match(/export\s+const\s+\w+Definition\s*=\s*\{([\s\S]*?)\n\}/);
457
+ if (!match) return null;
458
+ const body = match[1];
459
+ const field = (key) => {
460
+ const m = body.match(new RegExp(`(?:^|\\n)\\s*${key}\\s*:\\s*(?:"([^"]*)"|'([^']*)'|\`([^\`]*)\`)`));
461
+ return m ? m[1] ?? m[2] ?? m[3] : void 0;
462
+ };
463
+ return {
464
+ name: field("name"),
465
+ heading: field("heading"),
466
+ category: field("category"),
467
+ description: field("description"),
468
+ icon: field("icon")
469
+ };
470
+ }
471
+ function parseImports(source) {
472
+ const internal = /* @__PURE__ */ new Set();
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() };
482
+ }
483
+ var SOURCE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
484
+ async function resolveImport(sectionsDir, spec) {
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;
494
+ }
495
+ async function deriveManifests(sectionsDir, options) {
496
+ const registrySource = await readFile4(path4.join(sectionsDir, REGISTRY_FILE), "utf8");
497
+ const entries = parseRegistry(registrySource);
498
+ const out = [];
499
+ for (const entry of entries) {
500
+ const entryFile = await resolveImport(sectionsDir, entry.importPath);
501
+ if (!entryFile) continue;
502
+ const source = await readFile4(entryFile, "utf8");
503
+ const meta = parseDefinition(source);
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 });
527
+ }
528
+ return out;
529
+ }
530
+ async function readNpmVersions(dir) {
531
+ let current = path4.resolve(dir);
532
+ for (; ; ) {
533
+ try {
534
+ const raw = await readFile4(path4.join(current, "package.json"), "utf8");
535
+ const pkg = JSON.parse(raw);
536
+ return { ...pkg.devDependencies, ...pkg.dependencies };
537
+ } catch {
538
+ const parent = path4.dirname(current);
539
+ if (parent === current) return {};
540
+ current = parent;
541
+ }
542
+ }
543
+ }
544
+ async function hasRegistry(sectionsDir) {
545
+ try {
546
+ return (await readdir2(sectionsDir)).includes(REGISTRY_FILE);
547
+ } catch {
548
+ return false;
549
+ }
550
+ }
551
+
552
+ // src/commands/init.ts
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`);
556
+ }
557
+ const existing = new Set(await listManifests(config.sectionsDir));
558
+ const npmVersions = await readNpmVersions(config.sectionsDir);
559
+ const derived = await deriveManifests(config.sectionsDir, { framework: config.framework, npmVersions });
560
+ const results = [];
561
+ for (const { name, manifest, entryFile } of derived) {
562
+ const skip = existing.has(name) && !options.force;
563
+ let file = `${name}.botlr.json`;
564
+ if (!skip && !options.dryRun) {
565
+ file = await writeManifest(config.sectionsDir, name, manifest);
566
+ }
567
+ results.push({ name, file, entry: entryFile, written: !skip && !options.dryRun });
568
+ }
569
+ return results;
570
+ }
571
+
572
+ // src/output.ts
573
+ function table(rows, header) {
574
+ const all = header ? [header, ...rows] : rows;
575
+ if (all.length === 0) return "";
576
+ const widths = all[0].map((_, col) => Math.max(...all.map((row) => (row[col] ?? "").length)));
577
+ const line = (row) => row.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd();
578
+ const out = all.map(line);
579
+ if (header) out.splice(1, 0, widths.map((w) => "-".repeat(w)).join(" "));
580
+ return out.join("\n");
581
+ }
582
+ function fail(message) {
583
+ process.stderr.write(`botlr: ${message}
584
+ `);
585
+ process.exit(1);
586
+ }
587
+
588
+ // src/cli.ts
589
+ var require2 = createRequire(import.meta.url);
590
+ var { version } = require2("../package.json");
591
+ function parseVersion(value) {
592
+ const n = Number.parseInt(value, 10);
593
+ if (!Number.isInteger(n) || n < 1) throw new InvalidArgumentError("version must be a positive integer");
594
+ return n;
595
+ }
596
+ function overrides(opts) {
597
+ return { framework: opts.framework, dir: opts.dir };
598
+ }
599
+ function withCommon(cmd) {
600
+ return cmd.option("--framework <name>", "source variant, for example react-next (default from config)").option("--dir <path>", "sections directory (default from config, components/sections)");
601
+ }
602
+ async function run(action) {
603
+ try {
604
+ await action();
605
+ } catch (error) {
606
+ if (error instanceof PushConflict) {
607
+ const latest = error.latest ? `v${error.latest.version} (${error.latest.contentHash.slice(0, 12)})` : "none";
608
+ fail(`${error.message}
609
+ latest on server: ${latest}
610
+ pull first, or push --force to overwrite`);
611
+ }
612
+ fail(error.message);
613
+ }
614
+ }
615
+ var program = new Command("botlr").description("Pull and push section source between a Botlr site and this app").version(version).configureHelp({ sortSubcommands: true });
616
+ 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")
618
+ ).action(
619
+ (names, opts) => run(async () => {
620
+ if (names.length === 0 && !opts.all) fail("name a section or pass --all");
621
+ if (opts.all && opts.version !== void 0) fail("--version needs a single section name");
622
+ const config = await loadConfig(process.cwd(), overrides(opts));
623
+ const results = await pullSections(config, {
624
+ names,
625
+ all: opts.all,
626
+ version: opts.version,
627
+ shared: opts.shared,
628
+ force: opts.force
629
+ });
630
+ if (results.length === 0) {
631
+ console.log("nothing to pull");
632
+ return;
633
+ }
634
+ for (const r of results) {
635
+ if (r.skipped) console.log(`skip ${r.name}: ${r.skipped}`);
636
+ else console.log(`pull ${r.name} ${r.scope} v${r.version} (${r.files.length} file${r.files.length === 1 ? "" : "s"})`);
637
+ }
638
+ })
639
+ );
640
+ withCommon(
641
+ program.command("push <name>").description("push the section's files and manifest as a new version (needs BOTLR_PAT)").option("-m, --message <text>", "a note on the version, at most 500 characters").option("--shared", "write the shared row (per section definition) instead of this app's row").option("--create", "create the section definition when the dashboard does not have it").option("--force", "skip the conflict check (omits baseVersion)")
642
+ ).action(
643
+ (name, opts) => run(async () => {
644
+ const config = await loadConfig(process.cwd(), overrides(opts));
645
+ const result = await pushSection(config, {
646
+ name,
647
+ message: opts.message,
648
+ shared: opts.shared,
649
+ create: opts.create,
650
+ force: opts.force
651
+ });
652
+ if (result.created) console.log(`pushed ${result.name} ${result.scope} v${result.version}`);
653
+ else console.log(`${result.name} ${result.scope} v${result.version} already matches, nothing pushed`);
654
+ })
655
+ );
656
+ var sections = program.command("sections").description("list, inspect and bootstrap sections");
657
+ withCommon(sections.command("list").description("every section the server has source for")).action(
658
+ (opts) => run(async () => {
659
+ const config = await loadConfig(process.cwd(), overrides(opts));
660
+ const rows = await listSections(config);
661
+ if (rows.length === 0) {
662
+ console.log(`no ${config.framework} sections for app ${config.appId}`);
663
+ return;
664
+ }
665
+ const v = (s) => s ? `v${s.version}` : "-";
666
+ console.log(
667
+ table(
668
+ rows.map((s) => [s.name, s.title, v(s.instance), v(s.shared), s.effective]),
669
+ ["name", "title", "instance", "shared", "effective"]
670
+ )
671
+ );
672
+ })
673
+ );
674
+ withCommon(
675
+ sections.command("status [names...]").description("clean | modified | behind | diverged per local manifest").option("--offline", "compare with the lock file only, without asking the server")
676
+ ).action(
677
+ (names, opts) => run(async () => {
678
+ const config = await loadConfig(process.cwd(), overrides(opts));
679
+ const rows = await sectionStatuses(config, { names, offline: opts.offline });
680
+ if (rows.length === 0) {
681
+ console.log(`no *.botlr.json manifests in ${config.sectionsDir}`);
682
+ return;
683
+ }
684
+ console.log(
685
+ table(
686
+ rows.map((r) => [
687
+ r.name,
688
+ r.status,
689
+ r.local === null ? "-" : `v${r.local}`,
690
+ r.remote === null ? "-" : `v${r.remote}`,
691
+ r.scope,
692
+ r.detail ?? ""
693
+ ]),
694
+ ["name", "status", "local", "remote", "scope", ""]
695
+ )
696
+ );
697
+ if (rows.some((r) => r.status === "diverged" || r.status === "error")) process.exitCode = 1;
698
+ })
699
+ );
700
+ withCommon(
701
+ sections.command("init").description("derive first <name>.botlr.json manifests from publisher-section-registry.ts and the XDefinition exports").option("--force", "overwrite manifests that already exist").option("--dry-run", "show what would be written")
702
+ ).action(
703
+ (opts) => run(async () => {
704
+ const config = await loadConfig(process.cwd(), overrides(opts));
705
+ const results = await initSections(config, { force: opts.force, dryRun: opts.dryRun });
706
+ for (const r of results) {
707
+ console.log(`${r.written ? "write" : opts.dryRun ? "would" : "keep "} ${r.name} -> ${r.file}`);
708
+ }
709
+ if (results.length === 0) console.log("the registry maps no components with a file next to it");
710
+ else console.log(`
711
+ review the manifests, then push each section: botlr push <name> --create`);
712
+ })
713
+ );
714
+ program.parseAsync(process.argv).catch((error) => fail(error.message));
715
+ //# sourceMappingURL=cli.js.map