busa-cli 0.9.6 → 0.9.9

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.
@@ -1,1564 +0,0 @@
1
- // src/format.ts
2
- function render(value, output) {
3
- if (output === "json") {
4
- return JSON.stringify(value, null, 2);
5
- }
6
- if (output === "text") {
7
- return renderText(value);
8
- }
9
- if (Array.isArray(value)) {
10
- return renderTable(value);
11
- }
12
- if (value && typeof value === "object") {
13
- return renderRecord(value);
14
- }
15
- return String(value);
16
- }
17
- function cell(value) {
18
- if (value === null || value === void 0) return "";
19
- if (typeof value === "object") return compactJson(value);
20
- return truncate(String(value));
21
- }
22
- function compactJson(value) {
23
- if (Array.isArray(value)) {
24
- if (value.length === 0) return "[]";
25
- if (value.every((item) => typeof item !== "object" || item === null)) {
26
- return truncate(value.map((item) => String(item)).join(", "));
27
- }
28
- return `[${value.length} items]`;
29
- }
30
- if (value && typeof value === "object") {
31
- const keys = Object.keys(value);
32
- if (keys.length === 0) return "{}";
33
- return `{${keys.slice(0, 3).join(", ")}${keys.length > 3 ? ", ..." : ""}}`;
34
- }
35
- return truncate(String(value));
36
- }
37
- function truncate(value, max = 72) {
38
- const singleLine = value.replace(/\s+/g, " ").trim();
39
- if (singleLine.length <= max) return singleLine;
40
- return `${singleLine.slice(0, max - 1)}\u2026`;
41
- }
42
- function renderRecord(row) {
43
- const keys = Object.keys(row);
44
- const width = Math.max(0, ...keys.map((k) => k.length));
45
- return keys.map((k) => `${k.padEnd(width)} ${cell(row[k])}`).join("\n");
46
- }
47
- function renderTable(rows) {
48
- if (rows.length === 0) return "(no rows)";
49
- if (typeof rows[0] !== "object" || rows[0] === null) {
50
- return rows.map((r) => cell(r)).join("\n");
51
- }
52
- const records = rows;
53
- const columns = [...new Set(records.flatMap((r) => Object.keys(r)))];
54
- const widths = columns.map((c2) => Math.max(c2.length, ...records.map((r) => cell(r[c2]).length)));
55
- const line = (cells) => cells.map((value, i) => value.padEnd(widths[i])).join(" ").trimEnd();
56
- const header = line(columns);
57
- const sep = widths.map((w) => "-".repeat(w)).join(" ");
58
- const body = records.map((r) => line(columns.map((c2) => cell(r[c2]))));
59
- return [header, sep, ...body].join("\n");
60
- }
61
- function renderText(value) {
62
- if (Array.isArray(value)) {
63
- if (value.length === 0) return "(no rows)";
64
- if (isNodeTree(value)) return renderNodeTree(value);
65
- if (typeof value[0] !== "object" || value[0] === null) {
66
- return value.map((item) => cell(item)).join("\n");
67
- }
68
- return renderTable(value);
69
- }
70
- if (value && typeof value === "object") {
71
- return renderRecord(value);
72
- }
73
- return String(value);
74
- }
75
- function isNodeTree(value) {
76
- return value.every(isNodeLike);
77
- }
78
- function isNodeLike(value) {
79
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
80
- const row = value;
81
- return typeof row.id === "string" && typeof row.type === "string" && typeof row.slug === "string" && typeof row.name === "string" && Array.isArray(row.children);
82
- }
83
- function renderNodeTree(nodes) {
84
- const lines = [];
85
- const walk = (items, prefix, isRoot) => {
86
- items.forEach((node, index) => {
87
- const isLast = index === items.length - 1;
88
- const branch = isRoot ? "" : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
89
- const childPrefix = isRoot ? "" : `${prefix}${isLast ? " " : "\u2502 "}`;
90
- lines.push(`${prefix}${branch}${formatNode(node)}`);
91
- const children = Array.isArray(node.children) ? node.children.filter(isNodeLike) : [];
92
- walk(children, childPrefix, false);
93
- });
94
- };
95
- walk(nodes, "", true);
96
- return lines.join("\n");
97
- }
98
- function formatNode(node) {
99
- const type = String(node.type ?? "node");
100
- const name = String(node.name ?? node.slug ?? node.id ?? "Untitled");
101
- const slug = typeof node.slug === "string" && node.slug !== name ? ` /${node.slug}` : "";
102
- const base = typeof node.baseId === "string" && node.baseId ? ` base=${node.baseId}` : "";
103
- return `${nodeIcon(type)} ${name}${slug} (${type}${base}, id=${node.id})`;
104
- }
105
- function nodeIcon(type) {
106
- switch (type) {
107
- case "folder":
108
- return "[folder]";
109
- case "base":
110
- return "[base]";
111
- case "doc":
112
- return "[doc]";
113
- case "skill":
114
- return "[skill]";
115
- case "drive":
116
- return "[drive]";
117
- default:
118
- return "[node]";
119
- }
120
- }
121
-
122
- // src/run.ts
123
- import { createHash as createHash2 } from "crypto";
124
- import { readFileSync as readFileSync2 } from "fs";
125
- import { basename, extname } from "path";
126
- import {
127
- CREATABLE_NODE_TYPES,
128
- cloudContract,
129
- createBusabaseClient,
130
- DEFAULT_BASE_URL as DEFAULT_BASE_URL2,
131
- normalizeBaseUrl as normalizeBaseUrl2
132
- } from "busabase-sdk";
133
- import {
134
- Command,
135
- CommanderError,
136
- InvalidArgumentError,
137
- Option
138
- } from "commander";
139
-
140
- // src/banner.ts
141
- var useColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
142
- var paint = (code, s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s;
143
- var c = {
144
- brand: (s) => paint("38;5;43", s),
145
- // teal — Busabase accent
146
- bold: (s) => paint("1", s),
147
- dim: (s) => paint("2", s),
148
- cyan: (s) => paint("36", s)
149
- };
150
- var LOGO = [
151
- " ____ _ ",
152
- " | __ ) _ _ ___ __ _| |__ __ _ ___ ___ ",
153
- " | _ \\| | | / __|/ _` | '_ \\ / _` / __|/ _ \\ ",
154
- " | |_) | |_| \\__ \\ (_| | |_) | (_| \\__ \\ __/ ",
155
- " |____/ \\__,_|___/\\__,_|_.__/ \\__,_|___/\\___| "
156
- ];
157
- function banner(baseUrl) {
158
- const out = [""];
159
- for (const line of LOGO) out.push(c.brand(line));
160
- out.push(`${c.dim(" OpenAPI client for Busabase")} ${c.dim("\xB7")} ${c.dim("talks to /api/v1")}`);
161
- out.push(` ${c.bold("Server")} ${c.cyan(baseUrl)}`);
162
- out.push("");
163
- return out.join("\n");
164
- }
165
-
166
- // src/config-file.ts
167
- import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
168
- import { homedir } from "os";
169
- import { join } from "path";
170
- var dotEnvPath = () => join(homedir(), ".busabase", ".env");
171
- function loadDotEnvFile() {
172
- let text;
173
- try {
174
- text = readFileSync(dotEnvPath(), "utf8");
175
- } catch {
176
- return {};
177
- }
178
- const out = {};
179
- for (const line of text.split("\n")) {
180
- const trimmed = line.trim();
181
- if (!trimmed || trimmed.startsWith("#")) continue;
182
- const eq = trimmed.indexOf("=");
183
- if (eq === -1) continue;
184
- const key = trimmed.slice(0, eq).trim();
185
- const value = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
186
- if (key) out[key] = value;
187
- }
188
- return out;
189
- }
190
- function writeDotEnvFile(updates) {
191
- const merged = { ...loadDotEnvFile() };
192
- for (const [key, value] of Object.entries(updates)) {
193
- if (value === null) delete merged[key];
194
- else merged[key] = value;
195
- }
196
- const path = dotEnvPath();
197
- mkdirSync(join(homedir(), ".busabase"), { recursive: true });
198
- const body = Object.entries(merged).map(([key, value]) => `${key}=${value}`).join("\n");
199
- writeFileSync(path, body ? `${body}
200
- ` : "", "utf8");
201
- try {
202
- chmodSync(path, 384);
203
- } catch {
204
- }
205
- }
206
-
207
- // src/login.ts
208
- import { spawn } from "child_process";
209
- import { createHash, randomBytes } from "crypto";
210
- import { createServer } from "http";
211
- import { createInterface } from "readline/promises";
212
- import { DEFAULT_BASE_URL, normalizeBaseUrl } from "busabase-sdk";
213
- var CLI_CLIENT_ID = "busabase-cli";
214
- var CLI_CLIENT_PLATFORM = "cli";
215
- var OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
216
- var EXPIRES_AT_KEY = "BUSABASE_TOKEN_EXPIRES_AT";
217
- var SESSION_TOKEN_PREFIX = "bss_";
218
- var AUTO_REFRESH_THRESHOLD_MS = 2 * 24 * 60 * 60 * 1e3;
219
- var DEFAULT_LOCAL_URL = "http://localhost:15419";
220
- var base64url = (buffer) => buffer.toString("base64url");
221
- var isInteractive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
222
- var say = (message) => {
223
- process.stderr.write(`${message}
224
- `);
225
- };
226
- var isLocalHost = (baseUrl) => {
227
- try {
228
- const { hostname } = new URL(baseUrl);
229
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
230
- } catch {
231
- return false;
232
- }
233
- };
234
- async function probeAuthRequired(baseUrl) {
235
- let res;
236
- try {
237
- res = await fetch(`${baseUrl}/api/v1/bases`);
238
- } catch {
239
- throw new Error(
240
- `Could not reach ${baseUrl}. Is the server running? Start a local one with \`busabase server\`, or pass a reachable --base-url.`
241
- );
242
- }
243
- return res.status === 401 || res.status === 403;
244
- }
245
- async function ask(question) {
246
- const rl = createInterface({ input: process.stdin, output: process.stderr });
247
- try {
248
- return (await rl.question(question)).trim();
249
- } finally {
250
- rl.close();
251
- }
252
- }
253
- function openBrowser(url) {
254
- const platform = process.platform;
255
- const [command, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
256
- try {
257
- spawn(command, args, { stdio: "ignore", detached: true }).unref();
258
- } catch {
259
- }
260
- }
261
- async function verifyAuth(baseUrl, token) {
262
- const res = await fetch(`${baseUrl}/api/v1/auth`, {
263
- headers: { authorization: `Bearer ${token}` }
264
- });
265
- if (res.status === 401) {
266
- throw new Error(
267
- `The credential was rejected (401) by ${baseUrl}. If it's an API key, check it in Dashboard \u2192 Settings \u2192 API Keys; if it's a login session, run \`busabase-cli login\` again.`
268
- );
269
- }
270
- if (!res.ok) {
271
- throw new Error(`Could not verify the credential (HTTP ${res.status}) from ${baseUrl}.`);
272
- }
273
- return await res.json();
274
- }
275
- async function oauthLogin(baseUrl, useBrowser) {
276
- const codeVerifier = base64url(randomBytes(32));
277
- const codeChallenge = base64url(createHash("sha256").update(codeVerifier).digest());
278
- const state = base64url(randomBytes(16));
279
- const { code, redirectUri } = await new Promise(
280
- (resolve, reject) => {
281
- const server = createServer((req, res) => {
282
- const url = new URL(req.url ?? "/", "http://127.0.0.1");
283
- if (url.pathname !== "/callback") {
284
- res.writeHead(404).end("Not found");
285
- return;
286
- }
287
- const respond = (title, body) => {
288
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
289
- res.end(
290
- `<!doctype html><meta charset="utf-8"><title>${title}</title><body style="font-family:system-ui;max-width:32rem;margin:4rem auto;text-align:center"><h2>${title}</h2><p>${body}</p></body>`
291
- );
292
- };
293
- const error = url.searchParams.get("error");
294
- const returnedState = url.searchParams.get("state");
295
- const returnedCode = url.searchParams.get("code");
296
- if (error) {
297
- respond("Sign-in failed", "You can close this tab and return to the terminal.");
298
- cleanup();
299
- reject(new Error(`Authorization failed: ${error}`));
300
- return;
301
- }
302
- if (!returnedCode || returnedState !== state) {
303
- respond("Sign-in failed", "State mismatch. You can close this tab and try again.");
304
- cleanup();
305
- reject(new Error("OAuth state mismatch \u2014 the callback did not match this request."));
306
- return;
307
- }
308
- respond("Signed in \u2713", "You can close this tab and return to the terminal.");
309
- const address = server.address();
310
- cleanup();
311
- resolve({ code: returnedCode, redirectUri: `http://127.0.0.1:${address.port}/callback` });
312
- });
313
- const timeout = setTimeout(() => {
314
- cleanup();
315
- reject(new Error("Timed out waiting for the browser sign-in (5 minutes)."));
316
- }, OAUTH_TIMEOUT_MS);
317
- function cleanup() {
318
- clearTimeout(timeout);
319
- server.close();
320
- }
321
- server.on("error", (err) => {
322
- cleanup();
323
- reject(err);
324
- });
325
- server.listen(0, "127.0.0.1", () => {
326
- const { port } = server.address();
327
- const redirect = `http://127.0.0.1:${port}/callback`;
328
- const authorizeUrl = new URL(`${baseUrl}/api/oauth/authorize`);
329
- authorizeUrl.searchParams.set("response_type", "code");
330
- authorizeUrl.searchParams.set("client_id", CLI_CLIENT_ID);
331
- authorizeUrl.searchParams.set("client_platform", CLI_CLIENT_PLATFORM);
332
- authorizeUrl.searchParams.set("code_challenge", codeChallenge);
333
- authorizeUrl.searchParams.set("code_challenge_method", "S256");
334
- authorizeUrl.searchParams.set("redirect_uri", redirect);
335
- authorizeUrl.searchParams.set("state", state);
336
- const href = authorizeUrl.toString();
337
- say("");
338
- say("Open this URL in your browser to sign in:");
339
- say(` ${href}`);
340
- say("");
341
- if (useBrowser) {
342
- say("Opening your browser\u2026");
343
- openBrowser(href);
344
- }
345
- say("Waiting for you to finish signing in\u2026");
346
- });
347
- }
348
- );
349
- const tokenRes = await fetch(`${baseUrl}/api/oauth/token`, {
350
- method: "POST",
351
- headers: { "content-type": "application/json" },
352
- body: JSON.stringify({
353
- grant_type: "authorization_code",
354
- client_id: CLI_CLIENT_ID,
355
- client_platform: CLI_CLIENT_PLATFORM,
356
- code,
357
- code_verifier: codeVerifier,
358
- redirect_uri: redirectUri,
359
- state
360
- })
361
- });
362
- if (!tokenRes.ok) {
363
- const text = await tokenRes.text();
364
- throw new Error(`Token exchange failed (HTTP ${tokenRes.status})${text ? `: ${text}` : ""}`);
365
- }
366
- const payload = await tokenRes.json();
367
- const token = payload.apiKey ?? payload.token ?? payload.accessToken;
368
- if (!token) throw new Error("Token exchange returned no credential.");
369
- return { token, expiresAt: payload.expiresAt ?? void 0 };
370
- }
371
- async function refreshToken(baseUrl, token) {
372
- const res = await fetch(`${baseUrl}/api/oauth/refresh`, {
373
- method: "POST",
374
- headers: { authorization: `Bearer ${token}` }
375
- });
376
- if (res.status === 401) throw new Error("SESSION_EXPIRED");
377
- if (!res.ok) throw new Error(`Refresh failed (HTTP ${res.status}) from ${baseUrl}.`);
378
- const payload = await res.json();
379
- return { token: payload.token ?? payload.accessToken ?? token, expiresAt: payload.expiresAt };
380
- }
381
- async function pickSpaceId(verify, preselected) {
382
- if (preselected) return preselected;
383
- const spaces = verify.spaces ?? [];
384
- if (spaces.length <= 1) return verify.space?.id ?? spaces[0]?.id;
385
- if (!isInteractive()) {
386
- const choices = spaces.map((space) => `${space.name} [${space.id}]`).join(", ");
387
- throw new Error(
388
- `You belong to ${spaces.length} spaces; pass --space-id <id> to choose one. Available spaces: ${choices}`
389
- );
390
- }
391
- say("");
392
- say("Which space should this CLI target?");
393
- spaces.forEach((space, index) => {
394
- const marker = space.id === verify.space?.id ? " (default)" : "";
395
- say(` ${index + 1}. ${space.name}${marker} [${space.id}]`);
396
- });
397
- const answer = await ask(`Choose 1-${spaces.length} (Enter for default): `);
398
- if (!answer) return verify.space?.id ?? spaces[0]?.id;
399
- const choice = Number(answer);
400
- if (Number.isInteger(choice) && choice >= 1 && choice <= spaces.length) {
401
- return spaces[choice - 1]?.id;
402
- }
403
- throw new Error("Not a valid space choice. Re-run login and choose one of the listed spaces.");
404
- }
405
- async function chooseTarget() {
406
- const cloud = normalizeBaseUrl(DEFAULT_BASE_URL);
407
- say("Busabase is an approval-first database and knowledge base for AI agents.");
408
- say("Agents propose changes; humans review and merge what becomes trusted data.");
409
- say("This login connects the CLI to the Busabase instance you want to use.");
410
- say("");
411
- say("How should this CLI connect?");
412
- say(" 1. Local/Desktop on this computer \u2014 no account, no login");
413
- say(" Use when you run `busabase server` or the Busabase Desktop app locally.");
414
- say(" 2. Busabase Cloud \u2014 browser sign-in (recommended)");
415
- say(" Best for humans: opens the browser and saves a refreshable CLI session.");
416
- say(" 3. Busabase Cloud \u2014 paste an API key");
417
- say(" Best for CI, servers, or agents where a browser is not available.");
418
- say(" 4. Self-hosted Busabase \u2014 browser sign-in");
419
- say(" Use your team's Busabase URL when it supports OAuth login.");
420
- say(" 5. Self-hosted Busabase \u2014 paste an API key");
421
- say(" Use your team's Busabase URL with a long-lived key for automation.");
422
- const choice = await ask("Choose 1-5 [2]: ");
423
- const askSelfHostedUrl = async () => {
424
- const url = await ask("Self-hosted base URL (e.g. https://busabase.example.com): ");
425
- if (!url) throw new Error("A self-hosted base URL is required.");
426
- return url;
427
- };
428
- switch (choice) {
429
- case "1": {
430
- const url = await ask(`Local server URL [${DEFAULT_LOCAL_URL}]: `) || DEFAULT_LOCAL_URL;
431
- return { baseUrl: url, method: "none" };
432
- }
433
- case "3":
434
- return { baseUrl: cloud, method: "api-key" };
435
- case "4":
436
- return { baseUrl: await askSelfHostedUrl(), method: "oauth" };
437
- case "5":
438
- return { baseUrl: await askSelfHostedUrl(), method: "api-key" };
439
- default:
440
- return { baseUrl: cloud, method: "oauth" };
441
- }
442
- }
443
- async function runLogin(options) {
444
- let baseUrl = normalizeBaseUrl(options.baseUrl);
445
- let apiKey = options.apiKey;
446
- let method;
447
- if (apiKey) {
448
- method = "api-key";
449
- } else if (options.oauth) {
450
- method = "oauth";
451
- } else if (isInteractive()) {
452
- const target = await chooseTarget();
453
- baseUrl = normalizeBaseUrl(target.baseUrl);
454
- method = target.method;
455
- } else {
456
- method = isLocalHost(baseUrl) ? "none" : "oauth";
457
- }
458
- if (method === "none") {
459
- if (await probeAuthRequired(baseUrl)) {
460
- throw new Error(
461
- `${baseUrl} requires sign-in. Re-run \`busabase-cli login\` and pick a Cloud or self-hosted option (OAuth or API key).`
462
- );
463
- }
464
- writeDotEnvFile({
465
- BUSABASE_BASE_URL: baseUrl,
466
- BUSABASE_API_KEY: null,
467
- BUSABASE_SPACE_ID: null,
468
- [EXPIRES_AT_KEY]: null
469
- });
470
- say(`\u2713 Connected to ${baseUrl} \u2014 open server, no login needed.`);
471
- say(` Saved to ${dotEnvPath()}. Try: busabase-cli bases list`);
472
- return { status: "connected (no auth)", baseUrl, config: dotEnvPath() };
473
- }
474
- let token;
475
- let expiresAt;
476
- if (method === "oauth") {
477
- const result = await oauthLogin(baseUrl, options.browser);
478
- token = result.token;
479
- expiresAt = result.expiresAt;
480
- } else {
481
- if (!apiKey) {
482
- say("Create a key in the dashboard \u2192 Settings \u2192 API Keys (shown once).");
483
- apiKey = await ask("Paste your API key (sk_\u2026): ");
484
- }
485
- if (!apiKey) throw new Error("No API key provided.");
486
- token = apiKey;
487
- }
488
- say("Verifying\u2026");
489
- const verify = await verifyAuth(baseUrl, token);
490
- const spaceId = await pickSpaceId(verify, options.spaceId);
491
- writeDotEnvFile({
492
- BUSABASE_BASE_URL: baseUrl,
493
- BUSABASE_API_KEY: token,
494
- BUSABASE_SPACE_ID: spaceId ?? null,
495
- // Clear any stale expiry when switching to an API key.
496
- [EXPIRES_AT_KEY]: expiresAt ?? null
497
- });
498
- say("");
499
- say(`\u2713 Signed in and saved to ${dotEnvPath()}`);
500
- return {
501
- status: "signed in",
502
- method,
503
- user: verify.user?.email ?? verify.user?.name ?? verify.user?.id ?? "(unknown)",
504
- space: spaceId ?? "(server default)",
505
- expiresAt: expiresAt ?? "(no expiry \u2014 API key)",
506
- baseUrl,
507
- config: dotEnvPath()
508
- };
509
- }
510
- async function runRefresh(options) {
511
- const baseUrl = normalizeBaseUrl(options.baseUrl);
512
- const token = options.apiKey;
513
- if (!token) throw new Error("Not signed in \u2014 run `busabase-cli login` first.");
514
- if (!token.startsWith(SESSION_TOKEN_PREFIX)) {
515
- return {
516
- status: "nothing to refresh",
517
- detail: "This credential is an API key, which doesn't expire. Only OAuth sessions refresh."
518
- };
519
- }
520
- let refreshed;
521
- try {
522
- refreshed = await refreshToken(baseUrl, token);
523
- } catch (error) {
524
- if (error instanceof Error && error.message === "SESSION_EXPIRED") {
525
- throw new Error(
526
- "Your login session has expired \u2014 run `busabase-cli login` to sign in again."
527
- );
528
- }
529
- throw error;
530
- }
531
- writeDotEnvFile({
532
- BUSABASE_API_KEY: refreshed.token,
533
- [EXPIRES_AT_KEY]: refreshed.expiresAt ?? null
534
- });
535
- say(`\u2713 Session refreshed${refreshed.expiresAt ? ` \u2014 valid until ${refreshed.expiresAt}` : ""}`);
536
- return {
537
- status: "refreshed",
538
- expiresAt: refreshed.expiresAt ?? "(unknown)",
539
- config: dotEnvPath()
540
- };
541
- }
542
- async function maybeAutoRefresh(baseUrl, apiKey) {
543
- if (!apiKey?.startsWith(SESSION_TOKEN_PREFIX)) return;
544
- const file = loadDotEnvFile();
545
- if (file.BUSABASE_API_KEY !== apiKey) return;
546
- const expiresRaw = file[EXPIRES_AT_KEY];
547
- if (!expiresRaw) return;
548
- const expiresAt = Date.parse(expiresRaw);
549
- if (Number.isNaN(expiresAt)) return;
550
- if (expiresAt - Date.now() > AUTO_REFRESH_THRESHOLD_MS) return;
551
- try {
552
- const refreshed = await refreshToken(normalizeBaseUrl(baseUrl), apiKey);
553
- writeDotEnvFile({
554
- BUSABASE_API_KEY: refreshed.token,
555
- [EXPIRES_AT_KEY]: refreshed.expiresAt ?? null
556
- });
557
- } catch {
558
- }
559
- }
560
- async function runLogout(options) {
561
- const baseUrl = normalizeBaseUrl(options.baseUrl);
562
- const token = options.apiKey;
563
- let revoked = "no session to revoke";
564
- if (token?.startsWith("bss_")) {
565
- try {
566
- const res = await fetch(`${baseUrl}/api/oauth/revoke`, {
567
- method: "POST",
568
- headers: { authorization: `Bearer ${token}` }
569
- });
570
- revoked = res.ok ? "session revoked" : `revoke returned HTTP ${res.status}`;
571
- } catch {
572
- revoked = "revoke request failed (cleared locally anyway)";
573
- }
574
- } else if (token) {
575
- revoked = "API key cleared locally (revoke it in the dashboard to disable it)";
576
- }
577
- writeDotEnvFile({ BUSABASE_API_KEY: null, BUSABASE_SPACE_ID: null, [EXPIRES_AT_KEY]: null });
578
- say(`\u2713 Cleared the saved credential from ${dotEnvPath()}`);
579
- return { status: "signed out", detail: revoked, config: dotEnvPath() };
580
- }
581
-
582
- // src/run.ts
583
- var DOCS_TROUBLESHOOTING = "https://busabase.com/docs/troubleshooting";
584
- function resolveConfig(opts) {
585
- const file = loadDotEnvFile();
586
- return {
587
- baseUrl: opts.baseUrl ?? process.env.BUSABASE_BASE_URL ?? file.BUSABASE_BASE_URL ?? DEFAULT_BASE_URL2,
588
- apiKey: opts.apiKey ?? process.env.BUSABASE_API_KEY ?? file.BUSABASE_API_KEY,
589
- spaceId: opts.spaceId ?? process.env.BUSABASE_SPACE_ID ?? file.BUSABASE_SPACE_ID,
590
- output: opts.output ?? "text"
591
- };
592
- }
593
- var GLOBAL_LONG_FLAGS = /* @__PURE__ */ new Set(["--base-url", "--api-key", "--space-id", "--output"]);
594
- function addGlobalFlags(cmd) {
595
- return cmd.option(
596
- "--base-url <url>",
597
- `server base URL (env BUSABASE_BASE_URL, default ${DEFAULT_BASE_URL2})`
598
- ).option("--api-key <token>", "bearer token for cloud hosts (env BUSABASE_API_KEY)").option("--space-id <id>", "target Busabase space (env BUSABASE_SPACE_ID)").addOption(
599
- new Option("--output <fmt>", "text | table | json (default text)").choices([
600
- "text",
601
- "table",
602
- "json"
603
- ])
604
- );
605
- }
606
- var parseNum = (value) => {
607
- const parsed = Number(value);
608
- if (!Number.isFinite(parsed)) throw new InvalidArgumentError("expected a number");
609
- return parsed;
610
- };
611
- var parseLimit = (value) => {
612
- const parsed = parseNum(value);
613
- if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {
614
- throw new InvalidArgumentError("expected an integer from 1 to 100");
615
- }
616
- return parsed;
617
- };
618
- var parsePositiveInt = (value) => {
619
- const parsed = parseNum(value);
620
- if (!Number.isInteger(parsed) || parsed < 1) {
621
- throw new InvalidArgumentError("expected a positive integer");
622
- }
623
- return parsed;
624
- };
625
- var parseBoolean = (value) => {
626
- if (value === "true") return true;
627
- if (value === "false") return false;
628
- throw new InvalidArgumentError("expected true or false");
629
- };
630
- var collectValues = (value, previous = []) => [...previous, value];
631
- var MIME_BY_EXT = {
632
- ".gif": "image/gif",
633
- ".jpeg": "image/jpeg",
634
- ".jpg": "image/jpeg",
635
- ".md": "text/markdown",
636
- ".pdf": "application/pdf",
637
- ".png": "image/png",
638
- ".svg": "image/svg+xml",
639
- ".txt": "text/plain",
640
- ".webp": "image/webp"
641
- };
642
- var guessMimeType = (filePath) => MIME_BY_EXT[extname(filePath).toLowerCase()] ?? "application/octet-stream";
643
- function createAttachmentOptions(opts) {
644
- const maxFiles = opts.maxFiles;
645
- const maxFileSize = opts.maxFileSize;
646
- const allowedMimeTypes = opts.allowedMime;
647
- if (!maxFiles && !maxFileSize && (!allowedMimeTypes || allowedMimeTypes.length === 0)) {
648
- return void 0;
649
- }
650
- return {
651
- attachment: {
652
- ...maxFiles ? { maxFiles } : {},
653
- ...maxFileSize ? { maxFileSize } : {},
654
- ...allowedMimeTypes && allowedMimeTypes.length > 0 ? { allowedMimeTypes } : {}
655
- }
656
- };
657
- }
658
- function mergeFieldOptions(existing, attachmentOptions) {
659
- if (!attachmentOptions) return existing;
660
- if (!existing || typeof existing !== "object" || Array.isArray(existing))
661
- return attachmentOptions;
662
- return { ...existing, ...attachmentOptions };
663
- }
664
- function parseFieldSpecs(specs) {
665
- return specs.map((spec) => {
666
- const [slug, name, type] = spec.split(":");
667
- return {
668
- slug,
669
- name: name ?? slug,
670
- ...type ? { type } : {}
671
- };
672
- });
673
- }
674
- function parseFieldDefinitions(opts) {
675
- const specs = opts.field ?? [];
676
- const fieldsJson = opts.fieldsJson;
677
- if (specs.length > 0 && fieldsJson) {
678
- throw new Error("Pass either --field or --fields-json, not both.");
679
- }
680
- if (fieldsJson) {
681
- const parsed = parseJsonValue(fieldsJson, "fields-json");
682
- if (!Array.isArray(parsed)) {
683
- throw new Error(
684
- '--fields-json must be a JSON array of field definitions. Example: [{"slug":"status","name":"Status","type":"select","options":{"choices":[{"id":"live","name":"Live"}]}}]'
685
- );
686
- }
687
- return parsed;
688
- }
689
- if (specs.length === 0) {
690
- throw new Error("Pass at least one --field or provide --fields-json @fields.json.");
691
- }
692
- return parseFieldSpecs(specs);
693
- }
694
- function parseFileNodeMetadata(opts) {
695
- const assetId = opts.assetId;
696
- if (!assetId) {
697
- throw new Error("--asset-id is required with --type file.");
698
- }
699
- return { assetId };
700
- }
701
- function parseJsonValue(raw, flagName) {
702
- const text = raw.startsWith("@") ? readFileSync2(raw.slice(1), "utf8") : raw;
703
- try {
704
- return JSON.parse(text);
705
- } catch (error) {
706
- const hint = raw.startsWith("@") ? `File ${raw.slice(1)} is not valid JSON.` : `Flag --${flagName} must be valid JSON. For complex values, write a file and pass --${flagName} @record.json.`;
707
- const reason = error instanceof Error ? error.message : String(error);
708
- throw new Error(`${hint}
709
- JSON parse error: ${reason}`);
710
- }
711
- }
712
- async function rawFetch(config, method, path, body) {
713
- const url = `${normalizeBaseUrl2(config.baseUrl)}${path}`;
714
- const res = await fetch(url, {
715
- method: method.toUpperCase(),
716
- headers: {
717
- ...body !== void 0 ? { "content-type": "application/json" } : {},
718
- ...config.apiKey ? { authorization: `Bearer ${config.apiKey}` } : {},
719
- ...config.spaceId ? { "x-busabase-space": config.spaceId } : {}
720
- },
721
- body: body !== void 0 ? JSON.stringify(body) : void 0
722
- });
723
- const text = await res.text();
724
- if (!res.ok) {
725
- throw new Error(`HTTP ${res.status} ${res.statusText}: ${text}`);
726
- }
727
- if (!text) return null;
728
- try {
729
- return JSON.parse(text);
730
- } catch {
731
- return text;
732
- }
733
- }
734
- async function rawRequest(config, method, path, body) {
735
- return rawFetch(config, method, path, body);
736
- }
737
- async function uploadAsset(config, opts) {
738
- const filePath = opts.file;
739
- const file = readFileSync2(filePath);
740
- const fileName = opts.fileName ?? basename(filePath);
741
- const mimeType = opts.mimeType ?? guessMimeType(filePath);
742
- const context = opts.context ?? "record-field";
743
- const contentHash = `sha256:${createHash2("sha256").update(file).digest("hex")}`;
744
- const requested = await rawRequest(config, "POST", "/api/v1/assets/upload-urls", {
745
- fileName,
746
- mimeType,
747
- sizeBytes: file.byteLength,
748
- context,
749
- contentHash
750
- });
751
- if (requested.duplicate) {
752
- return {
753
- id: requested.assetId ?? requested.attachmentId,
754
- assetId: requested.assetId,
755
- attachmentId: requested.attachmentId,
756
- url: requested.publicUrl,
757
- fileName,
758
- mimeType,
759
- size: file.byteLength
760
- };
761
- }
762
- if (requested.uploadUrl.startsWith("/")) {
763
- throw new Error(
764
- `Server returned a browser-relative upload URL (${requested.uploadUrl}). Use the Busabase UI for local/dev uploads or a cloud host with a presigned absolute upload URL.`
765
- );
766
- }
767
- const uploadResponse = await fetch(requested.uploadUrl, {
768
- body: new Uint8Array(file),
769
- headers: { "content-type": mimeType },
770
- method: "PUT"
771
- });
772
- if (!uploadResponse.ok) {
773
- const text = await uploadResponse.text();
774
- throw new Error(
775
- `Asset byte upload failed (${uploadResponse.status} ${uploadResponse.statusText})${text ? `: ${text}` : ""}`
776
- );
777
- }
778
- const confirmed = await rawRequest(config, "POST", "/api/v1/assets/confirmations", {
779
- storageKey: requested.storageKey,
780
- fileName,
781
- mimeType,
782
- sizeBytes: file.byteLength,
783
- context,
784
- contentHash
785
- });
786
- return {
787
- id: confirmed.assetId ?? confirmed.attachmentId,
788
- assetId: confirmed.assetId,
789
- attachmentId: confirmed.attachmentId,
790
- url: confirmed.publicUrl,
791
- fileName,
792
- mimeType,
793
- size: file.byteLength
794
- };
795
- }
796
- var INLINE_TEXT_MAX_BYTES = 1024 * 1024;
797
- async function putTextCommand(client, opts) {
798
- const assetId = opts.assetId;
799
- const text = opts.file ? readFileSync2(opts.file, "utf8") : opts.text;
800
- if (opts.none) {
801
- return client.assets.putText({ assetId, none: true });
802
- }
803
- if (text === void 0) {
804
- throw new Error("Provide --text <string>, --file <path>, or --none.");
805
- }
806
- const sizeBytes = Buffer.byteLength(text, "utf8");
807
- if (sizeBytes <= INLINE_TEXT_MAX_BYTES) {
808
- return client.assets.putText({ assetId, text });
809
- }
810
- const upload = await client.assets.createTextUploadUrl({ assetId, sizeBytes });
811
- const uploadResponse = await fetch(upload.uploadUrl, {
812
- method: "PUT",
813
- headers: { "content-type": "text/plain; charset=utf-8" },
814
- body: text
815
- });
816
- if (!uploadResponse.ok) {
817
- const body = await uploadResponse.text();
818
- throw new Error(
819
- `Text byte upload failed (${uploadResponse.status} ${uploadResponse.statusText})${body ? `: ${body}` : ""}`
820
- );
821
- }
822
- return client.assets.putText({ assetId, storageKey: upload.storageKey });
823
- }
824
- function runAction(state, handler) {
825
- return async (_opts, cmd) => {
826
- const opts = cmd.optsWithGlobals();
827
- const config = resolveConfig(opts);
828
- state.config = config;
829
- await maybeAutoRefresh(config.baseUrl, config.apiKey);
830
- const client = createBusabaseClient(config);
831
- const result = await handler(client, opts, config);
832
- console.log(render(result, config.output));
833
- };
834
- }
835
- function pkgVersion() {
836
- try {
837
- const pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
838
- return pkg.version ?? "0.0.0";
839
- } catch {
840
- return "0.0.0";
841
- }
842
- }
843
- var kebab = (value) => value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
844
- var isProcedure = (node) => typeof node === "object" && node !== null && typeof node["~orpc"]?.route?.method === "string";
845
- function unwrapSchema(schema) {
846
- let inner = schema;
847
- let optional = false;
848
- for (let i = 0; i < 6 && inner?.def; i++) {
849
- const type = inner.def.type;
850
- if (type === "optional" || type === "default") {
851
- optional = true;
852
- inner = inner.def.innerType;
853
- } else if (type === "nullable") {
854
- inner = inner.def.innerType;
855
- } else break;
856
- }
857
- return { inner, optional };
858
- }
859
- function classifyKind(inner) {
860
- const type = inner?.def?.type;
861
- if (type === "string") return { kind: "string" };
862
- if (type === "number") return { kind: "number" };
863
- if (type === "boolean") return { kind: "boolean" };
864
- if (type === "literal") return { kind: "string" };
865
- if (type === "enum") {
866
- const choices = Array.isArray(inner.options) ? inner.options : Object.values(inner.def?.entries ?? {});
867
- return choices.length ? { kind: "enum", choices } : { kind: "string" };
868
- }
869
- return { kind: "json" };
870
- }
871
- function inputFields(inputSchema) {
872
- if (inputSchema === void 0) return [];
873
- const { inner } = unwrapSchema(inputSchema);
874
- const shape = inner?.shape ?? (inner?.def?.type === "object" ? inner.def.shape : void 0);
875
- if (!shape || typeof shape !== "object") {
876
- return [{ key: "input", kind: "json", required: true, root: true }];
877
- }
878
- const fields = [];
879
- for (const [key, sub] of Object.entries(shape)) {
880
- const { inner: unwrapped, optional } = unwrapSchema(sub);
881
- const { kind, choices } = classifyKind(unwrapped);
882
- fields.push({ key, kind, required: !optional, choices });
883
- }
884
- return fields;
885
- }
886
- var GENERATED_SKIP = /* @__PURE__ */ new Set([
887
- "search",
888
- // top-level `search`
889
- "auth.verify",
890
- // `whoami`
891
- "records.search",
892
- // `records by-field-text`
893
- "records.listChangeRequests",
894
- // `records change-requests`
895
- "airapps.listFiles"
896
- // `airapps files`
897
- ]);
898
- function registerGeneratedCommands(program, state) {
899
- const getGroup = (displayName, tag) => program.commands.find((c2) => c2.name() === displayName) ?? program.command(displayName).description(tag ?? `${displayName} endpoints`);
900
- const addLeaf = (parent, navPath, procKey, proc) => {
901
- const { route, inputSchema } = proc["~orpc"];
902
- const name = kebab(procKey);
903
- if (parent.commands.some((c2) => c2.name() === name)) return;
904
- const fields = inputFields(inputSchema);
905
- const leaf = parent.command(name).description(route.summary ?? `${route.method} ${route.path}`);
906
- for (const f of fields) {
907
- const flag = `--${kebab(f.key)}`;
908
- if (GLOBAL_LONG_FLAGS.has(flag)) continue;
909
- if (f.kind === "boolean") {
910
- leaf.option(flag, `${f.key} (boolean)`);
911
- } else if (f.kind === "json") {
912
- const jsonFlag = `${flag}-json <json|@file>`;
913
- const desc = `${f.key} as JSON${f.required ? "" : " (optional)"}`;
914
- if (f.required) leaf.requiredOption(jsonFlag, desc);
915
- else leaf.option(jsonFlag, desc);
916
- } else if (f.kind === "enum") {
917
- const opt = new Option(`${flag} <value>`, f.key).choices(f.choices ?? []);
918
- if (f.required) opt.makeOptionMandatory();
919
- leaf.addOption(opt);
920
- } else if (f.kind === "number") {
921
- const label = `${flag} <value>`;
922
- const desc = `${f.key}${f.required ? "" : " (optional)"}`;
923
- if (f.required) leaf.requiredOption(label, desc, parseNum);
924
- else leaf.option(label, desc, parseNum);
925
- } else {
926
- const label = `${flag} <value>`;
927
- const desc = `${f.key}${f.required ? "" : " (optional)"}`;
928
- if (f.required) leaf.requiredOption(label, desc);
929
- else leaf.option(label, desc);
930
- }
931
- }
932
- addGlobalFlags(leaf);
933
- leaf.addHelpText("after", `
934
- OpenAPI: ${route.method} ${route.path}`);
935
- leaf.action(
936
- runAction(state, (client, opts) => {
937
- let input = {};
938
- for (const f of fields) {
939
- if (GLOBAL_LONG_FLAGS.has(`--${kebab(f.key)}`)) continue;
940
- const optKey = f.kind === "json" ? `${f.key}Json` : f.key;
941
- const value = opts[optKey];
942
- if (value === void 0) continue;
943
- const parsed = f.kind === "json" ? parseJsonValue(value, `${kebab(f.key)}-json`) : value;
944
- if (f.root) input = parsed;
945
- else input[f.key] = parsed;
946
- }
947
- let target = client;
948
- for (const segment of navPath) target = target[segment];
949
- return fields.length > 0 ? target[procKey](input) : target[procKey]();
950
- })
951
- );
952
- };
953
- const walk = (node, navPath) => {
954
- for (const key of Object.keys(node)) {
955
- if (key === "~orpc") continue;
956
- const child = node[key];
957
- const id = [...navPath, key].join(".");
958
- if (isProcedure(child)) {
959
- if (GENERATED_SKIP.has(id)) continue;
960
- if (navPath.length > 1) continue;
961
- const parent = navPath.length ? getGroup(kebab(navPath[0]), child["~orpc"].route.tags?.[0]) : program;
962
- addLeaf(parent, navPath, key, child);
963
- } else if (child && typeof child === "object") {
964
- walk(child, [...navPath, key]);
965
- }
966
- }
967
- };
968
- walk(cloudContract, []);
969
- }
970
- function flagSignature(cmd) {
971
- const parts = [];
972
- for (const option of cmd.options) {
973
- if (option.name() === "help" || GLOBAL_LONG_FLAGS.has(option.long ?? "")) continue;
974
- parts.push(option.mandatory ? option.flags : `[${option.flags}]`);
975
- }
976
- return parts.join(" ");
977
- }
978
- var isHelpHidden = (cmd) => Boolean(cmd._noHelp);
979
- function commandsSection(program) {
980
- const lines = ["Commands:"];
981
- const entry = (prefix, cmd) => {
982
- const sig = [prefix, flagSignature(cmd)].filter(Boolean).join(" ");
983
- const desc = cmd.description();
984
- if (!desc) return ` ${sig}`;
985
- if (sig.length <= 40) return ` ${sig.padEnd(42)}${desc}`;
986
- return ` ${sig}
987
- ${" ".repeat(44)}${desc}`;
988
- };
989
- for (const cmd of program.commands) {
990
- if (cmd.name() === "help" || isHelpHidden(cmd)) continue;
991
- const leaves = cmd.commands.filter((leaf) => leaf.name() !== "help" && !isHelpHidden(leaf));
992
- if (leaves.length > 0) {
993
- lines.push("");
994
- for (const leaf of leaves) lines.push(entry(`${cmd.name()} ${leaf.name()}`, leaf));
995
- } else {
996
- lines.push(entry(cmd.name(), cmd));
997
- }
998
- }
999
- return lines.join("\n");
1000
- }
1001
- var HELP_FOOTER = `
1002
- Config is read from flags, then env vars, then ~/.busabase/.env (auto-loaded \u2014 no
1003
- need to source it). An exported env var overrides the file.
1004
-
1005
- Docs: https://busabase.com/docs \xB7 Troubleshooting: ${DOCS_TROUBLESHOOTING}`;
1006
- function buildProgram(state = {}) {
1007
- const program = new Command("busabase-cli");
1008
- program.description("Client for the Busabase OpenAPI REST API \u2014 talks to `busabase server` or Cloud.").usage("[global flags] <command> [flags]").version(pkgVersion(), "-v, --version", "print busabase-cli version").exitOverride().showHelpAfterError("(run `busabase-cli --help` for the full command list)").addHelpText("beforeAll", () => banner(resolveConfig(program.opts()).baseUrl));
1009
- addGlobalFlags(program);
1010
- addGlobalFlags(program.command("health")).description("Server health check (GET /api/health)").action(runAction(state, (_client, _opts, config) => rawFetch(config, "GET", "/api/health")));
1011
- addGlobalFlags(program.command("openapi")).description("Fetch the OpenAPI document").action(
1012
- runAction(state, (_client, _opts, config) => rawFetch(config, "GET", "/api/v1/openapi.json"))
1013
- );
1014
- addGlobalFlags(program.command("whoami")).description("Active space, user, and membership").action(runAction(state, (client) => client.auth.verify()));
1015
- addGlobalFlags(program.command("login")).description("Connect the CLI to Busabase \u2014 Personal/local, Cloud, or self-hosted").option("--oauth", "force browser OAuth and skip the method prompt").option("--no-browser", "OAuth: print the sign-in URL instead of opening a browser").option("--refresh", "slide the saved OAuth session forward (no browser, no re-login)").addHelpText(
1016
- "after",
1017
- `
1018
- Busabase is an approval-first database and knowledge base for AI agents: agents
1019
- propose changes, humans review and merge what becomes trusted data.
1020
-
1021
- Run interactively, login explains the connection choices and writes the selected
1022
- Busabase instance to ~/.busabase/.env:
1023
- 1. Local/Desktop on this computer \u2014 no account, no login
1024
- 2. Busabase Cloud \u2014 browser sign-in (recommended)
1025
- 3. Busabase Cloud \u2014 paste an API key
1026
- 4. Self-hosted Busabase \u2014 browser sign-in
1027
- 5. Self-hosted Busabase \u2014 paste an API key
1028
- The flags below skip the menu (handy for scripts / CI):
1029
-
1030
- busabase-cli login # pick from the menu
1031
- busabase-cli login --oauth # Cloud browser sign-in
1032
- busabase-cli login --api-key sk_\u2026 # Cloud API key (headless/CI)
1033
- busabase-cli login --base-url http://localhost:15419 # connect to a local server (no auth)
1034
- busabase-cli login --refresh # extend the current session (auto-runs too)`
1035
- ).action(async (_opts, cmd) => {
1036
- const opts = cmd.optsWithGlobals();
1037
- const config = resolveConfig(opts);
1038
- state.config = config;
1039
- const summary = opts.refresh ? await runRefresh({ baseUrl: config.baseUrl, apiKey: config.apiKey }) : await runLogin({
1040
- baseUrl: config.baseUrl,
1041
- apiKey: opts.apiKey,
1042
- spaceId: config.spaceId,
1043
- oauth: Boolean(opts.oauth),
1044
- browser: opts.browser !== false
1045
- });
1046
- console.log(render(summary, config.output));
1047
- });
1048
- addGlobalFlags(program.command("logout")).description("Revoke the saved OAuth session (if any) and clear ~/.busabase/.env").action(async (_opts, cmd) => {
1049
- const opts = cmd.optsWithGlobals();
1050
- const config = resolveConfig(opts);
1051
- state.config = config;
1052
- const summary = await runLogout({ baseUrl: config.baseUrl, apiKey: config.apiKey });
1053
- console.log(render(summary, config.output));
1054
- });
1055
- const nodes = program.command("nodes").description("Workspace node tree");
1056
- addGlobalFlags(nodes.command("list")).description("Workspace node tree").action(runAction(state, (client) => client.nodes.list()));
1057
- addGlobalFlags(nodes.command("create-change-request")).description("Propose a new node via a Change Request").addOption(
1058
- new Option(`--type <${CREATABLE_NODE_TYPES.join("|")}>`, "node type").choices(CREATABLE_NODE_TYPES).makeOptionMandatory()
1059
- ).requiredOption("--slug <slug>", "node slug").requiredOption("--name <name>", "node name").option("--description <text>", "optional node description").option("--parent-node-id <id>", "parent folder node id; omit for root").option("--message <text>", "reviewer-facing Change Request message").option("--submitted-by <name>", "producer label").option("--field <slug:name:type...>", "base field, repeatable (for --type base)").option("--fields-json <json|@file>", "base fields as JSON array (for --type base)").option("--asset-id <id>", "backing Asset id (required for --type file)").addHelpText(
1060
- "after",
1061
- `
1062
- Examples:
1063
- busabase-cli nodes create-change-request --type folder --slug cms --name "\u5185\u5BB9\u7BA1\u7406 CMS"
1064
- busabase-cli nodes create-change-request --type base --slug blog --name "\u535A\u5BA2\u6587\u7AE0 Blog Posts" --field title:Title:text --field body:Body:markdown
1065
- busabase-cli nodes create-change-request --type base --slug products --name "\u4EA7\u54C1\u76EE\u5F55 Products" --fields-json @fields.json
1066
- busabase-cli nodes create-change-request --type file --slug board-plan --name "Board Plan" --asset-id ast_123`
1067
- ).action(
1068
- runAction(state, (client, opts) => {
1069
- const nodeType = opts.type;
1070
- const name = opts.name;
1071
- if (nodeType !== "base" && (opts.field || opts.fieldsJson)) {
1072
- throw new Error("--field and --fields-json are only valid with --type base.");
1073
- }
1074
- if (nodeType !== "file" && opts.assetId) {
1075
- throw new Error("--asset-id is only valid with --type file.");
1076
- }
1077
- return client.nodes.createChangeRequest({
1078
- message: opts.message ?? `Create ${nodeType} ${name}`,
1079
- submittedBy: opts.submittedBy,
1080
- operations: [
1081
- {
1082
- kind: "create",
1083
- nodeType,
1084
- slug: opts.slug,
1085
- name,
1086
- description: opts.description,
1087
- parentNodeId: opts.parentNodeId,
1088
- ...nodeType === "base" ? { fields: parseFieldDefinitions(opts) } : {},
1089
- ...nodeType === "file" ? { metadata: parseFileNodeMetadata(opts) } : {}
1090
- }
1091
- ]
1092
- });
1093
- })
1094
- );
1095
- addGlobalFlags(nodes.command("move")).description("Move or reorder a node (applied immediately, no review needed)").requiredOption("--node-id <id>", "node id to move").option("--parent-node-id <id>", "new parent folder node id; omit to keep the current parent").option(
1096
- "--position <n>",
1097
- "new position among the target parent's children",
1098
- (v) => Number.parseInt(v, 10)
1099
- ).option("--message <text>", "Change Request message").option("--submitted-by <name>", "producer label").addHelpText(
1100
- "after",
1101
- `
1102
- Examples:
1103
- busabase-cli nodes move --node-id nod_123 --position 0
1104
- busabase-cli nodes move --node-id nod_123 --parent-node-id nod_456 --position 2`
1105
- ).action(
1106
- runAction(
1107
- state,
1108
- (client, opts) => client.nodes.move({
1109
- nodeId: opts.nodeId,
1110
- parentNodeId: opts.parentNodeId,
1111
- position: opts.position,
1112
- message: opts.message,
1113
- submittedBy: opts.submittedBy
1114
- })
1115
- )
1116
- );
1117
- const bases = program.command("bases").description("Bases (structured tables)");
1118
- addGlobalFlags(bases.command("list")).description("List Bases in the active space").action(runAction(state, (client) => client.bases.list()));
1119
- addGlobalFlags(bases.command("get")).description("Get one Base by slug").requiredOption("--slug <slug>", "Base slug").action(
1120
- runAction(state, async (client, opts) => {
1121
- const slug = opts.slug;
1122
- const found = (await client.bases.list()).find((base) => base.slug === slug);
1123
- if (!found) throw new Error(`no Base with slug "${slug}"`);
1124
- return found;
1125
- })
1126
- );
1127
- addGlobalFlags(bases.command("create")).description("Create a Base \u2014 review-first by default: returns a pending Change Request").requiredOption("--slug <slug>", "Base slug").requiredOption("--name <name>", "Base name").option("--field <slug:name:type...>", "field definition, repeatable").option("--fields-json <json|@file>", "field definitions as JSON array").option("--description <text>", "optional description").option("--parent-node-id <id>", "parent folder node id; omit for root").option(
1128
- "--auto-merge",
1129
- "skip review and create the Base immediately (default: propose a pending Change Request)"
1130
- ).addHelpText(
1131
- "after",
1132
- `
1133
- Examples:
1134
- busabase-cli bases create --slug products --name "\u4EA7\u54C1\u76EE\u5F55 Products" --field product_name:"Product Name":text
1135
- busabase-cli bases create --slug products --name "\u4EA7\u54C1\u76EE\u5F55 Products" --fields-json @fields.json
1136
- busabase-cli bases create --slug products --name "Products" --auto-merge # skip review, create immediately`
1137
- ).action(
1138
- runAction(
1139
- state,
1140
- (client, opts) => client.bases.create({
1141
- slug: opts.slug,
1142
- name: opts.name,
1143
- description: opts.description,
1144
- parentNodeId: opts.parentNodeId,
1145
- fields: parseFieldDefinitions(opts),
1146
- ...opts.autoMerge ? { autoMerge: true } : {}
1147
- })
1148
- )
1149
- );
1150
- addGlobalFlags(bases.command("create-field")).description("Add a field to a Base").requiredOption("--base-id <id>", "Base id").requiredOption("--slug <slug>", "field slug").requiredOption("--name <name>", "field name").option("--field-type <type>", "field type (default text)").option("--required", "mark the field as required").option("--max-files <n>", "attachment option: max files", parsePositiveInt).option("--max-file-size <bytes>", "attachment option: max size in bytes", parsePositiveInt).option(
1151
- "--allowed-mime <mime>",
1152
- "attachment option: allowed MIME type, repeatable",
1153
- collectValues
1154
- ).action(
1155
- runAction(
1156
- state,
1157
- (client, opts) => client.bases.createField({
1158
- baseId: opts.baseId,
1159
- slug: opts.slug,
1160
- name: opts.name,
1161
- ...opts.fieldType ? { type: opts.fieldType } : {},
1162
- required: Boolean(opts.required),
1163
- ...createAttachmentOptions(opts) ? { options: createAttachmentOptions(opts) } : {}
1164
- })
1165
- )
1166
- );
1167
- addGlobalFlags(bases.command("update-field-change-request")).description("Propose updating field metadata/options via a Change Request").requiredOption("--base-id <id>", "Base id").requiredOption("--field-id <id>", "field id").option("--name <name>", "new field name").option("--required <true|false>", "set required flag", parseBoolean).option("--options-json <json|@file>", "full field options JSON, or @file.json").option("--max-files <n>", "attachment option: max files", parsePositiveInt).option("--max-file-size <bytes>", "attachment option: max size in bytes", parsePositiveInt).option(
1168
- "--allowed-mime <mime>",
1169
- "attachment option: allowed MIME type, repeatable",
1170
- collectValues
1171
- ).option("--message <text>", "reviewer-facing Change Request message").option("--submitted-by <name>", "producer label").addHelpText(
1172
- "after",
1173
- `
1174
- Examples:
1175
- busabase-cli bases update-field-change-request --base-id bse_123 --field-id bsf_123 --name "\u5C01\u9762 Cover Image" --max-files 1 --allowed-mime image/png --allowed-mime image/svg+xml
1176
- busabase-cli bases update-field-change-request --base-id bse_123 --field-id bsf_123 --options-json @field-options.json`
1177
- ).action(
1178
- runAction(state, (client, opts) => {
1179
- const patch = {};
1180
- if (opts.name) patch.name = opts.name;
1181
- if (opts.required !== void 0) patch.required = opts.required;
1182
- if (opts.optionsJson) {
1183
- patch.options = parseJsonValue(opts.optionsJson, "options-json");
1184
- }
1185
- const attachmentOptions = createAttachmentOptions(opts);
1186
- patch.options = mergeFieldOptions(patch.options, attachmentOptions);
1187
- if (Object.keys(patch).length === 0) {
1188
- throw new Error(
1189
- "No field patch supplied. Pass --name, --required, --options-json, or attachment option flags."
1190
- );
1191
- }
1192
- return client.bases.updateFieldChangeRequest({
1193
- baseId: opts.baseId,
1194
- fieldId: opts.fieldId,
1195
- patch,
1196
- message: opts.message,
1197
- submittedBy: opts.submittedBy
1198
- });
1199
- })
1200
- );
1201
- addGlobalFlags(bases.command("create-change-request")).description("Propose a new record via a Change Request").requiredOption("--base-id <id>", "target Base id").requiredOption("--fields-json <json|@file>", "record fields as JSON, or @file.json").option("--message <text>", "reviewer-facing Change Request message").option("--submitted-by <name>", "producer label").addHelpText(
1202
- "after",
1203
- `
1204
- Examples:
1205
- busabase-cli bases create-change-request --base-id bse_123 --fields-json '{"title":"Hello","status":"draft"}'
1206
- busabase-cli bases create-change-request --base-id bse_123 --fields-json @record.json`
1207
- ).action(
1208
- runAction(
1209
- state,
1210
- (client, opts) => client.bases.createChangeRequest({
1211
- baseId: opts.baseId,
1212
- fields: parseJsonValue(opts.fieldsJson, "fields-json"),
1213
- message: opts.message,
1214
- submittedBy: opts.submittedBy
1215
- })
1216
- )
1217
- );
1218
- const airapps = program.command("airapps").description("AirApps (sandboxed mini-apps)");
1219
- addGlobalFlags(airapps.command("list")).description("List AirApps in the active space").action(runAction(state, (client) => client.airapps.list()));
1220
- addGlobalFlags(airapps.command("get")).description("Get one AirApp by node id").requiredOption("--node-id <id>", "AirApp node id").action(
1221
- runAction(state, (client, opts) => client.airapps.get({ nodeId: opts.nodeId }))
1222
- );
1223
- addGlobalFlags(airapps.command("create")).description("Create an AirApp \u2014 review-first by default: returns a pending Change Request").requiredOption("--slug <slug>", "AirApp slug").requiredOption("--name <name>", "AirApp name").option("--description <text>", "optional description").option("--parent-node-id <id>", "parent folder node id; omit for root").addOption(
1224
- new Option(
1225
- "--visibility <private|workspace|public>",
1226
- "AirApp visibility (default private)"
1227
- ).choices(["private", "workspace", "public"])
1228
- ).option("--version <semver>", "AirApp version (default 0.1.0)").option(
1229
- "--files-json <json|@file>",
1230
- 'files as a JSON array, or @file.json \u2014 text files {"path","content","mimeType?"} or asset-backed files {"path","assetId","displayName?","mimeType?"}'
1231
- ).addOption(
1232
- new Option(
1233
- "--merge-mode <merge|replace>",
1234
- "how --files-json combines with the default scaffold (default merge)"
1235
- ).choices(["merge", "replace"])
1236
- ).option(
1237
- "--auto-merge",
1238
- "skip review and create the AirApp immediately (default: propose a pending Change Request)"
1239
- ).addHelpText(
1240
- "after",
1241
- `
1242
- mergeMode explains how --files-json combines with the AirApp's default scaffold:
1243
- merge (default) \u2014 your files are layered on top of the default Hono-template
1244
- scaffold by path; supply just a few files (e.g. one custom
1245
- route) and still get the rest of the scaffold for any path
1246
- you didn't provide yourself.
1247
- replace \u2014 your files replace the scaffold entirely; use this when
1248
- handing over a complete, self-contained project (e.g. a Vite
1249
- app) so you don't end up with stray unrelated default files
1250
- mixed in.
1251
-
1252
- Examples:
1253
- busabase-cli airapps create --slug hello-app --name "Hello App" --files-json @files.json
1254
- busabase-cli airapps create --slug vite-app --name "Vite App" --files-json @files.json --merge-mode replace
1255
- busabase-cli airapps create --slug hello-app --name "Hello App" --files-json @files.json --auto-merge # skip review, create immediately`
1256
- ).action(
1257
- runAction(
1258
- state,
1259
- (client, opts) => client.airapps.create({
1260
- slug: opts.slug,
1261
- name: opts.name,
1262
- description: opts.description,
1263
- parentNodeId: opts.parentNodeId,
1264
- visibility: opts.visibility,
1265
- version: opts.version,
1266
- files: opts.filesJson ? parseJsonValue(opts.filesJson, "files-json") : void 0,
1267
- mergeMode: opts.mergeMode,
1268
- ...opts.autoMerge ? { autoMerge: true } : {}
1269
- })
1270
- )
1271
- );
1272
- addGlobalFlags(airapps.command("files")).description("List an AirApp's files").requiredOption("--node-id <id>", "AirApp node id").action(
1273
- runAction(
1274
- state,
1275
- (client, opts) => client.airapps.listFiles({ nodeId: opts.nodeId })
1276
- )
1277
- );
1278
- addGlobalFlags(airapps.command("read-file")).description("Read one AirApp file's content").requiredOption("--node-id <id>", "AirApp node id").requiredOption("--path <path>", "file path within the AirApp").action(
1279
- runAction(
1280
- state,
1281
- (client, opts) => client.airapps.readFile({ nodeId: opts.nodeId, filePath: opts.path })
1282
- )
1283
- );
1284
- addGlobalFlags(airapps.command("create-change-request")).description("Propose file changes to an existing AirApp via a Change Request").requiredOption("--node-id <id>", "AirApp node id").requiredOption(
1285
- "--operations-json <json|@file>",
1286
- 'file operations as a JSON array, or @file.json \u2014 "create"/"update" (text {"kind","path","content","mimeType?"} or asset-backed {"kind","path","assetId","displayName?","mimeType?"}), "delete" {"kind","path"}, or "metadata_update" {"kind","metadata":{"entryFile?","visibility?","version?"}}'
1287
- ).option("--message <text>", "reviewer-facing Change Request message").option("--submitted-by <name>", "producer label").addHelpText(
1288
- "after",
1289
- `
1290
- Examples:
1291
- busabase-cli airapps create-change-request --node-id nod_123 --operations-json @operations.json
1292
- busabase-cli airapps create-change-request --node-id nod_123 --operations-json '[{"kind":"update","path":"index.js","content":"..."}]' --message "Fix handler bug"`
1293
- ).action(
1294
- runAction(
1295
- state,
1296
- (client, opts) => client.airapps.createChangeRequest({
1297
- nodeId: opts.nodeId,
1298
- message: opts.message,
1299
- submittedBy: opts.submittedBy,
1300
- operations: parseJsonValue(
1301
- opts.operationsJson,
1302
- "operations-json"
1303
- )
1304
- })
1305
- )
1306
- );
1307
- const records = program.command("records").description("Records");
1308
- addGlobalFlags(records.command("list")).description("List records").option("--limit <n>", "max results (1-100)", parseLimit).option("--base-id <id>", "restrict to one Base").option("--cursor <cursor>", "opaque nextCursor from a previous page").action(
1309
- runAction(state, (_client, opts, config) => {
1310
- const query = new URLSearchParams();
1311
- if (opts.limit !== void 0) query.set("limit", String(opts.limit));
1312
- if (opts.baseId) query.set("baseId", opts.baseId);
1313
- if (opts.cursor) query.set("cursor", opts.cursor);
1314
- const qs = query.toString();
1315
- return rawRequest(config, "GET", `/api/v1/records/paged${qs ? `?${qs}` : ""}`);
1316
- })
1317
- );
1318
- addGlobalFlags(records.command("get")).description("Get one record").requiredOption("--record-id <id>", "record id").action(
1319
- runAction(state, (client, opts) => client.records.get({ recordId: opts.recordId }))
1320
- );
1321
- addGlobalFlags(records.command("by-field-text")).description("Find records by text field value").requiredOption("--field-slug <slug>", "field slug to match").requiredOption("--value-text <text>", "text value to match").option("--base-id <id>", "restrict to one Base").option("--limit <n>", "max results", parseNum).action(
1322
- runAction(
1323
- state,
1324
- (client, opts) => client.records.search({
1325
- fieldSlug: opts.fieldSlug,
1326
- valueText: opts.valueText,
1327
- baseId: opts.baseId,
1328
- limit: opts.limit
1329
- })
1330
- )
1331
- );
1332
- addGlobalFlags(records.command("change-requests")).description("Change Requests for a record").requiredOption("--record-id <id>", "record id").action(
1333
- runAction(
1334
- state,
1335
- (client, opts) => client.records.listChangeRequests({ recordId: opts.recordId })
1336
- )
1337
- );
1338
- const changeRequests = program.command("change-requests").description("Change Requests");
1339
- addGlobalFlags(changeRequests.command("list")).description("List Change Requests").option("--limit <n>", "max results", parseNum).action(
1340
- runAction(
1341
- state,
1342
- (client, opts) => client.changeRequests.list({ limit: opts.limit })
1343
- )
1344
- );
1345
- addGlobalFlags(changeRequests.command("get")).description("Get a Change Request").requiredOption("--change-request-id <id>", "Change Request id").action(
1346
- runAction(
1347
- state,
1348
- (client, opts) => client.changeRequests.get({ changeRequestId: opts.changeRequestId })
1349
- )
1350
- );
1351
- addGlobalFlags(changeRequests.command("review")).description("Review a Change Request (rejected = request changes, not terminal)").requiredOption("--change-request-id <id>", "Change Request id").addOption(
1352
- new Option("--verdict <approved|rejected>", "review verdict").choices(["approved", "rejected"]).makeOptionMandatory()
1353
- ).option("--reason <text>", "review reason").action(
1354
- runAction(
1355
- state,
1356
- (client, opts) => client.changeRequests.review({
1357
- changeRequestId: opts.changeRequestId,
1358
- verdict: opts.verdict,
1359
- reason: opts.reason
1360
- })
1361
- )
1362
- );
1363
- addGlobalFlags(changeRequests.command("close")).description("Terminally abandon/reject a Change Request").requiredOption("--change-request-id <id>", "Change Request id").option("--reason <text>", "close reason").action(
1364
- runAction(
1365
- state,
1366
- (client, opts) => client.changeRequests.close({
1367
- changeRequestId: opts.changeRequestId,
1368
- reason: opts.reason
1369
- })
1370
- )
1371
- );
1372
- addGlobalFlags(changeRequests.command("merge")).description("Merge a Change Request into its Base").requiredOption("--change-request-id <id>", "Change Request id").action(
1373
- runAction(
1374
- state,
1375
- (client, opts) => client.changeRequests.merge({ changeRequestId: opts.changeRequestId })
1376
- )
1377
- );
1378
- addGlobalFlags(program.command("search")).description("Full-text search").requiredOption("--query <q>", "search query").option("--limit <n>", "max results", parseNum).option("--offset <n>", "results offset", parseNum).option(
1379
- "--sources <sources...>",
1380
- 'restrict which content to search: "records", "files", and/or "names" (default: all three)'
1381
- ).action(
1382
- runAction(
1383
- state,
1384
- (client, opts) => client.search({
1385
- query: opts.query,
1386
- limit: opts.limit,
1387
- offset: opts.offset,
1388
- sources: opts.sources
1389
- })
1390
- )
1391
- );
1392
- addGlobalFlags(program.command("grep")).description(
1393
- "Search files, Docs, and Base records with one pattern (unified grep; use `assets grep` for files-only)"
1394
- ).requiredOption("--pattern <regex>", "literal or regex pattern").option("--flags <flags>", 'RegExp flags, e.g. "i" for case-insensitive').option(
1395
- "--sources <sources...>",
1396
- 'sources to scan: "files", "docs", and/or "records" (default: all three)'
1397
- ).option("--asset-ids <ids...>", "files scope: specific asset ids").option("--drive-path <path>", "files scope: Drive/Skill mounted path prefix").option("--mime-types <types...>", "files scope: MIME types").option("--node-ids <ids...>", "docs scope: specific Doc node ids").option("--base-ids <ids...>", "records scope: specific Base ids").option("--base-slugs <slugs...>", "records scope: specific Base slugs").option(
1398
- "--max-matches <n>",
1399
- "max matches, shared across sources (default 100, cap 1000)",
1400
- parsePositiveInt
1401
- ).option("--context-lines <n>", "lines of before/after context (default 0, cap 10)", parseNum).action(
1402
- runAction(state, (client, opts) => {
1403
- const filesScope = opts.assetIds || opts.drivePath || opts.mimeTypes ? {
1404
- assetIds: opts.assetIds,
1405
- drivePath: opts.drivePath,
1406
- mimeTypes: opts.mimeTypes
1407
- } : void 0;
1408
- const docsScope = opts.nodeIds ? { nodeIds: opts.nodeIds } : void 0;
1409
- const recordsScope = opts.baseIds || opts.baseSlugs ? {
1410
- baseIds: opts.baseIds,
1411
- baseSlugs: opts.baseSlugs
1412
- } : void 0;
1413
- return client.grep({
1414
- pattern: opts.pattern,
1415
- flags: opts.flags,
1416
- sources: opts.sources,
1417
- scope: filesScope || docsScope || recordsScope ? { files: filesScope, docs: docsScope, records: recordsScope } : void 0,
1418
- maxMatches: opts.maxMatches,
1419
- contextLines: opts.contextLines
1420
- });
1421
- })
1422
- );
1423
- const addUploadCommand = (parent, hidden = false) => addGlobalFlags(parent.command("upload", { noHelp: hidden })).description("Upload a file and print an asset ref for record fields").requiredOption("--file <path>", "local file to upload").option("--file-name <name>", "stored file name (default: basename of --file)").option("--mime-type <mime>", "MIME type (default: inferred from extension)").option("--context <value>", "upload context (default record-field)").addHelpText(
1424
- "after",
1425
- `
1426
- Example:
1427
- busabase-cli assets upload --file ./cover.png --output json
1428
-
1429
- Use the JSON output directly in an attachment field value, e.g. {"cover_image":[<output>]}.`
1430
- ).action(runAction(state, (_client, opts, config) => uploadAsset(config, opts)));
1431
- const assetsCommand = program.command("assets").description("Assets");
1432
- addUploadCommand(assetsCommand);
1433
- addGlobalFlags(assetsCommand.command("put-text")).description("Write (or mark none) an asset's Drive Grep Retrieval text slot").requiredOption("--asset-id <id>", "asset id").option("--text <string>", "inline text (\u22641MB)").option("--file <path>", "read text from a local file instead of --text").option("--none", "mark as having no extractable text (e.g. a scanned, image-only PDF)").addHelpText(
1434
- "after",
1435
- `
1436
- Examples:
1437
- busabase-cli assets put-text --asset-id ast_123 --file ./extracted.txt
1438
- busabase-cli assets put-text --asset-id ast_123 --text "plain text"
1439
- busabase-cli assets put-text --asset-id ast_123 --none`
1440
- ).action(runAction(state, (client, opts) => putTextCommand(client, opts)));
1441
- addGlobalFlags(assetsCommand.command("grep")).description("Search every text-bearing asset in scope").requiredOption("--pattern <regex>", "literal or regex pattern").option("--flags <flags>", 'RegExp flags, e.g. "i" for case-insensitive').option("--asset-ids <ids...>", "scope: specific asset ids").option("--drive-path <path>", "scope: Drive/Skill mounted path prefix").option("--mime-types <types...>", "scope: MIME types").option("--max-matches <n>", "max matches (default 100, cap 1000)", parsePositiveInt).option("--context-lines <n>", "lines of before/after context (default 0, cap 10)", parseNum).action(
1442
- runAction(
1443
- state,
1444
- (client, opts) => client.assets.grep({
1445
- pattern: opts.pattern,
1446
- flags: opts.flags,
1447
- scope: opts.assetIds || opts.drivePath || opts.mimeTypes ? {
1448
- assetIds: opts.assetIds,
1449
- drivePath: opts.drivePath,
1450
- mimeTypes: opts.mimeTypes
1451
- } : void 0,
1452
- maxMatches: opts.maxMatches,
1453
- contextLines: opts.contextLines
1454
- })
1455
- )
1456
- );
1457
- addGlobalFlags(assetsCommand.command("read-lines")).description("Read an exact line range from an asset's text (range capped at 2000 lines)").requiredOption("--asset-id <id>", "asset id").requiredOption("--start-line <n>", "first line (1-based)", parsePositiveInt).requiredOption("--end-line <n>", "last line (1-based)", parsePositiveInt).action(
1458
- runAction(
1459
- state,
1460
- (client, opts) => client.assets.readTextLines({
1461
- assetId: opts.assetId,
1462
- startLine: opts.startLine,
1463
- endLine: opts.endLine
1464
- })
1465
- )
1466
- );
1467
- addGlobalFlags(program.command("api")).description("Raw request to any /api/v1 endpoint").addOption(
1468
- new Option("--method <get|post|put|delete>", "HTTP method").choices(["get", "post", "put", "delete"]).makeOptionMandatory()
1469
- ).requiredOption("--path <p>", "path under /api/v1, e.g. /bases").option("--query <k=v...>", "query-string param, repeatable").option("--body-json <json>", "JSON request body").action(
1470
- runAction(state, (_client, opts, config) => {
1471
- const query = new URLSearchParams();
1472
- for (const pair of opts.query ?? []) {
1473
- const [key, ...rest] = pair.split("=");
1474
- query.set(key, rest.join("="));
1475
- }
1476
- const qs = query.toString();
1477
- const path = `/api/v1${opts.path}${qs ? `?${qs}` : ""}`;
1478
- const bodyJson = opts.bodyJson;
1479
- return rawFetch(
1480
- config,
1481
- opts.method,
1482
- path,
1483
- bodyJson ? parseJsonValue(bodyJson, "body-json") : void 0
1484
- );
1485
- })
1486
- );
1487
- registerGeneratedCommands(program, state);
1488
- for (const group of program.commands) {
1489
- const leaves = group.commands.length > 0 ? group.commands : [group];
1490
- for (const leaf of leaves) {
1491
- const sig = flagSignature(leaf);
1492
- leaf.usage(`${sig ? `${sig} ` : ""}[global flags]`);
1493
- }
1494
- }
1495
- program.addHelpText("after", () => `
1496
- ${commandsSection(program)}
1497
- ${HELP_FOOTER}`);
1498
- program.configureHelp({ visibleCommands: () => [] });
1499
- return program;
1500
- }
1501
- async function runCli(argv) {
1502
- const state = {};
1503
- const program = buildProgram(state);
1504
- if (argv.length === 0) {
1505
- program.outputHelp();
1506
- return 0;
1507
- }
1508
- try {
1509
- await program.parseAsync(argv, { from: "user" });
1510
- return 0;
1511
- } catch (error) {
1512
- if (error instanceof CommanderError) {
1513
- return error.exitCode === 0 ? 0 : 1;
1514
- }
1515
- console.error(explainError(error, state.config ?? resolveConfig({})));
1516
- return 1;
1517
- }
1518
- }
1519
- function renderedHelp() {
1520
- let out = "";
1521
- const program = buildProgram();
1522
- program.configureOutput({
1523
- writeOut: (str) => {
1524
- out += str;
1525
- },
1526
- writeErr: () => {
1527
- }
1528
- });
1529
- program.outputHelp();
1530
- return out;
1531
- }
1532
- var HELP = renderedHelp();
1533
- function explainError(error, config) {
1534
- const base = config.baseUrl;
1535
- const msg = error instanceof Error ? error.message : String(error);
1536
- const status = error.status;
1537
- const lower = msg.toLowerCase();
1538
- let body;
1539
- if (status === 401 || lower.includes("401") || lower.includes("unauthorized")) {
1540
- body = [
1541
- `Unauthorized (401) from ${base}.`,
1542
- config.apiKey ? " The credential was rejected or expired \u2014 run `busabase-cli login` to sign in again (browser OAuth or an API key)." : " This host needs sign-in. Run `busabase-cli login` (browser OAuth or an API key), or pass --api-key <token> / export BUSABASE_API_KEY=\u2026.",
1543
- " Meant to hit a local server? Add --base-url http://localhost:15419 (or export BUSABASE_BASE_URL=\u2026)."
1544
- ].join("\n");
1545
- } else if (lower.includes("fetch failed") || lower.includes("econnrefused") || lower.includes("enotfound") || lower.includes("getaddrinfo") || lower.includes("network")) {
1546
- body = [
1547
- `Could not reach ${base}.`,
1548
- " \u2022 Cloud: check your internet connection and that the URL is correct.",
1549
- " \u2022 Local: start it with `npx busabase server` (http://localhost:15419), then add --base-url http://localhost:15419 (or export BUSABASE_BASE_URL=\u2026).",
1550
- ` (underlying error: ${msg})`
1551
- ].join("\n");
1552
- } else {
1553
- body = `${msg}
1554
- (base URL: ${base}${config.apiKey ? ", with API key" : ", no API key"})`;
1555
- }
1556
- return `${body}
1557
- \u2192 Docs: ${DOCS_TROUBLESHOOTING}`;
1558
- }
1559
-
1560
- export {
1561
- render,
1562
- runCli,
1563
- HELP
1564
- };