busa-cli 0.19.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{run-FjzYcGKi.js → run--bA-jLSP.js} +753 -38
- package/package.json +2 -2
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { n as runCli, r as render, t as HELP } from "./run-
|
|
1
|
+
import { n as runCli, r as render, t as HELP } from "./run--bA-jLSP.js";
|
|
2
2
|
import { DEFAULT_BASE_URL, createBusabaseClient, normalizeBaseUrl } from "busabase-sdk";
|
|
3
3
|
export { DEFAULT_BASE_URL, HELP, createBusabaseClient, normalizeBaseUrl, render, runCli };
|
|
@@ -56,7 +56,7 @@ const OPERATIONS$1 = [
|
|
|
56
56
|
* audience, the exact silent-drop failure the server-side rejection exists to
|
|
57
57
|
* end. `params.appliesWhen` still documents which operations accept it.
|
|
58
58
|
*/
|
|
59
|
-
const mergeIntent$
|
|
59
|
+
const mergeIntent$5 = (input) => {
|
|
60
60
|
if (input.requireReview) return { autoMerge: false };
|
|
61
61
|
if (input.autoMerge) return { autoMerge: true };
|
|
62
62
|
return {};
|
|
@@ -246,7 +246,7 @@ const baseFieldChangeTask = {
|
|
|
246
246
|
baseId: input.baseId,
|
|
247
247
|
...input.message ? { message: input.message } : {},
|
|
248
248
|
...input.submittedBy ? { submittedBy: input.submittedBy } : {},
|
|
249
|
-
...mergeIntent$
|
|
249
|
+
...mergeIntent$5(input)
|
|
250
250
|
};
|
|
251
251
|
switch (input.operation) {
|
|
252
252
|
case "add": return client.bases.fieldChangeRequest({
|
|
@@ -629,7 +629,7 @@ const nodeReadFileTask = {
|
|
|
629
629
|
* A batch containing a `delete` is review-first server-side no matter what this
|
|
630
630
|
* sends, so don't imply otherwise in the flag's description.
|
|
631
631
|
*/
|
|
632
|
-
const mergeIntent$
|
|
632
|
+
const mergeIntent$4 = (input) => {
|
|
633
633
|
if (input.requireReview) return { autoMerge: false };
|
|
634
634
|
if (input.autoMerge) return { autoMerge: true };
|
|
635
635
|
return {};
|
|
@@ -699,7 +699,7 @@ const nodeFilesChangeRequestTask = {
|
|
|
699
699
|
operations: input.operations,
|
|
700
700
|
...input.message ? { message: input.message } : {},
|
|
701
701
|
...input.submittedBy ? { submittedBy: input.submittedBy } : {},
|
|
702
|
-
...mergeIntent$
|
|
702
|
+
...mergeIntent$4(input)
|
|
703
703
|
};
|
|
704
704
|
return client.fileTrees.createChangeRequest(payload);
|
|
705
705
|
}
|
|
@@ -1424,7 +1424,7 @@ const rejectMismatchedPayload = (input) => {
|
|
|
1424
1424
|
if (wrong.length > 0) throw new Error(`${wrong.join(", ")} ${wrong.length > 1 ? "are" : "is"} not valid for --type ${type}.`);
|
|
1425
1425
|
};
|
|
1426
1426
|
/** `autoMerge` is tri-state: unset (permission-aware default), forced on, forced off. */
|
|
1427
|
-
const mergeIntent$
|
|
1427
|
+
const mergeIntent$3 = (input) => {
|
|
1428
1428
|
if (input.requireReview) return { autoMerge: false };
|
|
1429
1429
|
if (input.autoMerge) return { autoMerge: true };
|
|
1430
1430
|
return {};
|
|
@@ -1637,7 +1637,7 @@ const nodeCreateTask = {
|
|
|
1637
1637
|
name: input.name,
|
|
1638
1638
|
description: input.description,
|
|
1639
1639
|
parentNodeId: input.parentNodeId,
|
|
1640
|
-
...mergeIntent$
|
|
1640
|
+
...mergeIntent$3(input)
|
|
1641
1641
|
};
|
|
1642
1642
|
if (FILE_TREE_TYPES.has(type)) {
|
|
1643
1643
|
const payload = {
|
|
@@ -1675,7 +1675,7 @@ const nodeCreateTask = {
|
|
|
1675
1675
|
return client.nodes.createChangeRequest({
|
|
1676
1676
|
message: input.message ?? `Create ${type} ${input.name}`,
|
|
1677
1677
|
submittedBy: input.submittedBy,
|
|
1678
|
-
...mergeIntent$
|
|
1678
|
+
...mergeIntent$3(input),
|
|
1679
1679
|
operations: [{
|
|
1680
1680
|
kind: "create",
|
|
1681
1681
|
nodeType: type,
|
|
@@ -1688,6 +1688,74 @@ const nodeCreateTask = {
|
|
|
1688
1688
|
}
|
|
1689
1689
|
};
|
|
1690
1690
|
//#endregion
|
|
1691
|
+
//#region ../../packages/busabase-contract/src/tasks/record-bulk-update.ts
|
|
1692
|
+
const mergeIntent$2 = (input) => {
|
|
1693
|
+
if (input.requireReview) return { autoMerge: false };
|
|
1694
|
+
if (input.autoMerge) return { autoMerge: true };
|
|
1695
|
+
return {};
|
|
1696
|
+
};
|
|
1697
|
+
const recordBulkUpdateTask = {
|
|
1698
|
+
name: "record_bulk_update_change_request",
|
|
1699
|
+
cliPath: ["records", "bulk-update-change-request"],
|
|
1700
|
+
summary: "Propose partial updates to many records in one ChangeRequest",
|
|
1701
|
+
guidance: "All updates must target active records in the same Base. Each recordId may appear once. Each fields object is a partial update: omitted keys stay unchanged and null clears a field. The batch is reviewed and merged atomically. Use baseCommitId per update when the caller must pin the version it read. Review is permission-aware; pass requireReview to force a pending ChangeRequest even when the key has write access.",
|
|
1702
|
+
annotations: {
|
|
1703
|
+
readOnly: false,
|
|
1704
|
+
destructive: false
|
|
1705
|
+
},
|
|
1706
|
+
params: [
|
|
1707
|
+
{
|
|
1708
|
+
name: "baseId",
|
|
1709
|
+
kind: "string",
|
|
1710
|
+
required: true,
|
|
1711
|
+
description: "Base owning every record."
|
|
1712
|
+
},
|
|
1713
|
+
{
|
|
1714
|
+
name: "updates",
|
|
1715
|
+
kind: "json",
|
|
1716
|
+
required: true,
|
|
1717
|
+
description: "JSON array of {recordId, fields, baseCommitId?, message?}, e.g. [{\"recordId\":\"rec_1\",\"fields\":{\"status\":\"published\"}}]."
|
|
1718
|
+
},
|
|
1719
|
+
{
|
|
1720
|
+
name: "message",
|
|
1721
|
+
kind: "string",
|
|
1722
|
+
description: "Reviewer-facing message for the batch."
|
|
1723
|
+
},
|
|
1724
|
+
{
|
|
1725
|
+
name: "submittedBy",
|
|
1726
|
+
kind: "string",
|
|
1727
|
+
description: "Producer label recorded on the change."
|
|
1728
|
+
},
|
|
1729
|
+
{
|
|
1730
|
+
name: "idempotencyKey",
|
|
1731
|
+
kind: "string",
|
|
1732
|
+
description: "Retry key scoped to this Base and submitter."
|
|
1733
|
+
},
|
|
1734
|
+
{
|
|
1735
|
+
name: "autoMerge",
|
|
1736
|
+
kind: "boolean",
|
|
1737
|
+
description: "Apply immediately when the actor has write access."
|
|
1738
|
+
},
|
|
1739
|
+
{
|
|
1740
|
+
name: "requireReview",
|
|
1741
|
+
kind: "boolean",
|
|
1742
|
+
description: "Force one pending ChangeRequest even when the actor has write access."
|
|
1743
|
+
}
|
|
1744
|
+
],
|
|
1745
|
+
examples: ["busabase-cli records bulk-update-change-request --base-id bas_1 --updates-json '[{\"recordId\":\"rec_1\",\"fields\":{\"status\":\"published\"}}]' --require-review", "busabase-cli records bulk-update-change-request --base-id bas_1 --updates-json @updates.json --idempotency-key august-review-v1"],
|
|
1746
|
+
execute: async (client, input) => {
|
|
1747
|
+
if (!Array.isArray(input.updates)) throw new Error("`updates` must be a JSON array.");
|
|
1748
|
+
return client.bases.createBulkUpdateChangeRequest({
|
|
1749
|
+
baseId: input.baseId,
|
|
1750
|
+
updates: input.updates,
|
|
1751
|
+
...input.message ? { message: input.message } : {},
|
|
1752
|
+
...input.submittedBy ? { submittedBy: input.submittedBy } : {},
|
|
1753
|
+
...input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {},
|
|
1754
|
+
...mergeIntent$2(input)
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
//#endregion
|
|
1691
1759
|
//#region ../../packages/busabase-contract/src/tasks/record-change.ts
|
|
1692
1760
|
/**
|
|
1693
1761
|
* Changing an existing record: update, delete (archive), or restore — one task
|
|
@@ -1905,7 +1973,7 @@ const recordQueryTask = {
|
|
|
1905
1973
|
examples: [
|
|
1906
1974
|
"busabase-cli records query --base-id bas_123 --limit 100",
|
|
1907
1975
|
"busabase-cli records count --base-id bas_123",
|
|
1908
|
-
"busabase-cli records count --base-id bas_123 --filters '[{\"fieldSlug\":\"branch\",\"fieldType\":\"text\",\"operator\":\"equals\",\"value\":\"main\"}]'",
|
|
1976
|
+
"busabase-cli records count --base-id bas_123 --filters-json '[{\"fieldSlug\":\"branch\",\"fieldType\":\"text\",\"operator\":\"equals\",\"value\":\"main\"}]'",
|
|
1909
1977
|
"busabase-cli records count --base-id bas_123 --view-id viw_456"
|
|
1910
1978
|
],
|
|
1911
1979
|
execute: async (client, input) => {
|
|
@@ -2120,11 +2188,60 @@ const BUSABASE_TASKS = [
|
|
|
2120
2188
|
changeRequestMergeTask,
|
|
2121
2189
|
baseFieldChangeTask,
|
|
2122
2190
|
recordChangeTask,
|
|
2191
|
+
recordBulkUpdateTask,
|
|
2123
2192
|
viewChangeTask,
|
|
2124
2193
|
nodePermissionTask,
|
|
2125
2194
|
nodeShareTask,
|
|
2126
2195
|
listArchivedTask
|
|
2127
2196
|
];
|
|
2197
|
+
/**
|
|
2198
|
+
* Endpoint-level MCP tools fully covered by a task above, keyed by the tool name
|
|
2199
|
+
* the OpenAPI generator produces. The MCP adapter excludes these so a catalog
|
|
2200
|
+
* carries one way to do each job rather than two.
|
|
2201
|
+
*
|
|
2202
|
+
* Kept as tool names (not contract paths) because that is what the MCP layer
|
|
2203
|
+
* filters on, and it is the name an agent would actually have seen.
|
|
2204
|
+
*
|
|
2205
|
+
* NOT listed on purpose:
|
|
2206
|
+
* - `nodes_create_change_request` — `node_create` covers only a single `create`
|
|
2207
|
+
* operation. The endpoint also does `rename`, and composes several operations
|
|
2208
|
+
* into one change request (`ref`/`parentNodeRef` builds a whole subtree
|
|
2209
|
+
* atomically). Excluding it would be a capability regression, not a cleanup.
|
|
2210
|
+
* - `forms_create` — configures an existing node, so it is a second step after
|
|
2211
|
+
* `node_create --type form`, not a duplicate of it.
|
|
2212
|
+
* - `nodes_purge` — permanent delete is a genuinely different operation from
|
|
2213
|
+
* `node_archive`, and `node_archive`'s guidance now tells an agent when each
|
|
2214
|
+
* one applies.
|
|
2215
|
+
*/
|
|
2216
|
+
const TASK_SUPERSEDED_MCP_TOOLS = [
|
|
2217
|
+
"file_trees_create",
|
|
2218
|
+
"docs_create",
|
|
2219
|
+
"bases_create",
|
|
2220
|
+
"files_create",
|
|
2221
|
+
"records_search",
|
|
2222
|
+
"file_trees_list_files",
|
|
2223
|
+
"file_trees_read_file",
|
|
2224
|
+
"file_trees_create_change_request",
|
|
2225
|
+
"records_list",
|
|
2226
|
+
"records_count",
|
|
2227
|
+
"change_requests_list",
|
|
2228
|
+
"change_requests_counts",
|
|
2229
|
+
"change_requests_review",
|
|
2230
|
+
"change_requests_review_many",
|
|
2231
|
+
"change_requests_merge",
|
|
2232
|
+
"change_requests_merge_many",
|
|
2233
|
+
"bases_field_change_request",
|
|
2234
|
+
"records_change_request",
|
|
2235
|
+
"bases_create_bulk_update_change_request",
|
|
2236
|
+
"views_change_request",
|
|
2237
|
+
"nodes_principals_list",
|
|
2238
|
+
"nodes_principals_add",
|
|
2239
|
+
"nodes_principals_remove",
|
|
2240
|
+
"nodes_share_get",
|
|
2241
|
+
"nodes_share_set",
|
|
2242
|
+
"nodes_share_disable",
|
|
2243
|
+
"bases_list_deleted_fields"
|
|
2244
|
+
];
|
|
2128
2245
|
//#endregion
|
|
2129
2246
|
//#region src/config-file.ts
|
|
2130
2247
|
/**
|
|
@@ -3269,15 +3386,168 @@ function banner(baseUrl) {
|
|
|
3269
3386
|
return out.join("\n");
|
|
3270
3387
|
}
|
|
3271
3388
|
//#endregion
|
|
3389
|
+
//#region src/errors.ts
|
|
3390
|
+
/**
|
|
3391
|
+
* Process exit code per class.
|
|
3392
|
+
*
|
|
3393
|
+
* `1` stays the code for an unclassified error, so a script testing `!= 0` is
|
|
3394
|
+
* unaffected and only a script testing `== 1` sees a change.
|
|
3395
|
+
*/
|
|
3396
|
+
const EXIT_CODES = {
|
|
3397
|
+
ERROR: 1,
|
|
3398
|
+
USAGE: 2,
|
|
3399
|
+
UNAUTHORIZED: 3,
|
|
3400
|
+
FORBIDDEN: 3,
|
|
3401
|
+
NOT_FOUND: 4,
|
|
3402
|
+
VALIDATION: 5,
|
|
3403
|
+
CONFLICT: 6,
|
|
3404
|
+
RATE_LIMITED: 7,
|
|
3405
|
+
SERVER_ERROR: 8,
|
|
3406
|
+
NETWORK: 9,
|
|
3407
|
+
REVIEW_REJECTED: 10,
|
|
3408
|
+
TIMEOUT: 11
|
|
3409
|
+
};
|
|
3410
|
+
const RETRYABLE = /* @__PURE__ */ new Set([
|
|
3411
|
+
"RATE_LIMITED",
|
|
3412
|
+
"SERVER_ERROR",
|
|
3413
|
+
"NETWORK"
|
|
3414
|
+
]);
|
|
3415
|
+
/** `data.issues` as the server sends it, flattened. Shared with `explainError`'s prose rendering. */
|
|
3416
|
+
function toIssues(raw) {
|
|
3417
|
+
if (!Array.isArray(raw) || raw.length === 0) return void 0;
|
|
3418
|
+
const issues = raw.map((issue) => {
|
|
3419
|
+
if (!issue || typeof issue !== "object") return {
|
|
3420
|
+
path: "",
|
|
3421
|
+
message: String(issue)
|
|
3422
|
+
};
|
|
3423
|
+
const { path, message } = issue;
|
|
3424
|
+
return {
|
|
3425
|
+
path: Array.isArray(path) ? path.join(".") : typeof path === "string" ? path : "",
|
|
3426
|
+
message: String(message ?? issue)
|
|
3427
|
+
};
|
|
3428
|
+
});
|
|
3429
|
+
return issues.length > 0 ? issues : void 0;
|
|
3430
|
+
}
|
|
3431
|
+
const codeForStatus = (status) => {
|
|
3432
|
+
if (status === 401) return "UNAUTHORIZED";
|
|
3433
|
+
if (status === 403) return "FORBIDDEN";
|
|
3434
|
+
if (status === 404) return "NOT_FOUND";
|
|
3435
|
+
if (status === 409) return "CONFLICT";
|
|
3436
|
+
if (status === 429) return "RATE_LIMITED";
|
|
3437
|
+
if (status === 400 || status === 422) return "VALIDATION";
|
|
3438
|
+
if (status >= 500) return "SERVER_ERROR";
|
|
3439
|
+
return "ERROR";
|
|
3440
|
+
};
|
|
3441
|
+
/**
|
|
3442
|
+
* A transport failure never reaches the server, so it has no status to read.
|
|
3443
|
+
* Node reports every one of them as a bare `TypeError: fetch failed` with the
|
|
3444
|
+
* real reason on `.cause`, which is why this matches on text rather than type.
|
|
3445
|
+
*/
|
|
3446
|
+
const isNetworkFailure = (message) => /fetch failed|econnrefused|enotfound|getaddrinfo|econnreset|etimedout|socket hang up|network/i.test(message);
|
|
3447
|
+
/**
|
|
3448
|
+
* `rawFetch` (health / openapi / `api` passthrough) throws a plain `Error`
|
|
3449
|
+
* carrying `HTTP <status> <statusText>: …` rather than an `ORPCError`, so the
|
|
3450
|
+
* status has to be read back out of the message for those paths.
|
|
3451
|
+
*/
|
|
3452
|
+
const statusFromMessage = (message) => {
|
|
3453
|
+
const match = /\bHTTP (\d{3})\b/.exec(message);
|
|
3454
|
+
return match ? Number(match[1]) : void 0;
|
|
3455
|
+
};
|
|
3456
|
+
/** Classify any thrown value into the stable shape above. Never throws. */
|
|
3457
|
+
function classifyError(error) {
|
|
3458
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3459
|
+
const outcome = error?.cliCode;
|
|
3460
|
+
if (typeof outcome === "string" && outcome in EXIT_CODES) {
|
|
3461
|
+
const code = outcome;
|
|
3462
|
+
return {
|
|
3463
|
+
code,
|
|
3464
|
+
message,
|
|
3465
|
+
retryable: RETRYABLE.has(code),
|
|
3466
|
+
exitCode: EXIT_CODES[code]
|
|
3467
|
+
};
|
|
3468
|
+
}
|
|
3469
|
+
const issues = toIssues(error?.data?.issues);
|
|
3470
|
+
const status = typeof error?.status === "number" ? error.status : statusFromMessage(message);
|
|
3471
|
+
let code;
|
|
3472
|
+
if (status !== void 0) code = codeForStatus(status);
|
|
3473
|
+
else if (isNetworkFailure(message)) code = "NETWORK";
|
|
3474
|
+
else code = "ERROR";
|
|
3475
|
+
const orpcCode = error?.code;
|
|
3476
|
+
if (code === "ERROR" && typeof orpcCode === "string") {
|
|
3477
|
+
if (orpcCode === "NOT_FOUND") code = "NOT_FOUND";
|
|
3478
|
+
else if (orpcCode === "UNAUTHORIZED") code = "UNAUTHORIZED";
|
|
3479
|
+
else if (orpcCode === "FORBIDDEN") code = "FORBIDDEN";
|
|
3480
|
+
else if (orpcCode === "CONFLICT") code = "CONFLICT";
|
|
3481
|
+
else if (orpcCode === "BAD_REQUEST" || orpcCode === "UNPROCESSABLE_CONTENT") code = "VALIDATION";
|
|
3482
|
+
else if (orpcCode === "TOO_MANY_REQUESTS") code = "RATE_LIMITED";
|
|
3483
|
+
}
|
|
3484
|
+
return {
|
|
3485
|
+
code,
|
|
3486
|
+
...status !== void 0 ? { status } : {},
|
|
3487
|
+
message,
|
|
3488
|
+
...issues ? { issues } : {},
|
|
3489
|
+
retryable: RETRYABLE.has(code),
|
|
3490
|
+
exitCode: EXIT_CODES[code]
|
|
3491
|
+
};
|
|
3492
|
+
}
|
|
3493
|
+
/** The stdout envelope for a failed command under `--output json`. */
|
|
3494
|
+
function errorEnvelope(classified) {
|
|
3495
|
+
return {
|
|
3496
|
+
ok: false,
|
|
3497
|
+
code: classified.code,
|
|
3498
|
+
...classified.status !== void 0 ? { status: classified.status } : {},
|
|
3499
|
+
message: classified.message,
|
|
3500
|
+
...classified.issues ? { issues: classified.issues } : {},
|
|
3501
|
+
retryable: classified.retryable
|
|
3502
|
+
};
|
|
3503
|
+
}
|
|
3504
|
+
/**
|
|
3505
|
+
* A command that ran fine but ended in an outcome the caller must branch on —
|
|
3506
|
+
* a proposal a reviewer turned down, or a `--wait` that ran out of budget.
|
|
3507
|
+
*
|
|
3508
|
+
* Carries its own class so {@link classifyError} does not have to guess from a
|
|
3509
|
+
* message, and so the envelope reports it exactly like any other failure.
|
|
3510
|
+
*/
|
|
3511
|
+
var CliOutcomeError = class extends Error {
|
|
3512
|
+
cliCode;
|
|
3513
|
+
constructor(cliCode, message) {
|
|
3514
|
+
super(message);
|
|
3515
|
+
this.name = "CliOutcomeError";
|
|
3516
|
+
this.cliCode = cliCode;
|
|
3517
|
+
}
|
|
3518
|
+
};
|
|
3519
|
+
//#endregion
|
|
3272
3520
|
//#region src/format.ts
|
|
3273
3521
|
/** Render a command result as JSON, a compact text view, or an aligned table. */
|
|
3274
3522
|
function render(value, output) {
|
|
3275
3523
|
if (output === "json") return JSON.stringify(value, null, 2);
|
|
3276
3524
|
if (output === "text") return renderText(value);
|
|
3277
3525
|
if (Array.isArray(value)) return renderTable(value);
|
|
3526
|
+
const envelope = asEnvelope(value);
|
|
3527
|
+
if (envelope) return renderEnvelope(envelope, renderTable);
|
|
3278
3528
|
if (value && typeof value === "object") return renderRecord(value);
|
|
3279
3529
|
return String(value);
|
|
3280
3530
|
}
|
|
3531
|
+
function asEnvelope(value) {
|
|
3532
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
3533
|
+
const entries = Object.entries(value);
|
|
3534
|
+
const arrays = entries.filter((entry) => Array.isArray(entry[1]));
|
|
3535
|
+
if (arrays.length !== 1) return void 0;
|
|
3536
|
+
const meta = entries.filter(([, item]) => !Array.isArray(item));
|
|
3537
|
+
if (!meta.every(([, item]) => item === null || item === void 0 || typeof item !== "object")) return void 0;
|
|
3538
|
+
return {
|
|
3539
|
+
key: arrays[0][0],
|
|
3540
|
+
rows: arrays[0][1],
|
|
3541
|
+
meta
|
|
3542
|
+
};
|
|
3543
|
+
}
|
|
3544
|
+
function renderEnvelope(envelope, renderRows) {
|
|
3545
|
+
const body = envelope.rows.length === 0 ? `(no ${envelope.key})` : renderRows(envelope.rows);
|
|
3546
|
+
const meta = envelope.meta.filter(([, item]) => item !== null && item !== void 0);
|
|
3547
|
+
if (meta.length === 0) return body;
|
|
3548
|
+
const width = Math.max(...meta.map(([key]) => key.length));
|
|
3549
|
+
return `${body}\n\n${meta.map(([key, item]) => `${key.padEnd(width)} ${cell(item)}`).join("\n")}`;
|
|
3550
|
+
}
|
|
3281
3551
|
function cell(value) {
|
|
3282
3552
|
if (value === null || value === void 0) return "";
|
|
3283
3553
|
if (typeof value === "object") return compactJson(value);
|
|
@@ -3320,6 +3590,8 @@ function renderTable(rows) {
|
|
|
3320
3590
|
].join("\n");
|
|
3321
3591
|
}
|
|
3322
3592
|
function renderText(value) {
|
|
3593
|
+
const envelope = asEnvelope(value);
|
|
3594
|
+
if (envelope) return renderEnvelope(envelope, renderText);
|
|
3323
3595
|
if (Array.isArray(value)) {
|
|
3324
3596
|
if (value.length === 0) return "(no rows)";
|
|
3325
3597
|
if (isNodeTree(value)) return renderNodeTree$1(value);
|
|
@@ -3369,6 +3641,48 @@ function nodeIcon(type) {
|
|
|
3369
3641
|
default: return "[node]";
|
|
3370
3642
|
}
|
|
3371
3643
|
}
|
|
3644
|
+
/**
|
|
3645
|
+
* Drop hydrated parents from a result, keeping the id that points at them.
|
|
3646
|
+
*
|
|
3647
|
+
* A record VO is ~4.5 KB, of which 3.6 KB is a full copy of its parent `base`
|
|
3648
|
+
* — repeated identically on every row, so ten records cost 151 KB and nine
|
|
3649
|
+
* copies of that Base are pure waste to any caller that already knows which
|
|
3650
|
+
* Base it asked about. `createdByUser` and `headCommit` are the same pattern.
|
|
3651
|
+
*
|
|
3652
|
+
* The test for "this is a hydration, not content" is deliberately structural
|
|
3653
|
+
* rather than a list of field names, so it does not rot as VOs change: an
|
|
3654
|
+
* object-valued key is dropped only when a sibling STRING key already
|
|
3655
|
+
* identifies the same thing — `base`/`baseId`, `headCommit`/`headCommitId`,
|
|
3656
|
+
* `createdByUser`/`createdBy`. Nothing that lacks such a sibling is touched,
|
|
3657
|
+
* which is why a Base's `fields`, a node's `children`, and `whoami`'s `space`
|
|
3658
|
+
* all survive: dropping those would remove the answer, not a duplicate.
|
|
3659
|
+
*/
|
|
3660
|
+
function slim(value) {
|
|
3661
|
+
if (Array.isArray(value)) return value.map(slim);
|
|
3662
|
+
if (!value || typeof value !== "object") return value;
|
|
3663
|
+
const row = value;
|
|
3664
|
+
const scalarKeys = Object.entries(row).filter(([, item]) => typeof item === "string").map(([key]) => key);
|
|
3665
|
+
const out = {};
|
|
3666
|
+
for (const [key, item] of Object.entries(row)) {
|
|
3667
|
+
if (isHydrated(key, item, scalarKeys)) continue;
|
|
3668
|
+
out[key] = slim(item);
|
|
3669
|
+
}
|
|
3670
|
+
return out;
|
|
3671
|
+
}
|
|
3672
|
+
/**
|
|
3673
|
+
* `carriesPayload` is the guard that stops this from eating the answer. A
|
|
3674
|
+
* record's `headCommit` is structurally indistinguishable from its `base` — an
|
|
3675
|
+
* object whose `id` is already on the row as `headCommitId` — but it is where
|
|
3676
|
+
* the record's FIELD VALUES live (`headCommit.payload`), so dropping it would
|
|
3677
|
+
* return ten rows of ids and no data. Every commit-shaped object in this
|
|
3678
|
+
* contract carries `payload`; no hydrated parent does.
|
|
3679
|
+
*/
|
|
3680
|
+
const isHydrated = (key, item, scalarKeys) => {
|
|
3681
|
+
if (item === null || typeof item !== "object") return false;
|
|
3682
|
+
if (carriesPayload(item)) return false;
|
|
3683
|
+
return scalarKeys.some((scalar) => scalar !== key && (scalar === `${key}Id` || key.startsWith(scalar)));
|
|
3684
|
+
};
|
|
3685
|
+
const carriesPayload = (item) => Array.isArray(item) ? item.some((entry) => entry !== null && typeof entry === "object" && carriesPayload(entry)) : "payload" in item;
|
|
3372
3686
|
//#endregion
|
|
3373
3687
|
//#region ../../packages/busabase-contract/src/auth/device-authorization.ts
|
|
3374
3688
|
/** Public client identifier shared by the Busabase CLI and Cloud auth server. */
|
|
@@ -4545,8 +4859,7 @@ z.discriminatedUnion("operation", [archiveBaseInputSchema.extend({
|
|
|
4545
4859
|
* when a node carries no `icon`, every host falls back to the type icon, same
|
|
4546
4860
|
* as before this field existed.
|
|
4547
4861
|
*
|
|
4548
|
-
* The `attachment` variant
|
|
4549
|
-
* `apps/buda/src/domains/agent-controller/components/use-logo-crop-upload.tsx`):
|
|
4862
|
+
* The `attachment` variant follows the shared avatar-cropping model:
|
|
4550
4863
|
* `url`/`attachmentId` are the CROPPED display image actually rendered, while
|
|
4551
4864
|
* `originalUrl`/`originalAttachmentId` + `crop` are kept so the crop dialog can
|
|
4552
4865
|
* re-open non-destructively against the untouched source image instead of
|
|
@@ -8495,6 +8808,151 @@ const readDirectoryFiles = async (dir) => {
|
|
|
8495
8808
|
return files;
|
|
8496
8809
|
};
|
|
8497
8810
|
//#endregion
|
|
8811
|
+
//#region src/paginate.ts
|
|
8812
|
+
/**
|
|
8813
|
+
* `--all`: follow the cursor to the end instead of making the caller loop.
|
|
8814
|
+
*
|
|
8815
|
+
* Every listing in this CLI is bounded — that is the point of the `limit` work —
|
|
8816
|
+
* but "bounded" moves the burden onto the caller, who now has to notice
|
|
8817
|
+
* `nextCursor`, thread it back in, and know when to stop. An agent that misses
|
|
8818
|
+
* that reports a truncated answer as the whole answer, which is the failure the
|
|
8819
|
+
* `record_query` task guidance warns about in prose. This does it in code.
|
|
8820
|
+
*
|
|
8821
|
+
* Rows stream out as NDJSON — one JSON object per line — rather than
|
|
8822
|
+
* accumulating into one array, so memory stays flat and a consumer can start
|
|
8823
|
+
* work on page 1 while page 9 is still in flight. That is why `--all` overrides
|
|
8824
|
+
* `--output`: a 40,000-row table is not a thing anyone wanted.
|
|
8825
|
+
*/
|
|
8826
|
+
/**
|
|
8827
|
+
* Read a page out of whatever the command returned.
|
|
8828
|
+
*
|
|
8829
|
+
* Two protocols exist and both are honoured. Most paginated endpoints return
|
|
8830
|
+
* `{ rows, nextCursor }`. `GET /assets` returns a bare array and pages by the
|
|
8831
|
+
* last row's `id`, with a short page meaning the end — a shape chosen there to
|
|
8832
|
+
* keep the response backwards compatible for the web and mobile clients.
|
|
8833
|
+
*/
|
|
8834
|
+
function readPage(result, limit) {
|
|
8835
|
+
const envelope = asEnvelope(result);
|
|
8836
|
+
if (envelope) {
|
|
8837
|
+
const cursor = envelope.meta.find(([key]) => key === "nextCursor")?.[1];
|
|
8838
|
+
if (cursor === void 0) return void 0;
|
|
8839
|
+
return {
|
|
8840
|
+
rows: envelope.rows,
|
|
8841
|
+
nextCursor: typeof cursor === "string" ? cursor : null
|
|
8842
|
+
};
|
|
8843
|
+
}
|
|
8844
|
+
if (!Array.isArray(result)) return void 0;
|
|
8845
|
+
if (limit === void 0 || result.length < limit) return {
|
|
8846
|
+
rows: result,
|
|
8847
|
+
nextCursor: null
|
|
8848
|
+
};
|
|
8849
|
+
const id = result[result.length - 1]?.id;
|
|
8850
|
+
return {
|
|
8851
|
+
rows: result,
|
|
8852
|
+
nextCursor: typeof id === "string" ? id : null
|
|
8853
|
+
};
|
|
8854
|
+
}
|
|
8855
|
+
/** Stops a broken server or a cursor that never advances from looping forever. */
|
|
8856
|
+
const MAX_PAGES = 1e4;
|
|
8857
|
+
/**
|
|
8858
|
+
* Stream every page as NDJSON. Returns the number of rows written.
|
|
8859
|
+
*
|
|
8860
|
+
* A cap that truncates is always announced on stderr: a silently short answer
|
|
8861
|
+
* that looks complete is the exact failure this is meant to prevent.
|
|
8862
|
+
*/
|
|
8863
|
+
async function paginateAll(options) {
|
|
8864
|
+
const { fetchPage, limit, maxItems, write, notify } = options;
|
|
8865
|
+
let cursor;
|
|
8866
|
+
let written = 0;
|
|
8867
|
+
let seen = 0;
|
|
8868
|
+
for (let pageIndex = 0; pageIndex < MAX_PAGES; pageIndex += 1) {
|
|
8869
|
+
const result = await fetchPage(cursor);
|
|
8870
|
+
const page = readPage(result, limit);
|
|
8871
|
+
if (!page) {
|
|
8872
|
+
write(JSON.stringify(result));
|
|
8873
|
+
return 1;
|
|
8874
|
+
}
|
|
8875
|
+
seen += page.rows.length;
|
|
8876
|
+
for (const row of page.rows) {
|
|
8877
|
+
if (maxItems !== void 0 && written >= maxItems) {
|
|
8878
|
+
notify(`[busabase-cli] --max-items ${maxItems} reached; stopped early with more rows available.`);
|
|
8879
|
+
return written;
|
|
8880
|
+
}
|
|
8881
|
+
write(JSON.stringify(row));
|
|
8882
|
+
written += 1;
|
|
8883
|
+
}
|
|
8884
|
+
if (!page.nextCursor) return written;
|
|
8885
|
+
if (page.nextCursor === cursor) {
|
|
8886
|
+
notify("[busabase-cli] the server returned the same cursor twice; stopping to avoid a loop.");
|
|
8887
|
+
return written;
|
|
8888
|
+
}
|
|
8889
|
+
cursor = page.nextCursor;
|
|
8890
|
+
}
|
|
8891
|
+
notify(`[busabase-cli] stopped after ${MAX_PAGES} pages (${seen} rows scanned).`);
|
|
8892
|
+
return written;
|
|
8893
|
+
}
|
|
8894
|
+
//#endregion
|
|
8895
|
+
//#region src/retry.ts
|
|
8896
|
+
const BASE_DELAY_MS = 500;
|
|
8897
|
+
const MAX_DELAY_MS = 2e4;
|
|
8898
|
+
const READ_METHODS = /* @__PURE__ */ new Set([
|
|
8899
|
+
"GET",
|
|
8900
|
+
"HEAD",
|
|
8901
|
+
"OPTIONS"
|
|
8902
|
+
]);
|
|
8903
|
+
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
8904
|
+
const isNetworkError = (error) => error instanceof TypeError || /fetch failed|econnreset|econnrefused|etimedout|socket hang up/i.test(error instanceof Error ? error.message : String(error));
|
|
8905
|
+
/** `Retry-After` is either delay-seconds or an HTTP-date; both are legal. */
|
|
8906
|
+
function parseRetryAfter(header, now = Date.now()) {
|
|
8907
|
+
if (!header) return void 0;
|
|
8908
|
+
const seconds = Number(header.trim());
|
|
8909
|
+
if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds * 1e3, MAX_DELAY_MS);
|
|
8910
|
+
const at = Date.parse(header);
|
|
8911
|
+
if (Number.isNaN(at)) return void 0;
|
|
8912
|
+
return Math.min(Math.max(at - now, 0), MAX_DELAY_MS);
|
|
8913
|
+
}
|
|
8914
|
+
/** Whether this outcome is worth another attempt for a request using `method`. */
|
|
8915
|
+
function shouldRetry(method, status) {
|
|
8916
|
+
if (status === 429) return true;
|
|
8917
|
+
if (!READ_METHODS.has(method.toUpperCase())) return false;
|
|
8918
|
+
return status === void 0 || status >= 500;
|
|
8919
|
+
}
|
|
8920
|
+
/**
|
|
8921
|
+
* Wrap a fetch so transient failures are retried with exponential backoff.
|
|
8922
|
+
* Returns `base` unchanged when `retries` is 0, so the no-retry path adds no
|
|
8923
|
+
* wrapper and no behaviour.
|
|
8924
|
+
*/
|
|
8925
|
+
function withRetry(base, options) {
|
|
8926
|
+
const { retries } = options;
|
|
8927
|
+
if (retries <= 0) return base;
|
|
8928
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
8929
|
+
const notify = options.notify ?? ((message) => console.error(message));
|
|
8930
|
+
const jitter = options.jitter ?? Math.random;
|
|
8931
|
+
return (async (input, init) => {
|
|
8932
|
+
const method = init?.method ?? (input instanceof Request ? input.method : "GET");
|
|
8933
|
+
for (let attempt = 0;; attempt += 1) {
|
|
8934
|
+
let response;
|
|
8935
|
+
let failure;
|
|
8936
|
+
try {
|
|
8937
|
+
response = await base(input, init);
|
|
8938
|
+
} catch (error) {
|
|
8939
|
+
if (!isNetworkError(error)) throw error;
|
|
8940
|
+
failure = error;
|
|
8941
|
+
}
|
|
8942
|
+
const status = response?.status;
|
|
8943
|
+
const last = attempt >= retries;
|
|
8944
|
+
if (!shouldRetry(method, status) || last) {
|
|
8945
|
+
if (response) return response;
|
|
8946
|
+
throw failure;
|
|
8947
|
+
}
|
|
8948
|
+
const backoff = Math.min(BASE_DELAY_MS * 2 ** attempt, MAX_DELAY_MS);
|
|
8949
|
+
const delay = parseRetryAfter(response?.headers.get("retry-after") ?? null) ?? Math.round(backoff * (.5 + jitter() * .5));
|
|
8950
|
+
notify(`[busabase-cli] ${status ? `HTTP ${status}` : "connection failed"} — retrying in ${Math.round(delay / 1e3)}s (attempt ${attempt + 2} of ${retries + 1})`);
|
|
8951
|
+
await sleep(delay);
|
|
8952
|
+
}
|
|
8953
|
+
});
|
|
8954
|
+
}
|
|
8955
|
+
//#endregion
|
|
8498
8956
|
//#region src/task-command.ts
|
|
8499
8957
|
const collect = (value, previous = []) => [...previous, value];
|
|
8500
8958
|
/** Range-checks before the request goes out, so a cap surfaces as a usage error. */
|
|
@@ -8533,10 +8991,13 @@ const applyParam = (cmd, param) => {
|
|
|
8533
8991
|
case "stringArray":
|
|
8534
8992
|
cmd.option(`${flag} <value>`, `${description} (repeatable)`, collect);
|
|
8535
8993
|
return;
|
|
8536
|
-
case "json":
|
|
8537
|
-
|
|
8994
|
+
case "json": {
|
|
8995
|
+
const jsonOption = `${jsonFlagFor(param)} <json|@file>`;
|
|
8996
|
+
if (param.required) cmd.requiredOption(jsonOption, description);
|
|
8997
|
+
else cmd.option(jsonOption, description);
|
|
8538
8998
|
if (param.cliShorthand) cmd.option(`${param.cliShorthand.flag} ${param.cliShorthand.placeholder}`, param.cliShorthand.description, collect);
|
|
8539
8999
|
return;
|
|
9000
|
+
}
|
|
8540
9001
|
default: if (param.required) cmd.requiredOption(`${flag} <value>`, description);
|
|
8541
9002
|
else cmd.option(`${flag} <value>`, description);
|
|
8542
9003
|
}
|
|
@@ -8586,6 +9047,7 @@ const registerOne = (program, task, deps, path, preset = {}, summaryOverride) =>
|
|
|
8586
9047
|
}
|
|
8587
9048
|
applyParam(cmd, param);
|
|
8588
9049
|
}
|
|
9050
|
+
if (task.guidance) cmd.addHelpText("after", `\n${wrapGuidance(rewriteTaskNames(task.guidance))}`);
|
|
8589
9051
|
if (task.examples && task.examples.length > 0 && Object.keys(preset).length === 0) cmd.addHelpText("after", `\nExamples:\n ${task.examples.join("\n ")}`);
|
|
8590
9052
|
cmd.action(deps.runAction(async (client, opts) => task.execute(client, {
|
|
8591
9053
|
...buildInput(task, opts, deps),
|
|
@@ -8593,11 +9055,108 @@ const registerOne = (program, task, deps, path, preset = {}, summaryOverride) =>
|
|
|
8593
9055
|
})));
|
|
8594
9056
|
};
|
|
8595
9057
|
const registerTaskCommands = (program, tasks, deps) => {
|
|
9058
|
+
for (const task of tasks) cliPathByTaskName.set(task.name, `busabase-cli ${task.cliPath.join(" ")}`);
|
|
8596
9059
|
for (const task of tasks) {
|
|
8597
9060
|
registerOne(program, task, deps, task.cliPath);
|
|
8598
9061
|
for (const variant of task.cliVariants ?? []) registerOne(program, task, deps, variant.path, variant.preset, variant.summary);
|
|
8599
9062
|
}
|
|
8600
9063
|
};
|
|
9064
|
+
/**
|
|
9065
|
+
* Guidance is written once and read by two surfaces, so it names sibling tasks
|
|
9066
|
+
* by their MCP tool name (`record_find_by_field`). That is not something a CLI
|
|
9067
|
+
* caller can type — the command is `records by-field-text` — so the shared text
|
|
9068
|
+
* is rewritten to this surface's vocabulary rather than forked.
|
|
9069
|
+
*/
|
|
9070
|
+
const rewriteTaskNames = (text) => text.replace(/`([a-z][a-z0-9_]*)`/g, (match, name) => {
|
|
9071
|
+
const path = cliPathByTaskName.get(name);
|
|
9072
|
+
return path ? `\`${path}\`` : match;
|
|
9073
|
+
});
|
|
9074
|
+
/** Filled by {@link registerTaskCommands}; empty until the surface is built. */
|
|
9075
|
+
const cliPathByTaskName = /* @__PURE__ */ new Map();
|
|
9076
|
+
/** Reflow guidance to the width commander uses for the rest of the help screen. */
|
|
9077
|
+
const wrapGuidance = (text, width = 78) => {
|
|
9078
|
+
const lines = [];
|
|
9079
|
+
let line = "";
|
|
9080
|
+
for (const word of text.split(/\s+/).filter(Boolean)) if (line && `${line} ${word}`.length > width) {
|
|
9081
|
+
lines.push(line);
|
|
9082
|
+
line = word;
|
|
9083
|
+
} else line = line ? `${line} ${word}` : word;
|
|
9084
|
+
if (line) lines.push(line);
|
|
9085
|
+
return lines.join("\n");
|
|
9086
|
+
};
|
|
9087
|
+
//#endregion
|
|
9088
|
+
//#region src/wait.ts
|
|
9089
|
+
/**
|
|
9090
|
+
* `--wait`: block until a submitted proposal is actually decided.
|
|
9091
|
+
*
|
|
9092
|
+
* Review-first is the product's whole point, so the useful unit of work for a
|
|
9093
|
+
* caller is not "the change request was filed" — it is "the change request was
|
|
9094
|
+
* merged, or it wasn't". Without this every caller writes the same poll loop
|
|
9095
|
+
* against `change-requests get`, and an agent that skips it reports a proposal
|
|
9096
|
+
* nobody has looked at as a completed task.
|
|
9097
|
+
*/
|
|
9098
|
+
/** Statuses that will not change again on their own. */
|
|
9099
|
+
const TERMINAL = /* @__PURE__ */ new Set([
|
|
9100
|
+
"merged",
|
|
9101
|
+
"rejected",
|
|
9102
|
+
"closed",
|
|
9103
|
+
"abandoned"
|
|
9104
|
+
]);
|
|
9105
|
+
const isChangeRequest = (value) => {
|
|
9106
|
+
if (!value || typeof value !== "object") return false;
|
|
9107
|
+
const row = value;
|
|
9108
|
+
return typeof row.id === "string" && row.id.startsWith("crq") && typeof row.status === "string";
|
|
9109
|
+
};
|
|
9110
|
+
/**
|
|
9111
|
+
* Find the change request a command's result is about.
|
|
9112
|
+
*
|
|
9113
|
+
* The shape differs across the surface — some routes return the change request
|
|
9114
|
+
* itself, some wrap it (`{ changeRequest }`), some return the materialized thing
|
|
9115
|
+
* with `materialized: false` alongside — so this looks for the id rather than
|
|
9116
|
+
* asking every command to declare it.
|
|
9117
|
+
*/
|
|
9118
|
+
function findChangeRequestId(result) {
|
|
9119
|
+
if (isChangeRequest(result)) return result.id;
|
|
9120
|
+
if (Array.isArray(result)) {
|
|
9121
|
+
for (const item of result) {
|
|
9122
|
+
const found = findChangeRequestId(item);
|
|
9123
|
+
if (found) return found;
|
|
9124
|
+
}
|
|
9125
|
+
return;
|
|
9126
|
+
}
|
|
9127
|
+
if (!result || typeof result !== "object") return void 0;
|
|
9128
|
+
const row = result;
|
|
9129
|
+
if (typeof row.changeRequestId === "string") return row.changeRequestId;
|
|
9130
|
+
for (const key of ["changeRequest", "changeRequests"]) {
|
|
9131
|
+
const found = findChangeRequestId(row[key]);
|
|
9132
|
+
if (found) return found;
|
|
9133
|
+
}
|
|
9134
|
+
}
|
|
9135
|
+
const DEFAULT_POLL_MS = 3e3;
|
|
9136
|
+
/** Poll until the change request settles or the budget runs out. */
|
|
9137
|
+
async function waitForChangeRequest(options) {
|
|
9138
|
+
const { changeRequestId, read, timeoutMs, pollMs = DEFAULT_POLL_MS, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), now = Date.now, notify = () => {} } = options;
|
|
9139
|
+
const deadline = now() + timeoutMs;
|
|
9140
|
+
let latest;
|
|
9141
|
+
let status = "unknown";
|
|
9142
|
+
for (;;) {
|
|
9143
|
+
latest = await read(changeRequestId);
|
|
9144
|
+
status = isChangeRequest(latest) ? latest.status : "unknown";
|
|
9145
|
+
if (TERMINAL.has(status)) return {
|
|
9146
|
+
kind: "settled",
|
|
9147
|
+
status,
|
|
9148
|
+
changeRequest: latest
|
|
9149
|
+
};
|
|
9150
|
+
const remaining = deadline - now();
|
|
9151
|
+
if (remaining <= 0) return {
|
|
9152
|
+
kind: "timeout",
|
|
9153
|
+
status,
|
|
9154
|
+
changeRequest: latest
|
|
9155
|
+
};
|
|
9156
|
+
notify(`[busabase-cli] ${changeRequestId} is ${status}; waiting for a reviewer (${Math.round(remaining / 1e3)}s left)`);
|
|
9157
|
+
await sleep(Math.min(pollMs, remaining));
|
|
9158
|
+
}
|
|
9159
|
+
}
|
|
8601
9160
|
//#endregion
|
|
8602
9161
|
//#region src/run.ts
|
|
8603
9162
|
/** Public docs page covering every error below, for both Cloud and local. Linked from each error. */
|
|
@@ -8629,9 +9188,12 @@ function resolveConfig(opts) {
|
|
|
8629
9188
|
webUrl: process.env.BUSABASE_WEB_URL ?? file.BUSABASE_WEB_URL ?? baseUrl,
|
|
8630
9189
|
apiKey: opts.apiKey ?? process.env.BUSABASE_API_KEY ?? file.BUSABASE_API_KEY,
|
|
8631
9190
|
spaceId: opts.spaceId ?? process.env.BUSABASE_SPACE_ID ?? file.BUSABASE_SPACE_ID,
|
|
8632
|
-
output: opts.output ?? configuredOutput() ?? "text"
|
|
9191
|
+
output: opts.output ?? configuredOutput() ?? "text",
|
|
9192
|
+
fetch: withRetry(fetch, { retries: opts.retries ?? DEFAULT_RETRIES })
|
|
8633
9193
|
};
|
|
8634
9194
|
}
|
|
9195
|
+
/** Enough to ride out a rate limiter or a proxy blip; not enough to hide a real outage. */
|
|
9196
|
+
const DEFAULT_RETRIES = 2;
|
|
8635
9197
|
/**
|
|
8636
9198
|
* Config flags accepted anywhere on the line (`busabase-cli --output json bases list` and
|
|
8637
9199
|
* `busabase-cli bases list --output json` both work): declared on the root program AND on every
|
|
@@ -8644,14 +9206,20 @@ const GLOBAL_LONG_FLAGS = /* @__PURE__ */ new Set([
|
|
|
8644
9206
|
"--space-id",
|
|
8645
9207
|
"--output",
|
|
8646
9208
|
"--profile",
|
|
8647
|
-
"--config"
|
|
9209
|
+
"--config",
|
|
9210
|
+
"--slim",
|
|
9211
|
+
"--retries",
|
|
9212
|
+
"--all",
|
|
9213
|
+
"--max-items",
|
|
9214
|
+
"--wait",
|
|
9215
|
+
"--wait-timeout"
|
|
8648
9216
|
]);
|
|
8649
9217
|
function addGlobalFlags(cmd) {
|
|
8650
9218
|
return cmd.option("--base-url <url>", `server base URL (env BUSABASE_BASE_URL, default ${DEFAULT_BASE_URL})`).option("--api-key <token>", "bearer token for cloud hosts (env BUSABASE_API_KEY)").option("--space-id <id>", "target Busabase space (env BUSABASE_SPACE_ID)").option("--profile <name>", "account to use for this command (env BUSABASE_PROFILE)").option("--config <path>", "read credentials from this file instead (env BUSABASE_CONFIG)").addOption(new Option("--output <fmt>", "text | table | json (default text)").choices([
|
|
8651
9219
|
"text",
|
|
8652
9220
|
"table",
|
|
8653
9221
|
"json"
|
|
8654
|
-
]));
|
|
9222
|
+
])).option("--slim", "drop hydrated parents (a record's full `base`, `createdByUser`, `headCommit`) and keep their ids").option("--retries <n>", "extra attempts on 429 / 5xx / dropped connections (default 2, 0 disables)", parseNonNegativeInt).option("--all", "follow the cursor to the end and stream every row as NDJSON").option("--max-items <n>", "stop --all after this many rows", parsePositiveInt).option("--wait", "after submitting a Change Request, block until a reviewer merges or rejects it").option("--wait-timeout <seconds>", "budget for --wait (default 300)", parsePositiveInt);
|
|
8655
9223
|
}
|
|
8656
9224
|
const parseNum = (value) => {
|
|
8657
9225
|
const parsed = Number(value);
|
|
@@ -8663,6 +9231,11 @@ const parsePositiveInt = (value) => {
|
|
|
8663
9231
|
if (!Number.isInteger(parsed) || parsed < 1) throw new InvalidArgumentError("expected a positive integer");
|
|
8664
9232
|
return parsed;
|
|
8665
9233
|
};
|
|
9234
|
+
const parseNonNegativeInt = (value) => {
|
|
9235
|
+
const parsed = parseNum(value);
|
|
9236
|
+
if (!Number.isInteger(parsed) || parsed < 0) throw new InvalidArgumentError("expected a non-negative integer");
|
|
9237
|
+
return parsed;
|
|
9238
|
+
};
|
|
8666
9239
|
const parseBoolean = (value) => {
|
|
8667
9240
|
if (value === "true") return true;
|
|
8668
9241
|
if (value === "false") return false;
|
|
@@ -8739,13 +9312,9 @@ function parseJsonValue(raw, flagName) {
|
|
|
8739
9312
|
* better off than when the field was silently dropped.
|
|
8740
9313
|
*/
|
|
8741
9314
|
function formatIssues(issues) {
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
const { path, message } = issue;
|
|
8746
|
-
const pathLabel = Array.isArray(path) && path.length > 0 ? path.join(".") : void 0;
|
|
8747
|
-
return pathLabel ? `${pathLabel}: ${message}` : String(message ?? issue);
|
|
8748
|
-
}).join("; ");
|
|
9315
|
+
const parsed = toIssues(issues);
|
|
9316
|
+
if (!parsed) return void 0;
|
|
9317
|
+
return parsed.map(({ path, message }) => path ? `${path}: ${message}` : message).join("; ");
|
|
8749
9318
|
}
|
|
8750
9319
|
function formatRawErrorBody(status, statusText, text) {
|
|
8751
9320
|
try {
|
|
@@ -8760,7 +9329,7 @@ function formatRawErrorBody(status, statusText, text) {
|
|
|
8760
9329
|
/** Raw fetch for endpoints outside the typed contract (health / openapi / api passthrough). */
|
|
8761
9330
|
async function rawFetch(config, method, path, body) {
|
|
8762
9331
|
const url = `${normalizeBaseUrl(config.baseUrl)}${path}`;
|
|
8763
|
-
const res = await fetch(url, {
|
|
9332
|
+
const res = await (config.fetch ?? fetch)(url, {
|
|
8764
9333
|
method: method.toUpperCase(),
|
|
8765
9334
|
headers: {
|
|
8766
9335
|
...body !== void 0 ? { "content-type": "application/json" } : {},
|
|
@@ -8878,10 +9447,60 @@ function runAction(state, handler) {
|
|
|
8878
9447
|
if (refreshedToken) config.apiKey = refreshedToken;
|
|
8879
9448
|
state.config = config;
|
|
8880
9449
|
assertCredentialNotExpired(config.apiKey);
|
|
8881
|
-
const
|
|
8882
|
-
|
|
9450
|
+
const client = createBusabaseClient(config);
|
|
9451
|
+
if (opts.all) {
|
|
9452
|
+
await streamAllPages(client, opts, config, handler);
|
|
9453
|
+
return;
|
|
9454
|
+
}
|
|
9455
|
+
const result = await handler(client, opts, config);
|
|
9456
|
+
console.log(render(opts.slim ? slim(result) : result, config.output));
|
|
9457
|
+
if (opts.wait) await awaitReview(client, opts, config, result);
|
|
8883
9458
|
};
|
|
8884
9459
|
}
|
|
9460
|
+
/** Default `--wait` budget: long enough for a reviewer who is at their desk. */
|
|
9461
|
+
const DEFAULT_WAIT_SECONDS = 300;
|
|
9462
|
+
/**
|
|
9463
|
+
* Block until the Change Request this command just filed is decided.
|
|
9464
|
+
*
|
|
9465
|
+
* The result is printed first, so a caller that times out still has the id it
|
|
9466
|
+
* needs to come back to. Exit codes carry the outcome (see `errors.ts`): merged
|
|
9467
|
+
* is success, rejected is a decision the caller must branch on, and a timeout is
|
|
9468
|
+
* neither — nothing was decided, so the write must NOT be retried.
|
|
9469
|
+
*/
|
|
9470
|
+
async function awaitReview(client, opts, config, result) {
|
|
9471
|
+
const changeRequestId = findChangeRequestId(result);
|
|
9472
|
+
if (!changeRequestId) {
|
|
9473
|
+
console.error("[busabase-cli] --wait: this command filed no Change Request; nothing to wait for.");
|
|
9474
|
+
return;
|
|
9475
|
+
}
|
|
9476
|
+
const outcome = await waitForChangeRequest({
|
|
9477
|
+
changeRequestId,
|
|
9478
|
+
read: (id) => client.changeRequests.get({ changeRequestId: id }),
|
|
9479
|
+
timeoutMs: (opts.waitTimeout ?? DEFAULT_WAIT_SECONDS) * 1e3,
|
|
9480
|
+
notify: (message) => console.error(message)
|
|
9481
|
+
});
|
|
9482
|
+
console.log(render(opts.slim ? slim(outcome.changeRequest) : outcome.changeRequest, config.output));
|
|
9483
|
+
if (outcome.kind === "timeout") throw new CliOutcomeError("TIMEOUT", `--wait timed out: ${changeRequestId} is still ${outcome.status}. Nothing was decided — do NOT resubmit; poll it with \`busabase-cli change-requests get --change-request-id ${changeRequestId}\`.`);
|
|
9484
|
+
if (outcome.status !== "merged") throw new CliOutcomeError("REVIEW_REJECTED", `${changeRequestId} was ${outcome.status}, not merged.`);
|
|
9485
|
+
}
|
|
9486
|
+
/**
|
|
9487
|
+
* `--all` re-runs the same handler with a moving cursor. The handler reads its
|
|
9488
|
+
* input off `opts`, so advancing the page is a matter of setting `opts.cursor`
|
|
9489
|
+
* before each call rather than teaching every command a second code path.
|
|
9490
|
+
*/
|
|
9491
|
+
async function streamAllPages(client, opts, config, handler) {
|
|
9492
|
+
if (await paginateAll({
|
|
9493
|
+
fetchPage: async (cursor) => {
|
|
9494
|
+
opts.cursor = cursor;
|
|
9495
|
+
const result = await handler(client, opts, config);
|
|
9496
|
+
return opts.slim ? slim(result) : result;
|
|
9497
|
+
},
|
|
9498
|
+
limit: typeof opts.limit === "number" ? opts.limit : void 0,
|
|
9499
|
+
maxItems: typeof opts.maxItems === "number" ? opts.maxItems : void 0,
|
|
9500
|
+
write: (line) => console.log(line),
|
|
9501
|
+
notify: (message) => console.error(message)
|
|
9502
|
+
}) === 0) console.error("[busabase-cli] no rows.");
|
|
9503
|
+
}
|
|
8885
9504
|
/**
|
|
8886
9505
|
* {@link runAction} for a command with one positional argument. Commander passes
|
|
8887
9506
|
* `(arg, opts, cmd)` to such an action, so the arity differs from a flags-only leaf.
|
|
@@ -8895,7 +9514,7 @@ function runArgAction(state, handler) {
|
|
|
8895
9514
|
state.config = config;
|
|
8896
9515
|
assertCredentialNotExpired(config.apiKey);
|
|
8897
9516
|
const result = await handler(arg, createBusabaseClient(config), opts, config);
|
|
8898
|
-
console.log(render(result, config.output));
|
|
9517
|
+
console.log(render(opts.slim ? slim(result) : result, config.output));
|
|
8899
9518
|
};
|
|
8900
9519
|
}
|
|
8901
9520
|
/**
|
|
@@ -8912,7 +9531,7 @@ function runLocalArgAction(handler) {
|
|
|
8912
9531
|
const opts = cmd.optsWithGlobals();
|
|
8913
9532
|
const config = resolveConfig(opts);
|
|
8914
9533
|
const result = await handler(arg, opts, config);
|
|
8915
|
-
console.log(render(result, config.output));
|
|
9534
|
+
console.log(render(opts.slim ? slim(result) : result, config.output));
|
|
8916
9535
|
};
|
|
8917
9536
|
}
|
|
8918
9537
|
function pkgVersion() {
|
|
@@ -9045,11 +9664,33 @@ function unionVariants(inputSchema, discriminator = "operation") {
|
|
|
9045
9664
|
return variants;
|
|
9046
9665
|
}
|
|
9047
9666
|
/**
|
|
9667
|
+
* Words of an identifier, however it was spelled: `changeRequests` + `reviewMany`
|
|
9668
|
+
* and `change_requests_review_many` both reduce to
|
|
9669
|
+
* `change requests review many`. Lets the CLI compare its own generated command
|
|
9670
|
+
* paths against the MCP tool names in {@link TASK_SUPERSEDED_MCP_TOOLS} without
|
|
9671
|
+
* either side having to know how the other spells things.
|
|
9672
|
+
*/
|
|
9673
|
+
const identifierWords = (parts) => parts.flatMap((part) => part.split(/[_\-\s]+/)).flatMap((part) => part.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(" ")).filter(Boolean).map((word) => word.toLowerCase()).join(" ");
|
|
9674
|
+
/**
|
|
9675
|
+
* Endpoint commands a curated task command already covers.
|
|
9676
|
+
*
|
|
9677
|
+
* The MCP adapter has always dropped these so "a catalog carries one way to do
|
|
9678
|
+
* each job rather than two" — and then the CLI published BOTH spellings, which
|
|
9679
|
+
* is a third of a 30 KB help screen an agent reads to orient itself. Same list,
|
|
9680
|
+
* same reason, one more consumer.
|
|
9681
|
+
*
|
|
9682
|
+
* Hidden, never removed: every one of them still runs, so no existing script
|
|
9683
|
+
* breaks, and `--help-all` prints them.
|
|
9684
|
+
*/
|
|
9685
|
+
const SUPERSEDED_COMMAND_PATHS = new Set(TASK_SUPERSEDED_MCP_TOOLS.map((tool) => identifierWords([tool])));
|
|
9686
|
+
/**
|
|
9048
9687
|
* Procedures a curated command already exposes under a DIFFERENT name (so the
|
|
9049
9688
|
* name-collision guard wouldn't catch them). Keyed `group.procKey`.
|
|
9050
9689
|
*/
|
|
9051
9690
|
const GENERATED_SKIP = /* @__PURE__ */ new Set([
|
|
9052
9691
|
"search",
|
|
9692
|
+
"guides.list",
|
|
9693
|
+
"guides.read",
|
|
9053
9694
|
"auth.verify",
|
|
9054
9695
|
"records.search",
|
|
9055
9696
|
"records.listChangeRequests",
|
|
@@ -9079,7 +9720,8 @@ function registerGeneratedCommands(program, state) {
|
|
|
9079
9720
|
const addOneLeaf = (parent, navPath, procKey, proc, name, fields, description) => {
|
|
9080
9721
|
const { route } = proc["~orpc"];
|
|
9081
9722
|
if (parent.commands.some((c) => c.name() === name || c.aliases().includes(name))) return;
|
|
9082
|
-
const
|
|
9723
|
+
const superseded = SUPERSEDED_COMMAND_PATHS.has(identifierWords([parent.name(), name]));
|
|
9724
|
+
const leaf = parent.command(name, superseded ? { hidden: true } : void 0).description(description ?? route.summary ?? `${route.method} ${route.path}`);
|
|
9083
9725
|
for (const f of fields) {
|
|
9084
9726
|
if (f.pinned !== void 0) continue;
|
|
9085
9727
|
const flag = `--${kebab(f.key)}`;
|
|
@@ -9154,14 +9796,26 @@ function flagSignature(cmd) {
|
|
|
9154
9796
|
}
|
|
9155
9797
|
return parts.join(" ");
|
|
9156
9798
|
}
|
|
9157
|
-
|
|
9799
|
+
/**
|
|
9800
|
+
* Commander records `command(name, { hidden })` and `{ noHelp }` on `_hidden`;
|
|
9801
|
+
* this read `_noHelp`, which commander never sets, so it always answered false
|
|
9802
|
+
* and nothing was ever actually hidden from the root help.
|
|
9803
|
+
*/
|
|
9804
|
+
const isHelpHidden = (cmd) => Boolean(cmd._hidden) || Boolean(cmd._noHelp);
|
|
9805
|
+
/**
|
|
9806
|
+
* Set for the duration of a `--help-all` render. A module-level flag rather than
|
|
9807
|
+
* a parameter because commander calls the help hook itself, with no way to
|
|
9808
|
+
* thread an argument through.
|
|
9809
|
+
*/
|
|
9810
|
+
let showAllCommands = false;
|
|
9158
9811
|
/**
|
|
9159
9812
|
* Root help "Commands:" section: every leaf with its full flag signature (the flat
|
|
9160
9813
|
* commander listing would only show group names). Generated from the command tree,
|
|
9161
9814
|
* never hand-written.
|
|
9162
9815
|
*/
|
|
9163
|
-
function commandsSection(program) {
|
|
9816
|
+
function commandsSection(program, showAll = false) {
|
|
9164
9817
|
const lines = ["Commands:"];
|
|
9818
|
+
const skip = (cmd) => cmd.name() === "help" || !showAll && isHelpHidden(cmd);
|
|
9165
9819
|
const entry = (prefix, cmd) => {
|
|
9166
9820
|
const sig = [prefix, flagSignature(cmd)].filter(Boolean).join(" ");
|
|
9167
9821
|
const desc = cmd.description();
|
|
@@ -9170,12 +9824,12 @@ function commandsSection(program) {
|
|
|
9170
9824
|
return ` ${sig}\n${" ".repeat(44)}${desc}`;
|
|
9171
9825
|
};
|
|
9172
9826
|
for (const cmd of program.commands) {
|
|
9173
|
-
if (
|
|
9174
|
-
const leaves = cmd.commands.filter((leaf) =>
|
|
9827
|
+
if (skip(cmd)) continue;
|
|
9828
|
+
const leaves = cmd.commands.filter((leaf) => !skip(leaf));
|
|
9175
9829
|
if (leaves.length > 0) {
|
|
9176
9830
|
lines.push("");
|
|
9177
9831
|
for (const leaf of leaves) lines.push(entry(`${cmd.name()} ${leaf.name()}`, leaf));
|
|
9178
|
-
} else lines.push(entry(cmd.name(), cmd));
|
|
9832
|
+
} else if (cmd.commands.length === 0) lines.push(entry(cmd.name(), cmd));
|
|
9179
9833
|
}
|
|
9180
9834
|
return lines.join("\n");
|
|
9181
9835
|
}
|
|
@@ -9187,7 +9841,12 @@ Several accounts: \`login --profile <name>\` adds one, \`auth status\` lists the
|
|
|
9187
9841
|
\`auth switch\` changes which is active. Several spaces on one account (the norm on
|
|
9188
9842
|
Cloud): \`space list\` / \`space use\`.
|
|
9189
9843
|
|
|
9844
|
+
New here? \`busabase-cli guide\` is the operating manual — read \`workspace\` before
|
|
9845
|
+
proposing changes. Endpoint commands a task command already covers are hidden;
|
|
9846
|
+
\`--help-all\` prints them (they all still run).
|
|
9847
|
+
|
|
9190
9848
|
Docs: https://busabase.com/docs · Troubleshooting: ${DOCS_TROUBLESHOOTING}`;
|
|
9849
|
+
/** Exported for the help-example test, which walks the command tree in-process. */
|
|
9191
9850
|
function buildProgram(state = {}) {
|
|
9192
9851
|
const program = new Command("busabase-cli");
|
|
9193
9852
|
program.description("Client for the Busabase OpenAPI REST API — 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));
|
|
@@ -9195,6 +9854,29 @@ function buildProgram(state = {}) {
|
|
|
9195
9854
|
addGlobalFlags(program.command("health")).description("Server health check (GET /api/health)").action(runAction(state, (_client, _opts, config) => rawFetch(config, "GET", "/api/health")));
|
|
9196
9855
|
addGlobalFlags(program.command("openapi")).description("Fetch the OpenAPI document").action(runAction(state, (_client, _opts, config) => rawFetch(config, "GET", "/api/v1/openapi.json")));
|
|
9197
9856
|
addGlobalFlags(program.command("whoami")).description("Active space, user, and membership").action(runAction(state, (client) => client.auth.verify()));
|
|
9857
|
+
addGlobalFlags(program.command("guide")).argument("[topic]", "which guide to read; omit to list the topics this server serves").description("Read the Busabase operating manual (approval-first rules, AirApp contract)").addHelpText("after", `
|
|
9858
|
+
Read this BEFORE unfamiliar work. It carries the conventions this workspace
|
|
9859
|
+
expects, which no flag or schema can express on its own.
|
|
9860
|
+
|
|
9861
|
+
Examples:
|
|
9862
|
+
busabase-cli guide # what is available here
|
|
9863
|
+
busabase-cli guide workspace # the approval-first workflow
|
|
9864
|
+
busabase-cli guide airapp # REQUIRED before writing any AirApp file`).action(async (topic, _opts, cmd) => {
|
|
9865
|
+
const config = resolveConfig(cmd.optsWithGlobals());
|
|
9866
|
+
state.config = config;
|
|
9867
|
+
const client = createBusabaseClient(config);
|
|
9868
|
+
if (!topic) {
|
|
9869
|
+
console.log(render(await client.guides.list(), config.output));
|
|
9870
|
+
return;
|
|
9871
|
+
}
|
|
9872
|
+
const guide = await client.guides.read({ topic });
|
|
9873
|
+
if (config.output === "json") {
|
|
9874
|
+
console.log(render(guide, config.output));
|
|
9875
|
+
return;
|
|
9876
|
+
}
|
|
9877
|
+
console.log(guide.content);
|
|
9878
|
+
console.error(`\n[busabase-cli] other topics: ${guide.otherTopics.join(", ") || "(none)"}`);
|
|
9879
|
+
});
|
|
9198
9880
|
addGlobalFlags(program.command("login")).description("Connect the CLI to Busabase — Personal/local, Cloud, or self-hosted").option("--device-code", "force device authorization and skip the method prompt").option("--oauth", "legacy same-machine OAuth callback flow").option("--no-browser", "print the sign-in URL instead of opening a browser").option("--refresh", "rotate the saved OAuth token set (no browser, no re-login)").addHelpText("after", `
|
|
9199
9881
|
Busabase is an approval-first database and knowledge base for AI agents: agents
|
|
9200
9882
|
propose changes, humans review and merge what becomes trusted data.
|
|
@@ -9614,7 +10296,7 @@ To add content to a space that is already in use, use \`busabase-cli install\` i
|
|
|
9614
10296
|
leaf.usage(`${sig ? `${sig} ` : ""}[global flags]`);
|
|
9615
10297
|
}
|
|
9616
10298
|
}
|
|
9617
|
-
program.addHelpText("after", () => `\n${commandsSection(program)}\n${HELP_FOOTER}`);
|
|
10299
|
+
program.addHelpText("after", () => `\n${commandsSection(program, showAllCommands)}\n${HELP_FOOTER}`);
|
|
9618
10300
|
program.configureHelp({ visibleCommands: () => [] });
|
|
9619
10301
|
return program;
|
|
9620
10302
|
}
|
|
@@ -9629,17 +10311,48 @@ async function runCli(argv) {
|
|
|
9629
10311
|
program.outputHelp();
|
|
9630
10312
|
return 0;
|
|
9631
10313
|
}
|
|
10314
|
+
if (argv.includes("--help-all")) {
|
|
10315
|
+
console.log(renderedHelp({ showAll: true }));
|
|
10316
|
+
return 0;
|
|
10317
|
+
}
|
|
9632
10318
|
try {
|
|
9633
10319
|
await program.parseAsync(argv, { from: "user" });
|
|
9634
10320
|
return 0;
|
|
9635
10321
|
} catch (error) {
|
|
9636
|
-
|
|
10322
|
+
const json = errorOutputIsJson(state, argv);
|
|
10323
|
+
if (error instanceof CommanderError) {
|
|
10324
|
+
if (error.exitCode === 0) return 0;
|
|
10325
|
+
if (json) console.log(JSON.stringify({
|
|
10326
|
+
ok: false,
|
|
10327
|
+
code: "USAGE",
|
|
10328
|
+
message: error.message,
|
|
10329
|
+
retryable: false
|
|
10330
|
+
}, null, 2));
|
|
10331
|
+
return EXIT_CODES.USAGE;
|
|
10332
|
+
}
|
|
10333
|
+
const classified = classifyError(error);
|
|
10334
|
+
if (json) console.log(JSON.stringify(errorEnvelope(classified), null, 2));
|
|
9637
10335
|
console.error(explainError(error, state.config ?? resolveConfig({})));
|
|
9638
|
-
return
|
|
10336
|
+
return classified.exitCode;
|
|
9639
10337
|
}
|
|
9640
10338
|
}
|
|
9641
|
-
|
|
10339
|
+
/**
|
|
10340
|
+
* Whether a failed run should print the machine-readable envelope.
|
|
10341
|
+
*
|
|
10342
|
+
* `state.config` is set by the first command that resolves one, but a failure
|
|
10343
|
+
* can happen before that (an unknown option, a bad `--config` path), so the raw
|
|
10344
|
+
* argv is the fallback. Both spellings commander accepts are checked.
|
|
10345
|
+
*/
|
|
10346
|
+
function errorOutputIsJson(state, argv) {
|
|
10347
|
+
if (state.config) return state.config.output === "json";
|
|
10348
|
+
const flag = argv.indexOf("--output");
|
|
10349
|
+
if (flag !== -1 && argv[flag + 1] === "json") return true;
|
|
10350
|
+
if (argv.includes("--output=json")) return true;
|
|
10351
|
+
return configuredOutput() === "json";
|
|
10352
|
+
}
|
|
10353
|
+
function renderedHelp({ showAll = false } = {}) {
|
|
9642
10354
|
let out = "";
|
|
10355
|
+
showAllCommands = showAll;
|
|
9643
10356
|
const program = buildProgram();
|
|
9644
10357
|
program.configureOutput({
|
|
9645
10358
|
writeOut: (str) => {
|
|
@@ -9648,10 +10361,12 @@ function renderedHelp() {
|
|
|
9648
10361
|
writeErr: () => {}
|
|
9649
10362
|
});
|
|
9650
10363
|
program.outputHelp();
|
|
10364
|
+
showAllCommands = false;
|
|
9651
10365
|
return out;
|
|
9652
10366
|
}
|
|
9653
10367
|
/** Full root help text (what `busabase-cli --help` prints). Generated from the command tree. */
|
|
9654
10368
|
const HELP = renderedHelp();
|
|
10369
|
+
renderedHelp({ showAll: true });
|
|
9655
10370
|
/**
|
|
9656
10371
|
* Turn a low-level transport/HTTP error into an actionable message: which host was tried, the
|
|
9657
10372
|
* concrete next step (set a key for Cloud, point at a local server, or check connectivity), and a
|
|
@@ -9661,7 +10376,7 @@ function explainError(error, config) {
|
|
|
9661
10376
|
const base = config.baseUrl;
|
|
9662
10377
|
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
9663
10378
|
const issueDetail = formatIssues(error.data?.issues);
|
|
9664
|
-
const msg = issueDetail ? `${rawMessage} — ${issueDetail}` : rawMessage;
|
|
10379
|
+
const msg = issueDetail && !rawMessage.includes(issueDetail) ? `${rawMessage} — ${issueDetail}` : rawMessage;
|
|
9665
10380
|
const status = error.status;
|
|
9666
10381
|
const lower = msg.toLowerCase();
|
|
9667
10382
|
let body;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "busa-cli",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "Command-line client for the Busabase OpenAPI REST API. Talks to a local or remote `busabase server`. Short-name alias for busabase-cli.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/busabase/busabase/tree/main/apps/busabase-cli",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@zip.js/zip.js": "^2.8.20",
|
|
35
35
|
"busabase-package": "workspace:*",
|
|
36
|
-
"busabase-sdk": "0.19.
|
|
36
|
+
"busabase-sdk": "0.19.1",
|
|
37
37
|
"commander": "^14.0.0",
|
|
38
38
|
"tar-stream": "^3.1.7",
|
|
39
39
|
"yaml": "^2.9.0",
|