busa-cli 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-DGRADZF2.js → chunk-QJJMAA3K.js} +230 -62
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
|
@@ -284,6 +284,10 @@ var baseFieldChangeTask = {
|
|
|
284
284
|
|
|
285
285
|
// ../../packages/busabase-contract/src/tasks/change-request.ts
|
|
286
286
|
var REVIEW_GUIDANCE = "This records a HUMAN decision. Never call it unless the user explicitly asked for this specific verdict on these specific change requests. Summarising a change request for the user is not permission to approve it.";
|
|
287
|
+
var singleItemError = (result) => Object.assign(new Error(result?.error ?? "Change request action returned no result"), {
|
|
288
|
+
...result?.code ? { code: result.code } : {},
|
|
289
|
+
...result?.data === void 0 ? {} : { data: result.data }
|
|
290
|
+
});
|
|
287
291
|
var changeRequestQueryTask = {
|
|
288
292
|
name: "change_request_query",
|
|
289
293
|
cliPath: ["change-requests", "query"],
|
|
@@ -363,18 +367,15 @@ var changeRequestReviewTask = {
|
|
|
363
367
|
throw new Error("changeRequestIds must contain at least one id.");
|
|
364
368
|
}
|
|
365
369
|
const verdict = input.verdict;
|
|
366
|
-
|
|
367
|
-
return client.changeRequests.review({
|
|
368
|
-
changeRequestId: ids[0],
|
|
369
|
-
verdict,
|
|
370
|
-
...input.reason ? { reason: input.reason } : {}
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
return client.changeRequests.reviewMany({
|
|
370
|
+
const response = await client.changeRequests.review({
|
|
374
371
|
changeRequestIds: ids,
|
|
375
372
|
verdict,
|
|
376
373
|
...input.reason ? { reason: input.reason } : {}
|
|
377
374
|
});
|
|
375
|
+
if (ids.length !== 1) return response;
|
|
376
|
+
const result = response.results[0];
|
|
377
|
+
if (!result?.ok) throw singleItemError(result);
|
|
378
|
+
return result.changeRequest;
|
|
378
379
|
}
|
|
379
380
|
};
|
|
380
381
|
var changeRequestMergeTask = {
|
|
@@ -401,8 +402,15 @@ var changeRequestMergeTask = {
|
|
|
401
402
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
402
403
|
throw new Error("changeRequestIds must contain at least one id.");
|
|
403
404
|
}
|
|
404
|
-
|
|
405
|
-
|
|
405
|
+
const response = await client.changeRequests.merge({ changeRequestIds: ids });
|
|
406
|
+
if (ids.length !== 1) return response;
|
|
407
|
+
const result = response.results[0];
|
|
408
|
+
if (!result?.ok) throw singleItemError(result);
|
|
409
|
+
return {
|
|
410
|
+
changeRequest: result.changeRequest,
|
|
411
|
+
record: result.record,
|
|
412
|
+
view: result.view
|
|
413
|
+
};
|
|
406
414
|
}
|
|
407
415
|
};
|
|
408
416
|
|
|
@@ -434,25 +442,25 @@ var nodeListTask = {
|
|
|
434
442
|
{
|
|
435
443
|
path: ["skills", "list"],
|
|
436
444
|
preset: { kind: "skill" },
|
|
437
|
-
summary: "List Skill nodes
|
|
445
|
+
summary: "List Skill nodes (summaries; use `skills get` for one node's files)"
|
|
438
446
|
},
|
|
439
447
|
{
|
|
440
448
|
path: ["drives", "list"],
|
|
441
449
|
preset: { kind: "drive" },
|
|
442
|
-
summary: "List Drive nodes
|
|
450
|
+
summary: "List Drive nodes (summaries; use `drives get` for one node's files)"
|
|
443
451
|
},
|
|
444
452
|
{
|
|
445
453
|
path: ["airapps", "list"],
|
|
446
454
|
preset: { kind: "airapp" },
|
|
447
|
-
summary: "List AirApp nodes
|
|
455
|
+
summary: "List AirApp nodes (summaries; use `airapps get` for one node's files)"
|
|
448
456
|
}
|
|
449
457
|
],
|
|
450
|
-
summary: "List Skill, Drive, or AirApp nodes
|
|
451
|
-
guidance: "Returns every node of the given kind
|
|
458
|
+
summary: "List Skill, Drive, or AirApp nodes",
|
|
459
|
+
guidance: "Returns a lightweight summary row for every node of the given kind \u2014 id, name, slug, type, and metadata, but NOT the node's file list. Follow up with `node_get_file_tree` for one node's files, or `node_files_list` for just the file inventory. For the workspace tree across all node types, use `nodes_list` instead.",
|
|
452
460
|
annotations: { readOnly: true, destructive: false },
|
|
453
461
|
params: [kindParam],
|
|
454
462
|
examples: ["busabase-cli nodes list-file-trees --kind skill"],
|
|
455
|
-
execute: async (client, input) => client.
|
|
463
|
+
execute: async (client, input) => client.nodes.list({ types: [resolveKind(input.kind)] })
|
|
456
464
|
};
|
|
457
465
|
var nodeGetTask = {
|
|
458
466
|
name: "node_get_file_tree",
|
|
@@ -478,7 +486,10 @@ var nodeGetTask = {
|
|
|
478
486
|
annotations: { readOnly: true, destructive: false },
|
|
479
487
|
params: [kindParam, nodeIdParam],
|
|
480
488
|
examples: ["busabase-cli nodes get-file-tree --kind airapp --node-id nod_123"],
|
|
481
|
-
|
|
489
|
+
// Reads through the unified Node detail route. The payload is unchanged apart
|
|
490
|
+
// from the `type` discriminator the union carries, so a caller still gets
|
|
491
|
+
// `{ node, entryFile, visibility, version, files }`.
|
|
492
|
+
execute: async (client, input) => client.nodes.get({ nodeId: input.nodeId, type: resolveKind(input.kind) })
|
|
482
493
|
};
|
|
483
494
|
var nodeListFilesTask = {
|
|
484
495
|
name: "node_files_list",
|
|
@@ -567,7 +578,7 @@ var nodeFilesChangeRequestTask = {
|
|
|
567
578
|
}
|
|
568
579
|
],
|
|
569
580
|
summary: "Propose file changes inside a Skill, Drive, or AirApp node",
|
|
570
|
-
guidance:
|
|
581
|
+
guidance: 'Approval-first: this proposes a ChangeRequest for a human to review, it does not write directly. Each operation is one of create / update / delete / metadata_update. Include `baseContentHash` (from `node_file_read`) on an update so a concurrent edit is caught. For kind "airapp", keep the project runnable by `npm run dev` \u2014 editing package.json must leave a "dev" script starting a plain Node server, not a bundler dev server.',
|
|
571
582
|
annotations: { readOnly: false, destructive: false },
|
|
572
583
|
params: [
|
|
573
584
|
kindParam,
|
|
@@ -1189,7 +1200,7 @@ var nodeCreateTask = {
|
|
|
1189
1200
|
name: "files",
|
|
1190
1201
|
appliesWhen: { param: "type", values: ["skill", "drive", "airapp"] },
|
|
1191
1202
|
kind: "json",
|
|
1192
|
-
description: 'Seed files for a Skill/Drive/AirApp: [{"path":"SKILL.md","content":"..."}]. Layered over the default scaffold unless mergeMode is "replace".'
|
|
1203
|
+
description: 'Seed files for a Skill/Drive/AirApp: [{"path":"SKILL.md","content":"..."}]. Layered over the default scaffold unless mergeMode is "replace" \u2014 a path you supply REPLACES the scaffold\'s file at that path. For --type airapp the project MUST be runnable by `npm run dev`: give package.json a "dev" script running a plain Node server (`node server.js`, Hono or node:http). A bundler dev server (Vite/webpack/Next) and any native-binary dependency CANNOT boot in the AirApp runtime, and browser files must need no build step.'
|
|
1193
1204
|
},
|
|
1194
1205
|
{
|
|
1195
1206
|
name: "mergeMode",
|
|
@@ -1433,11 +1444,16 @@ var recordQueryTask = {
|
|
|
1433
1444
|
|
|
1434
1445
|
// ../../packages/busabase-contract/src/tasks/view-change.ts
|
|
1435
1446
|
var ACTIONS = ["create", "update", "delete", "restore"];
|
|
1447
|
+
var mergeIntent2 = (input) => {
|
|
1448
|
+
if (input.requireReview) return { autoMerge: false };
|
|
1449
|
+
if (input.autoMerge) return { autoMerge: true };
|
|
1450
|
+
return {};
|
|
1451
|
+
};
|
|
1436
1452
|
var viewChangeTask = {
|
|
1437
1453
|
name: "view_change_request",
|
|
1438
1454
|
cliPath: ["views", "change-request"],
|
|
1439
1455
|
summary: "Propose a Base view change (create / update / delete / restore)",
|
|
1440
|
-
guidance: "
|
|
1456
|
+
guidance: "Review is permission-aware, decided server-side: the change merges immediately when your key has write access on the Base's node and lands as a pending ChangeRequest otherwise \u2014 check the response's `materialized` field to see which happened. Pass requireReview to always propose instead of merging. Views are structure, not content \u2014 when you are building starter structure (a Base plus the views that make it usable) let it merge so the workspace is not left half-built behind a review queue, exactly as you would for `node_create` and `POST /bases`. `create` needs the BASE id; update / delete / restore need the VIEW id \u2014 the endpoint split follows whichever id identifies the target, which this task handles for you.",
|
|
1441
1457
|
annotations: { readOnly: false, destructive: false },
|
|
1442
1458
|
params: [
|
|
1443
1459
|
{
|
|
@@ -1484,16 +1500,30 @@ var viewChangeTask = {
|
|
|
1484
1500
|
description: "Changes to apply to the view (update only)."
|
|
1485
1501
|
},
|
|
1486
1502
|
{ name: "message", kind: "string", description: "Explanation for the reviewer." },
|
|
1487
|
-
{ name: "submittedBy", kind: "string", description: "Producer label recorded on the change." }
|
|
1503
|
+
{ name: "submittedBy", kind: "string", description: "Producer label recorded on the change." },
|
|
1504
|
+
{
|
|
1505
|
+
name: "autoMerge",
|
|
1506
|
+
kind: "boolean",
|
|
1507
|
+
description: "Skip review and apply the change immediately if you have write access. Not a permission override \u2014 a changeRequest-level key still gets a pending CR. Default is permission-aware: merge when you can, otherwise propose."
|
|
1508
|
+
},
|
|
1509
|
+
{
|
|
1510
|
+
name: "requireReview",
|
|
1511
|
+
kind: "boolean",
|
|
1512
|
+
description: "Always propose a pending ChangeRequest, even with write access."
|
|
1513
|
+
}
|
|
1488
1514
|
],
|
|
1489
1515
|
examples: [
|
|
1490
1516
|
'busabase-cli views change-request --action create --base-id bas_1 --name "Published" --type table',
|
|
1517
|
+
'busabase-cli views change-request --action create --base-id bas_1 --name "Published" --require-review',
|
|
1491
1518
|
"busabase-cli views change-request --action delete --view-id viw_1"
|
|
1492
1519
|
],
|
|
1493
1520
|
execute: async (client, input) => {
|
|
1494
1521
|
const meta = {
|
|
1495
1522
|
...input.message ? { message: input.message } : {},
|
|
1496
|
-
...input.submittedBy ? { submittedBy: input.submittedBy } : {}
|
|
1523
|
+
...input.submittedBy ? { submittedBy: input.submittedBy } : {},
|
|
1524
|
+
// Tri-state, like node_create's: omitted keeps the endpoint's
|
|
1525
|
+
// permission-aware default, explicit true/false forces the branch.
|
|
1526
|
+
...mergeIntent2(input)
|
|
1497
1527
|
};
|
|
1498
1528
|
if (input.action === "create") {
|
|
1499
1529
|
if (!input.baseId || !input.name) {
|
|
@@ -1556,6 +1586,8 @@ import {
|
|
|
1556
1586
|
cloudContract,
|
|
1557
1587
|
createBusabaseClient,
|
|
1558
1588
|
DEFAULT_BASE_URL as DEFAULT_BASE_URL2,
|
|
1589
|
+
getRecordByField,
|
|
1590
|
+
grepAssets,
|
|
1559
1591
|
normalizeBaseUrl as normalizeBaseUrl2
|
|
1560
1592
|
} from "busabase-sdk";
|
|
1561
1593
|
import {
|
|
@@ -2241,8 +2273,11 @@ async function exportFull(options) {
|
|
|
2241
2273
|
let docBodyCount = 0;
|
|
2242
2274
|
for (const node of docNodes) {
|
|
2243
2275
|
const nodeId = node.id;
|
|
2244
|
-
const
|
|
2245
|
-
|
|
2276
|
+
const detail = await client.nodes.get({ nodeId, type: "doc" });
|
|
2277
|
+
if (detail.type !== "doc") {
|
|
2278
|
+
throw new Error(`Expected a doc node for ${nodeId}, got "${detail.type}"`);
|
|
2279
|
+
}
|
|
2280
|
+
await writer.addBuffer(`docs/${nodeId}.md`, detail.body ?? "");
|
|
2246
2281
|
docBodyCount += 1;
|
|
2247
2282
|
}
|
|
2248
2283
|
log(`exported ${docBodyCount} doc bodies`);
|
|
@@ -2486,15 +2521,15 @@ var reportProgress = (message) => {
|
|
|
2486
2521
|
};
|
|
2487
2522
|
var STATE_ONLY_BACKUP_MESSAGE = [
|
|
2488
2523
|
"--state-only backup is not implemented, and will not be \u2014 it is superseded by",
|
|
2489
|
-
"`busabase-cli
|
|
2524
|
+
"`busabase-cli export`, which writes the same state-level content as a readable,",
|
|
2490
2525
|
"diffable directory you can push to GitHub:",
|
|
2491
2526
|
"",
|
|
2492
|
-
" busabase-cli
|
|
2527
|
+
" busabase-cli export <node-slug> -o ./my-package",
|
|
2493
2528
|
" busabase-cli install https://github.com/<you>/<repo>",
|
|
2494
2529
|
"",
|
|
2495
2530
|
"`busabase-cli backup` stays the full-fidelity backup path (raw rows, original ids,",
|
|
2496
2531
|
"history) \u2014 run it without --state-only. The two formats are deliberately separate:",
|
|
2497
|
-
"a backup carries the audit events, comments, reviews and ACLs that
|
|
2532
|
+
"a backup carries the audit events, comments, reviews and ACLs that an exported",
|
|
2498
2533
|
"package must never leak."
|
|
2499
2534
|
].join("\n");
|
|
2500
2535
|
var STATE_ONLY_RESTORE_MESSAGE = [
|
|
@@ -3463,7 +3498,33 @@ var listNodesInputSchema = z7.object({
|
|
|
3463
3498
|
* soft-archived nodes for the Trash view — no `parentId`/`depth` walk, since
|
|
3464
3499
|
* archived nodes are shown as a list, not a tree.
|
|
3465
3500
|
*/
|
|
3466
|
-
status: z7.enum(["active", "archived"]).optional().default("active")
|
|
3501
|
+
status: z7.enum(["active", "archived"]).optional().default("active"),
|
|
3502
|
+
/**
|
|
3503
|
+
* Narrow to specific node types and return a FLAT list of lightweight node
|
|
3504
|
+
* summaries (`children: []`) instead of walking the tree. This is what
|
|
3505
|
+
* replaced the four retired narrow listings (`GET /docs`, `/files`,
|
|
3506
|
+
* `/folders`, `/file-trees`); file-trees are selected with their real
|
|
3507
|
+
* discriminators `skill` / `drive` / `airapp`, since there is no synthetic
|
|
3508
|
+
* "file-tree" node type.
|
|
3509
|
+
*
|
|
3510
|
+
* Omitting `types` leaves every existing caller on exactly today's
|
|
3511
|
+
* behaviour (full tree, or a `parentId`/`depth`-bounded walk, or the
|
|
3512
|
+
* archived flat list) — the two modes never interfere.
|
|
3513
|
+
*
|
|
3514
|
+
* NOTE — no `projection` parameter, deliberately. The consolidation roadmap
|
|
3515
|
+
* sketched `?projection=summary`, but it also rules out adding
|
|
3516
|
+
* `projection=detail` in this batch (the retired detail lists were the
|
|
3517
|
+
* N+1 payloads this change exists to remove). That would leave a parameter
|
|
3518
|
+
* with exactly one legal value, which is noise in OpenAPI/MCP/CLI rather
|
|
3519
|
+
* than a decision a caller gets to make. Detail is `GET /nodes/{nodeId}`.
|
|
3520
|
+
*
|
|
3521
|
+
* A GET query param that occurs exactly once (`?types=doc`) arrives as a
|
|
3522
|
+
* bare string, not a 1-element array — only a REPEATED occurrence
|
|
3523
|
+
* (`?types=doc&types=file`) becomes an array. Accept both and normalize.
|
|
3524
|
+
*/
|
|
3525
|
+
types: z7.union([z7.array(z7.enum(NODE_TYPES)), z7.enum(NODE_TYPES)]).transform((value) => Array.isArray(value) ? value : [value]).optional().describe(
|
|
3526
|
+
"Return a flat list of lightweight summaries for these node types instead of the tree. Read one node's full detail with GET /nodes/{nodeId}."
|
|
3527
|
+
)
|
|
3467
3528
|
}).optional();
|
|
3468
3529
|
var isDescendantInputSchema = z7.object({
|
|
3469
3530
|
nodeId: z7.string(),
|
|
@@ -3631,11 +3692,19 @@ var liveEventSchema = z7.object({
|
|
|
3631
3692
|
// via the audit funnel, but nothing needs reviewing). Consumed by
|
|
3632
3693
|
// `use-live-sync.ts` to pop a desktop Notification, and by
|
|
3633
3694
|
// busabase-cloud's host hook to persist an inbox notification row.
|
|
3634
|
-
"change_request.pending_review"
|
|
3695
|
+
"change_request.pending_review",
|
|
3696
|
+
// A node's metadata was written directly, outside the change-request flow
|
|
3697
|
+
// (`PATCH /api/v1/nodes/{nodeId}/metadata` — agents, the SDK, an MCP tool,
|
|
3698
|
+
// and every rich-node editor's own Save). Carries the touched node in
|
|
3699
|
+
// `nodeIds` so open dashboards refetch the node tree instead of showing a
|
|
3700
|
+
// stale whiteboard/workflow/HTML document until the next reload.
|
|
3701
|
+
"node.metadata_updated"
|
|
3635
3702
|
]),
|
|
3636
3703
|
spaceId: z7.string(),
|
|
3637
3704
|
actorId: z7.string(),
|
|
3638
|
-
|
|
3705
|
+
// Null for events that aren't about a change request at all
|
|
3706
|
+
// (`node.metadata_updated`), which is every direct, auto-audited write.
|
|
3707
|
+
changeRequestId: z7.string().nullable(),
|
|
3639
3708
|
baseId: z7.string().nullable(),
|
|
3640
3709
|
nodeIds: z7.array(z7.string()),
|
|
3641
3710
|
recordIds: z7.array(z7.string()),
|
|
@@ -3946,6 +4015,9 @@ var viewSchema = z8.object({
|
|
|
3946
4015
|
createdAt: z8.string(),
|
|
3947
4016
|
updatedAt: z8.string()
|
|
3948
4017
|
});
|
|
4018
|
+
var autoMergeSchema = z8.boolean().optional().describe(
|
|
4019
|
+
"Whether to approve and merge this view change immediately. Omitted defaults to merging immediately if the actor has write access on the Base's node, otherwise falling back to a pending Change Request; pass explicit false to force review even with write access."
|
|
4020
|
+
);
|
|
3949
4021
|
var createViewInputSchema = z8.object({
|
|
3950
4022
|
config: viewConfigSchema.optional().default({ filters: [], sorts: [] }),
|
|
3951
4023
|
description: z8.string().optional().default(""),
|
|
@@ -3953,7 +4025,8 @@ var createViewInputSchema = z8.object({
|
|
|
3953
4025
|
name: z8.string().min(1),
|
|
3954
4026
|
type: viewTypeSchema.optional().default("table"),
|
|
3955
4027
|
slug: z8.string().min(1).regex(/^[a-z0-9-]+$/).optional(),
|
|
3956
|
-
submittedBy: z8.string().optional().default("local-producer")
|
|
4028
|
+
submittedBy: z8.string().optional().default("local-producer"),
|
|
4029
|
+
autoMerge: autoMergeSchema
|
|
3957
4030
|
});
|
|
3958
4031
|
var updateViewInputSchema = z8.object({
|
|
3959
4032
|
config: viewConfigSchema.optional(),
|
|
@@ -3961,15 +4034,18 @@ var updateViewInputSchema = z8.object({
|
|
|
3961
4034
|
message: z8.string().optional().default("Update view"),
|
|
3962
4035
|
name: z8.string().min(1).optional(),
|
|
3963
4036
|
type: viewTypeSchema.optional(),
|
|
3964
|
-
submittedBy: z8.string().optional().default("local-producer")
|
|
4037
|
+
submittedBy: z8.string().optional().default("local-producer"),
|
|
4038
|
+
autoMerge: autoMergeSchema
|
|
3965
4039
|
});
|
|
3966
4040
|
var deleteViewInputSchema = z8.object({
|
|
3967
4041
|
message: z8.string().optional().default("Delete view"),
|
|
3968
|
-
submittedBy: z8.string().optional().default("local-producer")
|
|
4042
|
+
submittedBy: z8.string().optional().default("local-producer"),
|
|
4043
|
+
autoMerge: autoMergeSchema
|
|
3969
4044
|
});
|
|
3970
4045
|
var restoreViewInputSchema = z8.object({
|
|
3971
4046
|
message: z8.string().optional().default("Restore view"),
|
|
3972
|
-
submittedBy: z8.string().optional().default("local-producer")
|
|
4047
|
+
submittedBy: z8.string().optional().default("local-producer"),
|
|
4048
|
+
autoMerge: autoMergeSchema
|
|
3973
4049
|
});
|
|
3974
4050
|
var viewChangeRequestInputSchema = z8.discriminatedUnion("operation", [
|
|
3975
4051
|
createViewInputSchema.extend({
|
|
@@ -4009,7 +4085,7 @@ var PACKAGE_COMPUTED_FIELD_TYPES = [
|
|
|
4009
4085
|
"auto_number",
|
|
4010
4086
|
"formula",
|
|
4011
4087
|
// `lookup` is derived from OTHER records and resolved at read time, so it
|
|
4012
|
-
// shows up in `headCommit.fields` like any other value — but
|
|
4088
|
+
// shows up in `headCommit.fields` like any other value — but exporting it
|
|
4013
4089
|
// would ship derived data as if it were authored, and install would strip it
|
|
4014
4090
|
// server-side anyway.
|
|
4015
4091
|
"lookup",
|
|
@@ -4484,16 +4560,25 @@ var createBase = async (client, node, parentNodeId, options, state, bases, field
|
|
|
4484
4560
|
bases.push({ node, baseId: result.id });
|
|
4485
4561
|
indexFields(fieldIds, node.slug, result.fields);
|
|
4486
4562
|
for (const view of node.base.views) {
|
|
4487
|
-
const
|
|
4563
|
+
const created = await client.views.changeRequest({
|
|
4488
4564
|
operation: "create",
|
|
4489
4565
|
baseId: result.id,
|
|
4490
4566
|
slug: view.slug,
|
|
4491
4567
|
name: view.name,
|
|
4492
4568
|
description: view.description,
|
|
4493
4569
|
config: view.config,
|
|
4494
|
-
submittedBy: options.submittedBy
|
|
4570
|
+
submittedBy: options.submittedBy,
|
|
4571
|
+
// Structure, exactly like the Base above — so ask for it outright instead
|
|
4572
|
+
// of proposing it and immediately approving+merging our own proposal,
|
|
4573
|
+
// which is what this loop used to do only because the endpoint had no
|
|
4574
|
+
// `autoMerge`.
|
|
4575
|
+
autoMerge: true
|
|
4495
4576
|
});
|
|
4496
|
-
|
|
4577
|
+
if (!created.materialized) {
|
|
4578
|
+
throw new Error(
|
|
4579
|
+
`View "${view.slug}" was requested immediately but the server returned a pending change request; install cannot leave a Base's views behind in review.`
|
|
4580
|
+
);
|
|
4581
|
+
}
|
|
4497
4582
|
state.created.views++;
|
|
4498
4583
|
}
|
|
4499
4584
|
};
|
|
@@ -4716,8 +4801,32 @@ var toKeyArray = (value) => {
|
|
|
4716
4801
|
return [];
|
|
4717
4802
|
};
|
|
4718
4803
|
var approveAndMerge = async (client, changeRequestId) => {
|
|
4719
|
-
await client.changeRequests.review({
|
|
4720
|
-
|
|
4804
|
+
const reviewed = await client.changeRequests.review({
|
|
4805
|
+
changeRequestIds: [changeRequestId],
|
|
4806
|
+
verdict: "approved"
|
|
4807
|
+
});
|
|
4808
|
+
const reviewResult = reviewed.results[0];
|
|
4809
|
+
if (!reviewResult?.ok) {
|
|
4810
|
+
throw Object.assign(
|
|
4811
|
+
new Error(reviewResult?.error ?? "Change request review returned no result"),
|
|
4812
|
+
{
|
|
4813
|
+
code: reviewResult?.code,
|
|
4814
|
+
data: reviewResult?.data
|
|
4815
|
+
}
|
|
4816
|
+
);
|
|
4817
|
+
}
|
|
4818
|
+
const merged = await client.changeRequests.merge({ changeRequestIds: [changeRequestId] });
|
|
4819
|
+
const mergeResult = merged.results[0];
|
|
4820
|
+
if (!mergeResult?.ok) {
|
|
4821
|
+
throw Object.assign(
|
|
4822
|
+
new Error(mergeResult?.error ?? "Change request merge returned no result"),
|
|
4823
|
+
{
|
|
4824
|
+
code: mergeResult?.code,
|
|
4825
|
+
data: mergeResult?.data
|
|
4826
|
+
}
|
|
4827
|
+
);
|
|
4828
|
+
}
|
|
4829
|
+
return mergeResult;
|
|
4721
4830
|
};
|
|
4722
4831
|
|
|
4723
4832
|
// ../../packages/busabase-package/src/collect.ts
|
|
@@ -4753,7 +4862,7 @@ var collectNode = async (client, source, options) => {
|
|
|
4753
4862
|
slug: source.slug,
|
|
4754
4863
|
name: source.name,
|
|
4755
4864
|
description: source.description ?? "",
|
|
4756
|
-
// `
|
|
4865
|
+
// `export` always writes position, so round trips preserve sibling order exactly.
|
|
4757
4866
|
position: source.position
|
|
4758
4867
|
};
|
|
4759
4868
|
switch (source.type) {
|
|
@@ -4764,7 +4873,10 @@ var collectNode = async (client, source, options) => {
|
|
|
4764
4873
|
children: await collectNodes(client, source.children ?? [], options)
|
|
4765
4874
|
};
|
|
4766
4875
|
case "doc": {
|
|
4767
|
-
const doc = await client.
|
|
4876
|
+
const doc = await client.nodes.get({ nodeId: source.id, type: "doc" });
|
|
4877
|
+
if (doc.type !== "doc") {
|
|
4878
|
+
throw new Error(`Expected a doc node for "${source.slug}", got "${doc.type}".`);
|
|
4879
|
+
}
|
|
4768
4880
|
return { ...common, type: "doc", body: doc.body };
|
|
4769
4881
|
}
|
|
4770
4882
|
case "base":
|
|
@@ -4785,7 +4897,7 @@ var collectBase = async (client, source, common, options) => {
|
|
|
4785
4897
|
const base = await client.bases.get({ baseId });
|
|
4786
4898
|
if (!base) {
|
|
4787
4899
|
throw new Error(
|
|
4788
|
-
`Base node "${source.slug}" has no Base behind it (looked up ${baseId}). The space may be mid-migration; re-run
|
|
4900
|
+
`Base node "${source.slug}" has no Base behind it (looked up ${baseId}). The space may be mid-migration; re-run export.`
|
|
4789
4901
|
);
|
|
4790
4902
|
}
|
|
4791
4903
|
const views = await client.bases.listViews({ baseId: base.id });
|
|
@@ -4836,7 +4948,7 @@ var toPackageField = (field, context) => {
|
|
|
4836
4948
|
const targetSlug = context.baseSlugById.get(targetBaseId);
|
|
4837
4949
|
if (!targetSlug) {
|
|
4838
4950
|
throw new Error(
|
|
4839
|
-
`Field "${context.baseSlug}.${field.slug}" relates to a Base that no longer exists (${targetBaseId}). Fix the field before
|
|
4951
|
+
`Field "${context.baseSlug}.${field.slug}" relates to a Base that no longer exists (${targetBaseId}). Fix the field before exporting.`
|
|
4840
4952
|
);
|
|
4841
4953
|
}
|
|
4842
4954
|
options.targetBaseSlug = targetSlug;
|
|
@@ -4948,7 +5060,10 @@ var collectFileTree = async (client, source, type, common, options) => {
|
|
|
4948
5060
|
};
|
|
4949
5061
|
};
|
|
4950
5062
|
var collectFile = async (client, source, common, options) => {
|
|
4951
|
-
const fileNode = await client.
|
|
5063
|
+
const fileNode = await client.nodes.get({ nodeId: source.id, type: "file" });
|
|
5064
|
+
if (fileNode.type !== "file") {
|
|
5065
|
+
throw new Error(`Expected a file node for "${source.slug}", got "${fileNode.type}".`);
|
|
5066
|
+
}
|
|
4952
5067
|
const asset = fileNode.asset;
|
|
4953
5068
|
if (!asset?.url) {
|
|
4954
5069
|
options.warn(`Skipped file node "${source.slug}": it has no downloadable asset.`);
|
|
@@ -4986,7 +5101,7 @@ var assertSelfContained = (tree, options) => {
|
|
|
4986
5101
|
if (field.type !== "relation" || !targetBaseSlug) continue;
|
|
4987
5102
|
if (!packagedBaseSlugs.has(targetBaseSlug)) {
|
|
4988
5103
|
throw new Error(
|
|
4989
|
-
`Field "${node.slug}.${field.slug}" relates to Base "${targetBaseSlug}", which is outside the subtree being
|
|
5104
|
+
`Field "${node.slug}.${field.slug}" relates to Base "${targetBaseSlug}", which is outside the subtree being exported. A package must be self-contained \u2014 export a subtree that includes both Bases, or remove the field.`
|
|
4990
5105
|
);
|
|
4991
5106
|
}
|
|
4992
5107
|
}
|
|
@@ -5003,7 +5118,7 @@ var assertSelfContained = (tree, options) => {
|
|
|
5003
5118
|
}
|
|
5004
5119
|
if (dangling > 0) {
|
|
5005
5120
|
options.warn(
|
|
5006
|
-
`Base "${node.slug}" has ${dangling} relation value(s) pointing at records outside the
|
|
5121
|
+
`Base "${node.slug}" has ${dangling} relation value(s) pointing at records outside the exported subtree \u2014 they were dropped.`
|
|
5007
5122
|
);
|
|
5008
5123
|
}
|
|
5009
5124
|
}
|
|
@@ -5913,7 +6028,7 @@ var renderInstallReport = (targetFolderSlug, result) => {
|
|
|
5913
6028
|
}
|
|
5914
6029
|
return lines.join("\n");
|
|
5915
6030
|
};
|
|
5916
|
-
var
|
|
6031
|
+
var runExport = async (client, nodeSlugOrId, options) => {
|
|
5917
6032
|
const nodes = await client.nodes.list();
|
|
5918
6033
|
const source = findSourceNode(nodes, nodeSlugOrId);
|
|
5919
6034
|
const warnings = [];
|
|
@@ -5930,14 +6045,14 @@ var runPublish = async (client, nodeSlugOrId, options) => {
|
|
|
5930
6045
|
}
|
|
5931
6046
|
if (options.json) {
|
|
5932
6047
|
return {
|
|
5933
|
-
|
|
6048
|
+
exported: !options.dryRun,
|
|
5934
6049
|
package: manifest.name,
|
|
5935
6050
|
outDir: options.outDir,
|
|
5936
6051
|
files: [...files.keys()].sort(),
|
|
5937
6052
|
warnings
|
|
5938
6053
|
};
|
|
5939
6054
|
}
|
|
5940
|
-
return
|
|
6055
|
+
return renderExportReport(options, manifest, files, warnings);
|
|
5941
6056
|
};
|
|
5942
6057
|
var resolveManifest = async (outDir, nameFlag, nodeName, nodeSlug) => {
|
|
5943
6058
|
const existing = await readExistingManifest(outDir);
|
|
@@ -5959,7 +6074,7 @@ var readExistingManifest = async (outDir) => {
|
|
|
5959
6074
|
return void 0;
|
|
5960
6075
|
}
|
|
5961
6076
|
};
|
|
5962
|
-
var
|
|
6077
|
+
var renderExportReport = (options, manifest, files, warnings) => {
|
|
5963
6078
|
const lines = [];
|
|
5964
6079
|
lines.push(
|
|
5965
6080
|
options.dryRun ? `Dry run \u2014 would write ${files.size} file(s) to ${options.outDir}:` : `Wrote ${files.size} file(s) to ${options.outDir}:`
|
|
@@ -5972,7 +6087,7 @@ var renderPublishReport = (options, manifest, files, warnings) => {
|
|
|
5972
6087
|
}
|
|
5973
6088
|
if (!options.dryRun) {
|
|
5974
6089
|
lines.push("");
|
|
5975
|
-
lines.push("Now
|
|
6090
|
+
lines.push("Now push it to GitHub:");
|
|
5976
6091
|
lines.push(` cd ${options.outDir}`);
|
|
5977
6092
|
lines.push(
|
|
5978
6093
|
' git init && git add . && git commit -m "Add ${name} package"'.replace(
|
|
@@ -6486,12 +6601,17 @@ var GENERATED_SKIP = /* @__PURE__ */ new Set([
|
|
|
6486
6601
|
// The `/file-trees` group is exposed by the task layer as `nodes
|
|
6487
6602
|
// list-file-trees` / `get-file-tree` / `files` / `read-file` /
|
|
6488
6603
|
// `files-change-request`, each taking `--kind` (see tasks/file-tree.ts).
|
|
6489
|
-
|
|
6604
|
+
// `fileTrees.list`/`fileTrees.get` are absent because the procedures are: the
|
|
6605
|
+
// two tasks that used to call them now call `nodes.list({ types })` /
|
|
6606
|
+
// `nodes.get`, so there is nothing left for the generator to skip.
|
|
6490
6607
|
"fileTrees.create",
|
|
6491
|
-
"fileTrees.get",
|
|
6492
6608
|
"fileTrees.listFiles",
|
|
6493
6609
|
"fileTrees.readFile",
|
|
6494
6610
|
"fileTrees.createChangeRequest"
|
|
6611
|
+
// `nodes get` is the curated `docs|files|folders get` facade's underlying
|
|
6612
|
+
// route, but it stays generated too: it is the ONE command that reads a node
|
|
6613
|
+
// of any type without knowing the type first, which is the whole point of the
|
|
6614
|
+
// consolidation.
|
|
6495
6615
|
]);
|
|
6496
6616
|
function registerGeneratedCommands(program, state) {
|
|
6497
6617
|
const getGroup = (displayName, tag) => program.commands.find((c2) => c2.name() === displayName) ?? program.command(displayName).description(tag ?? `${displayName} endpoints`);
|
|
@@ -6893,6 +7013,16 @@ Examples:
|
|
|
6893
7013
|
addGlobalFlags(records.command("get")).description("Get one record").requiredOption("--record-id <id>", "record id").action(
|
|
6894
7014
|
runAction(state, (client, opts) => client.records.get({ recordId: opts.recordId }))
|
|
6895
7015
|
);
|
|
7016
|
+
addGlobalFlags(records.command("get-by-field")).description("Get one record by an exact field value").requiredOption("--base-id <id>", "Base id").requiredOption("--field-slug <slug>", "field slug").requiredOption("--value-text <value>", "exact field value").action(
|
|
7017
|
+
runAction(
|
|
7018
|
+
state,
|
|
7019
|
+
(client, opts) => getRecordByField(client, {
|
|
7020
|
+
baseId: opts.baseId,
|
|
7021
|
+
fieldSlug: opts.fieldSlug,
|
|
7022
|
+
valueText: opts.valueText
|
|
7023
|
+
})
|
|
7024
|
+
)
|
|
7025
|
+
);
|
|
6896
7026
|
addGlobalFlags(records.command("change-requests")).description("Change Requests for a record").requiredOption("--record-id <id>", "record id").action(
|
|
6897
7027
|
runAction(
|
|
6898
7028
|
state,
|
|
@@ -6981,7 +7111,7 @@ Examples:
|
|
|
6981
7111
|
addGlobalFlags(assetsCommand.command("grep")).description("Search every text-bearing asset in scope").requiredOption("--pattern <regex>", "literal or regex pattern").option("--flags <flags>", 'RegExp flags, e.g. "i" for case-insensitive').option("--asset-ids <ids...>", "scope: specific asset ids").option("--drive-path <path>", "scope: Drive/Skill mounted path prefix").option("--mime-types <types...>", "scope: MIME types").option("--max-matches <n>", "max matches (default 100, cap 1000)", parsePositiveInt).option("--context-lines <n>", "lines of before/after context (default 0, cap 10)", parseNum).action(
|
|
6982
7112
|
runAction(
|
|
6983
7113
|
state,
|
|
6984
|
-
(client, opts) => client
|
|
7114
|
+
(client, opts) => grepAssets(client, {
|
|
6985
7115
|
pattern: opts.pattern,
|
|
6986
7116
|
flags: opts.flags,
|
|
6987
7117
|
scope: opts.assetIds || opts.drivePath || opts.mimeTypes ? {
|
|
@@ -7043,19 +7173,19 @@ field with nothing linked yet does not trigger this.`
|
|
|
7043
7173
|
})
|
|
7044
7174
|
)
|
|
7045
7175
|
);
|
|
7046
|
-
addGlobalFlags(program.command("
|
|
7176
|
+
addGlobalFlags(program.command("export")).description("Export a node subtree as a Busabase package directory you can push to GitHub").argument("<node-slug-or-id>", "the folder/node to export").requiredOption("-o, --out-dir <dir>", "output directory for the package").option("--name <name>", "package name (default: reuse busabase.json, else the node slug)").option("--dry-run", "list the files that would be written and write nothing").addHelpText(
|
|
7047
7177
|
"after",
|
|
7048
7178
|
`
|
|
7049
|
-
Output is deterministic:
|
|
7179
|
+
Output is deterministic: exporting twice from an unchanged space produces
|
|
7050
7180
|
byte-identical files, so a GitHub diff shows exactly what changed.
|
|
7051
7181
|
|
|
7052
7182
|
Examples:
|
|
7053
|
-
busabase-cli
|
|
7054
|
-
busabase-cli
|
|
7183
|
+
busabase-cli export support-kb -o ./support-kb-template
|
|
7184
|
+
busabase-cli export support-kb -o ./support-kb-template --name "Support KB" --dry-run`
|
|
7055
7185
|
).action(
|
|
7056
7186
|
runArgAction(
|
|
7057
7187
|
state,
|
|
7058
|
-
(nodeSlugOrId, client, opts, config) =>
|
|
7188
|
+
(nodeSlugOrId, client, opts, config) => runExport(client, nodeSlugOrId, {
|
|
7059
7189
|
outDir: opts.outDir,
|
|
7060
7190
|
name: opts.name,
|
|
7061
7191
|
dryRun: Boolean(opts.dryRun),
|
|
@@ -7067,7 +7197,7 @@ Examples:
|
|
|
7067
7197
|
addGlobalFlags(program.command("backup")).description("Back up the active space to a full-fidelity .bbdump archive").option("-o, --out-file <file>", "output archive path", "space.bbdump").option(
|
|
7068
7198
|
"--no-history",
|
|
7069
7199
|
"full-fidelity rows, but omit the history tables (commits, change requests, operations, comments, reviews, audit events)"
|
|
7070
|
-
).option("--state-only", "superseded by `busabase-cli
|
|
7200
|
+
).option("--state-only", "superseded by `busabase-cli export` \u2014 see the error text").addHelpText(
|
|
7071
7201
|
"after",
|
|
7072
7202
|
`
|
|
7073
7203
|
A backup is full fidelity: raw rows with their original ids, every doc body, and
|
|
@@ -7079,7 +7209,7 @@ Examples:
|
|
|
7079
7209
|
busabase-cli backup -o ./backups/acme-2026-07-19.bbdump
|
|
7080
7210
|
busabase-cli backup --no-history # smaller: current state only
|
|
7081
7211
|
|
|
7082
|
-
To hand content to someone else rather than back it up, use \`busabase-cli
|
|
7212
|
+
To hand content to someone else rather than back it up, use \`busabase-cli export\`,
|
|
7083
7213
|
which writes a readable, diffable package directory you can push to GitHub.`
|
|
7084
7214
|
).action(
|
|
7085
7215
|
runAction(
|
|
@@ -7138,6 +7268,44 @@ To add content to a space that is already in use, use \`busabase-cli install\` i
|
|
|
7138
7268
|
);
|
|
7139
7269
|
})
|
|
7140
7270
|
);
|
|
7271
|
+
for (const facade of [
|
|
7272
|
+
{
|
|
7273
|
+
group: "docs",
|
|
7274
|
+
type: "doc",
|
|
7275
|
+
label: "Doc",
|
|
7276
|
+
detail: "body included",
|
|
7277
|
+
omitted: "no bodies"
|
|
7278
|
+
},
|
|
7279
|
+
{
|
|
7280
|
+
group: "files",
|
|
7281
|
+
type: "file",
|
|
7282
|
+
label: "File",
|
|
7283
|
+
detail: "backing Asset included",
|
|
7284
|
+
omitted: "no Asset detail"
|
|
7285
|
+
},
|
|
7286
|
+
{
|
|
7287
|
+
group: "folders",
|
|
7288
|
+
type: "folder",
|
|
7289
|
+
label: "Folder",
|
|
7290
|
+
detail: "direct children included",
|
|
7291
|
+
omitted: "no children"
|
|
7292
|
+
}
|
|
7293
|
+
]) {
|
|
7294
|
+
const group = program.commands.find((c2) => c2.name() === facade.group) ?? program.command(facade.group).description(`${facade.label} nodes`);
|
|
7295
|
+
addGlobalFlags(group.command("list")).description(`List ${facade.label} nodes \u2014 summaries only (${facade.omitted})`).addHelpText(
|
|
7296
|
+
"after",
|
|
7297
|
+
`
|
|
7298
|
+
OpenAPI: GET /nodes?types=${facade.type}
|
|
7299
|
+
Follow up with \`${facade.group} get --node-id <id>\` for one node's full detail.`
|
|
7300
|
+
).action(runAction(state, (client) => client.nodes.list({ types: [facade.type] })));
|
|
7301
|
+
addGlobalFlags(group.command("get")).description(`Get one ${facade.label} node (${facade.detail})`).requiredOption("--node-id <id>", `${facade.label} node id, or a slug unique to this type`).addHelpText("after", `
|
|
7302
|
+
OpenAPI: GET /nodes/{nodeId}?type=${facade.type}`).action(
|
|
7303
|
+
runAction(
|
|
7304
|
+
state,
|
|
7305
|
+
(client, opts) => client.nodes.get({ nodeId: opts.nodeId, type: facade.type })
|
|
7306
|
+
)
|
|
7307
|
+
);
|
|
7308
|
+
}
|
|
7141
7309
|
registerTaskCommands(program, BUSABASE_TASKS, {
|
|
7142
7310
|
addGlobalFlags,
|
|
7143
7311
|
parseFieldSpecs,
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "busa-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
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.
|
|
36
|
+
"busabase-sdk": "0.11.0",
|
|
37
37
|
"commander": "^14.0.0",
|
|
38
38
|
"tar-stream": "^3.1.7",
|
|
39
39
|
"yaml": "^2.9.0",
|