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.
- package/dist/{chunk-ZC4HCQAB.js → chunk-IO4ARUEI.js} +202 -52
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -2
|
@@ -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
|
|
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
|
|
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",
|
|
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:
|
|
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
|
|
976
|
-
say("
|
|
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
|
|
980
|
-
say(" Use your team's Busabase URL when it supports
|
|
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: "
|
|
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: "
|
|
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" : "
|
|
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
|
-
|
|
1038
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
2392
|
+
// ../../packages/busabase-package/src/apply.ts
|
|
2249
2393
|
import { createHash as createHash5 } from "crypto";
|
|
2250
2394
|
|
|
2251
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
3924
|
-
const
|
|
3925
|
-
|
|
3926
|
-
|
|
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("--
|
|
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
|
|
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
|
|
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 --
|
|
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
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "busa-cli",
|
|
3
|
-
"version": "0.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-
|
|
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",
|