busa-cli 0.9.9 → 0.9.11

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.
@@ -769,12 +769,16 @@ import { spawn } from "child_process";
769
769
  import { createHash as createHash4, randomBytes } from "crypto";
770
770
  import { createServer } from "http";
771
771
  import { createInterface } from "readline/promises";
772
+
773
+ // ../../packages/busabase-contract/src/auth/device-authorization.ts
774
+ var BUSABASE_CLI_CLIENT_ID = "busabase-cli";
775
+
776
+ // src/login.ts
772
777
  import { DEFAULT_BASE_URL, normalizeBaseUrl } from "busabase-sdk";
773
- var CLI_CLIENT_ID = "busabase-cli";
774
778
  var CLI_CLIENT_PLATFORM = "cli";
775
779
  var OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
776
780
  var EXPIRES_AT_KEY = "BUSABASE_TOKEN_EXPIRES_AT";
777
- var SESSION_TOKEN_PREFIX = "bss_";
781
+ var LEGACY_SESSION_TOKEN_PREFIX = "bss_";
778
782
  var AUTO_REFRESH_THRESHOLD_MS = 2 * 24 * 60 * 60 * 1e3;
779
783
  var DEFAULT_LOCAL_URL = "http://localhost:15419";
780
784
  var base64url = (buffer) => buffer.toString("base64url");
@@ -832,7 +836,7 @@ async function verifyAuth(baseUrl, token) {
832
836
  }
833
837
  return await res.json();
834
838
  }
835
- async function oauthLogin(baseUrl, useBrowser) {
839
+ async function loopbackOauthLogin(baseUrl, useBrowser) {
836
840
  const codeVerifier = base64url(randomBytes(32));
837
841
  const codeChallenge = base64url(createHash4("sha256").update(codeVerifier).digest());
838
842
  const state = base64url(randomBytes(16));
@@ -887,7 +891,7 @@ async function oauthLogin(baseUrl, useBrowser) {
887
891
  const redirect = `http://127.0.0.1:${port}/callback`;
888
892
  const authorizeUrl = new URL(`${baseUrl}/api/oauth/authorize`);
889
893
  authorizeUrl.searchParams.set("response_type", "code");
890
- authorizeUrl.searchParams.set("client_id", CLI_CLIENT_ID);
894
+ authorizeUrl.searchParams.set("client_id", BUSABASE_CLI_CLIENT_ID);
891
895
  authorizeUrl.searchParams.set("client_platform", CLI_CLIENT_PLATFORM);
892
896
  authorizeUrl.searchParams.set("code_challenge", codeChallenge);
893
897
  authorizeUrl.searchParams.set("code_challenge_method", "S256");
@@ -912,7 +916,7 @@ async function oauthLogin(baseUrl, useBrowser) {
912
916
  headers: { "content-type": "application/json" },
913
917
  body: JSON.stringify({
914
918
  grant_type: "authorization_code",
915
- client_id: CLI_CLIENT_ID,
919
+ client_id: BUSABASE_CLI_CLIENT_ID,
916
920
  client_platform: CLI_CLIENT_PLATFORM,
917
921
  code,
918
922
  code_verifier: codeVerifier,
@@ -927,7 +931,110 @@ async function oauthLogin(baseUrl, useBrowser) {
927
931
  const payload = await tokenRes.json();
928
932
  const token = payload.apiKey ?? payload.token ?? payload.accessToken;
929
933
  if (!token) throw new Error("Token exchange returned no credential.");
930
- return { token, expiresAt: payload.expiresAt ?? void 0 };
934
+ return payload.apiKey ? { token, apiKeyExpiresAt: payload.expiresAt ?? void 0 } : { token, expiresAt: payload.expiresAt ?? void 0 };
935
+ }
936
+ var delay = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
937
+ async function readDeviceResponse(response) {
938
+ return await response.json().catch(() => ({}));
939
+ }
940
+ async function deviceLogin(baseUrl, useBrowser) {
941
+ const origin = new URL(baseUrl).origin;
942
+ let codeResponse;
943
+ try {
944
+ codeResponse = await fetch(`${baseUrl}/api/auth/device/code`, {
945
+ method: "POST",
946
+ headers: { "content-type": "application/json", origin },
947
+ body: JSON.stringify({
948
+ client_id: BUSABASE_CLI_CLIENT_ID,
949
+ scope: "openid profile email"
950
+ })
951
+ });
952
+ } catch {
953
+ throw new Error(`Could not start device sign-in because ${baseUrl} could not be reached.`);
954
+ }
955
+ const code = await codeResponse.json().catch(() => ({}));
956
+ if (!codeResponse.ok) {
957
+ throw new Error(
958
+ `Could not start device sign-in (HTTP ${codeResponse.status})${code.error_description ? `: ${code.error_description}` : "."}`
959
+ );
960
+ }
961
+ if (!code.device_code || !code.user_code || !code.verification_uri) {
962
+ throw new Error("Device sign-in returned an incomplete authorization response.");
963
+ }
964
+ const verificationUrl = code.verification_uri_complete ?? code.verification_uri;
965
+ say("");
966
+ say("Authorize Busabase CLI in any browser:");
967
+ say(` ${code.verification_uri}`);
968
+ say(` Code: ${code.user_code}`);
969
+ say("");
970
+ if (useBrowser) openBrowser(verificationUrl);
971
+ say("Waiting for authorization\u2026");
972
+ const expiresInSeconds = Math.max(1, code.expires_in ?? 15 * 60);
973
+ const deadline = Date.now() + expiresInSeconds * 1e3;
974
+ let intervalSeconds = Math.max(1, code.interval ?? 5);
975
+ let consecutiveNetworkFailures = 0;
976
+ while (Date.now() < deadline) {
977
+ await delay(intervalSeconds * 1e3);
978
+ let tokenResponse;
979
+ try {
980
+ tokenResponse = await fetch(`${baseUrl}/api/auth/device/token`, {
981
+ method: "POST",
982
+ headers: { "content-type": "application/json", origin },
983
+ body: JSON.stringify({
984
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
985
+ device_code: code.device_code,
986
+ client_id: BUSABASE_CLI_CLIENT_ID
987
+ })
988
+ });
989
+ consecutiveNetworkFailures = 0;
990
+ } catch {
991
+ consecutiveNetworkFailures += 1;
992
+ if (consecutiveNetworkFailures >= 3) {
993
+ throw new Error(
994
+ "Device sign-in lost its network connection. Check connectivity and run `busabase-cli login` again."
995
+ );
996
+ }
997
+ continue;
998
+ }
999
+ const payload = await readDeviceResponse(tokenResponse);
1000
+ if (tokenResponse.ok && payload.access_token) {
1001
+ const finalizeResponse = await fetch(`${baseUrl}/api/v1/device/finalize`, {
1002
+ method: "POST",
1003
+ headers: {
1004
+ authorization: `Bearer ${payload.access_token}`,
1005
+ "content-type": "application/json"
1006
+ },
1007
+ body: JSON.stringify({ deviceCode: code.device_code })
1008
+ });
1009
+ const finalized = await finalizeResponse.json().catch(() => ({}));
1010
+ if (!finalizeResponse.ok || !finalized.apiKey || finalized.credentialType !== "api_key") {
1011
+ const finalizeError = typeof finalized.error === "string" ? finalized.error : finalized.error?.message ?? finalized.message;
1012
+ throw new Error(
1013
+ `Device sign-in could not finalize the selected API key${finalizeError ? `: ${finalizeError}` : ` (HTTP ${finalizeResponse.status})`}.`
1014
+ );
1015
+ }
1016
+ return {
1017
+ token: finalized.apiKey,
1018
+ apiKeyExpiresAt: finalized.expiresAt ?? void 0
1019
+ };
1020
+ }
1021
+ switch (payload.error) {
1022
+ case "authorization_pending":
1023
+ continue;
1024
+ case "slow_down":
1025
+ intervalSeconds += 5;
1026
+ continue;
1027
+ case "access_denied":
1028
+ throw new Error("Device sign-in was denied. No credential was saved.");
1029
+ case "expired_token":
1030
+ throw new Error("The device code expired. Run `busabase-cli login` to request a new one.");
1031
+ default:
1032
+ throw new Error(
1033
+ `Device sign-in failed${payload.error_description ? `: ${payload.error_description}` : ` (HTTP ${tokenResponse.status})`}.`
1034
+ );
1035
+ }
1036
+ }
1037
+ throw new Error("The device code expired. Run `busabase-cli login` to request a new one.");
931
1038
  }
932
1039
  async function refreshToken(baseUrl, token) {
933
1040
  const res = await fetch(`${baseUrl}/api/oauth/refresh`, {
@@ -972,12 +1079,12 @@ async function chooseTarget() {
972
1079
  say("How should this CLI connect?");
973
1080
  say(" 1. Local/Desktop on this computer \u2014 no account, no login");
974
1081
  say(" Use when you run `busabase server` or the Busabase Desktop app locally.");
975
- say(" 2. Busabase Cloud \u2014 browser sign-in (recommended)");
976
- say(" Best for humans: opens the browser and saves a refreshable CLI session.");
1082
+ say(" 2. Busabase Cloud \u2014 device sign-in (recommended)");
1083
+ say(" Works locally, over SSH, and in containers; approve from any browser.");
977
1084
  say(" 3. Busabase Cloud \u2014 paste an API key");
978
1085
  say(" Best for CI, servers, or agents where a browser is not available.");
979
- say(" 4. Self-hosted Busabase \u2014 browser sign-in");
980
- say(" Use your team's Busabase URL when it supports OAuth login.");
1086
+ say(" 4. Self-hosted Busabase \u2014 device sign-in");
1087
+ say(" Use your team's Busabase URL when it supports device authorization.");
981
1088
  say(" 5. Self-hosted Busabase \u2014 paste an API key");
982
1089
  say(" Use your team's Busabase URL with a long-lived key for automation.");
983
1090
  const choice = await ask("Choose 1-5 [2]: ");
@@ -994,11 +1101,11 @@ async function chooseTarget() {
994
1101
  case "3":
995
1102
  return { baseUrl: cloud, method: "api-key" };
996
1103
  case "4":
997
- return { baseUrl: await askSelfHostedUrl(), method: "oauth" };
1104
+ return { baseUrl: await askSelfHostedUrl(), method: "device" };
998
1105
  case "5":
999
1106
  return { baseUrl: await askSelfHostedUrl(), method: "api-key" };
1000
1107
  default:
1001
- return { baseUrl: cloud, method: "oauth" };
1108
+ return { baseUrl: cloud, method: "device" };
1002
1109
  }
1003
1110
  }
1004
1111
  async function runLogin(options) {
@@ -1007,14 +1114,16 @@ async function runLogin(options) {
1007
1114
  let method;
1008
1115
  if (apiKey) {
1009
1116
  method = "api-key";
1117
+ } else if (options.deviceCode) {
1118
+ method = "device";
1010
1119
  } else if (options.oauth) {
1011
- method = "oauth";
1120
+ method = "loopback-oauth";
1012
1121
  } else if (isInteractive()) {
1013
1122
  const target = await chooseTarget();
1014
1123
  baseUrl = normalizeBaseUrl(target.baseUrl);
1015
1124
  method = target.method;
1016
1125
  } else {
1017
- method = isLocalHost(baseUrl) ? "none" : "oauth";
1126
+ method = isLocalHost(baseUrl) ? "none" : "device";
1018
1127
  }
1019
1128
  if (method === "none") {
1020
1129
  if (await probeAuthRequired(baseUrl)) {
@@ -1034,10 +1143,16 @@ async function runLogin(options) {
1034
1143
  }
1035
1144
  let token;
1036
1145
  let expiresAt;
1037
- if (method === "oauth") {
1038
- const result = await oauthLogin(baseUrl, options.browser);
1146
+ let apiKeyExpiresAt;
1147
+ if (method === "device") {
1148
+ const result = await deviceLogin(baseUrl, options.browser);
1149
+ token = result.token;
1150
+ apiKeyExpiresAt = result.apiKeyExpiresAt;
1151
+ } else if (method === "loopback-oauth") {
1152
+ const result = await loopbackOauthLogin(baseUrl, options.browser);
1039
1153
  token = result.token;
1040
1154
  expiresAt = result.expiresAt;
1155
+ apiKeyExpiresAt = result.apiKeyExpiresAt;
1041
1156
  } else {
1042
1157
  if (!apiKey) {
1043
1158
  say("Create a key in the dashboard \u2192 Settings \u2192 API Keys (shown once).");
@@ -1060,10 +1175,13 @@ async function runLogin(options) {
1060
1175
  say(`\u2713 Signed in and saved to ${dotEnvPath()}`);
1061
1176
  return {
1062
1177
  status: "signed in",
1063
- method,
1178
+ method: method === "loopback-oauth" ? "oauth" : method,
1179
+ credentialType: method === "device" ? "api_key" : expiresAt ? "session" : "api_key",
1064
1180
  user: verify.user?.email ?? verify.user?.name ?? verify.user?.id ?? "(unknown)",
1065
1181
  space: spaceId ?? "(server default)",
1066
- expiresAt: expiresAt ?? "(no expiry \u2014 API key)",
1182
+ createdSpace: String(Boolean(verify.createdSpace)),
1183
+ bootstrapRequired: String(Boolean(verify.bootstrapRequired)),
1184
+ expiresAt: expiresAt ?? apiKeyExpiresAt ?? "(no expiry \u2014 API key)",
1067
1185
  baseUrl,
1068
1186
  config: dotEnvPath()
1069
1187
  };
@@ -1072,10 +1190,12 @@ async function runRefresh(options) {
1072
1190
  const baseUrl = normalizeBaseUrl(options.baseUrl);
1073
1191
  const token = options.apiKey;
1074
1192
  if (!token) throw new Error("Not signed in \u2014 run `busabase-cli login` first.");
1075
- if (!token.startsWith(SESSION_TOKEN_PREFIX)) {
1193
+ const file = loadDotEnvFile();
1194
+ const isSavedExpiringSession = file.BUSABASE_API_KEY === token && Boolean(file.BUSABASE_TOKEN_EXPIRES_AT);
1195
+ if (!token.startsWith(LEGACY_SESSION_TOKEN_PREFIX) && !isSavedExpiringSession) {
1076
1196
  return {
1077
1197
  status: "nothing to refresh",
1078
- detail: "This credential is an API key, which doesn't expire. Only OAuth sessions refresh."
1198
+ detail: "This credential is an API key and cannot be refreshed. Only OAuth sessions refresh."
1079
1199
  };
1080
1200
  }
1081
1201
  let refreshed;
@@ -1101,13 +1221,14 @@ async function runRefresh(options) {
1101
1221
  };
1102
1222
  }
1103
1223
  async function maybeAutoRefresh(baseUrl, apiKey) {
1104
- if (!apiKey?.startsWith(SESSION_TOKEN_PREFIX)) return;
1224
+ if (!apiKey) return;
1105
1225
  const file = loadDotEnvFile();
1106
1226
  if (file.BUSABASE_API_KEY !== apiKey) return;
1107
1227
  const expiresRaw = file[EXPIRES_AT_KEY];
1108
1228
  if (!expiresRaw) return;
1109
1229
  const expiresAt = Date.parse(expiresRaw);
1110
1230
  if (Number.isNaN(expiresAt)) return;
1231
+ if (expiresAt <= Date.now()) return;
1111
1232
  if (expiresAt - Date.now() > AUTO_REFRESH_THRESHOLD_MS) return;
1112
1233
  try {
1113
1234
  const refreshed = await refreshToken(normalizeBaseUrl(baseUrl), apiKey);
@@ -1118,11 +1239,25 @@ async function maybeAutoRefresh(baseUrl, apiKey) {
1118
1239
  } catch {
1119
1240
  }
1120
1241
  }
1242
+ function assertCredentialNotExpired(apiKey) {
1243
+ if (!apiKey) return;
1244
+ const file = loadDotEnvFile();
1245
+ if (file.BUSABASE_API_KEY !== apiKey) return;
1246
+ const expiresRaw = file[EXPIRES_AT_KEY];
1247
+ if (!expiresRaw) return;
1248
+ const expiresAt = Date.parse(expiresRaw);
1249
+ if (!Number.isNaN(expiresAt) && expiresAt <= Date.now()) {
1250
+ throw new Error(
1251
+ "Your saved device login has expired. Run `busabase-cli login --device-code` to authorize this CLI again."
1252
+ );
1253
+ }
1254
+ }
1121
1255
  async function runLogout(options) {
1122
1256
  const baseUrl = normalizeBaseUrl(options.baseUrl);
1123
1257
  const token = options.apiKey;
1124
1258
  let revoked = "no session to revoke";
1125
- if (token?.startsWith("bss_")) {
1259
+ const file = loadDotEnvFile();
1260
+ if (token && (token.startsWith(LEGACY_SESSION_TOKEN_PREFIX) || file.BUSABASE_API_KEY === token && file[EXPIRES_AT_KEY])) {
1126
1261
  try {
1127
1262
  const res = await fetch(`${baseUrl}/api/oauth/revoke`, {
1128
1263
  method: "POST",
@@ -1598,7 +1733,7 @@ var nodeSchema = z6.lazy(
1598
1733
  visibility: z6.enum(["private", "workspace", "public"]).optional(),
1599
1734
  version: z6.string().optional(),
1600
1735
  assetId: z6.string().optional()
1601
- }).default({}),
1736
+ }).catchall(z6.unknown()).default({}),
1602
1737
  position: z6.number(),
1603
1738
  createdAt: z6.string(),
1604
1739
  updatedAt: z6.string(),
@@ -1630,6 +1765,10 @@ var isDescendantInputSchema = z6.object({
1630
1765
  var isDescendantOutputSchema = z6.object({
1631
1766
  isDescendant: z6.boolean()
1632
1767
  });
1768
+ var updateNodeMetadataInputSchema = z6.object({
1769
+ nodeId: z6.string(),
1770
+ metadata: z6.record(z6.string(), z6.unknown())
1771
+ });
1633
1772
  var searchNodesByNameInputSchema = z6.object({
1634
1773
  query: z6.string().min(1),
1635
1774
  limit: z6.number().int().min(1).max(50).optional().default(20)
@@ -1818,6 +1957,7 @@ var auditActionSchema = z6.enum([
1818
1957
  "asset.metadata_updated",
1819
1958
  "asset.text_written",
1820
1959
  "asset.text_marked_none",
1960
+ "node.metadata_updated",
1821
1961
  "node.purged"
1822
1962
  ]);
1823
1963
  var auditEventSchema = z6.object({
@@ -2007,7 +2147,11 @@ var authInfoSchema = z6.object({
2007
2147
  * space-scoped — when this has more than one entry, callers should confirm the
2008
2148
  * intended space and target it explicitly via the `x-busabase-space` header.
2009
2149
  */
2010
- spaces: z6.array(authSpaceSchema)
2150
+ spaces: z6.array(authSpaceSchema),
2151
+ /** Cloud only: the server created this user's first Space during this exact request. */
2152
+ createdSpace: z6.boolean().optional(),
2153
+ /** Cloud only: this auto-created Space still needs its idempotent starter initialization. */
2154
+ bootstrapRequired: z6.boolean().optional()
2011
2155
  });
2012
2156
 
2013
2157
  // ../../packages/busabase-contract/src/domains/base/contract/view-schemas.ts
@@ -2245,10 +2389,10 @@ var PackageRecordLineSchema = z8.object({
2245
2389
  fields: z8.record(z8.string(), z8.unknown()).default({})
2246
2390
  });
2247
2391
 
2248
- // src/package/apply.ts
2392
+ // ../../packages/busabase-package/src/apply.ts
2249
2393
  import { createHash as createHash5 } from "crypto";
2250
2394
 
2251
- // src/package/tree.ts
2395
+ // ../../packages/busabase-package/src/tree.ts
2252
2396
  import { extname } from "path";
2253
2397
  var NODE_SLUG_PATTERN = /^[a-z0-9-]+$/;
2254
2398
  var assertSafeNodeSlug = (slug, context) => {
@@ -2349,7 +2493,7 @@ var walkNodes = function* (nodes) {
2349
2493
  };
2350
2494
  var collectBaseNodes = (nodes) => [...walkNodes(nodes)].filter((node) => node.type === "base");
2351
2495
 
2352
- // src/package/apply.ts
2496
+ // ../../packages/busabase-package/src/apply.ts
2353
2497
  var RECORD_BATCH_SIZE = 200;
2354
2498
  var fieldKey = (baseSlug, fieldSlug) => `${baseSlug}\0${fieldSlug}`;
2355
2499
  var applyInstall = async (client, plan, options) => {
@@ -2793,7 +2937,7 @@ var approveAndMerge = async (client, changeRequestId) => {
2793
2937
  return client.changeRequests.merge({ changeRequestId });
2794
2938
  };
2795
2939
 
2796
- // src/package/collect.ts
2940
+ // ../../packages/busabase-package/src/collect.ts
2797
2941
  var findSourceNode = (nodes, slugOrId) => {
2798
2942
  const stack = [...nodes];
2799
2943
  while (stack.length > 0) {
@@ -3084,7 +3228,7 @@ var walk = function* (nodes) {
3084
3228
  }
3085
3229
  };
3086
3230
 
3087
- // src/package/github.ts
3231
+ // ../../packages/busabase-package/src/github.ts
3088
3232
  import { Uint8ArrayReader, ZipReader } from "@zip.js/zip.js";
3089
3233
  var GITHUB_ALLOWED_HOSTS = [
3090
3234
  "github.com",
@@ -3243,7 +3387,7 @@ var fetchGithubPackageFiles = async (repoUrl, options = {}) => {
3243
3387
  return { source, files };
3244
3388
  };
3245
3389
 
3246
- // src/package/frontmatter.ts
3390
+ // ../../packages/busabase-package/src/frontmatter.ts
3247
3391
  import { parse, stringify } from "yaml";
3248
3392
  var DELIMITER = "---";
3249
3393
  var normalizeDocBody = (body) => body.replaceAll("\r\n", "\n").replace(/^\n+/, "").replace(/\n+$/, "");
@@ -3287,7 +3431,7 @@ ${normalizeDocBody(body)}
3287
3431
  `;
3288
3432
  };
3289
3433
 
3290
- // src/package/layout-read.ts
3434
+ // ../../packages/busabase-package/src/layout-read.ts
3291
3435
  var decodeText = (bytes, filePath) => {
3292
3436
  const text = bytes.toString("utf8");
3293
3437
  if (text.includes("\uFFFD")) {
@@ -3505,7 +3649,7 @@ var stripExtension = (fileName) => {
3505
3649
  return dot > 0 ? fileName.slice(0, dot) : fileName;
3506
3650
  };
3507
3651
 
3508
- // src/package/layout-write.ts
3652
+ // ../../packages/busabase-package/src/layout-write.ts
3509
3653
  import { mkdir, rm, writeFile } from "fs/promises";
3510
3654
  import { dirname, join as join2 } from "path";
3511
3655
  var sortKeysDeep = (value) => {
@@ -3696,9 +3840,20 @@ var writePackageFiles = async (files, outDir, options = {}) => {
3696
3840
  }
3697
3841
  };
3698
3842
 
3699
- // src/package/plan.ts
3843
+ // ../../packages/busabase-package/src/plan.ts
3844
+ var findTargetFolder = (nodes, slug) => {
3845
+ const roots = nodes.length === 1 && nodes[0].children ? nodes[0].children : nodes;
3846
+ return roots.find((node) => node.slug === slug && node.type === "folder");
3847
+ };
3848
+ var resolveTargetState = async (client, targetFolderSlug) => {
3849
+ const [nodes, bases] = await Promise.all([client.nodes.list(), client.bases.list()]);
3850
+ return {
3851
+ targetFolder: findTargetFolder(nodes, targetFolderSlug),
3852
+ existingBaseSlugs: new Set(bases.map((base) => base.slug))
3853
+ };
3854
+ };
3700
3855
  var buildInstallPlan = (tree, target, options = {}) => {
3701
- const targetFolderSlug = options.intoFolder ?? tree.manifest.name;
3856
+ const targetFolderSlug = options.intoFolder ?? suggestSlug(tree.manifest.name);
3702
3857
  const warnings = [];
3703
3858
  const existingChildSlugs = new Set(
3704
3859
  (target.targetFolder?.children ?? []).map((child) => child.slug)
@@ -3920,16 +4075,11 @@ var runInstall = async (client, repoUrl, options) => {
3920
4075
  });
3921
4076
  reportProgress2(`Downloaded ${files.size} file(s) from ${source.owner}/${source.repo}.`);
3922
4077
  const tree = readPackageTree(files);
3923
- const [nodes, bases] = await Promise.all([client.nodes.list(), client.bases.list()]);
3924
- const targetFolderSlug = options.intoFolder ?? tree.manifest.name;
3925
- const plan = buildInstallPlan(
3926
- tree,
3927
- {
3928
- targetFolder: findTargetFolder(nodes, targetFolderSlug),
3929
- existingBaseSlugs: new Set(bases.map((base) => base.slug))
3930
- },
3931
- { intoFolder: options.intoFolder, rename: options.rename }
3932
- );
4078
+ const targetFolderSlug = options.intoFolder ?? suggestSlug(tree.manifest.name);
4079
+ const plan = buildInstallPlan(tree, await resolveTargetState(client, targetFolderSlug), {
4080
+ intoFolder: options.intoFolder,
4081
+ rename: options.rename
4082
+ });
3933
4083
  if (options.dryRun) {
3934
4084
  const report = `${renderPlan(plan)}
3935
4085
 
@@ -3945,10 +4095,6 @@ Dry run \u2014 nothing was created.`;
3945
4095
  if (options.json) return { installed: true, ...result };
3946
4096
  return renderInstallReport(plan.targetFolderSlug, result);
3947
4097
  };
3948
- var findTargetFolder = (nodes, slug) => {
3949
- const roots = nodes.length === 1 && nodes[0].children ? nodes[0].children : nodes;
3950
- return roots.find((node) => node.slug === slug && node.type === "folder");
3951
- };
3952
4098
  var toPlanSummary = (plan) => ({
3953
4099
  targetFolder: plan.targetFolderSlug,
3954
4100
  package: plan.tree.manifest.name,
@@ -4324,6 +4470,7 @@ function runAction(state, handler) {
4324
4470
  const opts = cmd.optsWithGlobals();
4325
4471
  const config = resolveConfig(opts);
4326
4472
  state.config = config;
4473
+ assertCredentialNotExpired(config.apiKey);
4327
4474
  await maybeAutoRefresh(config.baseUrl, config.apiKey);
4328
4475
  const client = createBusabaseClient(config);
4329
4476
  const result = await handler(client, opts, config);
@@ -4335,6 +4482,7 @@ function runArgAction(state, handler) {
4335
4482
  const opts = cmd.optsWithGlobals();
4336
4483
  const config = resolveConfig(opts);
4337
4484
  state.config = config;
4485
+ assertCredentialNotExpired(config.apiKey);
4338
4486
  await maybeAutoRefresh(config.baseUrl, config.apiKey);
4339
4487
  const client = createBusabaseClient(config);
4340
4488
  const result = await handler(arg, client, opts, config);
@@ -4521,7 +4669,7 @@ function buildProgram(state = {}) {
4521
4669
  runAction(state, (_client, _opts, config) => rawFetch(config, "GET", "/api/v1/openapi.json"))
4522
4670
  );
4523
4671
  addGlobalFlags(program.command("whoami")).description("Active space, user, and membership").action(runAction(state, (client) => client.auth.verify()));
4524
- addGlobalFlags(program.command("login")).description("Connect the CLI to Busabase \u2014 Personal/local, Cloud, or self-hosted").option("--oauth", "force browser OAuth and skip the method prompt").option("--no-browser", "OAuth: print the sign-in URL instead of opening a browser").option("--refresh", "slide the saved OAuth session forward (no browser, no re-login)").addHelpText(
4672
+ addGlobalFlags(program.command("login")).description("Connect the CLI to Busabase \u2014 Personal/local, Cloud, or self-hosted").option("--device-code", "force device authorization and skip the method prompt").option("--oauth", "legacy same-machine OAuth callback flow").option("--no-browser", "print the sign-in URL instead of opening a browser").option("--refresh", "slide the saved OAuth session forward (no browser, no re-login)").addHelpText(
4525
4673
  "after",
4526
4674
  `
4527
4675
  Busabase is an approval-first database and knowledge base for AI agents: agents
@@ -4530,14 +4678,15 @@ propose changes, humans review and merge what becomes trusted data.
4530
4678
  Run interactively, login explains the connection choices and writes the selected
4531
4679
  Busabase instance to ~/.busabase/.env:
4532
4680
  1. Local/Desktop on this computer \u2014 no account, no login
4533
- 2. Busabase Cloud \u2014 browser sign-in (recommended)
4681
+ 2. Busabase Cloud \u2014 device sign-in (recommended; works over SSH)
4534
4682
  3. Busabase Cloud \u2014 paste an API key
4535
- 4. Self-hosted Busabase \u2014 browser sign-in
4683
+ 4. Self-hosted Busabase \u2014 device sign-in
4536
4684
  5. Self-hosted Busabase \u2014 paste an API key
4537
4685
  The flags below skip the menu (handy for scripts / CI):
4538
4686
 
4539
4687
  busabase-cli login # pick from the menu
4540
- busabase-cli login --oauth # Cloud browser sign-in
4688
+ busabase-cli login --device-code # Cloud device sign-in
4689
+ busabase-cli login --oauth # legacy same-machine callback
4541
4690
  busabase-cli login --api-key sk_\u2026 # Cloud API key (headless/CI)
4542
4691
  busabase-cli login --base-url http://localhost:15419 # connect to a local server (no auth)
4543
4692
  busabase-cli login --refresh # extend the current session (auto-runs too)`
@@ -4549,6 +4698,7 @@ The flags below skip the menu (handy for scripts / CI):
4549
4698
  baseUrl: config.baseUrl,
4550
4699
  apiKey: opts.apiKey,
4551
4700
  spaceId: config.spaceId,
4701
+ deviceCode: Boolean(opts.deviceCode),
4552
4702
  oauth: Boolean(opts.oauth),
4553
4703
  browser: opts.browser !== false
4554
4704
  });
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCli
4
- } from "./chunk-ZC4HCQAB.js";
4
+ } from "./chunk-IO4ARUEI.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-ZC4HCQAB.js";
5
+ } from "./chunk-IO4ARUEI.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.9.9",
3
+ "version": "0.9.11",
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",
@@ -32,7 +32,8 @@
32
32
  ],
33
33
  "dependencies": {
34
34
  "@zip.js/zip.js": "^2.8.20",
35
- "busabase-sdk": "0.9.9",
35
+ "busabase-package": "workspace:*",
36
+ "busabase-sdk": "0.9.11",
36
37
  "commander": "^14.0.0",
37
38
  "tar-stream": "^3.1.7",
38
39
  "yaml": "^2.9.0",