busa-cli 0.9.3
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/README.md +10 -0
- package/bin/busabase-cli.mjs +46 -0
- package/dist/chunk-C5BGRNMU.js +1342 -0
- package/dist/cli.js +7 -0
- package/dist/index.js +20 -0
- package/package.json +40 -0
|
@@ -0,0 +1,1342 @@
|
|
|
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
|
+
cloudContract,
|
|
128
|
+
createBusabaseClient,
|
|
129
|
+
DEFAULT_BASE_URL as DEFAULT_BASE_URL2,
|
|
130
|
+
normalizeBaseUrl as normalizeBaseUrl2
|
|
131
|
+
} from "busabase-sdk";
|
|
132
|
+
import {
|
|
133
|
+
Command,
|
|
134
|
+
CommanderError,
|
|
135
|
+
InvalidArgumentError,
|
|
136
|
+
Option
|
|
137
|
+
} from "commander";
|
|
138
|
+
|
|
139
|
+
// src/banner.ts
|
|
140
|
+
var useColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
141
|
+
var paint = (code, s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
142
|
+
var c = {
|
|
143
|
+
brand: (s) => paint("38;5;43", s),
|
|
144
|
+
// teal — Busabase accent
|
|
145
|
+
bold: (s) => paint("1", s),
|
|
146
|
+
dim: (s) => paint("2", s),
|
|
147
|
+
cyan: (s) => paint("36", s)
|
|
148
|
+
};
|
|
149
|
+
var LOGO = [
|
|
150
|
+
" ____ _ ",
|
|
151
|
+
" | __ ) _ _ ___ __ _| |__ __ _ ___ ___ ",
|
|
152
|
+
" | _ \\| | | / __|/ _` | '_ \\ / _` / __|/ _ \\ ",
|
|
153
|
+
" | |_) | |_| \\__ \\ (_| | |_) | (_| \\__ \\ __/ ",
|
|
154
|
+
" |____/ \\__,_|___/\\__,_|_.__/ \\__,_|___/\\___| "
|
|
155
|
+
];
|
|
156
|
+
function banner(baseUrl) {
|
|
157
|
+
const out = [""];
|
|
158
|
+
for (const line of LOGO) out.push(c.brand(line));
|
|
159
|
+
out.push(`${c.dim(" OpenAPI client for Busabase")} ${c.dim("\xB7")} ${c.dim("talks to /api/v1")}`);
|
|
160
|
+
out.push(` ${c.bold("Server")} ${c.cyan(baseUrl)}`);
|
|
161
|
+
out.push("");
|
|
162
|
+
return out.join("\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/config-file.ts
|
|
166
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
167
|
+
import { homedir } from "os";
|
|
168
|
+
import { join } from "path";
|
|
169
|
+
var dotEnvPath = () => join(homedir(), ".busabase", ".env");
|
|
170
|
+
function loadDotEnvFile() {
|
|
171
|
+
let text;
|
|
172
|
+
try {
|
|
173
|
+
text = readFileSync(dotEnvPath(), "utf8");
|
|
174
|
+
} catch {
|
|
175
|
+
return {};
|
|
176
|
+
}
|
|
177
|
+
const out = {};
|
|
178
|
+
for (const line of text.split("\n")) {
|
|
179
|
+
const trimmed = line.trim();
|
|
180
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
181
|
+
const eq = trimmed.indexOf("=");
|
|
182
|
+
if (eq === -1) continue;
|
|
183
|
+
const key = trimmed.slice(0, eq).trim();
|
|
184
|
+
const value = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
|
|
185
|
+
if (key) out[key] = value;
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
function writeDotEnvFile(updates) {
|
|
190
|
+
const merged = { ...loadDotEnvFile() };
|
|
191
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
192
|
+
if (value === null) delete merged[key];
|
|
193
|
+
else merged[key] = value;
|
|
194
|
+
}
|
|
195
|
+
const path = dotEnvPath();
|
|
196
|
+
mkdirSync(join(homedir(), ".busabase"), { recursive: true });
|
|
197
|
+
const body = Object.entries(merged).map(([key, value]) => `${key}=${value}`).join("\n");
|
|
198
|
+
writeFileSync(path, body ? `${body}
|
|
199
|
+
` : "", "utf8");
|
|
200
|
+
try {
|
|
201
|
+
chmodSync(path, 384);
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/login.ts
|
|
207
|
+
import { spawn } from "child_process";
|
|
208
|
+
import { createHash, randomBytes } from "crypto";
|
|
209
|
+
import { createServer } from "http";
|
|
210
|
+
import { createInterface } from "readline/promises";
|
|
211
|
+
import { DEFAULT_BASE_URL, normalizeBaseUrl } from "busabase-sdk";
|
|
212
|
+
var CLI_CLIENT_ID = "busabase-cli";
|
|
213
|
+
var CLI_CLIENT_PLATFORM = "cli";
|
|
214
|
+
var OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
215
|
+
var EXPIRES_AT_KEY = "BUSABASE_TOKEN_EXPIRES_AT";
|
|
216
|
+
var SESSION_TOKEN_PREFIX = "bss_";
|
|
217
|
+
var AUTO_REFRESH_THRESHOLD_MS = 2 * 24 * 60 * 60 * 1e3;
|
|
218
|
+
var DEFAULT_LOCAL_URL = "http://localhost:15419";
|
|
219
|
+
var base64url = (buffer) => buffer.toString("base64url");
|
|
220
|
+
var isInteractive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
221
|
+
var say = (message) => {
|
|
222
|
+
process.stderr.write(`${message}
|
|
223
|
+
`);
|
|
224
|
+
};
|
|
225
|
+
var isLocalHost = (baseUrl) => {
|
|
226
|
+
try {
|
|
227
|
+
const { hostname } = new URL(baseUrl);
|
|
228
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
229
|
+
} catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
async function probeAuthRequired(baseUrl) {
|
|
234
|
+
let res;
|
|
235
|
+
try {
|
|
236
|
+
res = await fetch(`${baseUrl}/api/v1/bases`);
|
|
237
|
+
} catch {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`Could not reach ${baseUrl}. Is the server running? Start a local one with \`busabase server\`, or pass a reachable --base-url.`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return res.status === 401 || res.status === 403;
|
|
243
|
+
}
|
|
244
|
+
async function ask(question) {
|
|
245
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
246
|
+
try {
|
|
247
|
+
return (await rl.question(question)).trim();
|
|
248
|
+
} finally {
|
|
249
|
+
rl.close();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function openBrowser(url) {
|
|
253
|
+
const platform = process.platform;
|
|
254
|
+
const [command, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
255
|
+
try {
|
|
256
|
+
spawn(command, args, { stdio: "ignore", detached: true }).unref();
|
|
257
|
+
} catch {
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function verifyAuth(baseUrl, token) {
|
|
261
|
+
const res = await fetch(`${baseUrl}/api/v1/auth`, {
|
|
262
|
+
headers: { authorization: `Bearer ${token}` }
|
|
263
|
+
});
|
|
264
|
+
if (res.status === 401) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`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.`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (!res.ok) {
|
|
270
|
+
throw new Error(`Could not verify the credential (HTTP ${res.status}) from ${baseUrl}.`);
|
|
271
|
+
}
|
|
272
|
+
return await res.json();
|
|
273
|
+
}
|
|
274
|
+
async function oauthLogin(baseUrl, useBrowser) {
|
|
275
|
+
const codeVerifier = base64url(randomBytes(32));
|
|
276
|
+
const codeChallenge = base64url(createHash("sha256").update(codeVerifier).digest());
|
|
277
|
+
const state = base64url(randomBytes(16));
|
|
278
|
+
const { code, redirectUri } = await new Promise(
|
|
279
|
+
(resolve, reject) => {
|
|
280
|
+
const server = createServer((req, res) => {
|
|
281
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
282
|
+
if (url.pathname !== "/callback") {
|
|
283
|
+
res.writeHead(404).end("Not found");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const respond = (title, body) => {
|
|
287
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
288
|
+
res.end(
|
|
289
|
+
`<!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>`
|
|
290
|
+
);
|
|
291
|
+
};
|
|
292
|
+
const error = url.searchParams.get("error");
|
|
293
|
+
const returnedState = url.searchParams.get("state");
|
|
294
|
+
const returnedCode = url.searchParams.get("code");
|
|
295
|
+
if (error) {
|
|
296
|
+
respond("Sign-in failed", "You can close this tab and return to the terminal.");
|
|
297
|
+
cleanup();
|
|
298
|
+
reject(new Error(`Authorization failed: ${error}`));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (!returnedCode || returnedState !== state) {
|
|
302
|
+
respond("Sign-in failed", "State mismatch. You can close this tab and try again.");
|
|
303
|
+
cleanup();
|
|
304
|
+
reject(new Error("OAuth state mismatch \u2014 the callback did not match this request."));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
respond("Signed in \u2713", "You can close this tab and return to the terminal.");
|
|
308
|
+
const address = server.address();
|
|
309
|
+
cleanup();
|
|
310
|
+
resolve({ code: returnedCode, redirectUri: `http://127.0.0.1:${address.port}/callback` });
|
|
311
|
+
});
|
|
312
|
+
const timeout = setTimeout(() => {
|
|
313
|
+
cleanup();
|
|
314
|
+
reject(new Error("Timed out waiting for the browser sign-in (5 minutes)."));
|
|
315
|
+
}, OAUTH_TIMEOUT_MS);
|
|
316
|
+
function cleanup() {
|
|
317
|
+
clearTimeout(timeout);
|
|
318
|
+
server.close();
|
|
319
|
+
}
|
|
320
|
+
server.on("error", (err) => {
|
|
321
|
+
cleanup();
|
|
322
|
+
reject(err);
|
|
323
|
+
});
|
|
324
|
+
server.listen(0, "127.0.0.1", () => {
|
|
325
|
+
const { port } = server.address();
|
|
326
|
+
const redirect = `http://127.0.0.1:${port}/callback`;
|
|
327
|
+
const authorizeUrl = new URL(`${baseUrl}/api/oauth/authorize`);
|
|
328
|
+
authorizeUrl.searchParams.set("response_type", "code");
|
|
329
|
+
authorizeUrl.searchParams.set("client_id", CLI_CLIENT_ID);
|
|
330
|
+
authorizeUrl.searchParams.set("client_platform", CLI_CLIENT_PLATFORM);
|
|
331
|
+
authorizeUrl.searchParams.set("code_challenge", codeChallenge);
|
|
332
|
+
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
333
|
+
authorizeUrl.searchParams.set("redirect_uri", redirect);
|
|
334
|
+
authorizeUrl.searchParams.set("state", state);
|
|
335
|
+
const href = authorizeUrl.toString();
|
|
336
|
+
say("");
|
|
337
|
+
say("Open this URL in your browser to sign in:");
|
|
338
|
+
say(` ${href}`);
|
|
339
|
+
say("");
|
|
340
|
+
if (useBrowser) {
|
|
341
|
+
say("Opening your browser\u2026");
|
|
342
|
+
openBrowser(href);
|
|
343
|
+
}
|
|
344
|
+
say("Waiting for you to finish signing in\u2026");
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
);
|
|
348
|
+
const tokenRes = await fetch(`${baseUrl}/api/oauth/token`, {
|
|
349
|
+
method: "POST",
|
|
350
|
+
headers: { "content-type": "application/json" },
|
|
351
|
+
body: JSON.stringify({
|
|
352
|
+
grant_type: "authorization_code",
|
|
353
|
+
client_id: CLI_CLIENT_ID,
|
|
354
|
+
client_platform: CLI_CLIENT_PLATFORM,
|
|
355
|
+
code,
|
|
356
|
+
code_verifier: codeVerifier,
|
|
357
|
+
redirect_uri: redirectUri,
|
|
358
|
+
state
|
|
359
|
+
})
|
|
360
|
+
});
|
|
361
|
+
if (!tokenRes.ok) {
|
|
362
|
+
const text = await tokenRes.text();
|
|
363
|
+
throw new Error(`Token exchange failed (HTTP ${tokenRes.status})${text ? `: ${text}` : ""}`);
|
|
364
|
+
}
|
|
365
|
+
const payload = await tokenRes.json();
|
|
366
|
+
const token = payload.token ?? payload.accessToken;
|
|
367
|
+
if (!token) throw new Error("Token exchange returned no session token.");
|
|
368
|
+
return { token, expiresAt: payload.expiresAt };
|
|
369
|
+
}
|
|
370
|
+
async function refreshToken(baseUrl, token) {
|
|
371
|
+
const res = await fetch(`${baseUrl}/api/oauth/refresh`, {
|
|
372
|
+
method: "POST",
|
|
373
|
+
headers: { authorization: `Bearer ${token}` }
|
|
374
|
+
});
|
|
375
|
+
if (res.status === 401) throw new Error("SESSION_EXPIRED");
|
|
376
|
+
if (!res.ok) throw new Error(`Refresh failed (HTTP ${res.status}) from ${baseUrl}.`);
|
|
377
|
+
const payload = await res.json();
|
|
378
|
+
return { token: payload.token ?? payload.accessToken ?? token, expiresAt: payload.expiresAt };
|
|
379
|
+
}
|
|
380
|
+
async function pickSpaceId(verify, preselected) {
|
|
381
|
+
if (preselected) return preselected;
|
|
382
|
+
const spaces = verify.spaces ?? [];
|
|
383
|
+
if (spaces.length <= 1) return verify.space?.id ?? spaces[0]?.id;
|
|
384
|
+
if (!isInteractive()) {
|
|
385
|
+
const choices = spaces.map((space) => `${space.name} [${space.id}]`).join(", ");
|
|
386
|
+
throw new Error(
|
|
387
|
+
`You belong to ${spaces.length} spaces; pass --space-id <id> to choose one. Available spaces: ${choices}`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
say("");
|
|
391
|
+
say("Which space should this CLI target?");
|
|
392
|
+
spaces.forEach((space, index) => {
|
|
393
|
+
const marker = space.id === verify.space?.id ? " (default)" : "";
|
|
394
|
+
say(` ${index + 1}. ${space.name}${marker} [${space.id}]`);
|
|
395
|
+
});
|
|
396
|
+
const answer = await ask(`Choose 1-${spaces.length} (Enter for default): `);
|
|
397
|
+
if (!answer) return verify.space?.id ?? spaces[0]?.id;
|
|
398
|
+
const choice = Number(answer);
|
|
399
|
+
if (Number.isInteger(choice) && choice >= 1 && choice <= spaces.length) {
|
|
400
|
+
return spaces[choice - 1]?.id;
|
|
401
|
+
}
|
|
402
|
+
throw new Error("Not a valid space choice. Re-run login and choose one of the listed spaces.");
|
|
403
|
+
}
|
|
404
|
+
async function chooseTarget() {
|
|
405
|
+
const cloud = normalizeBaseUrl(DEFAULT_BASE_URL);
|
|
406
|
+
say("Busabase is an approval-first database and knowledge base for AI agents.");
|
|
407
|
+
say("Agents propose changes; humans review and merge what becomes trusted data.");
|
|
408
|
+
say("This login connects the CLI to the Busabase instance you want to use.");
|
|
409
|
+
say("");
|
|
410
|
+
say("How should this CLI connect?");
|
|
411
|
+
say(" 1. Local/Desktop on this computer \u2014 no account, no login");
|
|
412
|
+
say(" Use when you run `busabase server` or the Busabase Desktop app locally.");
|
|
413
|
+
say(" 2. Busabase Cloud \u2014 browser sign-in (recommended)");
|
|
414
|
+
say(" Best for humans: opens the browser and saves a refreshable CLI session.");
|
|
415
|
+
say(" 3. Busabase Cloud \u2014 paste an API key");
|
|
416
|
+
say(" Best for CI, servers, or agents where a browser is not available.");
|
|
417
|
+
say(" 4. Self-hosted Busabase \u2014 browser sign-in");
|
|
418
|
+
say(" Use your team's Busabase URL when it supports OAuth login.");
|
|
419
|
+
say(" 5. Self-hosted Busabase \u2014 paste an API key");
|
|
420
|
+
say(" Use your team's Busabase URL with a long-lived key for automation.");
|
|
421
|
+
const choice = await ask("Choose 1-5 [2]: ");
|
|
422
|
+
const askSelfHostedUrl = async () => {
|
|
423
|
+
const url = await ask("Self-hosted base URL (e.g. https://busabase.example.com): ");
|
|
424
|
+
if (!url) throw new Error("A self-hosted base URL is required.");
|
|
425
|
+
return url;
|
|
426
|
+
};
|
|
427
|
+
switch (choice) {
|
|
428
|
+
case "1": {
|
|
429
|
+
const url = await ask(`Local server URL [${DEFAULT_LOCAL_URL}]: `) || DEFAULT_LOCAL_URL;
|
|
430
|
+
return { baseUrl: url, method: "none" };
|
|
431
|
+
}
|
|
432
|
+
case "3":
|
|
433
|
+
return { baseUrl: cloud, method: "api-key" };
|
|
434
|
+
case "4":
|
|
435
|
+
return { baseUrl: await askSelfHostedUrl(), method: "oauth" };
|
|
436
|
+
case "5":
|
|
437
|
+
return { baseUrl: await askSelfHostedUrl(), method: "api-key" };
|
|
438
|
+
default:
|
|
439
|
+
return { baseUrl: cloud, method: "oauth" };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async function runLogin(options) {
|
|
443
|
+
let baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
444
|
+
let apiKey = options.apiKey;
|
|
445
|
+
let method;
|
|
446
|
+
if (apiKey) {
|
|
447
|
+
method = "api-key";
|
|
448
|
+
} else if (options.oauth) {
|
|
449
|
+
method = "oauth";
|
|
450
|
+
} else if (isInteractive()) {
|
|
451
|
+
const target = await chooseTarget();
|
|
452
|
+
baseUrl = normalizeBaseUrl(target.baseUrl);
|
|
453
|
+
method = target.method;
|
|
454
|
+
} else {
|
|
455
|
+
method = isLocalHost(baseUrl) ? "none" : "oauth";
|
|
456
|
+
}
|
|
457
|
+
if (method === "none") {
|
|
458
|
+
if (await probeAuthRequired(baseUrl)) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
`${baseUrl} requires sign-in. Re-run \`busabase-cli login\` and pick a Cloud or self-hosted option (OAuth or API key).`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
writeDotEnvFile({
|
|
464
|
+
BUSABASE_BASE_URL: baseUrl,
|
|
465
|
+
BUSABASE_API_KEY: null,
|
|
466
|
+
BUSABASE_SPACE_ID: null,
|
|
467
|
+
[EXPIRES_AT_KEY]: null
|
|
468
|
+
});
|
|
469
|
+
say(`\u2713 Connected to ${baseUrl} \u2014 open server, no login needed.`);
|
|
470
|
+
say(` Saved to ${dotEnvPath()}. Try: busabase-cli bases list`);
|
|
471
|
+
return { status: "connected (no auth)", baseUrl, config: dotEnvPath() };
|
|
472
|
+
}
|
|
473
|
+
let token;
|
|
474
|
+
let expiresAt;
|
|
475
|
+
if (method === "oauth") {
|
|
476
|
+
const result = await oauthLogin(baseUrl, options.browser);
|
|
477
|
+
token = result.token;
|
|
478
|
+
expiresAt = result.expiresAt;
|
|
479
|
+
} else {
|
|
480
|
+
if (!apiKey) {
|
|
481
|
+
say("Create a key in the dashboard \u2192 Settings \u2192 API Keys (shown once).");
|
|
482
|
+
apiKey = await ask("Paste your API key (sk_\u2026): ");
|
|
483
|
+
}
|
|
484
|
+
if (!apiKey) throw new Error("No API key provided.");
|
|
485
|
+
token = apiKey;
|
|
486
|
+
}
|
|
487
|
+
say("Verifying\u2026");
|
|
488
|
+
const verify = await verifyAuth(baseUrl, token);
|
|
489
|
+
const spaceId = await pickSpaceId(verify, options.spaceId);
|
|
490
|
+
writeDotEnvFile({
|
|
491
|
+
BUSABASE_BASE_URL: baseUrl,
|
|
492
|
+
BUSABASE_API_KEY: token,
|
|
493
|
+
BUSABASE_SPACE_ID: spaceId ?? null,
|
|
494
|
+
// Clear any stale expiry when switching to an API key.
|
|
495
|
+
[EXPIRES_AT_KEY]: expiresAt ?? null
|
|
496
|
+
});
|
|
497
|
+
say("");
|
|
498
|
+
say(`\u2713 Signed in and saved to ${dotEnvPath()}`);
|
|
499
|
+
return {
|
|
500
|
+
status: "signed in",
|
|
501
|
+
method,
|
|
502
|
+
user: verify.user?.email ?? verify.user?.name ?? verify.user?.id ?? "(unknown)",
|
|
503
|
+
space: spaceId ?? "(server default)",
|
|
504
|
+
expiresAt: expiresAt ?? "(no expiry \u2014 API key)",
|
|
505
|
+
baseUrl,
|
|
506
|
+
config: dotEnvPath()
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
async function runRefresh(options) {
|
|
510
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
511
|
+
const token = options.apiKey;
|
|
512
|
+
if (!token) throw new Error("Not signed in \u2014 run `busabase-cli login` first.");
|
|
513
|
+
if (!token.startsWith(SESSION_TOKEN_PREFIX)) {
|
|
514
|
+
return {
|
|
515
|
+
status: "nothing to refresh",
|
|
516
|
+
detail: "This credential is an API key, which doesn't expire. Only OAuth sessions refresh."
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
let refreshed;
|
|
520
|
+
try {
|
|
521
|
+
refreshed = await refreshToken(baseUrl, token);
|
|
522
|
+
} catch (error) {
|
|
523
|
+
if (error instanceof Error && error.message === "SESSION_EXPIRED") {
|
|
524
|
+
throw new Error(
|
|
525
|
+
"Your login session has expired \u2014 run `busabase-cli login` to sign in again."
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
throw error;
|
|
529
|
+
}
|
|
530
|
+
writeDotEnvFile({
|
|
531
|
+
BUSABASE_API_KEY: refreshed.token,
|
|
532
|
+
[EXPIRES_AT_KEY]: refreshed.expiresAt ?? null
|
|
533
|
+
});
|
|
534
|
+
say(`\u2713 Session refreshed${refreshed.expiresAt ? ` \u2014 valid until ${refreshed.expiresAt}` : ""}`);
|
|
535
|
+
return {
|
|
536
|
+
status: "refreshed",
|
|
537
|
+
expiresAt: refreshed.expiresAt ?? "(unknown)",
|
|
538
|
+
config: dotEnvPath()
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
async function maybeAutoRefresh(baseUrl, apiKey) {
|
|
542
|
+
if (!apiKey?.startsWith(SESSION_TOKEN_PREFIX)) return;
|
|
543
|
+
const file = loadDotEnvFile();
|
|
544
|
+
if (file.BUSABASE_API_KEY !== apiKey) return;
|
|
545
|
+
const expiresRaw = file[EXPIRES_AT_KEY];
|
|
546
|
+
if (!expiresRaw) return;
|
|
547
|
+
const expiresAt = Date.parse(expiresRaw);
|
|
548
|
+
if (Number.isNaN(expiresAt)) return;
|
|
549
|
+
if (expiresAt - Date.now() > AUTO_REFRESH_THRESHOLD_MS) return;
|
|
550
|
+
try {
|
|
551
|
+
const refreshed = await refreshToken(normalizeBaseUrl(baseUrl), apiKey);
|
|
552
|
+
writeDotEnvFile({
|
|
553
|
+
BUSABASE_API_KEY: refreshed.token,
|
|
554
|
+
[EXPIRES_AT_KEY]: refreshed.expiresAt ?? null
|
|
555
|
+
});
|
|
556
|
+
} catch {
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
async function runLogout(options) {
|
|
560
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
561
|
+
const token = options.apiKey;
|
|
562
|
+
let revoked = "no session to revoke";
|
|
563
|
+
if (token?.startsWith("bss_")) {
|
|
564
|
+
try {
|
|
565
|
+
const res = await fetch(`${baseUrl}/api/oauth/revoke`, {
|
|
566
|
+
method: "POST",
|
|
567
|
+
headers: { authorization: `Bearer ${token}` }
|
|
568
|
+
});
|
|
569
|
+
revoked = res.ok ? "session revoked" : `revoke returned HTTP ${res.status}`;
|
|
570
|
+
} catch {
|
|
571
|
+
revoked = "revoke request failed (cleared locally anyway)";
|
|
572
|
+
}
|
|
573
|
+
} else if (token) {
|
|
574
|
+
revoked = "API key cleared locally (revoke it in the dashboard to disable it)";
|
|
575
|
+
}
|
|
576
|
+
writeDotEnvFile({ BUSABASE_API_KEY: null, BUSABASE_SPACE_ID: null, [EXPIRES_AT_KEY]: null });
|
|
577
|
+
say(`\u2713 Cleared the saved credential from ${dotEnvPath()}`);
|
|
578
|
+
return { status: "signed out", detail: revoked, config: dotEnvPath() };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// src/run.ts
|
|
582
|
+
var DOCS_TROUBLESHOOTING = "https://busabase.com/docs/troubleshooting";
|
|
583
|
+
function resolveConfig(opts) {
|
|
584
|
+
const file = loadDotEnvFile();
|
|
585
|
+
return {
|
|
586
|
+
baseUrl: opts.baseUrl ?? process.env.BUSABASE_BASE_URL ?? file.BUSABASE_BASE_URL ?? DEFAULT_BASE_URL2,
|
|
587
|
+
apiKey: opts.apiKey ?? process.env.BUSABASE_API_KEY ?? file.BUSABASE_API_KEY,
|
|
588
|
+
spaceId: opts.spaceId ?? process.env.BUSABASE_SPACE_ID ?? file.BUSABASE_SPACE_ID,
|
|
589
|
+
output: opts.output ?? "text"
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
var GLOBAL_LONG_FLAGS = /* @__PURE__ */ new Set(["--base-url", "--api-key", "--space-id", "--output"]);
|
|
593
|
+
function addGlobalFlags(cmd) {
|
|
594
|
+
return cmd.option(
|
|
595
|
+
"--base-url <url>",
|
|
596
|
+
`server base URL (env BUSABASE_BASE_URL, default ${DEFAULT_BASE_URL2})`
|
|
597
|
+
).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(
|
|
598
|
+
new Option("--output <fmt>", "text | table | json (default text)").choices([
|
|
599
|
+
"text",
|
|
600
|
+
"table",
|
|
601
|
+
"json"
|
|
602
|
+
])
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
var parseNum = (value) => {
|
|
606
|
+
const parsed = Number(value);
|
|
607
|
+
if (!Number.isFinite(parsed)) throw new InvalidArgumentError("expected a number");
|
|
608
|
+
return parsed;
|
|
609
|
+
};
|
|
610
|
+
var parseLimit = (value) => {
|
|
611
|
+
const parsed = parseNum(value);
|
|
612
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {
|
|
613
|
+
throw new InvalidArgumentError("expected an integer from 1 to 100");
|
|
614
|
+
}
|
|
615
|
+
return parsed;
|
|
616
|
+
};
|
|
617
|
+
var parsePositiveInt = (value) => {
|
|
618
|
+
const parsed = parseNum(value);
|
|
619
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
620
|
+
throw new InvalidArgumentError("expected a positive integer");
|
|
621
|
+
}
|
|
622
|
+
return parsed;
|
|
623
|
+
};
|
|
624
|
+
var parseBoolean = (value) => {
|
|
625
|
+
if (value === "true") return true;
|
|
626
|
+
if (value === "false") return false;
|
|
627
|
+
throw new InvalidArgumentError("expected true or false");
|
|
628
|
+
};
|
|
629
|
+
var collectValues = (value, previous = []) => [...previous, value];
|
|
630
|
+
var MIME_BY_EXT = {
|
|
631
|
+
".gif": "image/gif",
|
|
632
|
+
".jpeg": "image/jpeg",
|
|
633
|
+
".jpg": "image/jpeg",
|
|
634
|
+
".md": "text/markdown",
|
|
635
|
+
".pdf": "application/pdf",
|
|
636
|
+
".png": "image/png",
|
|
637
|
+
".svg": "image/svg+xml",
|
|
638
|
+
".txt": "text/plain",
|
|
639
|
+
".webp": "image/webp"
|
|
640
|
+
};
|
|
641
|
+
var guessMimeType = (filePath) => MIME_BY_EXT[extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
642
|
+
function createAttachmentOptions(opts) {
|
|
643
|
+
const maxFiles = opts.maxFiles;
|
|
644
|
+
const maxFileSize = opts.maxFileSize;
|
|
645
|
+
const allowedMimeTypes = opts.allowedMime;
|
|
646
|
+
if (!maxFiles && !maxFileSize && (!allowedMimeTypes || allowedMimeTypes.length === 0)) {
|
|
647
|
+
return void 0;
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
attachment: {
|
|
651
|
+
...maxFiles ? { maxFiles } : {},
|
|
652
|
+
...maxFileSize ? { maxFileSize } : {},
|
|
653
|
+
...allowedMimeTypes && allowedMimeTypes.length > 0 ? { allowedMimeTypes } : {}
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
function mergeFieldOptions(existing, attachmentOptions) {
|
|
658
|
+
if (!attachmentOptions) return existing;
|
|
659
|
+
if (!existing || typeof existing !== "object" || Array.isArray(existing))
|
|
660
|
+
return attachmentOptions;
|
|
661
|
+
return { ...existing, ...attachmentOptions };
|
|
662
|
+
}
|
|
663
|
+
function parseFieldSpecs(specs) {
|
|
664
|
+
return specs.map((spec) => {
|
|
665
|
+
const [slug, name, type] = spec.split(":");
|
|
666
|
+
return {
|
|
667
|
+
slug,
|
|
668
|
+
name: name ?? slug,
|
|
669
|
+
...type ? { type } : {}
|
|
670
|
+
};
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
function parseFieldDefinitions(opts) {
|
|
674
|
+
const specs = opts.field ?? [];
|
|
675
|
+
const fieldsJson = opts.fieldsJson;
|
|
676
|
+
if (specs.length > 0 && fieldsJson) {
|
|
677
|
+
throw new Error("Pass either --field or --fields-json, not both.");
|
|
678
|
+
}
|
|
679
|
+
if (fieldsJson) {
|
|
680
|
+
const parsed = parseJsonValue(fieldsJson, "fields-json");
|
|
681
|
+
if (!Array.isArray(parsed)) {
|
|
682
|
+
throw new Error(
|
|
683
|
+
'--fields-json must be a JSON array of field definitions. Example: [{"slug":"status","name":"Status","type":"select","options":{"choices":[{"id":"live","name":"Live"}]}}]'
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
return parsed;
|
|
687
|
+
}
|
|
688
|
+
if (specs.length === 0) {
|
|
689
|
+
throw new Error("Pass at least one --field or provide --fields-json @fields.json.");
|
|
690
|
+
}
|
|
691
|
+
return parseFieldSpecs(specs);
|
|
692
|
+
}
|
|
693
|
+
function parseFileNodeMetadata(opts) {
|
|
694
|
+
const assetId = opts.assetId;
|
|
695
|
+
if (!assetId) {
|
|
696
|
+
throw new Error("--asset-id is required with --type file.");
|
|
697
|
+
}
|
|
698
|
+
return { assetId };
|
|
699
|
+
}
|
|
700
|
+
function parseJsonValue(raw, flagName) {
|
|
701
|
+
const text = raw.startsWith("@") ? readFileSync2(raw.slice(1), "utf8") : raw;
|
|
702
|
+
try {
|
|
703
|
+
return JSON.parse(text);
|
|
704
|
+
} catch (error) {
|
|
705
|
+
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.`;
|
|
706
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
707
|
+
throw new Error(`${hint}
|
|
708
|
+
JSON parse error: ${reason}`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
async function rawFetch(config, method, path, body) {
|
|
712
|
+
const url = `${normalizeBaseUrl2(config.baseUrl)}${path}`;
|
|
713
|
+
const res = await fetch(url, {
|
|
714
|
+
method: method.toUpperCase(),
|
|
715
|
+
headers: {
|
|
716
|
+
...body !== void 0 ? { "content-type": "application/json" } : {},
|
|
717
|
+
...config.apiKey ? { authorization: `Bearer ${config.apiKey}` } : {},
|
|
718
|
+
...config.spaceId ? { "x-busabase-space": config.spaceId } : {}
|
|
719
|
+
},
|
|
720
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
721
|
+
});
|
|
722
|
+
const text = await res.text();
|
|
723
|
+
if (!res.ok) {
|
|
724
|
+
throw new Error(`HTTP ${res.status} ${res.statusText}: ${text}`);
|
|
725
|
+
}
|
|
726
|
+
if (!text) return null;
|
|
727
|
+
try {
|
|
728
|
+
return JSON.parse(text);
|
|
729
|
+
} catch {
|
|
730
|
+
return text;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
async function rawRequest(config, method, path, body) {
|
|
734
|
+
return rawFetch(config, method, path, body);
|
|
735
|
+
}
|
|
736
|
+
async function uploadAsset(config, opts) {
|
|
737
|
+
const filePath = opts.file;
|
|
738
|
+
const file = readFileSync2(filePath);
|
|
739
|
+
const fileName = opts.fileName ?? basename(filePath);
|
|
740
|
+
const mimeType = opts.mimeType ?? guessMimeType(filePath);
|
|
741
|
+
const context = opts.context ?? "record-field";
|
|
742
|
+
const contentHash = `sha256:${createHash2("sha256").update(file).digest("hex")}`;
|
|
743
|
+
const requested = await rawRequest(config, "POST", "/api/v1/assets/upload-urls", {
|
|
744
|
+
fileName,
|
|
745
|
+
mimeType,
|
|
746
|
+
sizeBytes: file.byteLength,
|
|
747
|
+
context,
|
|
748
|
+
contentHash
|
|
749
|
+
});
|
|
750
|
+
if (requested.duplicate) {
|
|
751
|
+
return {
|
|
752
|
+
id: requested.assetId ?? requested.attachmentId,
|
|
753
|
+
assetId: requested.assetId,
|
|
754
|
+
attachmentId: requested.attachmentId,
|
|
755
|
+
url: requested.publicUrl,
|
|
756
|
+
fileName,
|
|
757
|
+
mimeType,
|
|
758
|
+
size: file.byteLength
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
if (requested.uploadUrl.startsWith("/")) {
|
|
762
|
+
throw new Error(
|
|
763
|
+
`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.`
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
const uploadResponse = await fetch(requested.uploadUrl, {
|
|
767
|
+
body: new Uint8Array(file),
|
|
768
|
+
headers: { "content-type": mimeType },
|
|
769
|
+
method: "PUT"
|
|
770
|
+
});
|
|
771
|
+
if (!uploadResponse.ok) {
|
|
772
|
+
const text = await uploadResponse.text();
|
|
773
|
+
throw new Error(
|
|
774
|
+
`Asset byte upload failed (${uploadResponse.status} ${uploadResponse.statusText})${text ? `: ${text}` : ""}`
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
const confirmed = await rawRequest(config, "POST", "/api/v1/assets/confirmations", {
|
|
778
|
+
storageKey: requested.storageKey,
|
|
779
|
+
fileName,
|
|
780
|
+
mimeType,
|
|
781
|
+
sizeBytes: file.byteLength,
|
|
782
|
+
context,
|
|
783
|
+
contentHash
|
|
784
|
+
});
|
|
785
|
+
return {
|
|
786
|
+
id: confirmed.assetId ?? confirmed.attachmentId,
|
|
787
|
+
assetId: confirmed.assetId,
|
|
788
|
+
attachmentId: confirmed.attachmentId,
|
|
789
|
+
url: confirmed.publicUrl,
|
|
790
|
+
fileName,
|
|
791
|
+
mimeType,
|
|
792
|
+
size: file.byteLength
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
function runAction(state, handler) {
|
|
796
|
+
return async (_opts, cmd) => {
|
|
797
|
+
const opts = cmd.optsWithGlobals();
|
|
798
|
+
const config = resolveConfig(opts);
|
|
799
|
+
state.config = config;
|
|
800
|
+
await maybeAutoRefresh(config.baseUrl, config.apiKey);
|
|
801
|
+
const client = createBusabaseClient(config);
|
|
802
|
+
const result = await handler(client, opts, config);
|
|
803
|
+
console.log(render(result, config.output));
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
function pkgVersion() {
|
|
807
|
+
try {
|
|
808
|
+
const pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
|
|
809
|
+
return pkg.version ?? "0.0.0";
|
|
810
|
+
} catch {
|
|
811
|
+
return "0.0.0";
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
var kebab = (value) => value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
815
|
+
var isProcedure = (node) => typeof node === "object" && node !== null && typeof node["~orpc"]?.route?.method === "string";
|
|
816
|
+
function unwrapSchema(schema) {
|
|
817
|
+
let inner = schema;
|
|
818
|
+
let optional = false;
|
|
819
|
+
for (let i = 0; i < 6 && inner?.def; i++) {
|
|
820
|
+
const type = inner.def.type;
|
|
821
|
+
if (type === "optional" || type === "default") {
|
|
822
|
+
optional = true;
|
|
823
|
+
inner = inner.def.innerType;
|
|
824
|
+
} else if (type === "nullable") {
|
|
825
|
+
inner = inner.def.innerType;
|
|
826
|
+
} else break;
|
|
827
|
+
}
|
|
828
|
+
return { inner, optional };
|
|
829
|
+
}
|
|
830
|
+
function classifyKind(inner) {
|
|
831
|
+
const type = inner?.def?.type;
|
|
832
|
+
if (type === "string") return { kind: "string" };
|
|
833
|
+
if (type === "number") return { kind: "number" };
|
|
834
|
+
if (type === "boolean") return { kind: "boolean" };
|
|
835
|
+
if (type === "literal") return { kind: "string" };
|
|
836
|
+
if (type === "enum") {
|
|
837
|
+
const choices = Array.isArray(inner.options) ? inner.options : Object.values(inner.def?.entries ?? {});
|
|
838
|
+
return choices.length ? { kind: "enum", choices } : { kind: "string" };
|
|
839
|
+
}
|
|
840
|
+
return { kind: "json" };
|
|
841
|
+
}
|
|
842
|
+
function inputFields(inputSchema) {
|
|
843
|
+
const { inner } = unwrapSchema(inputSchema);
|
|
844
|
+
const shape = inner?.shape ?? (inner?.def?.type === "object" ? inner.def.shape : void 0);
|
|
845
|
+
if (!shape || typeof shape !== "object") return [];
|
|
846
|
+
const fields = [];
|
|
847
|
+
for (const [key, sub] of Object.entries(shape)) {
|
|
848
|
+
const { inner: unwrapped, optional } = unwrapSchema(sub);
|
|
849
|
+
const { kind, choices } = classifyKind(unwrapped);
|
|
850
|
+
fields.push({ key, kind, required: !optional, choices });
|
|
851
|
+
}
|
|
852
|
+
return fields;
|
|
853
|
+
}
|
|
854
|
+
var GENERATED_SKIP = /* @__PURE__ */ new Set([
|
|
855
|
+
"search",
|
|
856
|
+
// top-level `search`
|
|
857
|
+
"auth.verify",
|
|
858
|
+
// `whoami`
|
|
859
|
+
"records.search",
|
|
860
|
+
// `records by-field-text`
|
|
861
|
+
"records.listChangeRequests"
|
|
862
|
+
// `records change-requests`
|
|
863
|
+
]);
|
|
864
|
+
function registerGeneratedCommands(program, state) {
|
|
865
|
+
const getGroup = (displayName, tag) => program.commands.find((c2) => c2.name() === displayName) ?? program.command(displayName).description(tag ?? `${displayName} endpoints`);
|
|
866
|
+
const addLeaf = (parent, navPath, procKey, proc) => {
|
|
867
|
+
const { route, inputSchema } = proc["~orpc"];
|
|
868
|
+
const name = kebab(procKey);
|
|
869
|
+
if (parent.commands.some((c2) => c2.name() === name)) return;
|
|
870
|
+
const fields = inputFields(inputSchema);
|
|
871
|
+
const leaf = parent.command(name).description(route.summary ?? `${route.method} ${route.path}`);
|
|
872
|
+
for (const f of fields) {
|
|
873
|
+
const flag = `--${kebab(f.key)}`;
|
|
874
|
+
if (GLOBAL_LONG_FLAGS.has(flag)) continue;
|
|
875
|
+
if (f.kind === "boolean") {
|
|
876
|
+
leaf.option(flag, `${f.key} (boolean)`);
|
|
877
|
+
} else if (f.kind === "json") {
|
|
878
|
+
const jsonFlag = `${flag}-json <json|@file>`;
|
|
879
|
+
const desc = `${f.key} as JSON${f.required ? "" : " (optional)"}`;
|
|
880
|
+
if (f.required) leaf.requiredOption(jsonFlag, desc);
|
|
881
|
+
else leaf.option(jsonFlag, desc);
|
|
882
|
+
} else if (f.kind === "enum") {
|
|
883
|
+
const opt = new Option(`${flag} <value>`, f.key).choices(f.choices ?? []);
|
|
884
|
+
if (f.required) opt.makeOptionMandatory();
|
|
885
|
+
leaf.addOption(opt);
|
|
886
|
+
} else if (f.kind === "number") {
|
|
887
|
+
const label = `${flag} <value>`;
|
|
888
|
+
const desc = `${f.key}${f.required ? "" : " (optional)"}`;
|
|
889
|
+
if (f.required) leaf.requiredOption(label, desc, parseNum);
|
|
890
|
+
else leaf.option(label, desc, parseNum);
|
|
891
|
+
} else {
|
|
892
|
+
const label = `${flag} <value>`;
|
|
893
|
+
const desc = `${f.key}${f.required ? "" : " (optional)"}`;
|
|
894
|
+
if (f.required) leaf.requiredOption(label, desc);
|
|
895
|
+
else leaf.option(label, desc);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
addGlobalFlags(leaf);
|
|
899
|
+
leaf.addHelpText("after", `
|
|
900
|
+
OpenAPI: ${route.method} ${route.path}`);
|
|
901
|
+
leaf.action(
|
|
902
|
+
runAction(state, (client, opts) => {
|
|
903
|
+
const input = {};
|
|
904
|
+
for (const f of fields) {
|
|
905
|
+
if (GLOBAL_LONG_FLAGS.has(`--${kebab(f.key)}`)) continue;
|
|
906
|
+
const optKey = f.kind === "json" ? `${f.key}Json` : f.key;
|
|
907
|
+
const value = opts[optKey];
|
|
908
|
+
if (value === void 0) continue;
|
|
909
|
+
input[f.key] = f.kind === "json" ? parseJsonValue(value, `${kebab(f.key)}-json`) : value;
|
|
910
|
+
}
|
|
911
|
+
let target = client;
|
|
912
|
+
for (const segment of navPath) target = target[segment];
|
|
913
|
+
return fields.length > 0 ? target[procKey](input) : target[procKey]();
|
|
914
|
+
})
|
|
915
|
+
);
|
|
916
|
+
};
|
|
917
|
+
const walk = (node, navPath) => {
|
|
918
|
+
for (const key of Object.keys(node)) {
|
|
919
|
+
if (key === "~orpc") continue;
|
|
920
|
+
const child = node[key];
|
|
921
|
+
const id = [...navPath, key].join(".");
|
|
922
|
+
if (isProcedure(child)) {
|
|
923
|
+
if (GENERATED_SKIP.has(id)) continue;
|
|
924
|
+
if (navPath.length > 1) continue;
|
|
925
|
+
const parent = navPath.length ? getGroup(kebab(navPath[0]), child["~orpc"].route.tags?.[0]) : program;
|
|
926
|
+
addLeaf(parent, navPath, key, child);
|
|
927
|
+
} else if (child && typeof child === "object") {
|
|
928
|
+
walk(child, [...navPath, key]);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
walk(cloudContract, []);
|
|
933
|
+
}
|
|
934
|
+
function flagSignature(cmd) {
|
|
935
|
+
const parts = [];
|
|
936
|
+
for (const option of cmd.options) {
|
|
937
|
+
if (option.name() === "help" || GLOBAL_LONG_FLAGS.has(option.long ?? "")) continue;
|
|
938
|
+
parts.push(option.mandatory ? option.flags : `[${option.flags}]`);
|
|
939
|
+
}
|
|
940
|
+
return parts.join(" ");
|
|
941
|
+
}
|
|
942
|
+
var isHelpHidden = (cmd) => Boolean(cmd._noHelp);
|
|
943
|
+
function commandsSection(program) {
|
|
944
|
+
const lines = ["Commands:"];
|
|
945
|
+
const entry = (prefix, cmd) => {
|
|
946
|
+
const sig = [prefix, flagSignature(cmd)].filter(Boolean).join(" ");
|
|
947
|
+
const desc = cmd.description();
|
|
948
|
+
if (!desc) return ` ${sig}`;
|
|
949
|
+
if (sig.length <= 40) return ` ${sig.padEnd(42)}${desc}`;
|
|
950
|
+
return ` ${sig}
|
|
951
|
+
${" ".repeat(44)}${desc}`;
|
|
952
|
+
};
|
|
953
|
+
for (const cmd of program.commands) {
|
|
954
|
+
if (cmd.name() === "help" || isHelpHidden(cmd)) continue;
|
|
955
|
+
const leaves = cmd.commands.filter((leaf) => leaf.name() !== "help" && !isHelpHidden(leaf));
|
|
956
|
+
if (leaves.length > 0) {
|
|
957
|
+
lines.push("");
|
|
958
|
+
for (const leaf of leaves) lines.push(entry(`${cmd.name()} ${leaf.name()}`, leaf));
|
|
959
|
+
} else {
|
|
960
|
+
lines.push(entry(cmd.name(), cmd));
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return lines.join("\n");
|
|
964
|
+
}
|
|
965
|
+
var HELP_FOOTER = `
|
|
966
|
+
Config is read from flags, then env vars, then ~/.busabase/.env (auto-loaded \u2014 no
|
|
967
|
+
need to source it). An exported env var overrides the file.
|
|
968
|
+
|
|
969
|
+
Docs: https://busabase.com/docs \xB7 Troubleshooting: ${DOCS_TROUBLESHOOTING}`;
|
|
970
|
+
function buildProgram(state = {}) {
|
|
971
|
+
const program = new Command("busabase-cli");
|
|
972
|
+
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));
|
|
973
|
+
addGlobalFlags(program);
|
|
974
|
+
addGlobalFlags(program.command("health")).description("Server health check (GET /api/health)").action(runAction(state, (_client, _opts, config) => rawFetch(config, "GET", "/api/health")));
|
|
975
|
+
addGlobalFlags(program.command("openapi")).description("Fetch the OpenAPI document").action(
|
|
976
|
+
runAction(state, (_client, _opts, config) => rawFetch(config, "GET", "/api/v1/openapi.json"))
|
|
977
|
+
);
|
|
978
|
+
addGlobalFlags(program.command("whoami")).description("Active space, user, and membership").action(runAction(state, (client) => client.auth.verify()));
|
|
979
|
+
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(
|
|
980
|
+
"after",
|
|
981
|
+
`
|
|
982
|
+
Busabase is an approval-first database and knowledge base for AI agents: agents
|
|
983
|
+
propose changes, humans review and merge what becomes trusted data.
|
|
984
|
+
|
|
985
|
+
Run interactively, login explains the connection choices and writes the selected
|
|
986
|
+
Busabase instance to ~/.busabase/.env:
|
|
987
|
+
1. Local/Desktop on this computer \u2014 no account, no login
|
|
988
|
+
2. Busabase Cloud \u2014 browser sign-in (recommended)
|
|
989
|
+
3. Busabase Cloud \u2014 paste an API key
|
|
990
|
+
4. Self-hosted Busabase \u2014 browser sign-in
|
|
991
|
+
5. Self-hosted Busabase \u2014 paste an API key
|
|
992
|
+
The flags below skip the menu (handy for scripts / CI):
|
|
993
|
+
|
|
994
|
+
busabase-cli login # pick from the menu
|
|
995
|
+
busabase-cli login --oauth # Cloud browser sign-in
|
|
996
|
+
busabase-cli login --api-key sk_\u2026 # Cloud API key (headless/CI)
|
|
997
|
+
busabase-cli login --base-url http://localhost:15419 # connect to a local server (no auth)
|
|
998
|
+
busabase-cli login --refresh # extend the current session (auto-runs too)`
|
|
999
|
+
).action(async (_opts, cmd) => {
|
|
1000
|
+
const opts = cmd.optsWithGlobals();
|
|
1001
|
+
const config = resolveConfig(opts);
|
|
1002
|
+
state.config = config;
|
|
1003
|
+
const summary = opts.refresh ? await runRefresh({ baseUrl: config.baseUrl, apiKey: config.apiKey }) : await runLogin({
|
|
1004
|
+
baseUrl: config.baseUrl,
|
|
1005
|
+
apiKey: opts.apiKey,
|
|
1006
|
+
spaceId: config.spaceId,
|
|
1007
|
+
oauth: Boolean(opts.oauth),
|
|
1008
|
+
browser: opts.browser !== false
|
|
1009
|
+
});
|
|
1010
|
+
console.log(render(summary, config.output));
|
|
1011
|
+
});
|
|
1012
|
+
addGlobalFlags(program.command("logout")).description("Revoke the saved OAuth session (if any) and clear ~/.busabase/.env").action(async (_opts, cmd) => {
|
|
1013
|
+
const opts = cmd.optsWithGlobals();
|
|
1014
|
+
const config = resolveConfig(opts);
|
|
1015
|
+
state.config = config;
|
|
1016
|
+
const summary = await runLogout({ baseUrl: config.baseUrl, apiKey: config.apiKey });
|
|
1017
|
+
console.log(render(summary, config.output));
|
|
1018
|
+
});
|
|
1019
|
+
const nodes = program.command("nodes").description("Workspace node tree");
|
|
1020
|
+
addGlobalFlags(nodes.command("list")).description("Workspace node tree").action(runAction(state, (client) => client.nodes.list()));
|
|
1021
|
+
addGlobalFlags(nodes.command("create-change-request")).description("Propose a new node via a Change Request").addOption(
|
|
1022
|
+
new Option("--type <folder|base|skill|drive|file|doc>", "node type").choices(["folder", "base", "skill", "drive", "file", "doc"]).makeOptionMandatory()
|
|
1023
|
+
).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(
|
|
1024
|
+
"after",
|
|
1025
|
+
`
|
|
1026
|
+
Examples:
|
|
1027
|
+
busabase-cli nodes create-change-request --type folder --slug cms --name "\u5185\u5BB9\u7BA1\u7406 CMS"
|
|
1028
|
+
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
|
|
1029
|
+
busabase-cli nodes create-change-request --type base --slug products --name "\u4EA7\u54C1\u76EE\u5F55 Products" --fields-json @fields.json
|
|
1030
|
+
busabase-cli nodes create-change-request --type file --slug board-plan --name "Board Plan" --asset-id ast_123`
|
|
1031
|
+
).action(
|
|
1032
|
+
runAction(state, (client, opts) => {
|
|
1033
|
+
const nodeType = opts.type;
|
|
1034
|
+
const name = opts.name;
|
|
1035
|
+
if (nodeType !== "base" && (opts.field || opts.fieldsJson)) {
|
|
1036
|
+
throw new Error("--field and --fields-json are only valid with --type base.");
|
|
1037
|
+
}
|
|
1038
|
+
if (nodeType !== "file" && opts.assetId) {
|
|
1039
|
+
throw new Error("--asset-id is only valid with --type file.");
|
|
1040
|
+
}
|
|
1041
|
+
return client.nodes.createChangeRequest({
|
|
1042
|
+
message: opts.message ?? `Create ${nodeType} ${name}`,
|
|
1043
|
+
submittedBy: opts.submittedBy,
|
|
1044
|
+
operations: [
|
|
1045
|
+
{
|
|
1046
|
+
kind: "create",
|
|
1047
|
+
nodeType,
|
|
1048
|
+
slug: opts.slug,
|
|
1049
|
+
name,
|
|
1050
|
+
description: opts.description,
|
|
1051
|
+
parentNodeId: opts.parentNodeId,
|
|
1052
|
+
...nodeType === "base" ? { fields: parseFieldDefinitions(opts) } : {},
|
|
1053
|
+
...nodeType === "file" ? { metadata: parseFileNodeMetadata(opts) } : {}
|
|
1054
|
+
}
|
|
1055
|
+
]
|
|
1056
|
+
});
|
|
1057
|
+
})
|
|
1058
|
+
);
|
|
1059
|
+
const bases = program.command("bases").description("Bases (structured tables)");
|
|
1060
|
+
addGlobalFlags(bases.command("list")).description("List Bases in the active space").action(runAction(state, (client) => client.bases.list()));
|
|
1061
|
+
addGlobalFlags(bases.command("get")).description("Get one Base by slug").requiredOption("--slug <slug>", "Base slug").action(
|
|
1062
|
+
runAction(state, async (client, opts) => {
|
|
1063
|
+
const slug = opts.slug;
|
|
1064
|
+
const found = (await client.bases.list()).find((base) => base.slug === slug);
|
|
1065
|
+
if (!found) throw new Error(`no Base with slug "${slug}"`);
|
|
1066
|
+
return found;
|
|
1067
|
+
})
|
|
1068
|
+
);
|
|
1069
|
+
addGlobalFlags(bases.command("create")).description("Create a Base").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").addHelpText(
|
|
1070
|
+
"after",
|
|
1071
|
+
`
|
|
1072
|
+
Examples:
|
|
1073
|
+
busabase-cli bases create --slug products --name "\u4EA7\u54C1\u76EE\u5F55 Products" --field product_name:"Product Name":text
|
|
1074
|
+
busabase-cli bases create --slug products --name "\u4EA7\u54C1\u76EE\u5F55 Products" --fields-json @fields.json`
|
|
1075
|
+
).action(
|
|
1076
|
+
runAction(
|
|
1077
|
+
state,
|
|
1078
|
+
(client, opts) => client.bases.create({
|
|
1079
|
+
slug: opts.slug,
|
|
1080
|
+
name: opts.name,
|
|
1081
|
+
description: opts.description,
|
|
1082
|
+
parentNodeId: opts.parentNodeId,
|
|
1083
|
+
fields: parseFieldDefinitions(opts)
|
|
1084
|
+
})
|
|
1085
|
+
)
|
|
1086
|
+
);
|
|
1087
|
+
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(
|
|
1088
|
+
"--allowed-mime <mime>",
|
|
1089
|
+
"attachment option: allowed MIME type, repeatable",
|
|
1090
|
+
collectValues
|
|
1091
|
+
).action(
|
|
1092
|
+
runAction(
|
|
1093
|
+
state,
|
|
1094
|
+
(client, opts) => client.bases.createField({
|
|
1095
|
+
baseId: opts.baseId,
|
|
1096
|
+
slug: opts.slug,
|
|
1097
|
+
name: opts.name,
|
|
1098
|
+
...opts.fieldType ? { type: opts.fieldType } : {},
|
|
1099
|
+
required: Boolean(opts.required),
|
|
1100
|
+
...createAttachmentOptions(opts) ? { options: createAttachmentOptions(opts) } : {}
|
|
1101
|
+
})
|
|
1102
|
+
)
|
|
1103
|
+
);
|
|
1104
|
+
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(
|
|
1105
|
+
"--allowed-mime <mime>",
|
|
1106
|
+
"attachment option: allowed MIME type, repeatable",
|
|
1107
|
+
collectValues
|
|
1108
|
+
).option("--message <text>", "reviewer-facing Change Request message").option("--submitted-by <name>", "producer label").addHelpText(
|
|
1109
|
+
"after",
|
|
1110
|
+
`
|
|
1111
|
+
Examples:
|
|
1112
|
+
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
|
|
1113
|
+
busabase-cli bases update-field-change-request --base-id bse_123 --field-id bsf_123 --options-json @field-options.json`
|
|
1114
|
+
).action(
|
|
1115
|
+
runAction(state, (client, opts) => {
|
|
1116
|
+
const patch = {};
|
|
1117
|
+
if (opts.name) patch.name = opts.name;
|
|
1118
|
+
if (opts.required !== void 0) patch.required = opts.required;
|
|
1119
|
+
if (opts.optionsJson) {
|
|
1120
|
+
patch.options = parseJsonValue(opts.optionsJson, "options-json");
|
|
1121
|
+
}
|
|
1122
|
+
const attachmentOptions = createAttachmentOptions(opts);
|
|
1123
|
+
patch.options = mergeFieldOptions(patch.options, attachmentOptions);
|
|
1124
|
+
if (Object.keys(patch).length === 0) {
|
|
1125
|
+
throw new Error(
|
|
1126
|
+
"No field patch supplied. Pass --name, --required, --options-json, or attachment option flags."
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
return client.bases.updateFieldChangeRequest({
|
|
1130
|
+
baseId: opts.baseId,
|
|
1131
|
+
fieldId: opts.fieldId,
|
|
1132
|
+
patch,
|
|
1133
|
+
message: opts.message,
|
|
1134
|
+
submittedBy: opts.submittedBy
|
|
1135
|
+
});
|
|
1136
|
+
})
|
|
1137
|
+
);
|
|
1138
|
+
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(
|
|
1139
|
+
"after",
|
|
1140
|
+
`
|
|
1141
|
+
Examples:
|
|
1142
|
+
busabase-cli bases create-change-request --base-id bse_123 --fields-json '{"title":"Hello","status":"draft"}'
|
|
1143
|
+
busabase-cli bases create-change-request --base-id bse_123 --fields-json @record.json`
|
|
1144
|
+
).action(
|
|
1145
|
+
runAction(
|
|
1146
|
+
state,
|
|
1147
|
+
(client, opts) => client.bases.createChangeRequest({
|
|
1148
|
+
baseId: opts.baseId,
|
|
1149
|
+
fields: parseJsonValue(opts.fieldsJson, "fields-json"),
|
|
1150
|
+
message: opts.message,
|
|
1151
|
+
submittedBy: opts.submittedBy
|
|
1152
|
+
})
|
|
1153
|
+
)
|
|
1154
|
+
);
|
|
1155
|
+
const records = program.command("records").description("Records");
|
|
1156
|
+
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(
|
|
1157
|
+
runAction(state, (_client, opts, config) => {
|
|
1158
|
+
const query = new URLSearchParams();
|
|
1159
|
+
if (opts.limit !== void 0) query.set("limit", String(opts.limit));
|
|
1160
|
+
if (opts.baseId) query.set("baseId", opts.baseId);
|
|
1161
|
+
if (opts.cursor) query.set("cursor", opts.cursor);
|
|
1162
|
+
const qs = query.toString();
|
|
1163
|
+
return rawRequest(config, "GET", `/api/v1/records/paged${qs ? `?${qs}` : ""}`);
|
|
1164
|
+
})
|
|
1165
|
+
);
|
|
1166
|
+
addGlobalFlags(records.command("get")).description("Get one record").requiredOption("--record-id <id>", "record id").action(
|
|
1167
|
+
runAction(state, (client, opts) => client.records.get({ recordId: opts.recordId }))
|
|
1168
|
+
);
|
|
1169
|
+
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(
|
|
1170
|
+
runAction(
|
|
1171
|
+
state,
|
|
1172
|
+
(client, opts) => client.records.search({
|
|
1173
|
+
fieldSlug: opts.fieldSlug,
|
|
1174
|
+
valueText: opts.valueText,
|
|
1175
|
+
baseId: opts.baseId,
|
|
1176
|
+
limit: opts.limit
|
|
1177
|
+
})
|
|
1178
|
+
)
|
|
1179
|
+
);
|
|
1180
|
+
addGlobalFlags(records.command("change-requests")).description("Change Requests for a record").requiredOption("--record-id <id>", "record id").action(
|
|
1181
|
+
runAction(
|
|
1182
|
+
state,
|
|
1183
|
+
(client, opts) => client.records.listChangeRequests({ recordId: opts.recordId })
|
|
1184
|
+
)
|
|
1185
|
+
);
|
|
1186
|
+
const changeRequests = program.command("change-requests").description("Change Requests");
|
|
1187
|
+
addGlobalFlags(changeRequests.command("list")).description("List Change Requests").option("--limit <n>", "max results", parseNum).action(
|
|
1188
|
+
runAction(
|
|
1189
|
+
state,
|
|
1190
|
+
(client, opts) => client.changeRequests.list({ limit: opts.limit })
|
|
1191
|
+
)
|
|
1192
|
+
);
|
|
1193
|
+
addGlobalFlags(changeRequests.command("get")).description("Get a Change Request").requiredOption("--change-request-id <id>", "Change Request id").action(
|
|
1194
|
+
runAction(
|
|
1195
|
+
state,
|
|
1196
|
+
(client, opts) => client.changeRequests.get({ changeRequestId: opts.changeRequestId })
|
|
1197
|
+
)
|
|
1198
|
+
);
|
|
1199
|
+
addGlobalFlags(changeRequests.command("review")).description("Review a Change Request (rejected = request changes, not terminal)").requiredOption("--change-request-id <id>", "Change Request id").addOption(
|
|
1200
|
+
new Option("--verdict <approved|rejected>", "review verdict").choices(["approved", "rejected"]).makeOptionMandatory()
|
|
1201
|
+
).option("--reason <text>", "review reason").action(
|
|
1202
|
+
runAction(
|
|
1203
|
+
state,
|
|
1204
|
+
(client, opts) => client.changeRequests.review({
|
|
1205
|
+
changeRequestId: opts.changeRequestId,
|
|
1206
|
+
verdict: opts.verdict,
|
|
1207
|
+
reason: opts.reason
|
|
1208
|
+
})
|
|
1209
|
+
)
|
|
1210
|
+
);
|
|
1211
|
+
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(
|
|
1212
|
+
runAction(
|
|
1213
|
+
state,
|
|
1214
|
+
(client, opts) => client.changeRequests.close({
|
|
1215
|
+
changeRequestId: opts.changeRequestId,
|
|
1216
|
+
reason: opts.reason
|
|
1217
|
+
})
|
|
1218
|
+
)
|
|
1219
|
+
);
|
|
1220
|
+
addGlobalFlags(changeRequests.command("merge")).description("Merge a Change Request into its Base").requiredOption("--change-request-id <id>", "Change Request id").action(
|
|
1221
|
+
runAction(
|
|
1222
|
+
state,
|
|
1223
|
+
(client, opts) => client.changeRequests.merge({ changeRequestId: opts.changeRequestId })
|
|
1224
|
+
)
|
|
1225
|
+
);
|
|
1226
|
+
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).action(
|
|
1227
|
+
runAction(
|
|
1228
|
+
state,
|
|
1229
|
+
(client, opts) => client.search({
|
|
1230
|
+
query: opts.query,
|
|
1231
|
+
limit: opts.limit,
|
|
1232
|
+
offset: opts.offset
|
|
1233
|
+
})
|
|
1234
|
+
)
|
|
1235
|
+
);
|
|
1236
|
+
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(
|
|
1237
|
+
"after",
|
|
1238
|
+
`
|
|
1239
|
+
Example:
|
|
1240
|
+
busabase-cli assets upload --file ./cover.png --output json
|
|
1241
|
+
|
|
1242
|
+
Use the JSON output directly in an attachment field value, e.g. {"cover_image":[<output>]}.`
|
|
1243
|
+
).action(runAction(state, (_client, opts, config) => uploadAsset(config, opts)));
|
|
1244
|
+
addUploadCommand(program.command("assets").description("Assets"));
|
|
1245
|
+
addGlobalFlags(program.command("api")).description("Raw request to any /api/v1 endpoint").addOption(
|
|
1246
|
+
new Option("--method <get|post|put|delete>", "HTTP method").choices(["get", "post", "put", "delete"]).makeOptionMandatory()
|
|
1247
|
+
).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(
|
|
1248
|
+
runAction(state, (_client, opts, config) => {
|
|
1249
|
+
const query = new URLSearchParams();
|
|
1250
|
+
for (const pair of opts.query ?? []) {
|
|
1251
|
+
const [key, ...rest] = pair.split("=");
|
|
1252
|
+
query.set(key, rest.join("="));
|
|
1253
|
+
}
|
|
1254
|
+
const qs = query.toString();
|
|
1255
|
+
const path = `/api/v1${opts.path}${qs ? `?${qs}` : ""}`;
|
|
1256
|
+
const bodyJson = opts.bodyJson;
|
|
1257
|
+
return rawFetch(
|
|
1258
|
+
config,
|
|
1259
|
+
opts.method,
|
|
1260
|
+
path,
|
|
1261
|
+
bodyJson ? parseJsonValue(bodyJson, "body-json") : void 0
|
|
1262
|
+
);
|
|
1263
|
+
})
|
|
1264
|
+
);
|
|
1265
|
+
registerGeneratedCommands(program, state);
|
|
1266
|
+
for (const group of program.commands) {
|
|
1267
|
+
const leaves = group.commands.length > 0 ? group.commands : [group];
|
|
1268
|
+
for (const leaf of leaves) {
|
|
1269
|
+
const sig = flagSignature(leaf);
|
|
1270
|
+
leaf.usage(`${sig ? `${sig} ` : ""}[global flags]`);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
program.addHelpText("after", () => `
|
|
1274
|
+
${commandsSection(program)}
|
|
1275
|
+
${HELP_FOOTER}`);
|
|
1276
|
+
program.configureHelp({ visibleCommands: () => [] });
|
|
1277
|
+
return program;
|
|
1278
|
+
}
|
|
1279
|
+
async function runCli(argv) {
|
|
1280
|
+
const state = {};
|
|
1281
|
+
const program = buildProgram(state);
|
|
1282
|
+
if (argv.length === 0) {
|
|
1283
|
+
program.outputHelp();
|
|
1284
|
+
return 0;
|
|
1285
|
+
}
|
|
1286
|
+
try {
|
|
1287
|
+
await program.parseAsync(argv, { from: "user" });
|
|
1288
|
+
return 0;
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
if (error instanceof CommanderError) {
|
|
1291
|
+
return error.exitCode === 0 ? 0 : 1;
|
|
1292
|
+
}
|
|
1293
|
+
console.error(explainError(error, state.config ?? resolveConfig({})));
|
|
1294
|
+
return 1;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
function renderedHelp() {
|
|
1298
|
+
let out = "";
|
|
1299
|
+
const program = buildProgram();
|
|
1300
|
+
program.configureOutput({
|
|
1301
|
+
writeOut: (str) => {
|
|
1302
|
+
out += str;
|
|
1303
|
+
},
|
|
1304
|
+
writeErr: () => {
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
program.outputHelp();
|
|
1308
|
+
return out;
|
|
1309
|
+
}
|
|
1310
|
+
var HELP = renderedHelp();
|
|
1311
|
+
function explainError(error, config) {
|
|
1312
|
+
const base = config.baseUrl;
|
|
1313
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1314
|
+
const status = error.status;
|
|
1315
|
+
const lower = msg.toLowerCase();
|
|
1316
|
+
let body;
|
|
1317
|
+
if (status === 401 || lower.includes("401") || lower.includes("unauthorized")) {
|
|
1318
|
+
body = [
|
|
1319
|
+
`Unauthorized (401) from ${base}.`,
|
|
1320
|
+
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.",
|
|
1321
|
+
" Meant to hit a local server? Add --base-url http://localhost:15419 (or export BUSABASE_BASE_URL=\u2026)."
|
|
1322
|
+
].join("\n");
|
|
1323
|
+
} else if (lower.includes("fetch failed") || lower.includes("econnrefused") || lower.includes("enotfound") || lower.includes("getaddrinfo") || lower.includes("network")) {
|
|
1324
|
+
body = [
|
|
1325
|
+
`Could not reach ${base}.`,
|
|
1326
|
+
" \u2022 Cloud: check your internet connection and that the URL is correct.",
|
|
1327
|
+
" \u2022 Local: start it with `npx busabase server` (http://localhost:15419), then add --base-url http://localhost:15419 (or export BUSABASE_BASE_URL=\u2026).",
|
|
1328
|
+
` (underlying error: ${msg})`
|
|
1329
|
+
].join("\n");
|
|
1330
|
+
} else {
|
|
1331
|
+
body = `${msg}
|
|
1332
|
+
(base URL: ${base}${config.apiKey ? ", with API key" : ", no API key"})`;
|
|
1333
|
+
}
|
|
1334
|
+
return `${body}
|
|
1335
|
+
\u2192 Docs: ${DOCS_TROUBLESHOOTING}`;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
export {
|
|
1339
|
+
render,
|
|
1340
|
+
runCli,
|
|
1341
|
+
HELP
|
|
1342
|
+
};
|