busa-cli 0.11.3 → 0.13.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"];
@@ -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",
@@ -3021,29 +3027,16 @@ async function refreshToken(baseUrl, refreshToken2) {
3021
3027
  expiresAt: typeof payload.expires_in === "number" ? new Date(Date.now() + payload.expires_in * 1e3).toISOString() : void 0
3022
3028
  };
3023
3029
  }
3024
- async function pickSpaceId(verify, preselected) {
3030
+ function pickSpaceId(verify, preselected) {
3025
3031
  if (preselected) return preselected;
3026
3032
  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.");
3033
+ const fallback = verify.space?.id ?? spaces[0]?.id;
3034
+ if (spaces.length <= 1) return fallback;
3035
+ const choices = spaces.map((space) => `${space.name} [${space.id}]`).join(", ");
3036
+ say2(
3037
+ `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}`
3038
+ );
3039
+ return fallback;
3047
3040
  }
3048
3041
  async function chooseTarget() {
3049
3042
  const cloud = normalizeBaseUrl(DEFAULT_BASE_URL);
@@ -3140,7 +3133,7 @@ async function runLogin(options) {
3140
3133
  }
3141
3134
  say2("Verifying\u2026");
3142
3135
  const verify = await verifyAuth(baseUrl, token);
3143
- const spaceId = await pickSpaceId(verify, options.spaceId);
3136
+ const spaceId = pickSpaceId(verify, options.spaceId);
3144
3137
  writeDotEnvFile({
3145
3138
  BUSABASE_BASE_URL: baseUrl,
3146
3139
  BUSABASE_API_KEY: token,
@@ -3157,6 +3150,9 @@ async function runLogin(options) {
3157
3150
  credentialType: method === "device" ? "api_key" : expiresAt ? "oauth" : "api_key",
3158
3151
  user: verify.user?.email ?? verify.user?.name ?? verify.user?.id ?? "(unknown)",
3159
3152
  space: spaceId ?? "(server default)",
3153
+ // Surfaced so a non-interactive caller (typically an agent) can see whether the
3154
+ // fallback default is the space it actually wants, and switch with `space use <id>`.
3155
+ availableSpaces: (verify.spaces ?? []).map((space) => `${space.name} [${space.id}]`).join(", "),
3160
3156
  createdSpace: String(Boolean(verify.createdSpace)),
3161
3157
  bootstrapRequired: String(Boolean(verify.bootstrapRequired)),
3162
3158
  expiresAt: expiresAt ?? apiKeyExpiresAt ?? "(no expiry \u2014 API key)",
@@ -3642,7 +3638,11 @@ var updateNodeMetadataInputSchema = z8.object({
3642
3638
  });
3643
3639
  var searchNodesByNameInputSchema = z8.object({
3644
3640
  query: z8.string().min(1),
3645
- limit: z8.number().int().min(1).max(50).optional().default(20)
3641
+ // GET route — query params arrive as strings, and oRPC's OpenAPI handler
3642
+ // does not coerce them. A bare `z.number()` here rejected every real
3643
+ // `?limit=` call with "expected number, received string"; every other
3644
+ // limit/page field on a GET route in this file already uses `z.coerce`.
3645
+ limit: z8.coerce.number().int().min(1).max(50).optional().default(20)
3646
3646
  });
3647
3647
  var nodeSearchResultSchema = z8.object({
3648
3648
  id: z8.string(),
@@ -4189,11 +4189,13 @@ var viewChangeRequestInputSchema = z9.discriminatedUnion("operation", [
4189
4189
  var PACKAGE_FORMAT = "busabase-package@1";
4190
4190
  var PACKAGE_MANIFEST_FILENAME = "busabase.json";
4191
4191
  var PACKAGE_CONTENT_DIRNAME = "content";
4192
+ var PACKAGE_ASSETS_DIRNAME = "assets";
4192
4193
  var PACKAGE_NODE_META_FILENAME = "_node.json";
4193
4194
  var PACKAGE_FOLDER_META_FILENAME = "_folder.json";
4194
4195
  var PACKAGE_BASE_FILENAME = "base.json";
4195
4196
  var PACKAGE_RECORDS_FILENAME = "records.ndjson";
4196
4197
  var PACKAGE_NODE_META_SUFFIX = ".node.json";
4198
+ var PACKAGE_ASSET_META_SUFFIX = ".asset.json";
4197
4199
  var PACKAGE_FILE_TREE_NODE_TYPES = ["skill", "airapp", "drive"];
4198
4200
  var PACKAGE_AI_FIELD_TYPES = ["ai_summary", "ai_tags"];
4199
4201
  var PACKAGE_COMPUTED_FIELD_TYPES = [
@@ -4251,6 +4253,14 @@ var PackageFolderMetaSchema = z10.object({
4251
4253
  position: z10.number().int().optional()
4252
4254
  });
4253
4255
  var PackageFileNodeMetaSchema = PackageFolderMetaSchema;
4256
+ var PackageDocAssetMetaSchema = z10.object({
4257
+ assetId: z10.string().min(1),
4258
+ /** The asset's original name, restored on the target so its library entry reads the same. */
4259
+ fileName: z10.string().min(1),
4260
+ mimeType: z10.string().min(1),
4261
+ /** `sha256:<hex>` when the source host had one — lets the target dedupe instead of re-storing. */
4262
+ contentHash: z10.string().optional()
4263
+ });
4254
4264
  var PackageDocFrontmatterSchema = z10.object({
4255
4265
  name: z10.string().min(1),
4256
4266
  description: z10.string().default(""),
@@ -4363,6 +4373,9 @@ var PackageRecordLineSchema = z10.object({
4363
4373
  // ../../packages/busabase-package/src/apply.ts
4364
4374
  import { createHash as createHash5 } from "crypto";
4365
4375
 
4376
+ // ../../packages/busabase-package/src/doc-asset-refs.ts
4377
+ import { extname as extname2 } from "path";
4378
+
4366
4379
  // ../../packages/busabase-package/src/tree.ts
4367
4380
  import { extname } from "path";
4368
4381
  var NODE_SLUG_PATTERN = /^[a-z0-9-]+$/;
@@ -4464,6 +4477,39 @@ var walkNodes = function* (nodes) {
4464
4477
  };
4465
4478
  var collectBaseNodes = (nodes) => [...walkNodes(nodes)].filter((node) => node.type === "base");
4466
4479
 
4480
+ // ../../packages/busabase-package/src/doc-asset-refs.ts
4481
+ var ASSET_CONTENT_URL_PATTERN = /\/api\/assets\/([A-Za-z0-9_-]+)\/raw/g;
4482
+ var buildAssetContentUrl = (assetId) => `/api/assets/${encodeURIComponent(assetId)}/raw`;
4483
+ var extractDocAssetIds = (body) => {
4484
+ const ids = /* @__PURE__ */ new Set();
4485
+ for (const match of body.matchAll(ASSET_CONTENT_URL_PATTERN)) {
4486
+ const assetId = match[1];
4487
+ if (assetId) ids.add(assetId);
4488
+ }
4489
+ return [...ids];
4490
+ };
4491
+ var collectDocAssetIds = (nodes) => {
4492
+ const ids = /* @__PURE__ */ new Set();
4493
+ for (const node of walkNodes(nodes)) {
4494
+ if (node.type !== "doc") continue;
4495
+ for (const assetId of extractDocAssetIds(node.body)) ids.add(assetId);
4496
+ }
4497
+ return [...ids];
4498
+ };
4499
+ var docAssetBytesPath = (asset) => `${PACKAGE_ASSETS_DIRNAME}/${asset.assetId}${extname2(asset.fileName).toLowerCase()}`;
4500
+ var rewriteDocAssetIds = (body, idMap) => {
4501
+ if (idMap.size === 0) return body;
4502
+ return body.replace(ASSET_CONTENT_URL_PATTERN, (whole, assetId) => {
4503
+ const replacement = idMap.get(assetId);
4504
+ return replacement === void 0 ? whole : buildAssetContentUrl(replacement);
4505
+ });
4506
+ };
4507
+ var reportUnresolvableDocAssets = (slug, body, carried = /* @__PURE__ */ new Set()) => {
4508
+ const unresolved = extractDocAssetIds(body).filter((assetId) => !carried.has(assetId));
4509
+ if (unresolved.length === 0) return void 0;
4510
+ 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.`;
4511
+ };
4512
+
4467
4513
  // ../../packages/busabase-package/src/apply.ts
4468
4514
  var RECORD_BATCH_SIZE = 200;
4469
4515
  var fieldKey = (baseSlug, fieldSlug) => `${baseSlug}\0${fieldSlug}`;
@@ -4478,6 +4524,13 @@ var applyInstall = async (client, plan, options) => {
4478
4524
  const bases = [];
4479
4525
  const progress = options.onProgress ?? (() => {
4480
4526
  });
4527
+ const docAssetIds = await uploadDocAssets(
4528
+ client,
4529
+ plan.tree.assets ?? [],
4530
+ state,
4531
+ progress,
4532
+ options.serverUrl
4533
+ );
4481
4534
  progress("Pass 1/5: creating the node tree\u2026");
4482
4535
  state.targetFolderNodeId = await createFolderNode(client, {
4483
4536
  parentNodeId: void 0,
@@ -4494,7 +4547,8 @@ var applyInstall = async (client, plan, options) => {
4494
4547
  options,
4495
4548
  state,
4496
4549
  bases,
4497
- fieldIds
4550
+ fieldIds,
4551
+ docAssetIds
4498
4552
  );
4499
4553
  const deferred = bases.filter((base) => base.node.base.fields.some(isDeferredField));
4500
4554
  if (deferred.length > 0) {
@@ -4570,7 +4624,28 @@ var applyInstall = async (client, plan, options) => {
4570
4624
  }
4571
4625
  return state;
4572
4626
  };
4573
- var createStructure = async (client, nodes, parentNodeId, options, state, bases, fieldIds) => {
4627
+ var uploadDocAssets = async (client, assets, state, progress, serverUrl) => {
4628
+ const idMap = /* @__PURE__ */ new Map();
4629
+ if (assets.length === 0) return idMap;
4630
+ progress(`Pass 1/5: uploading ${assets.length} doc image(s)\u2026`);
4631
+ for (const asset of assets) {
4632
+ try {
4633
+ const uploaded = await uploadAsset(
4634
+ client,
4635
+ asset.fileName,
4636
+ asset.mimeType,
4637
+ asset.bytes,
4638
+ "doc",
4639
+ serverUrl
4640
+ );
4641
+ idMap.set(asset.assetId, uploaded.assetId);
4642
+ } catch (error) {
4643
+ state.warnings.push(asSkippableUpload(error, asset.fileName));
4644
+ }
4645
+ }
4646
+ return idMap;
4647
+ };
4648
+ var createStructure = async (client, nodes, parentNodeId, options, state, bases, fieldIds, docAssetIds) => {
4574
4649
  for (const node of nodes) {
4575
4650
  switch (node.type) {
4576
4651
  case "folder": {
@@ -4582,16 +4657,31 @@ var createStructure = async (client, nodes, parentNodeId, options, state, bases,
4582
4657
  submittedBy: options.submittedBy
4583
4658
  });
4584
4659
  state.created.folders++;
4585
- await createStructure(client, node.children, nodeId, options, state, bases, fieldIds);
4660
+ await createStructure(
4661
+ client,
4662
+ node.children,
4663
+ nodeId,
4664
+ options,
4665
+ state,
4666
+ bases,
4667
+ fieldIds,
4668
+ docAssetIds
4669
+ );
4586
4670
  break;
4587
4671
  }
4588
4672
  case "doc": {
4673
+ const warning = reportUnresolvableDocAssets(
4674
+ node.slug,
4675
+ node.body,
4676
+ new Set(docAssetIds.keys())
4677
+ );
4678
+ if (warning) state.warnings.push(warning);
4589
4679
  const result = await client.docs.create({
4590
4680
  parentNodeId,
4591
4681
  slug: node.slug,
4592
4682
  name: node.name,
4593
4683
  description: node.description,
4594
- body: node.body,
4684
+ body: rewriteDocAssetIds(node.body, docAssetIds),
4595
4685
  autoMerge: options.autoMerge
4596
4686
  });
4597
4687
  if (result.materialized) state.created.docs++;
@@ -4611,7 +4701,14 @@ var createStructure = async (client, nodes, parentNodeId, options, state, bases,
4611
4701
  case "file": {
4612
4702
  let asset;
4613
4703
  try {
4614
- asset = await uploadAsset(client, node.fileName, node.mimeType, node.bytes);
4704
+ asset = await uploadAsset(
4705
+ client,
4706
+ node.fileName,
4707
+ node.mimeType,
4708
+ node.bytes,
4709
+ void 0,
4710
+ options.serverUrl
4711
+ );
4615
4712
  } catch (error) {
4616
4713
  state.warnings.push(asSkippableUpload(error, node.fileName));
4617
4714
  break;
@@ -4722,7 +4819,9 @@ var createFileTreeNode = async (client, node, parentNodeId, options, state) => {
4722
4819
  client,
4723
4820
  entry.path.split("/").at(-1) ?? entry.path,
4724
4821
  mimeType,
4725
- entry.bytes
4822
+ entry.bytes,
4823
+ void 0,
4824
+ options.serverUrl
4726
4825
  );
4727
4826
  return { path: entry.path, assetId: asset.assetId, mimeType };
4728
4827
  } catch (error) {
@@ -4753,13 +4852,25 @@ var createFileTreeNode = async (client, node, parentNodeId, options, state) => {
4753
4852
  };
4754
4853
  var UploadTargetUnsupportedError = class extends Error {
4755
4854
  };
4756
- var uploadAsset = async (client, fileName, mimeType, bytes) => {
4855
+ var resolveUploadUrl = (uploadUrl, serverUrl) => {
4856
+ if (/^https?:\/\//.test(uploadUrl)) return uploadUrl;
4857
+ if (serverUrl) {
4858
+ try {
4859
+ return new URL(uploadUrl, serverUrl).toString();
4860
+ } catch {
4861
+ }
4862
+ }
4863
+ throw new UploadTargetUnsupportedError(
4864
+ `this server issued a root-relative upload url (${uploadUrl}) and no server origin was supplied to resolve it against`
4865
+ );
4866
+ };
4867
+ var uploadAsset = async (client, fileName, mimeType, bytes, context = "record-field", serverUrl) => {
4757
4868
  const contentHash = `sha256:${createHash5("sha256").update(bytes).digest("hex")}`;
4758
4869
  const requested = await client.assets.createUploadUrl({
4759
4870
  fileName,
4760
4871
  mimeType,
4761
4872
  sizeBytes: bytes.byteLength,
4762
- context: "record-field",
4873
+ context,
4763
4874
  contentHash
4764
4875
  });
4765
4876
  if (requested.duplicate) {
@@ -4768,12 +4879,8 @@ var uploadAsset = async (client, fileName, mimeType, bytes) => {
4768
4879
  throw new Error(`Server reported "${fileName}" as a duplicate but returned no asset id.`);
4769
4880
  return { assetId: assetId2 };
4770
4881
  }
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, {
4882
+ const uploadUrl = resolveUploadUrl(requested.uploadUrl, serverUrl);
4883
+ const uploaded = await fetch(uploadUrl, {
4777
4884
  method: "PUT",
4778
4885
  body: new Uint8Array(bytes),
4779
4886
  headers: { "content-type": mimeType }
@@ -4786,7 +4893,7 @@ var uploadAsset = async (client, fileName, mimeType, bytes) => {
4786
4893
  fileName,
4787
4894
  mimeType,
4788
4895
  sizeBytes: bytes.byteLength,
4789
- context: "record-field",
4896
+ context,
4790
4897
  contentHash
4791
4898
  });
4792
4899
  const assetId = confirmed.assetId ?? confirmed.attachmentId;
@@ -4979,10 +5086,35 @@ var findSourceNode = (nodes, slugOrId) => {
4979
5086
  var collectPackageTree = async (client, root, options) => {
4980
5087
  const children = root.children ?? [];
4981
5088
  const nodes = await collectNodes(client, children, options);
4982
- const tree = { manifest: { ...options.manifest, format: PACKAGE_FORMAT }, nodes };
5089
+ const tree = {
5090
+ manifest: { ...options.manifest, format: PACKAGE_FORMAT },
5091
+ nodes,
5092
+ assets: await collectDocAssets(client, nodes, options)
5093
+ };
4983
5094
  assertSelfContained(tree, options);
4984
5095
  return tree;
4985
5096
  };
5097
+ var collectDocAssets = async (client, nodes, options) => {
5098
+ const assets = [];
5099
+ for (const assetId of collectDocAssetIds(nodes)) {
5100
+ try {
5101
+ const detail = await client.assets.download({ assetId });
5102
+ assets.push({
5103
+ assetId,
5104
+ fileName: detail.fileName,
5105
+ mimeType: detail.mimeType,
5106
+ contentHash: detail.contentHash ?? void 0,
5107
+ // See CollectOptions.baseUrl — a local server's download url is root-relative.
5108
+ bytes: await downloadBytes(detail.downloadUrl, detail.fileName, options)
5109
+ });
5110
+ } catch (error) {
5111
+ options.warn(
5112
+ `Could not read the image ${assetId} embedded in a doc (${error instanceof Error ? error.message : String(error)}). The package ships without it.`
5113
+ );
5114
+ }
5115
+ }
5116
+ return assets;
5117
+ };
4986
5118
  var collectNodes = async (client, sources, options) => {
4987
5119
  const nodes = [];
4988
5120
  for (const source of sources) {
@@ -5221,7 +5353,13 @@ var downloadBytes = async (url, label, options) => {
5221
5353
  return Buffer.from(await response.arrayBuffer());
5222
5354
  };
5223
5355
  var assertSelfContained = (tree, options) => {
5224
- const baseNodes = [...walk(tree.nodes)].filter((node) => node.type === "base");
5356
+ const carriedAssetIds = new Set((tree.assets ?? []).map((asset) => asset.assetId));
5357
+ for (const node of walkNodes(tree.nodes)) {
5358
+ if (node.type !== "doc") continue;
5359
+ const warning = reportUnresolvableDocAssets(node.slug, node.body, carriedAssetIds);
5360
+ if (warning) options.warn(warning);
5361
+ }
5362
+ const baseNodes = [...walkNodes(tree.nodes)].filter((node) => node.type === "base");
5225
5363
  const packagedBaseSlugs = new Set(baseNodes.map((node) => node.slug));
5226
5364
  const packagedRecordKeys = new Set(
5227
5365
  baseNodes.flatMap(
@@ -5257,12 +5395,6 @@ var assertSelfContained = (tree, options) => {
5257
5395
  }
5258
5396
  }
5259
5397
  };
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
5398
 
5267
5399
  // ../../packages/busabase-package/src/github.ts
5268
5400
  import { Uint8ArrayReader, ZipReader } from "@zip.js/zip.js";
@@ -5526,7 +5658,24 @@ var readPackageTree = (files, options = {}) => {
5526
5658
  const manifest = zodParse(PackageManifestSchema, rawManifest, manifestPath);
5527
5659
  const contentDir = `${root}${PACKAGE_CONTENT_DIRNAME}/`;
5528
5660
  const nodes = readNodes(files, contentDir);
5529
- return { manifest, nodes };
5661
+ return { manifest, nodes, assets: readDocAssets(files, root) };
5662
+ };
5663
+ var readDocAssets = (files, root) => {
5664
+ const dir = `${root}${PACKAGE_ASSETS_DIRNAME}/`;
5665
+ const assets = [];
5666
+ for (const filePath of [...files.keys()].sort()) {
5667
+ if (!filePath.startsWith(dir) || !filePath.endsWith(PACKAGE_ASSET_META_SUFFIX)) continue;
5668
+ const meta = zodParse(PackageDocAssetMetaSchema, parseJsonFile(files, filePath), filePath);
5669
+ const bytesPath = filePath.slice(0, -PACKAGE_ASSET_META_SUFFIX.length);
5670
+ const bytes = files.get(bytesPath);
5671
+ if (!bytes) {
5672
+ throw new Error(
5673
+ `${filePath} describes asset "${meta.assetId}" but its bytes are missing (expected ${bytesPath}). The package is incomplete \u2014 nothing was installed.`
5674
+ );
5675
+ }
5676
+ assets.push({ ...meta, bytes });
5677
+ }
5678
+ return assets;
5530
5679
  };
5531
5680
  var assertKnownFormat = (rawManifest, manifestPath) => {
5532
5681
  const format = typeof rawManifest === "object" && rawManifest !== null ? rawManifest.format : void 0;
@@ -5758,8 +5907,29 @@ var renderPackageTree = (tree) => {
5758
5907
  const files = /* @__PURE__ */ new Map();
5759
5908
  files.set(PACKAGE_MANIFEST_FILENAME, serializeManifest(tree.manifest));
5760
5909
  renderNodes(tree.nodes, `${PACKAGE_CONTENT_DIRNAME}/`, files);
5910
+ renderDocAssets(tree.assets ?? [], files);
5761
5911
  return files;
5762
5912
  };
5913
+ var renderDocAssets = (assets, files) => {
5914
+ const byId = /* @__PURE__ */ new Map();
5915
+ for (const asset of assets) if (!byId.has(asset.assetId)) byId.set(asset.assetId, asset);
5916
+ for (const asset of [...byId.values()].sort((a, b) => a.assetId.localeCompare(b.assetId, "en"))) {
5917
+ const bytesPath = docAssetBytesPath(asset);
5918
+ assertSafeFilePath(bytesPath, `doc asset "${asset.assetId}"`);
5919
+ files.set(bytesPath, asset.bytes);
5920
+ files.set(
5921
+ `${bytesPath}${PACKAGE_ASSET_META_SUFFIX}`,
5922
+ toJsonFile(
5923
+ compact({
5924
+ assetId: asset.assetId,
5925
+ fileName: asset.fileName,
5926
+ mimeType: asset.mimeType,
5927
+ contentHash: asset.contentHash
5928
+ })
5929
+ )
5930
+ );
5931
+ }
5932
+ };
5763
5933
  var renderNodes = (nodes, dir, files) => {
5764
5934
  const ordered = sortNodes(nodes);
5765
5935
  assertNoSiblingCaseCollisions(
@@ -5868,6 +6038,7 @@ var renderNode = (node, dir, files) => {
5868
6038
  var writePackageFiles = async (files, outDir, options = {}) => {
5869
6039
  if (options.clean) {
5870
6040
  await rm(join2(outDir, PACKAGE_CONTENT_DIRNAME), { recursive: true, force: true });
6041
+ await rm(join2(outDir, PACKAGE_ASSETS_DIRNAME), { recursive: true, force: true });
5871
6042
  await rm(join2(outDir, PACKAGE_MANIFEST_FILENAME), { force: true });
5872
6043
  }
5873
6044
  for (const [filePath, bytes] of [...files].sort(([a], [b]) => a.localeCompare(b, "en"))) {
@@ -6044,6 +6215,7 @@ var countTree = (tree) => {
6044
6215
  break;
6045
6216
  }
6046
6217
  }
6218
+ counts.files += (tree.assets ?? []).length;
6047
6219
  return counts;
6048
6220
  };
6049
6221
  var renderPlan = (plan) => {
@@ -6127,7 +6299,8 @@ Dry run \u2014 nothing was created.`;
6127
6299
  const result = await applyInstall(client, plan, {
6128
6300
  autoMerge: Boolean(options.autoMerge),
6129
6301
  submittedBy: `busabase-cli install (${tree.manifest.name})`,
6130
- onProgress: reportProgress2
6302
+ onProgress: reportProgress2,
6303
+ serverUrl: options.serverUrl
6131
6304
  });
6132
6305
  if (options.json) return { installed: true, ...result };
6133
6306
  return renderInstallReport(plan.targetFolderSlug, result);
@@ -6435,7 +6608,7 @@ var MIME_BY_EXT2 = {
6435
6608
  ".txt": "text/plain",
6436
6609
  ".webp": "image/webp"
6437
6610
  };
6438
- var guessMimeType2 = (filePath) => MIME_BY_EXT2[extname2(filePath).toLowerCase()] ?? "application/octet-stream";
6611
+ var guessMimeType2 = (filePath) => MIME_BY_EXT2[extname3(filePath).toLowerCase()] ?? "application/octet-stream";
6439
6612
  function createAttachmentOptions(opts) {
6440
6613
  const maxFiles = opts.maxFiles;
6441
6614
  const maxFileSize = opts.maxFileSize;
@@ -6829,7 +7002,7 @@ OpenAPI: ${route.method} ${route.path}`);
6829
7002
  })
6830
7003
  );
6831
7004
  };
6832
- const walk2 = (node, navPath) => {
7005
+ const walk = (node, navPath) => {
6833
7006
  for (const key of Object.keys(node)) {
6834
7007
  if (key === "~orpc") continue;
6835
7008
  const child = node[key];
@@ -6839,11 +7012,11 @@ OpenAPI: ${route.method} ${route.path}`);
6839
7012
  const parent = navPath.length ? getGroup(kebab(navPath[0]), child["~orpc"].route.tags?.[0]) : program;
6840
7013
  addLeaf(parent, navPath, key, child);
6841
7014
  } else if (child && typeof child === "object") {
6842
- walk2(child, [...navPath, key]);
7015
+ walk(child, [...navPath, key]);
6843
7016
  }
6844
7017
  }
6845
7018
  };
6846
- walk2(cloudContract, []);
7019
+ walk(cloudContract, []);
6847
7020
  }
6848
7021
  function flagSignature(cmd) {
6849
7022
  const parts = [];
@@ -6915,7 +7088,11 @@ The flags below skip the menu (handy for scripts / CI):
6915
7088
  busabase-cli login --api-key sk_\u2026 # Cloud API key (headless/CI)
6916
7089
  busabase-cli login --base-url http://localhost:15419 # connect to a local server (no auth)
6917
7090
  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`
7091
+ busabase-cli login --profile work # add a SECOND account, then switch to it
7092
+
7093
+ Login never blocks on a Space question, interactive or not: with more than one Space it always
7094
+ accepts the server-resolved default (the one you were most recently active in) and prints the
7095
+ full list \u2014 switch anytime with \`busabase-cli space use <id>\`.`
6919
7096
  ).action(async (_opts, cmd) => {
6920
7097
  const opts = cmd.optsWithGlobals();
6921
7098
  const config = resolveConfig(opts);
@@ -7303,7 +7480,8 @@ field with nothing linked yet does not trigger this.`
7303
7480
  autoMerge: Boolean(opts.autoMerge),
7304
7481
  rename: Boolean(opts.rename),
7305
7482
  json: config.output === "json",
7306
- githubToken: process.env.GITHUB_TOKEN
7483
+ githubToken: process.env.GITHUB_TOKEN,
7484
+ serverUrl: config.baseUrl
7307
7485
  })
7308
7486
  )
7309
7487
  );
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-KX5SSSEN.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-KX5SSSEN.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.3",
3
+ "version": "0.13.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.3",
36
+ "busabase-sdk": "0.13.0",
37
37
  "commander": "^14.0.0",
38
38
  "tar-stream": "^3.1.7",
39
39
  "yaml": "^2.9.0",