busa-cli 0.9.12 → 0.9.14
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-DRYP6RY3.js → chunk-P4Z4SWET.js} +164 -65
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
|
@@ -775,11 +775,10 @@ var BUSABASE_CLI_CLIENT_ID = "busabase-cli";
|
|
|
775
775
|
|
|
776
776
|
// src/login.ts
|
|
777
777
|
import { DEFAULT_BASE_URL, normalizeBaseUrl } from "busabase-sdk";
|
|
778
|
-
var CLI_CLIENT_PLATFORM = "cli";
|
|
779
778
|
var OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
780
779
|
var EXPIRES_AT_KEY = "BUSABASE_TOKEN_EXPIRES_AT";
|
|
781
|
-
var
|
|
782
|
-
var AUTO_REFRESH_THRESHOLD_MS =
|
|
780
|
+
var REFRESH_TOKEN_KEY = "BUSABASE_REFRESH_TOKEN";
|
|
781
|
+
var AUTO_REFRESH_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
783
782
|
var DEFAULT_LOCAL_URL = "http://localhost:15419";
|
|
784
783
|
var base64url = (buffer) => buffer.toString("base64url");
|
|
785
784
|
var isInteractive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
@@ -828,7 +827,7 @@ async function verifyAuth(baseUrl, token) {
|
|
|
828
827
|
});
|
|
829
828
|
if (res.status === 401) {
|
|
830
829
|
throw new Error(
|
|
831
|
-
`The credential was rejected (401) by ${baseUrl}. If it's an API key, check it in Dashboard \u2192 Settings \u2192 API Keys; if it's
|
|
830
|
+
`The credential was rejected (401) by ${baseUrl}. If it's an API key, check it in Dashboard \u2192 Settings \u2192 API Keys; if it's an OAuth login, run \`busabase-cli login\` again.`
|
|
832
831
|
);
|
|
833
832
|
}
|
|
834
833
|
if (!res.ok) {
|
|
@@ -857,6 +856,7 @@ async function loopbackOauthLogin(baseUrl, useBrowser) {
|
|
|
857
856
|
const error = url.searchParams.get("error");
|
|
858
857
|
const returnedState = url.searchParams.get("state");
|
|
859
858
|
const returnedCode = url.searchParams.get("code");
|
|
859
|
+
const returnedIssuer = url.searchParams.get("iss");
|
|
860
860
|
if (error) {
|
|
861
861
|
respond("Sign-in failed", "You can close this tab and return to the terminal.");
|
|
862
862
|
cleanup();
|
|
@@ -869,6 +869,20 @@ async function loopbackOauthLogin(baseUrl, useBrowser) {
|
|
|
869
869
|
reject(new Error("OAuth state mismatch \u2014 the callback did not match this request."));
|
|
870
870
|
return;
|
|
871
871
|
}
|
|
872
|
+
let issuerMatches = false;
|
|
873
|
+
try {
|
|
874
|
+
issuerMatches = Boolean(
|
|
875
|
+
returnedIssuer && new URL(returnedIssuer).origin === new URL(baseUrl).origin
|
|
876
|
+
);
|
|
877
|
+
} catch {
|
|
878
|
+
issuerMatches = false;
|
|
879
|
+
}
|
|
880
|
+
if (!issuerMatches) {
|
|
881
|
+
respond("Sign-in failed", "Authorization server mismatch. Close this tab and try again.");
|
|
882
|
+
cleanup();
|
|
883
|
+
reject(new Error("OAuth authorization server issuer mismatch."));
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
872
886
|
respond("Signed in \u2713", "You can close this tab and return to the terminal.");
|
|
873
887
|
const address = server.address();
|
|
874
888
|
cleanup();
|
|
@@ -892,7 +906,8 @@ async function loopbackOauthLogin(baseUrl, useBrowser) {
|
|
|
892
906
|
const authorizeUrl = new URL(`${baseUrl}/api/oauth/authorize`);
|
|
893
907
|
authorizeUrl.searchParams.set("response_type", "code");
|
|
894
908
|
authorizeUrl.searchParams.set("client_id", BUSABASE_CLI_CLIENT_ID);
|
|
895
|
-
authorizeUrl.searchParams.set("
|
|
909
|
+
authorizeUrl.searchParams.set("resource", `${baseUrl}/api/v1`);
|
|
910
|
+
authorizeUrl.searchParams.set("scope", "api");
|
|
896
911
|
authorizeUrl.searchParams.set("code_challenge", codeChallenge);
|
|
897
912
|
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
898
913
|
authorizeUrl.searchParams.set("redirect_uri", redirect);
|
|
@@ -913,15 +928,14 @@ async function loopbackOauthLogin(baseUrl, useBrowser) {
|
|
|
913
928
|
);
|
|
914
929
|
const tokenRes = await fetch(`${baseUrl}/api/oauth/token`, {
|
|
915
930
|
method: "POST",
|
|
916
|
-
headers: { "content-type": "application/
|
|
917
|
-
body:
|
|
931
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
932
|
+
body: new URLSearchParams({
|
|
918
933
|
grant_type: "authorization_code",
|
|
919
934
|
client_id: BUSABASE_CLI_CLIENT_ID,
|
|
920
|
-
client_platform: CLI_CLIENT_PLATFORM,
|
|
921
935
|
code,
|
|
922
936
|
code_verifier: codeVerifier,
|
|
923
937
|
redirect_uri: redirectUri,
|
|
924
|
-
|
|
938
|
+
resource: `${baseUrl}/api/v1`
|
|
925
939
|
})
|
|
926
940
|
});
|
|
927
941
|
if (!tokenRes.ok) {
|
|
@@ -929,9 +943,13 @@ async function loopbackOauthLogin(baseUrl, useBrowser) {
|
|
|
929
943
|
throw new Error(`Token exchange failed (HTTP ${tokenRes.status})${text ? `: ${text}` : ""}`);
|
|
930
944
|
}
|
|
931
945
|
const payload = await tokenRes.json();
|
|
932
|
-
const token = payload.
|
|
946
|
+
const token = payload.access_token;
|
|
933
947
|
if (!token) throw new Error("Token exchange returned no credential.");
|
|
934
|
-
return
|
|
948
|
+
return {
|
|
949
|
+
token,
|
|
950
|
+
refreshToken: payload.refresh_token,
|
|
951
|
+
expiresAt: typeof payload.expires_in === "number" ? new Date(Date.now() + payload.expires_in * 1e3).toISOString() : void 0
|
|
952
|
+
};
|
|
935
953
|
}
|
|
936
954
|
var delay = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
937
955
|
async function readDeviceResponse(response) {
|
|
@@ -1036,15 +1054,26 @@ async function deviceLogin(baseUrl, useBrowser) {
|
|
|
1036
1054
|
}
|
|
1037
1055
|
throw new Error("The device code expired. Run `busabase-cli login` to request a new one.");
|
|
1038
1056
|
}
|
|
1039
|
-
async function refreshToken(baseUrl,
|
|
1040
|
-
const res = await fetch(`${baseUrl}/api/oauth/
|
|
1057
|
+
async function refreshToken(baseUrl, refreshToken2) {
|
|
1058
|
+
const res = await fetch(`${baseUrl}/api/oauth/token`, {
|
|
1041
1059
|
method: "POST",
|
|
1042
|
-
headers: {
|
|
1060
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
1061
|
+
body: new URLSearchParams({
|
|
1062
|
+
grant_type: "refresh_token",
|
|
1063
|
+
refresh_token: refreshToken2,
|
|
1064
|
+
client_id: BUSABASE_CLI_CLIENT_ID,
|
|
1065
|
+
resource: `${baseUrl}/api/v1`
|
|
1066
|
+
})
|
|
1043
1067
|
});
|
|
1044
|
-
if (res.status === 401) throw new Error("SESSION_EXPIRED");
|
|
1068
|
+
if (res.status === 400 || res.status === 401) throw new Error("SESSION_EXPIRED");
|
|
1045
1069
|
if (!res.ok) throw new Error(`Refresh failed (HTTP ${res.status}) from ${baseUrl}.`);
|
|
1046
1070
|
const payload = await res.json();
|
|
1047
|
-
|
|
1071
|
+
if (!payload.access_token) throw new Error("Refresh response had no access token");
|
|
1072
|
+
return {
|
|
1073
|
+
token: payload.access_token,
|
|
1074
|
+
refreshToken: payload.refresh_token,
|
|
1075
|
+
expiresAt: typeof payload.expires_in === "number" ? new Date(Date.now() + payload.expires_in * 1e3).toISOString() : void 0
|
|
1076
|
+
};
|
|
1048
1077
|
}
|
|
1049
1078
|
async function pickSpaceId(verify, preselected) {
|
|
1050
1079
|
if (preselected) return preselected;
|
|
@@ -1144,6 +1173,7 @@ async function runLogin(options) {
|
|
|
1144
1173
|
let token;
|
|
1145
1174
|
let expiresAt;
|
|
1146
1175
|
let apiKeyExpiresAt;
|
|
1176
|
+
let refreshTokenValue;
|
|
1147
1177
|
if (method === "device") {
|
|
1148
1178
|
const result = await deviceLogin(baseUrl, options.browser);
|
|
1149
1179
|
token = result.token;
|
|
@@ -1152,6 +1182,7 @@ async function runLogin(options) {
|
|
|
1152
1182
|
const result = await loopbackOauthLogin(baseUrl, options.browser);
|
|
1153
1183
|
token = result.token;
|
|
1154
1184
|
expiresAt = result.expiresAt;
|
|
1185
|
+
refreshTokenValue = result.refreshToken;
|
|
1155
1186
|
apiKeyExpiresAt = result.apiKeyExpiresAt;
|
|
1156
1187
|
} else {
|
|
1157
1188
|
if (!apiKey) {
|
|
@@ -1169,14 +1200,15 @@ async function runLogin(options) {
|
|
|
1169
1200
|
BUSABASE_API_KEY: token,
|
|
1170
1201
|
BUSABASE_SPACE_ID: spaceId ?? null,
|
|
1171
1202
|
// Clear any stale expiry when switching to an API key.
|
|
1172
|
-
[EXPIRES_AT_KEY]: expiresAt ?? null
|
|
1203
|
+
[EXPIRES_AT_KEY]: expiresAt ?? null,
|
|
1204
|
+
[REFRESH_TOKEN_KEY]: refreshTokenValue ?? null
|
|
1173
1205
|
});
|
|
1174
1206
|
say("");
|
|
1175
1207
|
say(`\u2713 Signed in and saved to ${dotEnvPath()}`);
|
|
1176
1208
|
return {
|
|
1177
1209
|
status: "signed in",
|
|
1178
1210
|
method: method === "loopback-oauth" ? "oauth" : method,
|
|
1179
|
-
credentialType: method === "device" ? "api_key" : expiresAt ? "
|
|
1211
|
+
credentialType: method === "device" ? "api_key" : expiresAt ? "oauth" : "api_key",
|
|
1180
1212
|
user: verify.user?.email ?? verify.user?.name ?? verify.user?.id ?? "(unknown)",
|
|
1181
1213
|
space: spaceId ?? "(server default)",
|
|
1182
1214
|
createdSpace: String(Boolean(verify.createdSpace)),
|
|
@@ -1191,29 +1223,34 @@ async function runRefresh(options) {
|
|
|
1191
1223
|
const token = options.apiKey;
|
|
1192
1224
|
if (!token) throw new Error("Not signed in \u2014 run `busabase-cli login` first.");
|
|
1193
1225
|
const file = loadDotEnvFile();
|
|
1194
|
-
const
|
|
1195
|
-
if (!
|
|
1226
|
+
const isSavedExpiringOAuthToken = file.BUSABASE_API_KEY === token && Boolean(file.BUSABASE_TOKEN_EXPIRES_AT);
|
|
1227
|
+
if (!isSavedExpiringOAuthToken) {
|
|
1196
1228
|
return {
|
|
1197
1229
|
status: "nothing to refresh",
|
|
1198
|
-
detail: "This credential is an API key and cannot be refreshed. Only OAuth
|
|
1230
|
+
detail: "This credential is an API key and cannot be refreshed. Only OAuth token sets refresh."
|
|
1199
1231
|
};
|
|
1200
1232
|
}
|
|
1233
|
+
const savedRefreshToken = file[REFRESH_TOKEN_KEY];
|
|
1234
|
+
if (!savedRefreshToken) throw new Error("OAuth refresh token is missing \u2014 sign in again.");
|
|
1201
1235
|
let refreshed;
|
|
1202
1236
|
try {
|
|
1203
|
-
refreshed = await refreshToken(baseUrl,
|
|
1237
|
+
refreshed = await refreshToken(baseUrl, savedRefreshToken);
|
|
1204
1238
|
} catch (error) {
|
|
1205
1239
|
if (error instanceof Error && error.message === "SESSION_EXPIRED") {
|
|
1206
1240
|
throw new Error(
|
|
1207
|
-
"Your
|
|
1241
|
+
"Your OAuth authorization has expired \u2014 run `busabase-cli login` to sign in again."
|
|
1208
1242
|
);
|
|
1209
1243
|
}
|
|
1210
1244
|
throw error;
|
|
1211
1245
|
}
|
|
1212
1246
|
writeDotEnvFile({
|
|
1213
1247
|
BUSABASE_API_KEY: refreshed.token,
|
|
1214
|
-
[EXPIRES_AT_KEY]: refreshed.expiresAt ?? null
|
|
1248
|
+
[EXPIRES_AT_KEY]: refreshed.expiresAt ?? null,
|
|
1249
|
+
[REFRESH_TOKEN_KEY]: refreshed.refreshToken ?? null
|
|
1215
1250
|
});
|
|
1216
|
-
say(
|
|
1251
|
+
say(
|
|
1252
|
+
`\u2713 OAuth token refreshed${refreshed.expiresAt ? ` \u2014 valid until ${refreshed.expiresAt}` : ""}`
|
|
1253
|
+
);
|
|
1217
1254
|
return {
|
|
1218
1255
|
status: "refreshed",
|
|
1219
1256
|
expiresAt: refreshed.expiresAt ?? "(unknown)",
|
|
@@ -1221,22 +1258,26 @@ async function runRefresh(options) {
|
|
|
1221
1258
|
};
|
|
1222
1259
|
}
|
|
1223
1260
|
async function maybeAutoRefresh(baseUrl, apiKey) {
|
|
1224
|
-
if (!apiKey) return;
|
|
1261
|
+
if (!apiKey) return void 0;
|
|
1225
1262
|
const file = loadDotEnvFile();
|
|
1226
|
-
if (file.BUSABASE_API_KEY !== apiKey) return;
|
|
1263
|
+
if (file.BUSABASE_API_KEY !== apiKey) return void 0;
|
|
1227
1264
|
const expiresRaw = file[EXPIRES_AT_KEY];
|
|
1228
|
-
if (!expiresRaw) return;
|
|
1265
|
+
if (!expiresRaw) return void 0;
|
|
1229
1266
|
const expiresAt = Date.parse(expiresRaw);
|
|
1230
|
-
if (Number.isNaN(expiresAt)) return;
|
|
1231
|
-
if (expiresAt
|
|
1232
|
-
if (expiresAt - Date.now() > AUTO_REFRESH_THRESHOLD_MS) return;
|
|
1267
|
+
if (Number.isNaN(expiresAt)) return void 0;
|
|
1268
|
+
if (expiresAt - Date.now() > AUTO_REFRESH_THRESHOLD_MS) return void 0;
|
|
1233
1269
|
try {
|
|
1234
|
-
const
|
|
1270
|
+
const savedRefreshToken = file[REFRESH_TOKEN_KEY];
|
|
1271
|
+
if (!savedRefreshToken) return void 0;
|
|
1272
|
+
const refreshed = await refreshToken(normalizeBaseUrl(baseUrl), savedRefreshToken);
|
|
1235
1273
|
writeDotEnvFile({
|
|
1236
1274
|
BUSABASE_API_KEY: refreshed.token,
|
|
1237
|
-
[EXPIRES_AT_KEY]: refreshed.expiresAt ?? null
|
|
1275
|
+
[EXPIRES_AT_KEY]: refreshed.expiresAt ?? null,
|
|
1276
|
+
[REFRESH_TOKEN_KEY]: refreshed.refreshToken ?? null
|
|
1238
1277
|
});
|
|
1278
|
+
return refreshed.token;
|
|
1239
1279
|
} catch {
|
|
1280
|
+
return void 0;
|
|
1240
1281
|
}
|
|
1241
1282
|
}
|
|
1242
1283
|
function assertCredentialNotExpired(apiKey) {
|
|
@@ -1248,29 +1289,36 @@ function assertCredentialNotExpired(apiKey) {
|
|
|
1248
1289
|
const expiresAt = Date.parse(expiresRaw);
|
|
1249
1290
|
if (!Number.isNaN(expiresAt) && expiresAt <= Date.now()) {
|
|
1250
1291
|
throw new Error(
|
|
1251
|
-
"Your saved
|
|
1292
|
+
"Your saved OAuth login has expired. Run `busabase-cli login` to sign in again."
|
|
1252
1293
|
);
|
|
1253
1294
|
}
|
|
1254
1295
|
}
|
|
1255
1296
|
async function runLogout(options) {
|
|
1256
1297
|
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
1257
1298
|
const token = options.apiKey;
|
|
1258
|
-
let revoked = "no
|
|
1299
|
+
let revoked = "no OAuth token to revoke";
|
|
1259
1300
|
const file = loadDotEnvFile();
|
|
1260
|
-
if (token &&
|
|
1301
|
+
if (token && file.BUSABASE_API_KEY === token && file[EXPIRES_AT_KEY]) {
|
|
1261
1302
|
try {
|
|
1303
|
+
const tokenToRevoke = file[REFRESH_TOKEN_KEY] ?? token;
|
|
1262
1304
|
const res = await fetch(`${baseUrl}/api/oauth/revoke`, {
|
|
1263
1305
|
method: "POST",
|
|
1264
|
-
headers: {
|
|
1306
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
1307
|
+
body: new URLSearchParams({ token: tokenToRevoke })
|
|
1265
1308
|
});
|
|
1266
|
-
revoked = res.ok ? "
|
|
1309
|
+
revoked = res.ok ? "OAuth token family revoked" : `revoke returned HTTP ${res.status}`;
|
|
1267
1310
|
} catch {
|
|
1268
1311
|
revoked = "revoke request failed (cleared locally anyway)";
|
|
1269
1312
|
}
|
|
1270
1313
|
} else if (token) {
|
|
1271
1314
|
revoked = "API key cleared locally (revoke it in the dashboard to disable it)";
|
|
1272
1315
|
}
|
|
1273
|
-
writeDotEnvFile({
|
|
1316
|
+
writeDotEnvFile({
|
|
1317
|
+
BUSABASE_API_KEY: null,
|
|
1318
|
+
BUSABASE_SPACE_ID: null,
|
|
1319
|
+
[EXPIRES_AT_KEY]: null,
|
|
1320
|
+
[REFRESH_TOKEN_KEY]: null
|
|
1321
|
+
});
|
|
1274
1322
|
say(`\u2713 Cleared the saved credential from ${dotEnvPath()}`);
|
|
1275
1323
|
return { status: "signed out", detail: revoked, config: dotEnvPath() };
|
|
1276
1324
|
}
|
|
@@ -1416,11 +1464,11 @@ var createBaseInputSchema = z5.object({
|
|
|
1416
1464
|
options: fieldOptionsSchema.optional().default({})
|
|
1417
1465
|
})
|
|
1418
1466
|
).default([]),
|
|
1419
|
-
//
|
|
1420
|
-
//
|
|
1421
|
-
//
|
|
1422
|
-
// review
|
|
1423
|
-
autoMerge: z5.boolean().optional()
|
|
1467
|
+
// Permission-aware default: omitted merges immediately if the actor has
|
|
1468
|
+
// write access on the parent node, otherwise falls back to a pending
|
|
1469
|
+
// ChangeRequest (status "in_review"). Pass explicit `autoMerge: false` to
|
|
1470
|
+
// force review even with write access.
|
|
1471
|
+
autoMerge: z5.boolean().optional()
|
|
1424
1472
|
});
|
|
1425
1473
|
var createBaseFieldInputSchema = z5.object({
|
|
1426
1474
|
name: fieldNameSchema,
|
|
@@ -1656,6 +1704,15 @@ var folderNodeType = {
|
|
|
1656
1704
|
operations: []
|
|
1657
1705
|
};
|
|
1658
1706
|
|
|
1707
|
+
// ../../packages/busabase-contract/src/domains/html/definition.ts
|
|
1708
|
+
var htmlNodeType = {
|
|
1709
|
+
type: "html",
|
|
1710
|
+
label: "HTML",
|
|
1711
|
+
icon: "code-xml",
|
|
1712
|
+
capabilities: { hasDetail: true, creatable: true },
|
|
1713
|
+
operations: []
|
|
1714
|
+
};
|
|
1715
|
+
|
|
1659
1716
|
// ../../packages/busabase-contract/src/domains/skill/definition.ts
|
|
1660
1717
|
var skillNodeType = makeFileTreeNodeType({
|
|
1661
1718
|
type: "skill",
|
|
@@ -1666,6 +1723,24 @@ var skillNodeType = makeFileTreeNodeType({
|
|
|
1666
1723
|
entryFile: "SKILL.md"
|
|
1667
1724
|
});
|
|
1668
1725
|
|
|
1726
|
+
// ../../packages/busabase-contract/src/domains/whiteboard/definition.ts
|
|
1727
|
+
var whiteboardNodeType = {
|
|
1728
|
+
type: "whiteboard",
|
|
1729
|
+
label: "Whiteboard",
|
|
1730
|
+
icon: "pen-tool",
|
|
1731
|
+
capabilities: { hasDetail: true, creatable: true },
|
|
1732
|
+
operations: []
|
|
1733
|
+
};
|
|
1734
|
+
|
|
1735
|
+
// ../../packages/busabase-contract/src/domains/workflow/definition.ts
|
|
1736
|
+
var workflowNodeType = {
|
|
1737
|
+
type: "workflow",
|
|
1738
|
+
label: "Workflow",
|
|
1739
|
+
icon: "workflow",
|
|
1740
|
+
capabilities: { hasDetail: true, creatable: true },
|
|
1741
|
+
operations: []
|
|
1742
|
+
};
|
|
1743
|
+
|
|
1669
1744
|
// ../../packages/busabase-contract/src/domains/registry.ts
|
|
1670
1745
|
var GENERIC_NODE_OPERATIONS = [
|
|
1671
1746
|
{
|
|
@@ -1693,7 +1768,10 @@ var BUILTIN_NODE_TYPES = [
|
|
|
1693
1768
|
driveNodeType,
|
|
1694
1769
|
airappNodeType,
|
|
1695
1770
|
fileNodeType,
|
|
1696
|
-
docNodeType
|
|
1771
|
+
docNodeType,
|
|
1772
|
+
whiteboardNodeType,
|
|
1773
|
+
workflowNodeType,
|
|
1774
|
+
htmlNodeType
|
|
1697
1775
|
];
|
|
1698
1776
|
for (const definition of BUILTIN_NODE_TYPES) {
|
|
1699
1777
|
registerNodeType(definition);
|
|
@@ -2038,8 +2116,8 @@ var createNodeChangeRequestInputSchema = z6.object({
|
|
|
2038
2116
|
'Explanation shown to the human reviewer. Write a conventional-commit style subject \u2014 imperative verb + what + why, e.g. "Reorganize marketing docs under a Campaigns folder".'
|
|
2039
2117
|
),
|
|
2040
2118
|
submittedBy: z6.string().optional().default("local-producer"),
|
|
2041
|
-
autoMerge: z6.boolean().optional().
|
|
2042
|
-
"Whether to approve and merge this structural node change immediately.
|
|
2119
|
+
autoMerge: z6.boolean().optional().describe(
|
|
2120
|
+
"Whether to approve and merge this structural node change immediately. Omitted defaults to merging immediately if the actor has write access on every target node, otherwise falling back to a pending Change Request; pass explicit false to force review even with write access."
|
|
2043
2121
|
),
|
|
2044
2122
|
operations: z6.array(nodeOperationInputSchema).min(1)
|
|
2045
2123
|
});
|
|
@@ -4477,9 +4555,10 @@ function runAction(state, handler) {
|
|
|
4477
4555
|
return async (_opts, cmd) => {
|
|
4478
4556
|
const opts = cmd.optsWithGlobals();
|
|
4479
4557
|
const config = resolveConfig(opts);
|
|
4558
|
+
const refreshedToken = await maybeAutoRefresh(config.baseUrl, config.apiKey);
|
|
4559
|
+
if (refreshedToken) config.apiKey = refreshedToken;
|
|
4480
4560
|
state.config = config;
|
|
4481
4561
|
assertCredentialNotExpired(config.apiKey);
|
|
4482
|
-
await maybeAutoRefresh(config.baseUrl, config.apiKey);
|
|
4483
4562
|
const client = createBusabaseClient(config);
|
|
4484
4563
|
const result = await handler(client, opts, config);
|
|
4485
4564
|
console.log(render(result, config.output));
|
|
@@ -4489,9 +4568,10 @@ function runArgAction(state, handler) {
|
|
|
4489
4568
|
return async (arg, _opts, cmd) => {
|
|
4490
4569
|
const opts = cmd.optsWithGlobals();
|
|
4491
4570
|
const config = resolveConfig(opts);
|
|
4571
|
+
const refreshedToken = await maybeAutoRefresh(config.baseUrl, config.apiKey);
|
|
4572
|
+
if (refreshedToken) config.apiKey = refreshedToken;
|
|
4492
4573
|
state.config = config;
|
|
4493
4574
|
assertCredentialNotExpired(config.apiKey);
|
|
4494
|
-
await maybeAutoRefresh(config.baseUrl, config.apiKey);
|
|
4495
4575
|
const client = createBusabaseClient(config);
|
|
4496
4576
|
const result = await handler(arg, client, opts, config);
|
|
4497
4577
|
console.log(render(result, config.output));
|
|
@@ -4564,7 +4644,7 @@ function registerGeneratedCommands(program, state) {
|
|
|
4564
4644
|
const getGroup = (displayName, tag) => program.commands.find((c2) => c2.name() === displayName) ?? program.command(displayName).description(tag ?? `${displayName} endpoints`);
|
|
4565
4645
|
const addLeaf = (parent, navPath, procKey, proc) => {
|
|
4566
4646
|
const { route, inputSchema } = proc["~orpc"];
|
|
4567
|
-
const name =
|
|
4647
|
+
const name = [...navPath.slice(1), procKey].map(kebab).join("-");
|
|
4568
4648
|
if (parent.commands.some((c2) => c2.name() === name)) return;
|
|
4569
4649
|
const fields = inputFields(inputSchema);
|
|
4570
4650
|
const leaf = parent.command(name).description(route.summary ?? `${route.method} ${route.path}`);
|
|
@@ -4622,7 +4702,6 @@ OpenAPI: ${route.method} ${route.path}`);
|
|
|
4622
4702
|
const id = [...navPath, key].join(".");
|
|
4623
4703
|
if (isProcedure(child)) {
|
|
4624
4704
|
if (GENERATED_SKIP.has(id)) continue;
|
|
4625
|
-
if (navPath.length > 1) continue;
|
|
4626
4705
|
const parent = navPath.length ? getGroup(kebab(navPath[0]), child["~orpc"].route.tags?.[0]) : program;
|
|
4627
4706
|
addLeaf(parent, navPath, key, child);
|
|
4628
4707
|
} else if (child && typeof child === "object") {
|
|
@@ -4677,7 +4756,7 @@ function buildProgram(state = {}) {
|
|
|
4677
4756
|
runAction(state, (_client, _opts, config) => rawFetch(config, "GET", "/api/v1/openapi.json"))
|
|
4678
4757
|
);
|
|
4679
4758
|
addGlobalFlags(program.command("whoami")).description("Active space, user, and membership").action(runAction(state, (client) => client.auth.verify()));
|
|
4680
|
-
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", "
|
|
4759
|
+
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", "rotate the saved OAuth token set (no browser, no re-login)").addHelpText(
|
|
4681
4760
|
"after",
|
|
4682
4761
|
`
|
|
4683
4762
|
Busabase is an approval-first database and knowledge base for AI agents: agents
|
|
@@ -4697,7 +4776,7 @@ The flags below skip the menu (handy for scripts / CI):
|
|
|
4697
4776
|
busabase-cli login --oauth # legacy same-machine callback
|
|
4698
4777
|
busabase-cli login --api-key sk_\u2026 # Cloud API key (headless/CI)
|
|
4699
4778
|
busabase-cli login --base-url http://localhost:15419 # connect to a local server (no auth)
|
|
4700
|
-
busabase-cli login --refresh #
|
|
4779
|
+
busabase-cli login --refresh # rotate the OAuth token set (auto-runs too)`
|
|
4701
4780
|
).action(async (_opts, cmd) => {
|
|
4702
4781
|
const opts = cmd.optsWithGlobals();
|
|
4703
4782
|
const config = resolveConfig(opts);
|
|
@@ -4712,7 +4791,7 @@ The flags below skip the menu (handy for scripts / CI):
|
|
|
4712
4791
|
});
|
|
4713
4792
|
console.log(render(summary, config.output));
|
|
4714
4793
|
});
|
|
4715
|
-
addGlobalFlags(program.command("logout")).description("Revoke the saved OAuth
|
|
4794
|
+
addGlobalFlags(program.command("logout")).description("Revoke the saved OAuth token family (if any) and clear ~/.busabase/.env").action(async (_opts, cmd) => {
|
|
4716
4795
|
const opts = cmd.optsWithGlobals();
|
|
4717
4796
|
const config = resolveConfig(opts);
|
|
4718
4797
|
state.config = config;
|
|
@@ -4723,14 +4802,21 @@ The flags below skip the menu (handy for scripts / CI):
|
|
|
4723
4802
|
addGlobalFlags(nodes.command("list")).description("Workspace node tree").action(runAction(state, (client) => client.nodes.list()));
|
|
4724
4803
|
addGlobalFlags(nodes.command("create-change-request")).description("Propose a new node via a Change Request").addOption(
|
|
4725
4804
|
new Option(`--type <${CREATABLE_NODE_TYPES2.join("|")}>`, "node type").choices(CREATABLE_NODE_TYPES2).makeOptionMandatory()
|
|
4726
|
-
).requiredOption("--slug <slug>", "node slug").requiredOption("--name <name>", "node name").option("--description <text>", "optional node description").option("--parent-node-id <id>", "parent folder node id; omit for root").option("--message <text>", "reviewer-facing Change Request message").option("--submitted-by <name>", "producer label").option("--field <slug:name:type...>", "base field, repeatable (for --type base)").option("--fields-json <json|@file>", "base fields as JSON array (for --type base)").option("--asset-id <id>", "backing Asset id (required for --type file)").
|
|
4805
|
+
).requiredOption("--slug <slug>", "node slug").requiredOption("--name <name>", "node name").option("--description <text>", "optional node description").option("--parent-node-id <id>", "parent folder node id; omit for root").option("--message <text>", "reviewer-facing Change Request message").option("--submitted-by <name>", "producer label").option("--field <slug:name:type...>", "base field, repeatable (for --type base)").option("--fields-json <json|@file>", "base fields as JSON array (for --type base)").option("--asset-id <id>", "backing Asset id (required for --type file)").option(
|
|
4806
|
+
"--auto-merge",
|
|
4807
|
+
"skip review and create the node immediately if you have write access (default: merge immediately if you have write access, otherwise propose a pending Change Request)"
|
|
4808
|
+
).option(
|
|
4809
|
+
"--require-review",
|
|
4810
|
+
"always propose a pending Change Request, even if you have write access"
|
|
4811
|
+
).addHelpText(
|
|
4727
4812
|
"after",
|
|
4728
4813
|
`
|
|
4729
4814
|
Examples:
|
|
4730
4815
|
busabase-cli nodes create-change-request --type folder --slug cms --name "\u5185\u5BB9\u7BA1\u7406 CMS"
|
|
4731
4816
|
busabase-cli nodes create-change-request --type base --slug blog --name "\u535A\u5BA2\u6587\u7AE0 Blog Posts" --field title:Title:text --field body:Body:markdown
|
|
4732
4817
|
busabase-cli nodes create-change-request --type base --slug products --name "\u4EA7\u54C1\u76EE\u5F55 Products" --fields-json @fields.json
|
|
4733
|
-
busabase-cli nodes create-change-request --type file --slug board-plan --name "Board Plan" --asset-id ast_123
|
|
4818
|
+
busabase-cli nodes create-change-request --type file --slug board-plan --name "Board Plan" --asset-id ast_123
|
|
4819
|
+
busabase-cli nodes create-change-request --type folder --slug cms --name "\u5185\u5BB9\u7BA1\u7406 CMS" --require-review # always propose a Change Request`
|
|
4734
4820
|
).action(
|
|
4735
4821
|
runAction(state, (client, opts) => {
|
|
4736
4822
|
const nodeType = opts.type;
|
|
@@ -4744,6 +4830,7 @@ Examples:
|
|
|
4744
4830
|
return client.nodes.createChangeRequest({
|
|
4745
4831
|
message: opts.message ?? `Create ${nodeType} ${name}`,
|
|
4746
4832
|
submittedBy: opts.submittedBy,
|
|
4833
|
+
...opts.requireReview ? { autoMerge: false } : opts.autoMerge ? { autoMerge: true } : {},
|
|
4747
4834
|
operations: [
|
|
4748
4835
|
{
|
|
4749
4836
|
kind: "create",
|
|
@@ -4823,14 +4910,18 @@ Examples:
|
|
|
4823
4910
|
);
|
|
4824
4911
|
addGlobalFlags(bases.command("create")).description("Create a Base \u2014 review-first by default: returns a pending Change Request").requiredOption("--slug <slug>", "Base slug").requiredOption("--name <name>", "Base name").option("--field <slug:name:type...>", "field definition, repeatable").option("--fields-json <json|@file>", "field definitions as JSON array").option("--description <text>", "optional description").option("--parent-node-id <id>", "parent folder node id; omit for root").option(
|
|
4825
4912
|
"--auto-merge",
|
|
4826
|
-
"skip review and create the Base immediately (default: propose a pending Change Request)"
|
|
4913
|
+
"skip review and create the Base immediately if you have write access (default: merge immediately if you have write access, otherwise propose a pending Change Request)"
|
|
4914
|
+
).option(
|
|
4915
|
+
"--require-review",
|
|
4916
|
+
"always propose a pending Change Request, even if you have write access"
|
|
4827
4917
|
).addHelpText(
|
|
4828
4918
|
"after",
|
|
4829
4919
|
`
|
|
4830
4920
|
Examples:
|
|
4831
4921
|
busabase-cli bases create --slug products --name "\u4EA7\u54C1\u76EE\u5F55 Products" --field product_name:"Product Name":text
|
|
4832
4922
|
busabase-cli bases create --slug products --name "\u4EA7\u54C1\u76EE\u5F55 Products" --fields-json @fields.json
|
|
4833
|
-
busabase-cli bases create --slug products --name "Products" --auto-merge # skip review, create immediately
|
|
4923
|
+
busabase-cli bases create --slug products --name "Products" --auto-merge # skip review, create immediately
|
|
4924
|
+
busabase-cli bases create --slug products --name "Products" --require-review # always propose a Change Request`
|
|
4834
4925
|
).action(
|
|
4835
4926
|
runAction(
|
|
4836
4927
|
state,
|
|
@@ -4840,7 +4931,7 @@ Examples:
|
|
|
4840
4931
|
description: opts.description,
|
|
4841
4932
|
parentNodeId: opts.parentNodeId,
|
|
4842
4933
|
fields: parseFieldDefinitions(opts),
|
|
4843
|
-
...opts.autoMerge ? { autoMerge: true } : {}
|
|
4934
|
+
...opts.requireReview ? { autoMerge: false } : opts.autoMerge ? { autoMerge: true } : {}
|
|
4844
4935
|
})
|
|
4845
4936
|
)
|
|
4846
4937
|
);
|
|
@@ -4897,14 +4988,18 @@ Examples:
|
|
|
4897
4988
|
);
|
|
4898
4989
|
addGlobalFlags(bases.command("create-change-request")).description("Propose a new record via a Change Request \u2014 review-first by default").requiredOption("--base-id <id>", "target Base id").requiredOption("--fields-json <json|@file>", "record fields as JSON, or @file.json").option("--message <text>", "reviewer-facing Change Request message").option("--submitted-by <name>", "producer label").option(
|
|
4899
4990
|
"--auto-merge",
|
|
4900
|
-
"skip review and create the record immediately (default: propose a pending Change Request)"
|
|
4991
|
+
"skip review and create the record immediately if you have write access (default: merge immediately if you have write access, otherwise propose a pending Change Request)"
|
|
4992
|
+
).option(
|
|
4993
|
+
"--require-review",
|
|
4994
|
+
"always propose a pending Change Request, even if you have write access"
|
|
4901
4995
|
).addHelpText(
|
|
4902
4996
|
"after",
|
|
4903
4997
|
`
|
|
4904
4998
|
Examples:
|
|
4905
4999
|
busabase-cli bases create-change-request --base-id bse_123 --fields-json '{"title":"Hello","status":"draft"}'
|
|
4906
5000
|
busabase-cli bases create-change-request --base-id bse_123 --fields-json @record.json
|
|
4907
|
-
busabase-cli bases create-change-request --base-id bse_123 --fields-json '{"title":"Hello"}' --auto-merge # skip review, create immediately
|
|
5001
|
+
busabase-cli bases create-change-request --base-id bse_123 --fields-json '{"title":"Hello"}' --auto-merge # skip review, create immediately
|
|
5002
|
+
busabase-cli bases create-change-request --base-id bse_123 --fields-json '{"title":"Hello"}' --require-review # always propose a Change Request`
|
|
4908
5003
|
).action(
|
|
4909
5004
|
runAction(
|
|
4910
5005
|
state,
|
|
@@ -4913,7 +5008,7 @@ Examples:
|
|
|
4913
5008
|
fields: parseJsonValue(opts.fieldsJson, "fields-json"),
|
|
4914
5009
|
message: opts.message,
|
|
4915
5010
|
submittedBy: opts.submittedBy,
|
|
4916
|
-
...opts.autoMerge ? { autoMerge: true } : {}
|
|
5011
|
+
...opts.requireReview ? { autoMerge: false } : opts.autoMerge ? { autoMerge: true } : {}
|
|
4917
5012
|
})
|
|
4918
5013
|
)
|
|
4919
5014
|
);
|
|
@@ -4937,7 +5032,10 @@ Examples:
|
|
|
4937
5032
|
).choices(["merge", "replace"])
|
|
4938
5033
|
).option(
|
|
4939
5034
|
"--auto-merge",
|
|
4940
|
-
"skip review and create the AirApp immediately (default: propose a pending Change Request)"
|
|
5035
|
+
"skip review and create the AirApp immediately if you have write access (default: merge immediately if you have write access, otherwise propose a pending Change Request)"
|
|
5036
|
+
).option(
|
|
5037
|
+
"--require-review",
|
|
5038
|
+
"always propose a pending Change Request, even if you have write access"
|
|
4941
5039
|
).addHelpText(
|
|
4942
5040
|
"after",
|
|
4943
5041
|
`
|
|
@@ -4954,7 +5052,8 @@ mergeMode explains how --files-json combines with the AirApp's default scaffold:
|
|
|
4954
5052
|
Examples:
|
|
4955
5053
|
busabase-cli airapps create --slug hello-app --name "Hello App" --files-json @files.json
|
|
4956
5054
|
busabase-cli airapps create --slug vite-app --name "Vite App" --files-json @files.json --merge-mode replace
|
|
4957
|
-
busabase-cli airapps create --slug hello-app --name "Hello App" --files-json @files.json --auto-merge # skip review, create immediately
|
|
5055
|
+
busabase-cli airapps create --slug hello-app --name "Hello App" --files-json @files.json --auto-merge # skip review, create immediately
|
|
5056
|
+
busabase-cli airapps create --slug hello-app --name "Hello App" --files-json @files.json --require-review # always propose a Change Request`
|
|
4958
5057
|
).action(
|
|
4959
5058
|
runAction(
|
|
4960
5059
|
state,
|
|
@@ -4967,7 +5066,7 @@ Examples:
|
|
|
4967
5066
|
version: opts.version,
|
|
4968
5067
|
files: opts.filesJson ? parseJsonValue(opts.filesJson, "files-json") : void 0,
|
|
4969
5068
|
mergeMode: opts.mergeMode,
|
|
4970
|
-
...opts.autoMerge ? { autoMerge: true } : {}
|
|
5069
|
+
...opts.requireReview ? { autoMerge: false } : opts.autoMerge ? { autoMerge: true } : {}
|
|
4971
5070
|
})
|
|
4972
5071
|
)
|
|
4973
5072
|
);
|
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.14",
|
|
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.9.
|
|
36
|
+
"busabase-sdk": "0.9.14",
|
|
37
37
|
"commander": "^14.0.0",
|
|
38
38
|
"tar-stream": "^3.1.7",
|
|
39
39
|
"yaml": "^2.9.0",
|