busa-cli 0.11.4 → 0.14.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.
@@ -82,17 +82,17 @@ function isNodeLike(value) {
82
82
  }
83
83
  function renderNodeTree(nodes) {
84
84
  const lines = [];
85
- const walk2 = (items, prefix, isRoot) => {
85
+ const walk = (items, prefix, isRoot) => {
86
86
  items.forEach((node, index) => {
87
87
  const isLast = index === items.length - 1;
88
88
  const branch = isRoot ? "" : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
89
89
  const childPrefix = isRoot ? "" : `${prefix}${isLast ? " " : "\u2502 "}`;
90
90
  lines.push(`${prefix}${branch}${formatNode(node)}`);
91
91
  const children = Array.isArray(node.children) ? node.children.filter(isNodeLike) : [];
92
- walk2(children, childPrefix, false);
92
+ walk(children, childPrefix, false);
93
93
  });
94
94
  };
95
- walk2(nodes, "", true);
95
+ walk(nodes, "", true);
96
96
  return lines.join("\n");
97
97
  }
98
98
  function formatNode(node) {
@@ -122,7 +122,7 @@ function nodeIcon(type) {
122
122
  // src/run.ts
123
123
  import { createHash as createHash6 } from "crypto";
124
124
  import { readFileSync as readFileSync2 } from "fs";
125
- import { basename as basename2, extname as extname2 } from "path";
125
+ import { basename as basename2, extname as extname3 } from "path";
126
126
 
127
127
  // ../../packages/busabase-contract/src/tasks/base-field.ts
128
128
  var OPERATIONS = ["add", "update", "delete", "convert", "reorder", "restore"];
@@ -1038,7 +1038,7 @@ var folderNodeType = {
1038
1038
  var formNodeType = {
1039
1039
  type: "form",
1040
1040
  label: "Form",
1041
- icon: "clipboard-list",
1041
+ icon: "form",
1042
1042
  capabilities: { hasDetail: true, creatable: true },
1043
1043
  operations: []
1044
1044
  };
@@ -1255,7 +1255,13 @@ var nodeCreateTask = {
1255
1255
  name: "version",
1256
1256
  kind: "string",
1257
1257
  appliesWhen: { param: "type", values: ["skill", "drive", "airapp"] },
1258
- description: "Skill/Drive/AirApp only. Defaults to 0.1.0."
1258
+ description: "Skill/Drive/AirApp only. Defaults to 0.1.0.",
1259
+ // A bare `--version` collides with the CLI's own root `-v, --version`
1260
+ // flag: commander resolves that flag at the root command regardless of
1261
+ // where it appears in argv, so `nodes create --version 0.4.0` silently
1262
+ // printed the CLI's package version instead of setting the app's
1263
+ // version. `--app-version` sidesteps the collision outright.
1264
+ cliFlag: "--app-version"
1259
1265
  },
1260
1266
  {
1261
1267
  name: "autoMerge",
@@ -2937,6 +2943,9 @@ async function deviceLogin(baseUrl, useBrowser) {
2937
2943
  const deadline = Date.now() + expiresInSeconds * 1e3;
2938
2944
  let intervalSeconds = Math.max(1, code.interval ?? 5);
2939
2945
  let consecutiveNetworkFailures = 0;
2946
+ let pendingPollCount = 0;
2947
+ let reassuredAfterApprovalHint = false;
2948
+ const REASSURE_AFTER_POLLS = 4;
2940
2949
  while (Date.now() < deadline) {
2941
2950
  await delay(intervalSeconds * 1e3);
2942
2951
  let tokenResponse;
@@ -2984,6 +2993,13 @@ async function deviceLogin(baseUrl, useBrowser) {
2984
2993
  }
2985
2994
  switch (payload.error) {
2986
2995
  case "authorization_pending":
2996
+ pendingPollCount += 1;
2997
+ if (pendingPollCount >= REASSURE_AFTER_POLLS && !reassuredAfterApprovalHint) {
2998
+ reassuredAfterApprovalHint = true;
2999
+ say2(
3000
+ "Still waiting\u2026 if you already approved in the browser, this can take a few extra seconds to register \u2014 no need to retry."
3001
+ );
3002
+ }
2987
3003
  continue;
2988
3004
  case "slow_down":
2989
3005
  intervalSeconds += 5;
@@ -3021,29 +3037,16 @@ async function refreshToken(baseUrl, refreshToken2) {
3021
3037
  expiresAt: typeof payload.expires_in === "number" ? new Date(Date.now() + payload.expires_in * 1e3).toISOString() : void 0
3022
3038
  };
3023
3039
  }
3024
- async function pickSpaceId(verify, preselected) {
3040
+ function pickSpaceId(verify, preselected) {
3025
3041
  if (preselected) return preselected;
3026
3042
  const spaces = verify.spaces ?? [];
3027
- if (spaces.length <= 1) return verify.space?.id ?? spaces[0]?.id;
3028
- if (!isInteractive()) {
3029
- const choices = spaces.map((space) => `${space.name} [${space.id}]`).join(", ");
3030
- throw new Error(
3031
- `You belong to ${spaces.length} spaces; pass --space-id <id> to choose one. Available spaces: ${choices}`
3032
- );
3033
- }
3034
- say2("");
3035
- say2("Which space should this CLI target?");
3036
- spaces.forEach((space, index) => {
3037
- const marker = space.id === verify.space?.id ? " (default)" : "";
3038
- say2(` ${index + 1}. ${space.name}${marker} [${space.id}]`);
3039
- });
3040
- const answer = await ask2(`Choose 1-${spaces.length} (Enter for default): `);
3041
- if (!answer) return verify.space?.id ?? spaces[0]?.id;
3042
- const choice = Number(answer);
3043
- if (Number.isInteger(choice) && choice >= 1 && choice <= spaces.length) {
3044
- return spaces[choice - 1]?.id;
3045
- }
3046
- throw new Error("Not a valid space choice. Re-run login and choose one of the listed spaces.");
3043
+ const fallback = verify.space?.id ?? spaces[0]?.id;
3044
+ if (spaces.length <= 1) return fallback;
3045
+ const choices = spaces.map((space) => `${space.name} [${space.id}]`).join(", ");
3046
+ say2(
3047
+ `Note: this account belongs to ${spaces.length} spaces; defaulted to "${verify.space?.name ?? spaces[0]?.name}". Run \`busabase-cli space use <id>\` to switch. Available: ${choices}`
3048
+ );
3049
+ return fallback;
3047
3050
  }
3048
3051
  async function chooseTarget() {
3049
3052
  const cloud = normalizeBaseUrl(DEFAULT_BASE_URL);
@@ -3140,7 +3143,7 @@ async function runLogin(options) {
3140
3143
  }
3141
3144
  say2("Verifying\u2026");
3142
3145
  const verify = await verifyAuth(baseUrl, token);
3143
- const spaceId = await pickSpaceId(verify, options.spaceId);
3146
+ const spaceId = pickSpaceId(verify, options.spaceId);
3144
3147
  writeDotEnvFile({
3145
3148
  BUSABASE_BASE_URL: baseUrl,
3146
3149
  BUSABASE_API_KEY: token,
@@ -3157,6 +3160,9 @@ async function runLogin(options) {
3157
3160
  credentialType: method === "device" ? "api_key" : expiresAt ? "oauth" : "api_key",
3158
3161
  user: verify.user?.email ?? verify.user?.name ?? verify.user?.id ?? "(unknown)",
3159
3162
  space: spaceId ?? "(server default)",
3163
+ // Surfaced so a non-interactive caller (typically an agent) can see whether the
3164
+ // fallback default is the space it actually wants, and switch with `space use <id>`.
3165
+ availableSpaces: (verify.spaces ?? []).map((space) => `${space.name} [${space.id}]`).join(", "),
3160
3166
  createdSpace: String(Boolean(verify.createdSpace)),
3161
3167
  bootstrapRequired: String(Boolean(verify.bootstrapRequired)),
3162
3168
  expiresAt: expiresAt ?? apiKeyExpiresAt ?? "(no expiry \u2014 API key)",
@@ -3642,7 +3648,11 @@ var updateNodeMetadataInputSchema = z8.object({
3642
3648
  });
3643
3649
  var searchNodesByNameInputSchema = z8.object({
3644
3650
  query: z8.string().min(1),
3645
- limit: z8.number().int().min(1).max(50).optional().default(20)
3651
+ // GET route — query params arrive as strings, and oRPC's OpenAPI handler
3652
+ // does not coerce them. A bare `z.number()` here rejected every real
3653
+ // `?limit=` call with "expected number, received string"; every other
3654
+ // limit/page field on a GET route in this file already uses `z.coerce`.
3655
+ limit: z8.coerce.number().int().min(1).max(50).optional().default(20)
3646
3656
  });
3647
3657
  var nodeSearchResultSchema = z8.object({
3648
3658
  id: z8.string(),
@@ -4189,11 +4199,13 @@ var viewChangeRequestInputSchema = z9.discriminatedUnion("operation", [
4189
4199
  var PACKAGE_FORMAT = "busabase-package@1";
4190
4200
  var PACKAGE_MANIFEST_FILENAME = "busabase.json";
4191
4201
  var PACKAGE_CONTENT_DIRNAME = "content";
4202
+ var PACKAGE_ASSETS_DIRNAME = "assets";
4192
4203
  var PACKAGE_NODE_META_FILENAME = "_node.json";
4193
4204
  var PACKAGE_FOLDER_META_FILENAME = "_folder.json";
4194
4205
  var PACKAGE_BASE_FILENAME = "base.json";
4195
4206
  var PACKAGE_RECORDS_FILENAME = "records.ndjson";
4196
4207
  var PACKAGE_NODE_META_SUFFIX = ".node.json";
4208
+ var PACKAGE_ASSET_META_SUFFIX = ".asset.json";
4197
4209
  var PACKAGE_FILE_TREE_NODE_TYPES = ["skill", "airapp", "drive"];
4198
4210
  var PACKAGE_AI_FIELD_TYPES = ["ai_summary", "ai_tags"];
4199
4211
  var PACKAGE_COMPUTED_FIELD_TYPES = [
@@ -4251,6 +4263,14 @@ var PackageFolderMetaSchema = z10.object({
4251
4263
  position: z10.number().int().optional()
4252
4264
  });
4253
4265
  var PackageFileNodeMetaSchema = PackageFolderMetaSchema;
4266
+ var PackageDocAssetMetaSchema = z10.object({
4267
+ assetId: z10.string().min(1),
4268
+ /** The asset's original name, restored on the target so its library entry reads the same. */
4269
+ fileName: z10.string().min(1),
4270
+ mimeType: z10.string().min(1),
4271
+ /** `sha256:<hex>` when the source host had one — lets the target dedupe instead of re-storing. */
4272
+ contentHash: z10.string().optional()
4273
+ });
4254
4274
  var PackageDocFrontmatterSchema = z10.object({
4255
4275
  name: z10.string().min(1),
4256
4276
  description: z10.string().default(""),
@@ -4363,6 +4383,9 @@ var PackageRecordLineSchema = z10.object({
4363
4383
  // ../../packages/busabase-package/src/apply.ts
4364
4384
  import { createHash as createHash5 } from "crypto";
4365
4385
 
4386
+ // ../../packages/busabase-package/src/doc-asset-refs.ts
4387
+ import { extname as extname2 } from "path";
4388
+
4366
4389
  // ../../packages/busabase-package/src/tree.ts
4367
4390
  import { extname } from "path";
4368
4391
  var NODE_SLUG_PATTERN = /^[a-z0-9-]+$/;
@@ -4464,6 +4487,39 @@ var walkNodes = function* (nodes) {
4464
4487
  };
4465
4488
  var collectBaseNodes = (nodes) => [...walkNodes(nodes)].filter((node) => node.type === "base");
4466
4489
 
4490
+ // ../../packages/busabase-package/src/doc-asset-refs.ts
4491
+ var ASSET_CONTENT_URL_PATTERN = /\/api\/assets\/([A-Za-z0-9_-]+)\/raw/g;
4492
+ var buildAssetContentUrl = (assetId) => `/api/assets/${encodeURIComponent(assetId)}/raw`;
4493
+ var extractDocAssetIds = (body) => {
4494
+ const ids = /* @__PURE__ */ new Set();
4495
+ for (const match of body.matchAll(ASSET_CONTENT_URL_PATTERN)) {
4496
+ const assetId = match[1];
4497
+ if (assetId) ids.add(assetId);
4498
+ }
4499
+ return [...ids];
4500
+ };
4501
+ var collectDocAssetIds = (nodes) => {
4502
+ const ids = /* @__PURE__ */ new Set();
4503
+ for (const node of walkNodes(nodes)) {
4504
+ if (node.type !== "doc") continue;
4505
+ for (const assetId of extractDocAssetIds(node.body)) ids.add(assetId);
4506
+ }
4507
+ return [...ids];
4508
+ };
4509
+ var docAssetBytesPath = (asset) => `${PACKAGE_ASSETS_DIRNAME}/${asset.assetId}${extname2(asset.fileName).toLowerCase()}`;
4510
+ var rewriteDocAssetIds = (body, idMap) => {
4511
+ if (idMap.size === 0) return body;
4512
+ return body.replace(ASSET_CONTENT_URL_PATTERN, (whole, assetId) => {
4513
+ const replacement = idMap.get(assetId);
4514
+ return replacement === void 0 ? whole : buildAssetContentUrl(replacement);
4515
+ });
4516
+ };
4517
+ var reportUnresolvableDocAssets = (slug, body, carried = /* @__PURE__ */ new Set()) => {
4518
+ const unresolved = extractDocAssetIds(body).filter((assetId) => !carried.has(assetId));
4519
+ if (unresolved.length === 0) return void 0;
4520
+ return `Doc "${slug}" embeds ${unresolved.length} image(s) that this package does not carry (${unresolved.join(", ")}). Everything else about the doc is intact, but those images will not render on another host. Re-upload them in the Doc editor, or check that the source host can still serve them.`;
4521
+ };
4522
+
4467
4523
  // ../../packages/busabase-package/src/apply.ts
4468
4524
  var RECORD_BATCH_SIZE = 200;
4469
4525
  var fieldKey = (baseSlug, fieldSlug) => `${baseSlug}\0${fieldSlug}`;
@@ -4478,6 +4534,13 @@ var applyInstall = async (client, plan, options) => {
4478
4534
  const bases = [];
4479
4535
  const progress = options.onProgress ?? (() => {
4480
4536
  });
4537
+ const docAssetIds = await uploadDocAssets(
4538
+ client,
4539
+ plan.tree.assets ?? [],
4540
+ state,
4541
+ progress,
4542
+ options.serverUrl
4543
+ );
4481
4544
  progress("Pass 1/5: creating the node tree\u2026");
4482
4545
  state.targetFolderNodeId = await createFolderNode(client, {
4483
4546
  parentNodeId: void 0,
@@ -4494,7 +4557,8 @@ var applyInstall = async (client, plan, options) => {
4494
4557
  options,
4495
4558
  state,
4496
4559
  bases,
4497
- fieldIds
4560
+ fieldIds,
4561
+ docAssetIds
4498
4562
  );
4499
4563
  const deferred = bases.filter((base) => base.node.base.fields.some(isDeferredField));
4500
4564
  if (deferred.length > 0) {
@@ -4570,7 +4634,28 @@ var applyInstall = async (client, plan, options) => {
4570
4634
  }
4571
4635
  return state;
4572
4636
  };
4573
- var createStructure = async (client, nodes, parentNodeId, options, state, bases, fieldIds) => {
4637
+ var uploadDocAssets = async (client, assets, state, progress, serverUrl) => {
4638
+ const idMap = /* @__PURE__ */ new Map();
4639
+ if (assets.length === 0) return idMap;
4640
+ progress(`Pass 1/5: uploading ${assets.length} doc image(s)\u2026`);
4641
+ for (const asset of assets) {
4642
+ try {
4643
+ const uploaded = await uploadAsset(
4644
+ client,
4645
+ asset.fileName,
4646
+ asset.mimeType,
4647
+ asset.bytes,
4648
+ "doc",
4649
+ serverUrl
4650
+ );
4651
+ idMap.set(asset.assetId, uploaded.assetId);
4652
+ } catch (error) {
4653
+ state.warnings.push(asSkippableUpload(error, asset.fileName));
4654
+ }
4655
+ }
4656
+ return idMap;
4657
+ };
4658
+ var createStructure = async (client, nodes, parentNodeId, options, state, bases, fieldIds, docAssetIds) => {
4574
4659
  for (const node of nodes) {
4575
4660
  switch (node.type) {
4576
4661
  case "folder": {
@@ -4582,16 +4667,31 @@ var createStructure = async (client, nodes, parentNodeId, options, state, bases,
4582
4667
  submittedBy: options.submittedBy
4583
4668
  });
4584
4669
  state.created.folders++;
4585
- await createStructure(client, node.children, nodeId, options, state, bases, fieldIds);
4670
+ await createStructure(
4671
+ client,
4672
+ node.children,
4673
+ nodeId,
4674
+ options,
4675
+ state,
4676
+ bases,
4677
+ fieldIds,
4678
+ docAssetIds
4679
+ );
4586
4680
  break;
4587
4681
  }
4588
4682
  case "doc": {
4683
+ const warning = reportUnresolvableDocAssets(
4684
+ node.slug,
4685
+ node.body,
4686
+ new Set(docAssetIds.keys())
4687
+ );
4688
+ if (warning) state.warnings.push(warning);
4589
4689
  const result = await client.docs.create({
4590
4690
  parentNodeId,
4591
4691
  slug: node.slug,
4592
4692
  name: node.name,
4593
4693
  description: node.description,
4594
- body: node.body,
4694
+ body: rewriteDocAssetIds(node.body, docAssetIds),
4595
4695
  autoMerge: options.autoMerge
4596
4696
  });
4597
4697
  if (result.materialized) state.created.docs++;
@@ -4611,7 +4711,14 @@ var createStructure = async (client, nodes, parentNodeId, options, state, bases,
4611
4711
  case "file": {
4612
4712
  let asset;
4613
4713
  try {
4614
- asset = await uploadAsset(client, node.fileName, node.mimeType, node.bytes);
4714
+ asset = await uploadAsset(
4715
+ client,
4716
+ node.fileName,
4717
+ node.mimeType,
4718
+ node.bytes,
4719
+ void 0,
4720
+ options.serverUrl
4721
+ );
4615
4722
  } catch (error) {
4616
4723
  state.warnings.push(asSkippableUpload(error, node.fileName));
4617
4724
  break;
@@ -4722,7 +4829,9 @@ var createFileTreeNode = async (client, node, parentNodeId, options, state) => {
4722
4829
  client,
4723
4830
  entry.path.split("/").at(-1) ?? entry.path,
4724
4831
  mimeType,
4725
- entry.bytes
4832
+ entry.bytes,
4833
+ void 0,
4834
+ options.serverUrl
4726
4835
  );
4727
4836
  return { path: entry.path, assetId: asset.assetId, mimeType };
4728
4837
  } catch (error) {
@@ -4753,13 +4862,25 @@ var createFileTreeNode = async (client, node, parentNodeId, options, state) => {
4753
4862
  };
4754
4863
  var UploadTargetUnsupportedError = class extends Error {
4755
4864
  };
4756
- var uploadAsset = async (client, fileName, mimeType, bytes) => {
4865
+ var resolveUploadUrl = (uploadUrl, serverUrl) => {
4866
+ if (/^https?:\/\//.test(uploadUrl)) return uploadUrl;
4867
+ if (serverUrl) {
4868
+ try {
4869
+ return new URL(uploadUrl, serverUrl).toString();
4870
+ } catch {
4871
+ }
4872
+ }
4873
+ throw new UploadTargetUnsupportedError(
4874
+ `this server issued a root-relative upload url (${uploadUrl}) and no server origin was supplied to resolve it against`
4875
+ );
4876
+ };
4877
+ var uploadAsset = async (client, fileName, mimeType, bytes, context = "record-field", serverUrl) => {
4757
4878
  const contentHash = `sha256:${createHash5("sha256").update(bytes).digest("hex")}`;
4758
4879
  const requested = await client.assets.createUploadUrl({
4759
4880
  fileName,
4760
4881
  mimeType,
4761
4882
  sizeBytes: bytes.byteLength,
4762
- context: "record-field",
4883
+ context,
4763
4884
  contentHash
4764
4885
  });
4765
4886
  if (requested.duplicate) {
@@ -4768,12 +4889,8 @@ var uploadAsset = async (client, fileName, mimeType, bytes) => {
4768
4889
  throw new Error(`Server reported "${fileName}" as a duplicate but returned no asset id.`);
4769
4890
  return { assetId: assetId2 };
4770
4891
  }
4771
- if (!/^https?:\/\//.test(requested.uploadUrl)) {
4772
- throw new UploadTargetUnsupportedError(
4773
- `this server issued a non-absolute upload url (${requested.uploadUrl}), which only its own web UI knows how to use`
4774
- );
4775
- }
4776
- const uploaded = await fetch(requested.uploadUrl, {
4892
+ const uploadUrl = resolveUploadUrl(requested.uploadUrl, serverUrl);
4893
+ const uploaded = await fetch(uploadUrl, {
4777
4894
  method: "PUT",
4778
4895
  body: new Uint8Array(bytes),
4779
4896
  headers: { "content-type": mimeType }
@@ -4786,7 +4903,7 @@ var uploadAsset = async (client, fileName, mimeType, bytes) => {
4786
4903
  fileName,
4787
4904
  mimeType,
4788
4905
  sizeBytes: bytes.byteLength,
4789
- context: "record-field",
4906
+ context,
4790
4907
  contentHash
4791
4908
  });
4792
4909
  const assetId = confirmed.assetId ?? confirmed.attachmentId;
@@ -4979,10 +5096,35 @@ var findSourceNode = (nodes, slugOrId) => {
4979
5096
  var collectPackageTree = async (client, root, options) => {
4980
5097
  const children = root.children ?? [];
4981
5098
  const nodes = await collectNodes(client, children, options);
4982
- const tree = { manifest: { ...options.manifest, format: PACKAGE_FORMAT }, nodes };
5099
+ const tree = {
5100
+ manifest: { ...options.manifest, format: PACKAGE_FORMAT },
5101
+ nodes,
5102
+ assets: await collectDocAssets(client, nodes, options)
5103
+ };
4983
5104
  assertSelfContained(tree, options);
4984
5105
  return tree;
4985
5106
  };
5107
+ var collectDocAssets = async (client, nodes, options) => {
5108
+ const assets = [];
5109
+ for (const assetId of collectDocAssetIds(nodes)) {
5110
+ try {
5111
+ const detail = await client.assets.download({ assetId });
5112
+ assets.push({
5113
+ assetId,
5114
+ fileName: detail.fileName,
5115
+ mimeType: detail.mimeType,
5116
+ contentHash: detail.contentHash ?? void 0,
5117
+ // See CollectOptions.baseUrl — a local server's download url is root-relative.
5118
+ bytes: await downloadBytes(detail.downloadUrl, detail.fileName, options)
5119
+ });
5120
+ } catch (error) {
5121
+ options.warn(
5122
+ `Could not read the image ${assetId} embedded in a doc (${error instanceof Error ? error.message : String(error)}). The package ships without it.`
5123
+ );
5124
+ }
5125
+ }
5126
+ return assets;
5127
+ };
4986
5128
  var collectNodes = async (client, sources, options) => {
4987
5129
  const nodes = [];
4988
5130
  for (const source of sources) {
@@ -5221,7 +5363,13 @@ var downloadBytes = async (url, label, options) => {
5221
5363
  return Buffer.from(await response.arrayBuffer());
5222
5364
  };
5223
5365
  var assertSelfContained = (tree, options) => {
5224
- const baseNodes = [...walk(tree.nodes)].filter((node) => node.type === "base");
5366
+ const carriedAssetIds = new Set((tree.assets ?? []).map((asset) => asset.assetId));
5367
+ for (const node of walkNodes(tree.nodes)) {
5368
+ if (node.type !== "doc") continue;
5369
+ const warning = reportUnresolvableDocAssets(node.slug, node.body, carriedAssetIds);
5370
+ if (warning) options.warn(warning);
5371
+ }
5372
+ const baseNodes = [...walkNodes(tree.nodes)].filter((node) => node.type === "base");
5225
5373
  const packagedBaseSlugs = new Set(baseNodes.map((node) => node.slug));
5226
5374
  const packagedRecordKeys = new Set(
5227
5375
  baseNodes.flatMap(
@@ -5257,12 +5405,6 @@ var assertSelfContained = (tree, options) => {
5257
5405
  }
5258
5406
  }
5259
5407
  };
5260
- var walk = function* (nodes) {
5261
- for (const node of nodes) {
5262
- yield node;
5263
- if (node.type === "folder") yield* walk(node.children);
5264
- }
5265
- };
5266
5408
 
5267
5409
  // ../../packages/busabase-package/src/github.ts
5268
5410
  import { Uint8ArrayReader, ZipReader } from "@zip.js/zip.js";
@@ -5526,7 +5668,24 @@ var readPackageTree = (files, options = {}) => {
5526
5668
  const manifest = zodParse(PackageManifestSchema, rawManifest, manifestPath);
5527
5669
  const contentDir = `${root}${PACKAGE_CONTENT_DIRNAME}/`;
5528
5670
  const nodes = readNodes(files, contentDir);
5529
- return { manifest, nodes };
5671
+ return { manifest, nodes, assets: readDocAssets(files, root) };
5672
+ };
5673
+ var readDocAssets = (files, root) => {
5674
+ const dir = `${root}${PACKAGE_ASSETS_DIRNAME}/`;
5675
+ const assets = [];
5676
+ for (const filePath of [...files.keys()].sort()) {
5677
+ if (!filePath.startsWith(dir) || !filePath.endsWith(PACKAGE_ASSET_META_SUFFIX)) continue;
5678
+ const meta = zodParse(PackageDocAssetMetaSchema, parseJsonFile(files, filePath), filePath);
5679
+ const bytesPath = filePath.slice(0, -PACKAGE_ASSET_META_SUFFIX.length);
5680
+ const bytes = files.get(bytesPath);
5681
+ if (!bytes) {
5682
+ throw new Error(
5683
+ `${filePath} describes asset "${meta.assetId}" but its bytes are missing (expected ${bytesPath}). The package is incomplete \u2014 nothing was installed.`
5684
+ );
5685
+ }
5686
+ assets.push({ ...meta, bytes });
5687
+ }
5688
+ return assets;
5530
5689
  };
5531
5690
  var assertKnownFormat = (rawManifest, manifestPath) => {
5532
5691
  const format = typeof rawManifest === "object" && rawManifest !== null ? rawManifest.format : void 0;
@@ -5758,8 +5917,29 @@ var renderPackageTree = (tree) => {
5758
5917
  const files = /* @__PURE__ */ new Map();
5759
5918
  files.set(PACKAGE_MANIFEST_FILENAME, serializeManifest(tree.manifest));
5760
5919
  renderNodes(tree.nodes, `${PACKAGE_CONTENT_DIRNAME}/`, files);
5920
+ renderDocAssets(tree.assets ?? [], files);
5761
5921
  return files;
5762
5922
  };
5923
+ var renderDocAssets = (assets, files) => {
5924
+ const byId = /* @__PURE__ */ new Map();
5925
+ for (const asset of assets) if (!byId.has(asset.assetId)) byId.set(asset.assetId, asset);
5926
+ for (const asset of [...byId.values()].sort((a, b) => a.assetId.localeCompare(b.assetId, "en"))) {
5927
+ const bytesPath = docAssetBytesPath(asset);
5928
+ assertSafeFilePath(bytesPath, `doc asset "${asset.assetId}"`);
5929
+ files.set(bytesPath, asset.bytes);
5930
+ files.set(
5931
+ `${bytesPath}${PACKAGE_ASSET_META_SUFFIX}`,
5932
+ toJsonFile(
5933
+ compact({
5934
+ assetId: asset.assetId,
5935
+ fileName: asset.fileName,
5936
+ mimeType: asset.mimeType,
5937
+ contentHash: asset.contentHash
5938
+ })
5939
+ )
5940
+ );
5941
+ }
5942
+ };
5763
5943
  var renderNodes = (nodes, dir, files) => {
5764
5944
  const ordered = sortNodes(nodes);
5765
5945
  assertNoSiblingCaseCollisions(
@@ -5868,6 +6048,7 @@ var renderNode = (node, dir, files) => {
5868
6048
  var writePackageFiles = async (files, outDir, options = {}) => {
5869
6049
  if (options.clean) {
5870
6050
  await rm(join2(outDir, PACKAGE_CONTENT_DIRNAME), { recursive: true, force: true });
6051
+ await rm(join2(outDir, PACKAGE_ASSETS_DIRNAME), { recursive: true, force: true });
5871
6052
  await rm(join2(outDir, PACKAGE_MANIFEST_FILENAME), { force: true });
5872
6053
  }
5873
6054
  for (const [filePath, bytes] of [...files].sort(([a], [b]) => a.localeCompare(b, "en"))) {
@@ -6044,6 +6225,7 @@ var countTree = (tree) => {
6044
6225
  break;
6045
6226
  }
6046
6227
  }
6228
+ counts.files += (tree.assets ?? []).length;
6047
6229
  return counts;
6048
6230
  };
6049
6231
  var renderPlan = (plan) => {
@@ -6127,7 +6309,8 @@ Dry run \u2014 nothing was created.`;
6127
6309
  const result = await applyInstall(client, plan, {
6128
6310
  autoMerge: Boolean(options.autoMerge),
6129
6311
  submittedBy: `busabase-cli install (${tree.manifest.name})`,
6130
- onProgress: reportProgress2
6312
+ onProgress: reportProgress2,
6313
+ serverUrl: options.serverUrl
6131
6314
  });
6132
6315
  if (options.json) return { installed: true, ...result };
6133
6316
  return renderInstallReport(plan.targetFolderSlug, result);
@@ -6435,7 +6618,7 @@ var MIME_BY_EXT2 = {
6435
6618
  ".txt": "text/plain",
6436
6619
  ".webp": "image/webp"
6437
6620
  };
6438
- var guessMimeType2 = (filePath) => MIME_BY_EXT2[extname2(filePath).toLowerCase()] ?? "application/octet-stream";
6621
+ var guessMimeType2 = (filePath) => MIME_BY_EXT2[extname3(filePath).toLowerCase()] ?? "application/octet-stream";
6439
6622
  function createAttachmentOptions(opts) {
6440
6623
  const maxFiles = opts.maxFiles;
6441
6624
  const maxFileSize = opts.maxFileSize;
@@ -6829,7 +7012,7 @@ OpenAPI: ${route.method} ${route.path}`);
6829
7012
  })
6830
7013
  );
6831
7014
  };
6832
- const walk2 = (node, navPath) => {
7015
+ const walk = (node, navPath) => {
6833
7016
  for (const key of Object.keys(node)) {
6834
7017
  if (key === "~orpc") continue;
6835
7018
  const child = node[key];
@@ -6839,11 +7022,11 @@ OpenAPI: ${route.method} ${route.path}`);
6839
7022
  const parent = navPath.length ? getGroup(kebab(navPath[0]), child["~orpc"].route.tags?.[0]) : program;
6840
7023
  addLeaf(parent, navPath, key, child);
6841
7024
  } else if (child && typeof child === "object") {
6842
- walk2(child, [...navPath, key]);
7025
+ walk(child, [...navPath, key]);
6843
7026
  }
6844
7027
  }
6845
7028
  };
6846
- walk2(cloudContract, []);
7029
+ walk(cloudContract, []);
6847
7030
  }
6848
7031
  function flagSignature(cmd) {
6849
7032
  const parts = [];
@@ -6915,7 +7098,11 @@ The flags below skip the menu (handy for scripts / CI):
6915
7098
  busabase-cli login --api-key sk_\u2026 # Cloud API key (headless/CI)
6916
7099
  busabase-cli login --base-url http://localhost:15419 # connect to a local server (no auth)
6917
7100
  busabase-cli login --refresh # rotate the OAuth token set (auto-runs too)
6918
- busabase-cli login --profile work # add a SECOND account, then switch to it`
7101
+ busabase-cli login --profile work # add a SECOND account, then switch to it
7102
+
7103
+ Login never blocks on a Space question, interactive or not: with more than one Space it always
7104
+ accepts the server-resolved default (the one you were most recently active in) and prints the
7105
+ full list \u2014 switch anytime with \`busabase-cli space use <id>\`.`
6919
7106
  ).action(async (_opts, cmd) => {
6920
7107
  const opts = cmd.optsWithGlobals();
6921
7108
  const config = resolveConfig(opts);
@@ -7303,7 +7490,8 @@ field with nothing linked yet does not trigger this.`
7303
7490
  autoMerge: Boolean(opts.autoMerge),
7304
7491
  rename: Boolean(opts.rename),
7305
7492
  json: config.output === "json",
7306
- githubToken: process.env.GITHUB_TOKEN
7493
+ githubToken: process.env.GITHUB_TOKEN,
7494
+ serverUrl: config.baseUrl
7307
7495
  })
7308
7496
  )
7309
7497
  );
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCli
4
- } from "./chunk-CEH54HWM.js";
4
+ } from "./chunk-SGFYTGDF.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-CEH54HWM.js";
5
+ } from "./chunk-SGFYTGDF.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.11.4",
3
+ "version": "0.14.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.11.4",
36
+ "busabase-sdk": "0.14.0",
37
37
  "commander": "^14.0.0",
38
38
  "tar-stream": "^3.1.7",
39
39
  "yaml": "^2.9.0",