busa-cli 0.16.1 → 0.16.2

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.
@@ -1049,7 +1049,13 @@ var htmlNodeType = {
1049
1049
  label: "HTML",
1050
1050
  icon: "code-xml",
1051
1051
  capabilities: { hasDetail: true, creatable: true },
1052
- operations: []
1052
+ operations: [
1053
+ {
1054
+ kind: "html_document_update",
1055
+ label: "Update HTML",
1056
+ tone: "border-blue-200 bg-blue-50 text-blue-800"
1057
+ }
1058
+ ]
1053
1059
  };
1054
1060
 
1055
1061
  // ../../packages/busabase-contract/src/domains/skill/definition.ts
@@ -1068,7 +1074,13 @@ var whiteboardNodeType = {
1068
1074
  label: "Whiteboard",
1069
1075
  icon: "pen-tool",
1070
1076
  capabilities: { hasDetail: true, creatable: true },
1071
- operations: []
1077
+ operations: [
1078
+ {
1079
+ kind: "whiteboard_document_update",
1080
+ label: "Update whiteboard",
1081
+ tone: "border-blue-200 bg-blue-50 text-blue-800"
1082
+ }
1083
+ ]
1072
1084
  };
1073
1085
 
1074
1086
  // ../../packages/busabase-contract/src/domains/workflow/definition.ts
@@ -1077,7 +1089,13 @@ var workflowNodeType = {
1077
1089
  label: "Workflow",
1078
1090
  icon: "workflow",
1079
1091
  capabilities: { hasDetail: true, creatable: true },
1080
- operations: []
1092
+ operations: [
1093
+ {
1094
+ kind: "workflow_document_update",
1095
+ label: "Update workflow",
1096
+ tone: "border-blue-200 bg-blue-50 text-blue-800"
1097
+ }
1098
+ ]
1081
1099
  };
1082
1100
 
1083
1101
  // ../../packages/busabase-contract/src/domains/registry.ts
@@ -3564,12 +3582,8 @@ var nodeSchema = z8.lazy(
3564
3582
  slug: z8.string(),
3565
3583
  name: z8.string(),
3566
3584
  description: z8.string(),
3567
- metadata: z8.object({
3568
- entryFile: z8.string().optional(),
3569
- visibility: z8.enum(["private", "workspace", "public"]).optional(),
3570
- version: z8.string().optional(),
3571
- assetId: z8.string().optional()
3572
- }).catchall(z8.unknown()).default({}),
3585
+ metadata: z8.object({ version: z8.string().optional() }).catchall(z8.unknown()).default({}),
3586
+ explicitVisibility: z8.enum(["private", "workspace", "public"]).nullable().default(null),
3573
3587
  position: z8.number(),
3574
3588
  createdAt: z8.string(),
3575
3589
  updatedAt: z8.string(),
@@ -3676,7 +3690,17 @@ var commitSchema = z8.object({
3676
3690
  nodeId: z8.string().nullable(),
3677
3691
  operationId: z8.string().nullable(),
3678
3692
  parentCommitId: z8.string().nullable(),
3679
- fields: z8.record(z8.string(), z8.unknown()),
3693
+ // Deliberately loose (`z.record`), NOT a discriminated union keyed on `operation`.
3694
+ //
3695
+ // Do not "tighten" this. Commits already in the database were written before
3696
+ // per-operation payload validation existed, so their shapes carry no guarantee.
3697
+ // The write path (`insertCommit`) and the merge path (`parseCommitPayload`) are
3698
+ // strict precisely because they only ever touch freshly-written payloads; this VO
3699
+ // is also used to render *history* and approval detail pages, which read arbitrarily
3700
+ // old commits. Making it strict would turn any legacy-shaped row into a 500 on a
3701
+ // read-only screen. This is not a compatibility shim — it is the requirement not to
3702
+ // break reading data that already exists.
3703
+ payload: z8.record(z8.string(), z8.unknown()),
3680
3704
  operation: z8.enum(OPERATION_KINDS),
3681
3705
  message: z8.string(),
3682
3706
  author: z8.string(),
@@ -4216,7 +4240,7 @@ var PACKAGE_COMPUTED_FIELD_TYPES = [
4216
4240
  "auto_number",
4217
4241
  "formula",
4218
4242
  // `lookup` is derived from OTHER records and resolved at read time, so it
4219
- // shows up in `headCommit.fields` like any other value — but exporting it
4243
+ // shows up in `headCommit.payload` like any other value — but exporting it
4220
4244
  // would ship derived data as if it were authored, and install would strip it
4221
4245
  // server-side anyway.
4222
4246
  "lookup",
@@ -4542,14 +4566,18 @@ var applyInstall = async (client, plan, options) => {
4542
4566
  options.serverUrl
4543
4567
  );
4544
4568
  progress("Pass 1/5: creating the node tree\u2026");
4545
- state.targetFolderNodeId = await createFolderNode(client, {
4546
- parentNodeId: void 0,
4547
- slug: plan.targetFolderSlug,
4548
- name: plan.tree.manifest.name,
4549
- description: plan.tree.manifest.description,
4550
- submittedBy: options.submittedBy
4551
- });
4552
- state.created.folders++;
4569
+ if (plan.existingTargetFolderNodeId) {
4570
+ state.targetFolderNodeId = plan.existingTargetFolderNodeId;
4571
+ } else {
4572
+ state.targetFolderNodeId = await createFolderNode(client, {
4573
+ parentNodeId: void 0,
4574
+ slug: plan.targetFolderSlug,
4575
+ name: plan.tree.manifest.name,
4576
+ description: plan.tree.manifest.description,
4577
+ submittedBy: options.submittedBy
4578
+ });
4579
+ state.created.folders++;
4580
+ }
4553
4581
  await createStructure(
4554
4582
  client,
4555
4583
  plan.tree.nodes,
@@ -5293,7 +5321,7 @@ var collectRecords = async (client, baseId, fields, options) => {
5293
5321
  for (const record of page.records) {
5294
5322
  if (record.status !== "active") continue;
5295
5323
  const values = {};
5296
- for (const [slug, value] of Object.entries(record.headCommit.fields ?? {})) {
5324
+ for (const [slug, value] of Object.entries(record.headCommit.payload ?? {})) {
5297
5325
  const type = typeBySlug.get(slug);
5298
5326
  if (!type) continue;
5299
5327
  if (PACKAGE_COMPUTED_FIELD_TYPES.includes(type)) continue;
@@ -6059,27 +6087,39 @@ var writePackageFiles = async (files, outDir, options = {}) => {
6059
6087
  };
6060
6088
 
6061
6089
  // ../../packages/busabase-package/src/plan.ts
6090
+ var flattenExistingNodes = (nodes) => {
6091
+ const result = [];
6092
+ const visit = (node) => {
6093
+ result.push(node);
6094
+ for (const child of node.children ?? []) visit(child);
6095
+ };
6096
+ for (const node of nodes) visit(node);
6097
+ return result;
6098
+ };
6062
6099
  var findTargetFolder = (nodes, slug) => {
6063
- const roots = nodes.length === 1 && nodes[0].children ? nodes[0].children : nodes;
6064
- return roots.find((node) => node.slug === slug && node.type === "folder");
6100
+ return flattenExistingNodes(nodes).find((node) => node.slug === slug && node.type === "folder");
6065
6101
  };
6066
6102
  var resolveTargetState = async (client, targetFolderSlug) => {
6067
- const [nodes, bases] = await Promise.all([client.nodes.list({}), client.bases.list({})]);
6103
+ const listedNodes = await client.nodes.list({});
6104
+ const slugsByType = /* @__PURE__ */ new Map();
6105
+ for (const node of flattenExistingNodes(listedNodes)) {
6106
+ const slugs = slugsByType.get(node.type) ?? /* @__PURE__ */ new Set();
6107
+ slugs.add(node.slug);
6108
+ slugsByType.set(node.type, slugs);
6109
+ }
6068
6110
  return {
6069
- targetFolder: findTargetFolder(nodes, targetFolderSlug),
6070
- existingBaseSlugs: new Set(bases.map((base) => base.slug))
6111
+ targetFolder: findTargetFolder(listedNodes, targetFolderSlug),
6112
+ existingNodeSlugsByType: slugsByType
6071
6113
  };
6072
6114
  };
6073
6115
  var buildInstallPlan = (tree, target, options = {}) => {
6074
6116
  const targetFolderSlug = options.intoFolder ?? suggestSlug(tree.manifest.name);
6075
6117
  const warnings = [];
6076
- const existingChildSlugs = new Set(
6077
- (target.targetFolder?.children ?? []).map((child) => child.slug)
6078
- );
6079
- const collisions = findCollisions(tree, targetFolderSlug, existingChildSlugs, target);
6118
+ assertPackageNodeIdentitiesUnique(tree);
6119
+ const collisions = findCollisions(tree, targetFolderSlug, target);
6080
6120
  let planned = tree;
6081
6121
  if (options.rename && collisions.length > 0) {
6082
- planned = applyRenames(tree, collisions, existingChildSlugs, target.existingBaseSlugs);
6122
+ planned = applyRenames(tree, collisions, target.existingNodeSlugsByType);
6083
6123
  }
6084
6124
  collectWarnings(planned, warnings);
6085
6125
  const counts = countTree(planned);
@@ -6090,6 +6130,7 @@ var buildInstallPlan = (tree, target, options = {}) => {
6090
6130
  }
6091
6131
  return {
6092
6132
  targetFolderSlug,
6133
+ existingTargetFolderNodeId: target.targetFolder?.id,
6093
6134
  tree: planned,
6094
6135
  collisions,
6095
6136
  warnings,
@@ -6107,24 +6148,33 @@ var hasUnlinkableRelationValues = (node) => {
6107
6148
  })
6108
6149
  );
6109
6150
  };
6110
- var findCollisions = (tree, targetFolderSlug, existingChildSlugs, target) => {
6151
+ var findCollisions = (tree, targetFolderSlug, target) => {
6111
6152
  const collisions = [];
6112
- for (const node of tree.nodes) {
6113
- if (existingChildSlugs.has(node.slug)) {
6114
- collisions.push({ kind: "node", slug: node.slug, path: `${targetFolderSlug}/${node.slug}` });
6115
- }
6116
- }
6117
- for (const baseNode of collectBaseNodes(tree.nodes)) {
6118
- if (target.existingBaseSlugs.has(baseNode.slug)) {
6153
+ for (const node of walkNodes(tree.nodes)) {
6154
+ if (target.existingNodeSlugsByType.get(node.type)?.has(node.slug)) {
6119
6155
  collisions.push({
6120
- kind: "base",
6121
- slug: baseNode.slug,
6122
- path: `${targetFolderSlug}/\u2026 /${baseNode.slug} (base slugs are unique per space)`
6156
+ kind: node.type === "base" ? "base" : "node",
6157
+ nodeType: node.type,
6158
+ slug: node.slug,
6159
+ path: `${targetFolderSlug}/\u2026/${node.slug} (${node.type} slugs are unique per space)`
6123
6160
  });
6124
6161
  }
6125
6162
  }
6126
6163
  return collisions;
6127
6164
  };
6165
+ var nodeIdentityKey = (node) => `${node.type}\0${node.slug}`;
6166
+ var assertPackageNodeIdentitiesUnique = (tree) => {
6167
+ const seen = /* @__PURE__ */ new Set();
6168
+ for (const node of walkNodes(tree.nodes)) {
6169
+ const key = nodeIdentityKey(node);
6170
+ if (seen.has(key)) {
6171
+ throw new Error(
6172
+ `Package contains duplicate ${node.type} slug "${node.slug}". Node slugs must be unique by type within a space.`
6173
+ );
6174
+ }
6175
+ seen.add(key);
6176
+ }
6177
+ };
6128
6178
  var nextFreeSlug = (slug, taken) => {
6129
6179
  for (let suffix = 2; suffix < 1e3; suffix++) {
6130
6180
  const candidate = `${slug}-${suffix}`;
@@ -6132,38 +6182,42 @@ var nextFreeSlug = (slug, taken) => {
6132
6182
  }
6133
6183
  throw new Error(`Could not find a free slug for "${slug}" after 999 attempts.`);
6134
6184
  };
6135
- var applyRenames = (tree, collisions, existingChildSlugs, existingBaseSlugs) => {
6136
- const packageBaseSlugs = new Set(collectBaseNodes(tree.nodes).map((node) => node.slug));
6137
- const takenNodeSlugs = /* @__PURE__ */ new Set([...existingChildSlugs, ...tree.nodes.map((node) => node.slug)]);
6138
- const takenBaseSlugs = /* @__PURE__ */ new Set([...existingBaseSlugs, ...packageBaseSlugs]);
6185
+ var applyRenames = (tree, collisions, existingNodeSlugsByType) => {
6186
+ const takenByType = /* @__PURE__ */ new Map();
6187
+ for (const [type, slugs] of existingNodeSlugsByType) {
6188
+ takenByType.set(type, new Set(slugs));
6189
+ }
6190
+ for (const node of walkNodes(tree.nodes)) {
6191
+ const taken = takenByType.get(node.type) ?? /* @__PURE__ */ new Set();
6192
+ taken.add(node.slug);
6193
+ takenByType.set(node.type, taken);
6194
+ }
6139
6195
  const nodeRenames = /* @__PURE__ */ new Map();
6140
6196
  const baseRenames = /* @__PURE__ */ new Map();
6141
6197
  for (const collision of collisions) {
6142
- if (collision.kind === "node") {
6143
- const renamed = nextFreeSlug(collision.slug, takenNodeSlugs);
6144
- takenNodeSlugs.add(renamed);
6145
- nodeRenames.set(collision.slug, renamed);
6146
- collision.renamedTo = renamed;
6147
- } else {
6148
- const renamed = nextFreeSlug(collision.slug, takenBaseSlugs);
6149
- takenBaseSlugs.add(renamed);
6150
- baseRenames.set(collision.slug, renamed);
6151
- collision.renamedTo = renamed;
6152
- }
6153
- }
6154
- const rewriteNode = (node, isTopLevel) => {
6198
+ const node = [...walkNodes(tree.nodes)].find(
6199
+ (candidate) => candidate.type === collision.nodeType && candidate.slug === collision.slug
6200
+ );
6201
+ if (!node) continue;
6202
+ const taken = takenByType.get(node.type) ?? /* @__PURE__ */ new Set();
6203
+ const renamed = nextFreeSlug(collision.slug, taken);
6204
+ taken.add(renamed);
6205
+ takenByType.set(node.type, taken);
6206
+ nodeRenames.set(nodeIdentityKey(node), renamed);
6207
+ if (node.type === "base") baseRenames.set(node.slug, renamed);
6208
+ collision.renamedTo = renamed;
6209
+ }
6210
+ const rewriteNode = (node) => {
6211
+ const slug = nodeRenames.get(nodeIdentityKey(node)) ?? node.slug;
6155
6212
  if (node.type === "base") {
6156
- const renamed = baseRenames.get(node.slug) ?? (isTopLevel ? nodeRenames.get(node.slug) : void 0);
6157
- const slug2 = renamed ?? node.slug;
6158
6213
  return {
6159
6214
  ...node,
6160
- slug: slug2,
6215
+ slug,
6161
6216
  base: { ...node.base, fields: node.base.fields.map(rewriteFieldTarget) }
6162
6217
  };
6163
6218
  }
6164
- const slug = (isTopLevel ? nodeRenames.get(node.slug) : void 0) ?? node.slug;
6165
6219
  if (node.type === "folder") {
6166
- return { ...node, slug, children: node.children.map((child) => rewriteNode(child, false)) };
6220
+ return { ...node, slug, children: node.children.map(rewriteNode) };
6167
6221
  }
6168
6222
  return { ...node, slug };
6169
6223
  };
@@ -6174,7 +6228,7 @@ var applyRenames = (tree, collisions, existingChildSlugs, existingBaseSlugs) =>
6174
6228
  if (!renamed) return field;
6175
6229
  return { ...field, options: { ...field.options, targetBaseSlug: renamed } };
6176
6230
  };
6177
- return { ...tree, nodes: tree.nodes.map((node) => rewriteNode(node, true)) };
6231
+ return { ...tree, nodes: tree.nodes.map(rewriteNode) };
6178
6232
  };
6179
6233
  var collectWarnings = (tree, warnings) => {
6180
6234
  for (const baseNode of collectBaseNodes(tree.nodes)) {
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCli
4
- } from "./chunk-FCF6L4Y2.js";
4
+ } from "./chunk-AHIRHKUB.js";
5
5
 
6
6
  // src/cli.ts
7
7
  process.exit(await runCli(process.argv.slice(2)));
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  HELP,
3
3
  render,
4
4
  runCli
5
- } from "./chunk-FCF6L4Y2.js";
5
+ } from "./chunk-AHIRHKUB.js";
6
6
 
7
7
  // src/index.ts
8
8
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "busa-cli",
3
- "version": "0.16.1",
3
+ "version": "0.16.2",
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.16.1",
36
+ "busabase-sdk": "0.16.2",
37
37
  "commander": "^14.0.0",
38
38
  "tar-stream": "^3.1.7",
39
39
  "yaml": "^2.9.0",