@go-labs-sg/bb 1.21.0 → 2.0.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.
- package/README.md +74 -50
- package/command-manifest.json +6606 -0
- package/command-reference.md +1153 -0
- package/dist/api-client.js +44 -8
- package/dist/commands.js +20 -2
- package/dist/index.js +264 -24
- package/dist/registry/generate-command-artifacts.js +36 -0
- package/dist/registry/index.js +583 -0
- package/dist/runtime/confirmation.js +76 -0
- package/dist/runtime/error.js +143 -0
- package/dist/runtime/index.js +6 -0
- package/dist/runtime/output.js +36 -0
- package/dist/runtime/process-runtime.js +28 -0
- package/dist/runtime/sanitize.js +50 -0
- package/dist/runtime/session.js +50 -0
- package/dist/runtime/types.js +1 -0
- package/package.json +12 -18
- package/role-aware-agent-guide.md +169 -0
package/dist/api-client.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
import { createTRPCProxyClient, httpBatchLink, loggerLink } from "@trpc/client";
|
|
1
|
+
import { createTRPCProxyClient, httpBatchLink, loggerLink, } from "@trpc/client";
|
|
2
2
|
import SuperJSON from "superjson";
|
|
3
3
|
import { sanitizeValueForTrace, shouldLogCliActions } from "./cli-trace.js";
|
|
4
|
-
export const
|
|
4
|
+
export const DEFAULT_BUDGET_BUILDER_API_BASE_URL = "https://budget-builder.getout.events";
|
|
5
|
+
const normalizeApiBaseUrl = (value) => {
|
|
6
|
+
const url = new URL(value);
|
|
7
|
+
const isLocalHttp = url.protocol === "http:" &&
|
|
8
|
+
(url.hostname === "localhost" || url.hostname === "127.0.0.1");
|
|
9
|
+
if (url.protocol !== "https:" && !isLocalHttp) {
|
|
10
|
+
throw new Error("Budget Builder API URL must use HTTPS, except for localhost development.");
|
|
11
|
+
}
|
|
12
|
+
url.pathname = url.pathname.replace(/\/$/, "");
|
|
13
|
+
return url.toString().replace(/\/$/, "");
|
|
14
|
+
};
|
|
15
|
+
export let BUDGET_BUILDER_API_BASE_URL = DEFAULT_BUDGET_BUILDER_API_BASE_URL;
|
|
5
16
|
function getAuthHeader() {
|
|
6
17
|
const key = process.env.BB_API_KEY;
|
|
7
18
|
if (!key?.trim())
|
|
@@ -17,7 +28,7 @@ const containsRawApiKey = (value, seen = new WeakSet()) => {
|
|
|
17
28
|
return Object.entries(value).some(([key, nestedValue]) => (key.toLowerCase() === "apikey" && typeof nestedValue === "string") ||
|
|
18
29
|
containsRawApiKey(nestedValue, seen));
|
|
19
30
|
};
|
|
20
|
-
export const
|
|
31
|
+
export const createBudgetBuilderClient = (apiBaseUrl = BUDGET_BUILDER_API_BASE_URL) => createTRPCProxyClient({
|
|
21
32
|
links: [
|
|
22
33
|
loggerLink({
|
|
23
34
|
enabled: (opts) => shouldLogCliActions() &&
|
|
@@ -31,7 +42,7 @@ export const api = createTRPCProxyClient({
|
|
|
31
42
|
},
|
|
32
43
|
}),
|
|
33
44
|
httpBatchLink({
|
|
34
|
-
url: `${
|
|
45
|
+
url: `${apiBaseUrl}/api/trpc`,
|
|
35
46
|
transformer: SuperJSON,
|
|
36
47
|
headers: () => {
|
|
37
48
|
const headers = {
|
|
@@ -45,13 +56,38 @@ export const api = createTRPCProxyClient({
|
|
|
45
56
|
}),
|
|
46
57
|
],
|
|
47
58
|
});
|
|
59
|
+
let activeClient = createBudgetBuilderClient();
|
|
60
|
+
export const configureBudgetBuilderApiBaseUrl = (value) => {
|
|
61
|
+
const normalized = normalizeApiBaseUrl(value);
|
|
62
|
+
BUDGET_BUILDER_API_BASE_URL = normalized;
|
|
63
|
+
activeClient = createBudgetBuilderClient(normalized);
|
|
64
|
+
return normalized;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Stable facade used by command handlers. Tests can replace the transport without
|
|
68
|
+
* teaching commands about tRPC or process-wide authentication.
|
|
69
|
+
*/
|
|
70
|
+
export const api = new Proxy({}, {
|
|
71
|
+
get: (_target, property, receiver) => Reflect.get(activeClient, property, receiver),
|
|
72
|
+
});
|
|
73
|
+
export const setBudgetBuilderClientForTesting = (client) => {
|
|
74
|
+
const previous = activeClient;
|
|
75
|
+
activeClient = client;
|
|
76
|
+
return () => {
|
|
77
|
+
activeClient = previous;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
export class MissingApiKeyError extends Error {
|
|
81
|
+
code = "AUTHENTICATION";
|
|
82
|
+
constructor() {
|
|
83
|
+
super("BB_API_KEY is required. Set it in the environment.");
|
|
84
|
+
this.name = "MissingApiKeyError";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
48
87
|
export function requireApiKey() {
|
|
49
88
|
const key = process.env.BB_API_KEY?.trim();
|
|
50
89
|
if (!key) {
|
|
51
|
-
|
|
52
|
-
error: "BB_API_KEY is required. Set it in the environment.",
|
|
53
|
-
}));
|
|
54
|
-
process.exit(1);
|
|
90
|
+
throw new MissingApiKeyError();
|
|
55
91
|
}
|
|
56
92
|
return key;
|
|
57
93
|
}
|
package/dist/commands.js
CHANGED
|
@@ -9,6 +9,7 @@ import { billStatusesForApi, } from "./parse-cli-enums.js";
|
|
|
9
9
|
import { parseBudgetDiscountPayload, parseCompanyUpdatePayload, parseContactCreatePayload, parseContactUpdatePayload, parseCreateBillPayload, parseCreateBudgetPayload, parseCreateCustomerInvoicePayload, parseCreateQuotationPayload, parseItemCreatePayload, parseItemUpdatePayload, parseSendCustomerInvoiceToContactPersonPayload, parseSendEstimateToContactPersonPayload, parseSupplierCreatePayload, parseSupplierUpdatePayload, parseUpdateBillPayload, parseUpdateBillPaymentEvidencePayload, parseUpdateBudgetCommissionPayload, parseUpdateBudgetPayload, parseUpdateProjectPayload, parseUpdateQuotationPayload, parseValidateBillSelectionPayload, } from "./parse-mutation-payload.js";
|
|
10
10
|
import { BillStatus, BudgetStatus, ProjectStatus } from "./prisma-enums.js";
|
|
11
11
|
import { createRichTextFromPlainText } from "./rich-text.js";
|
|
12
|
+
import { confirmCurrentCommand, emitCommandResult, getActiveCliSession, } from "./runtime/index.js";
|
|
12
13
|
const BUDGET = "BUDGET";
|
|
13
14
|
const BILL = "BILL";
|
|
14
15
|
const SUPPLIER = "SUPPLIER";
|
|
@@ -52,7 +53,7 @@ function isPendingRequest(n) {
|
|
|
52
53
|
return n.notificationType === "request" && n.status === "PENDING_APPROVAL";
|
|
53
54
|
}
|
|
54
55
|
const out = (data) => {
|
|
55
|
-
|
|
56
|
+
emitCommandResult(data);
|
|
56
57
|
};
|
|
57
58
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
58
59
|
const stringField = (record, key) => {
|
|
@@ -311,6 +312,14 @@ export const buildSensitiveWorkflowPreview = ({ action, entity, details, }) => {
|
|
|
311
312
|
].join("\n");
|
|
312
313
|
};
|
|
313
314
|
const assertSensitiveWorkflowConfirmed = async (input) => {
|
|
315
|
+
if (getActiveCliSession()?.invocation.legacy !== true) {
|
|
316
|
+
await confirmCurrentCommand({
|
|
317
|
+
action: input.action,
|
|
318
|
+
target: input.entity,
|
|
319
|
+
details: input.details,
|
|
320
|
+
});
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
314
323
|
const expected = "CONFIRM";
|
|
315
324
|
const preview = buildSensitiveWorkflowPreview(input);
|
|
316
325
|
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
@@ -335,6 +344,15 @@ const assertLockedBudgetChangeConfirmed = async ({ budget, action, }) => {
|
|
|
335
344
|
return;
|
|
336
345
|
}
|
|
337
346
|
const statusLabel = lockedBudgetStatusLabel(budget.status);
|
|
347
|
+
if (getActiveCliSession()?.invocation.legacy !== true) {
|
|
348
|
+
await confirmCurrentCommand({
|
|
349
|
+
action,
|
|
350
|
+
target: `budget ${budget.id}`,
|
|
351
|
+
details: `Changes locked budget "${budget.name}" in status ${statusLabel}.`,
|
|
352
|
+
effects: ["state-change"],
|
|
353
|
+
});
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
338
356
|
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
339
357
|
throw new Error(`${action} would change locked budget "${budget.name}" (${budget.id}) with status ${statusLabel}. Run this command in an interactive terminal and type "yes" to confirm.`);
|
|
340
358
|
}
|
|
@@ -1407,7 +1425,7 @@ export const syncCustomerInvoice = async (invoiceId) => {
|
|
|
1407
1425
|
await assertSensitiveWorkflowConfirmed({
|
|
1408
1426
|
action: "Sync customer invoice from QuickBooks",
|
|
1409
1427
|
entity: `invoice ${invoiceId}`,
|
|
1410
|
-
details: "refreshes local status, QuickBooks metadata, balance, and history; a
|
|
1428
|
+
details: "refreshes local status, QuickBooks metadata, balance, and history; QBO Paid or a zero balance becomes PAID, a reversed payment restores the delivery or approval status, and a QBO void becomes VOIDED",
|
|
1411
1429
|
});
|
|
1412
1430
|
const result = await api.customerInvoice.syncInvoiceStatus.mutate({
|
|
1413
1431
|
invoiceId,
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "./load-env.js";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
|
-
import {
|
|
3
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { BUDGET_BUILDER_API_BASE_URL, configureBudgetBuilderApiBaseUrl, DEFAULT_BUDGET_BUILDER_API_BASE_URL, requireApiKey, } from "./api-client.js";
|
|
5
7
|
import { consumeCliQuietFlags, logCliAction, sanitizeFlagsForTrace, setCliQuiet, shouldLogCliActions, } from "./cli-trace.js";
|
|
6
8
|
import { addBudgetItems, approveBill, approveBudget, approveCustomerInvoice, approveQuotation, approveSupplier, checkCustomerInvoiceReadiness, checkProjectReconciliation, cleanupStagedBillAttachments, cleanupStagedQuotationAttachments, completeProject, createApiKeyForUser, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createCustomerInvoice, createEstimate, createItemCategory, createItemFromPayload, createPlaceholderBillForBudgetItem, createProject, createQuotationFromPayload, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, createUser, deleteBillById, deleteBudgetById, deleteBudgetCategory, deleteBudgetCommission, deleteBudgetDiscount, deleteCompanyById, deleteCustomerInvoice, deleteItemById, deleteItemCategoriesByIds, deleteProjectById, deleteQuotationById, deleteSuppliersByIds, discardCreatingCustomerInvoice, downloadCustomerInvoicePaymentProof, downloadCustomerInvoicePdf, downloadQuotationPdf, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getCustomerInvoice, getCustomerInvoiceEmailContext, getDashboard, getErrorMetrics, getEstimatePerformance, getFinancialOverview, getItem, getItemPricingHistory, getMonthlyMetrics, getProject, getQuotationDetails, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getSystemOverview, getUserPerformance, importQuickBooksProjectId, listApiKeysForUser, listApprovals, listBills, listBudgets, listCompanies, listContacts, listCustomerInvoices, listEligibleCustomerInvoiceBudgets, listIntegrationOperations, listItemCategories, listItems, listProjects, listQuotations, listSuppliers, listUsers, markBudgetWonWithProof, markCustomerInvoicePaidFromPath, patchBillInvoiceNumber, patchBillPayment, reactivateSuppliersByIds, reconcileProject, rejectBill, rejectBudget, rejectCustomerInvoice, rejectQuotation, rejectSupplier, removeBudgetItem, renameBudgetVersion, reorderBudgetItemsCli, restoreBudgetVersion, retryIntegrationOperation, revokeApiKeyForUser, sendCustomerInvoiceToContactPersonFromPayload, sendEstimateToContactPersonFromPayload, setBudgetItemsNotUtilized, stageBillAttachmentsFromPaths, submitQuotation, syncCustomerInvoice, updateBillFromPayload, updateBillPaymentEvidenceFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateQuotationFromPayload, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBillAttachmentsFromPaths, uploadBillDocumentsFromPaths, uploadBudgetAttachmentFromPath, uploadQuotationAttachmentFromPath, validateBillSelectionFromPayload, voidCustomerInvoice, whoAmI, } from "./commands.js";
|
|
7
9
|
import { getFlag, parseArgs } from "./parse-args.js";
|
|
8
10
|
import { billStatusesForUpdateHelp, budgetStatusesForHelp, isBudgetStatusUpdate, parseApprovalTypeFlag, parseBillStatusForUpdate, parseCommaSeparatedBillStatuses, parseCommaSeparatedBudgetStatuses, parseCommaSeparatedCustomerInvoiceStatuses, parseCommaSeparatedIds, parseCommaSeparatedQuotationStatuses, parseCommaSeparatedSupplierStatuses, parseOptionalBillListSortBy, parseOptionalBillListSortDir, parseOptionalDashboardRole, parseOptionalDeals, parseOptionalErrorSeverity, parseOptionalErrorStatus, parseOptionalExtendedProjectStatus, parseOptionalFinancialRole, parseOptionalSupplierAnalyticsTimeFrame, parseOptionalTimeFrame, parseProjectStatusForUpdate, parseUserRole, projectStatusesForHelp, userRolesForHelp, } from "./parse-cli-enums.js";
|
|
9
11
|
import { parseJsonFlag, parseOptionalNumber as parseOptNum, } from "./parse-json-flag.js";
|
|
12
|
+
import { createCompletionScript, createHumanHelp, resolveCommand, } from "./registry/index.js";
|
|
13
|
+
import { CliRuntimeError, clearActiveCliSession, confirmCurrentCommand, createProcessRuntime, emitCommandError, emitCommandResult, setActiveCliSession, } from "./runtime/index.js";
|
|
10
14
|
function parsePositiveIntFlag(value, flagName) {
|
|
11
15
|
if (value === undefined)
|
|
12
16
|
return undefined;
|
|
@@ -139,7 +143,7 @@ function parseDashboardQueryFlags(flags) {
|
|
|
139
143
|
endDate: getFlag(flags, "endDate"),
|
|
140
144
|
};
|
|
141
145
|
}
|
|
142
|
-
function
|
|
146
|
+
function printLegacyHelp(runtime) {
|
|
143
147
|
const help = `
|
|
144
148
|
bb — Budget Builder CLI for AI agents (parity with MCP tools)
|
|
145
149
|
|
|
@@ -208,10 +212,10 @@ Bills
|
|
|
208
212
|
list-bills [--projectId] [--budgetId] [--status CSV] [--search <text>] [--isClaimable true|false] [--createdByIds <csv>] [--sortBy createdAt|amount|status] [--sortDir asc|desc] [--page] [--pageSize]
|
|
209
213
|
list-claims same flags as list-bills; only reimbursable claims (ignores --isClaimable)
|
|
210
214
|
isClaimable differentiates the shared bill/claim records: false = bill, true = claim.
|
|
211
|
-
|
|
215
|
+
Non-legacy supplier bills from 1 Jul 2026 00:00 SGT require approved quotation coverage for every selected line item, including already-paid bills.
|
|
212
216
|
create-bill-approval <billId> (also sends approval request emails)
|
|
213
217
|
create-bill --payload '<json>' (bill.create; payload.isClaimable false = bill, true = claim)
|
|
214
|
-
validate-bill-selection --payload '<json>' Check projectId, supplierId, budgetItemIds
|
|
218
|
+
validate-bill-selection --payload '<json>' Check projectId, supplierId, and budgetItemIds before creation; alreadyPaid never bypasses quotation checks.
|
|
215
219
|
stage-bill-attachment <projectId> <filePath...> [--file <path>] [--files <csv>] Upload files before create-bill; returns attachment JSON for attachments/paymentProofAttachments
|
|
216
220
|
cleanup-staged-bill-attachments <projectId> --keys <csv> Delete unattached staged bill/claim uploads.
|
|
217
221
|
update-bill --payload '<json>' (bill.update; must include id)
|
|
@@ -261,7 +265,7 @@ Customer invoices
|
|
|
261
265
|
Admin-only. Uploads proof, creates a QBO Payment for the live balance when needed, attaches proof in QBO when possible, and records payment history in BB.
|
|
262
266
|
download-customer-invoice-payment-proof <invoiceId> [--output <path>]
|
|
263
267
|
download-customer-invoice-pdf <invoiceId> [--output <path>]
|
|
264
|
-
sync-customer-invoice <invoiceId> Refresh local status and balance from QuickBooks; QBO voids can reopen
|
|
268
|
+
sync-customer-invoice <invoiceId> Refresh local status and balance from QuickBooks; paid invoices become PAID, reversals restore delivery/approval, and QBO voids can reopen an estimate.
|
|
265
269
|
|
|
266
270
|
Approvals
|
|
267
271
|
list-approvals | get-pending-approvals [--type budget|supplier|bill|quotation|customer_invoice|all]
|
|
@@ -361,7 +365,7 @@ Note: MCP also exposes a Prisma schema resource; the CLI has no equivalent.
|
|
|
361
365
|
|
|
362
366
|
Not covered vs MCP get_budget: "get-budget" also fetches line items in one call.
|
|
363
367
|
`.trim();
|
|
364
|
-
|
|
368
|
+
runtime.stdout.write(`${help}\n`);
|
|
365
369
|
}
|
|
366
370
|
const getCliPackageMetadata = () => {
|
|
367
371
|
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
@@ -370,30 +374,242 @@ const getCliPackageMetadata = () => {
|
|
|
370
374
|
}
|
|
371
375
|
return { name: packageJson.name, version: packageJson.version };
|
|
372
376
|
};
|
|
373
|
-
const
|
|
377
|
+
const getVersionData = () => {
|
|
374
378
|
const metadata = getCliPackageMetadata();
|
|
375
|
-
|
|
379
|
+
return {
|
|
376
380
|
...metadata,
|
|
377
381
|
apiBaseUrl: BUDGET_BUILDER_API_BASE_URL,
|
|
378
|
-
}
|
|
382
|
+
};
|
|
379
383
|
};
|
|
380
384
|
/** MCP registers tools with snake_case; CLI commands are kebab-case. */
|
|
381
385
|
function normalizeCliCommand(raw) {
|
|
382
386
|
return raw.replace(/_/g, "-");
|
|
383
387
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
388
|
+
const booleanFlagValues = new Set(["true", "false", "1", "0"]);
|
|
389
|
+
const isEnabledFlag = (argv, flag) => {
|
|
390
|
+
for (let index = 0; index < argv.length; index++) {
|
|
391
|
+
const value = argv[index];
|
|
392
|
+
if (value === `${flag}=true` || value === `${flag}=1`)
|
|
393
|
+
return true;
|
|
394
|
+
if (value?.startsWith(`${flag}=`))
|
|
395
|
+
return false;
|
|
396
|
+
if (value !== flag)
|
|
397
|
+
continue;
|
|
398
|
+
const next = argv[index + 1];
|
|
399
|
+
if (next !== undefined && booleanFlagValues.has(next)) {
|
|
400
|
+
return next === "true" || next === "1";
|
|
401
|
+
}
|
|
402
|
+
return true;
|
|
391
403
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
404
|
+
return false;
|
|
405
|
+
};
|
|
406
|
+
const runtimeOnlyFlags = [
|
|
407
|
+
"--allow-state-change",
|
|
408
|
+
"--allow-email",
|
|
409
|
+
"--allow-external-write",
|
|
410
|
+
"--allow-delete",
|
|
411
|
+
"--allow-financial-write",
|
|
412
|
+
"--debug",
|
|
413
|
+
"--quiet",
|
|
414
|
+
"--api-url",
|
|
415
|
+
];
|
|
416
|
+
const isRuntimeOnlyArgument = (value) => runtimeOnlyFlags.some((flag) => value === flag || value.startsWith(`${flag}=`)) || value === "-q";
|
|
417
|
+
const parseRootArguments = (argv) => {
|
|
418
|
+
const commandArgv = [];
|
|
419
|
+
let apiBaseUrl;
|
|
420
|
+
for (let index = 0; index < argv.length; index++) {
|
|
421
|
+
const value = argv[index];
|
|
422
|
+
if (value === undefined)
|
|
423
|
+
continue;
|
|
424
|
+
if (value.startsWith("--api-url=")) {
|
|
425
|
+
apiBaseUrl = value.slice("--api-url=".length);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (value === "--api-url") {
|
|
429
|
+
const next = argv[index + 1];
|
|
430
|
+
if (next === undefined || next.startsWith("-")) {
|
|
431
|
+
throw new CliRuntimeError("USAGE", "--api-url requires a URL.");
|
|
432
|
+
}
|
|
433
|
+
apiBaseUrl = next;
|
|
434
|
+
index++;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (isRuntimeOnlyArgument(value)) {
|
|
438
|
+
const next = argv[index + 1];
|
|
439
|
+
if (next !== undefined && booleanFlagValues.has(next))
|
|
440
|
+
index++;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
commandArgv.push(value);
|
|
395
444
|
}
|
|
396
|
-
|
|
445
|
+
return { apiBaseUrl, commandArgv };
|
|
446
|
+
};
|
|
447
|
+
const effectPermissionsFromArgv = (argv) => ({
|
|
448
|
+
allowStateChange: isEnabledFlag(argv, "--allow-state-change"),
|
|
449
|
+
allowEmail: isEnabledFlag(argv, "--allow-email"),
|
|
450
|
+
allowExternalWrite: isEnabledFlag(argv, "--allow-external-write"),
|
|
451
|
+
allowDelete: isEnabledFlag(argv, "--allow-delete"),
|
|
452
|
+
allowFinancialWrite: isEnabledFlag(argv, "--allow-financial-write"),
|
|
453
|
+
});
|
|
454
|
+
const isCompletionShell = (value) => value === "bash" ||
|
|
455
|
+
value === "zsh" ||
|
|
456
|
+
value === "fish" ||
|
|
457
|
+
value === "powershell";
|
|
458
|
+
const legacyArgvForCommand = (argv, resolvedCommand) => {
|
|
459
|
+
const legacyTarget = resolvedCommand.command.legacyTarget;
|
|
460
|
+
if (legacyTarget === undefined) {
|
|
461
|
+
throw new CliRuntimeError("INTERNAL", `Command ${resolvedCommand.command.path.join(" ")} has no dispatcher target.`);
|
|
462
|
+
}
|
|
463
|
+
return [
|
|
464
|
+
argv[0] ?? "node",
|
|
465
|
+
argv[1] ?? "bb",
|
|
466
|
+
legacyTarget,
|
|
467
|
+
...resolvedCommand.argv.filter((value) => !isRuntimeOnlyArgument(value)),
|
|
468
|
+
];
|
|
469
|
+
};
|
|
470
|
+
const normalizeDispatcherError = (error) => {
|
|
471
|
+
if (!(error instanceof Error))
|
|
472
|
+
return error;
|
|
473
|
+
if (/\brequires\b|\bmust be\b|\bmust include\b|^Invalid\b|^Missing\b|^--/.test(error.message)) {
|
|
474
|
+
return new CliRuntimeError("VALIDATION", error.message);
|
|
475
|
+
}
|
|
476
|
+
return error;
|
|
477
|
+
};
|
|
478
|
+
const commandConfirmationTarget = (resolvedCommand) => {
|
|
479
|
+
const positionalTarget = resolvedCommand.argv.find((value) => value !== "--" && !value.startsWith("-"));
|
|
480
|
+
if (positionalTarget !== undefined &&
|
|
481
|
+
!positionalTarget.startsWith("{") &&
|
|
482
|
+
!positionalTarget.startsWith("[")) {
|
|
483
|
+
return positionalTarget;
|
|
484
|
+
}
|
|
485
|
+
return resolvedCommand.command.path.join(" ");
|
|
486
|
+
};
|
|
487
|
+
export async function runCli(argv = process.argv, runtime = createProcessRuntime()) {
|
|
488
|
+
const rawArgv = argv.slice(2);
|
|
489
|
+
const metadata = getCliPackageMetadata();
|
|
490
|
+
const quiet = isEnabledFlag(rawArgv, "--quiet") ||
|
|
491
|
+
rawArgv.includes("-q") ||
|
|
492
|
+
runtime.env.BB_CLI_QUIET === "1";
|
|
493
|
+
const debug = isEnabledFlag(rawArgv, "--debug");
|
|
494
|
+
let rootArguments;
|
|
495
|
+
try {
|
|
496
|
+
rootArguments = parseRootArguments(rawArgv);
|
|
497
|
+
const apiBaseUrl = rootArguments.apiBaseUrl ?? runtime.env.BB_API_URL?.trim();
|
|
498
|
+
configureBudgetBuilderApiBaseUrl(apiBaseUrl || DEFAULT_BUDGET_BUILDER_API_BASE_URL);
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
setActiveCliSession({
|
|
502
|
+
runtime,
|
|
503
|
+
invocation: {
|
|
504
|
+
command: "startup",
|
|
505
|
+
cliVersion: metadata.version,
|
|
506
|
+
apiBaseUrl: BUDGET_BUILDER_API_BASE_URL,
|
|
507
|
+
quiet,
|
|
508
|
+
debug,
|
|
509
|
+
},
|
|
510
|
+
effects: [],
|
|
511
|
+
permissions: {},
|
|
512
|
+
});
|
|
513
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
514
|
+
const exitCode = emitCommandError(error instanceof CliRuntimeError
|
|
515
|
+
? error
|
|
516
|
+
: new CliRuntimeError("USAGE", `Invalid API URL: ${message}`));
|
|
517
|
+
clearActiveCliSession();
|
|
518
|
+
return exitCode;
|
|
519
|
+
}
|
|
520
|
+
const { commandArgv } = rootArguments;
|
|
521
|
+
if (commandArgv.length === 0 ||
|
|
522
|
+
commandArgv[0] === "help" ||
|
|
523
|
+
isEnabledFlag(commandArgv, "--help") ||
|
|
524
|
+
commandArgv.includes("-h")) {
|
|
525
|
+
if (commandArgv.includes("--legacy")) {
|
|
526
|
+
printLegacyHelp(runtime);
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
runtime.stdout.write(`${createHumanHelp()}\n`);
|
|
530
|
+
}
|
|
531
|
+
return 0;
|
|
532
|
+
}
|
|
533
|
+
if (commandArgv[0] === "completion") {
|
|
534
|
+
const shell = commandArgv[1];
|
|
535
|
+
if (shell === undefined || !isCompletionShell(shell)) {
|
|
536
|
+
setActiveCliSession({
|
|
537
|
+
runtime,
|
|
538
|
+
invocation: {
|
|
539
|
+
command: "completion",
|
|
540
|
+
cliVersion: metadata.version,
|
|
541
|
+
apiBaseUrl: BUDGET_BUILDER_API_BASE_URL,
|
|
542
|
+
quiet,
|
|
543
|
+
debug,
|
|
544
|
+
},
|
|
545
|
+
effects: [],
|
|
546
|
+
permissions: {},
|
|
547
|
+
});
|
|
548
|
+
const exitCode = emitCommandError(new CliRuntimeError("USAGE", "completion requires bash, zsh, fish, or powershell."));
|
|
549
|
+
clearActiveCliSession();
|
|
550
|
+
return exitCode;
|
|
551
|
+
}
|
|
552
|
+
runtime.stdout.write(createCompletionScript(shell));
|
|
553
|
+
return 0;
|
|
554
|
+
}
|
|
555
|
+
if (commandArgv[0] === "version" ||
|
|
556
|
+
isEnabledFlag(commandArgv, "--version") ||
|
|
557
|
+
commandArgv.includes("-v")) {
|
|
558
|
+
setActiveCliSession({
|
|
559
|
+
runtime,
|
|
560
|
+
invocation: {
|
|
561
|
+
command: "version",
|
|
562
|
+
cliVersion: metadata.version,
|
|
563
|
+
apiBaseUrl: BUDGET_BUILDER_API_BASE_URL,
|
|
564
|
+
quiet,
|
|
565
|
+
debug,
|
|
566
|
+
},
|
|
567
|
+
effects: [],
|
|
568
|
+
permissions: {},
|
|
569
|
+
});
|
|
570
|
+
emitCommandResult(getVersionData());
|
|
571
|
+
clearActiveCliSession();
|
|
572
|
+
return 0;
|
|
573
|
+
}
|
|
574
|
+
const resolvedCommand = resolveCommand(commandArgv);
|
|
575
|
+
if (resolvedCommand === undefined) {
|
|
576
|
+
setActiveCliSession({
|
|
577
|
+
runtime,
|
|
578
|
+
invocation: {
|
|
579
|
+
command: commandArgv[0] ?? "unknown",
|
|
580
|
+
cliVersion: metadata.version,
|
|
581
|
+
apiBaseUrl: BUDGET_BUILDER_API_BASE_URL,
|
|
582
|
+
quiet,
|
|
583
|
+
debug,
|
|
584
|
+
},
|
|
585
|
+
effects: [],
|
|
586
|
+
permissions: {},
|
|
587
|
+
});
|
|
588
|
+
const exitCode = emitCommandError(new CliRuntimeError("USAGE", `Unknown command: ${commandArgv.join(" ")}. Run 'bb help'.`));
|
|
589
|
+
clearActiveCliSession();
|
|
590
|
+
return exitCode;
|
|
591
|
+
}
|
|
592
|
+
const effectiveArgv = legacyArgvForCommand(argv, resolvedCommand);
|
|
593
|
+
const { command, positional, flags } = parseArgs(effectiveArgv);
|
|
594
|
+
consumeCliQuietFlags(flags);
|
|
595
|
+
setCliQuiet(resolvedCommand.isLegacyAlias ? quiet : quiet || !debug);
|
|
596
|
+
setActiveCliSession({
|
|
597
|
+
runtime,
|
|
598
|
+
invocation: {
|
|
599
|
+
command: resolvedCommand.command.path.join(" "),
|
|
600
|
+
cliVersion: metadata.version,
|
|
601
|
+
apiBaseUrl: BUDGET_BUILDER_API_BASE_URL,
|
|
602
|
+
legacy: resolvedCommand.isLegacyAlias,
|
|
603
|
+
quiet,
|
|
604
|
+
debug,
|
|
605
|
+
},
|
|
606
|
+
effects: resolvedCommand.command.effects,
|
|
607
|
+
permissions: effectPermissionsFromArgv(rawArgv),
|
|
608
|
+
});
|
|
609
|
+
if (resolvedCommand.isLegacyAlias && !quiet) {
|
|
610
|
+
runtime.stderr.write(`[bb] Deprecated command "${resolvedCommand.invokedAs.join(" ")}"; use "bb ${resolvedCommand.command.path.join(" ")}".\n`);
|
|
611
|
+
}
|
|
612
|
+
const cmd = normalizeCliCommand(command ?? "");
|
|
397
613
|
const t0 = Date.now();
|
|
398
614
|
if (shouldLogCliActions()) {
|
|
399
615
|
logCliAction({
|
|
@@ -405,6 +621,15 @@ async function main() {
|
|
|
405
621
|
});
|
|
406
622
|
}
|
|
407
623
|
try {
|
|
624
|
+
requireApiKey();
|
|
625
|
+
if (!resolvedCommand.isLegacyAlias &&
|
|
626
|
+
resolvedCommand.command.effects.length > 0) {
|
|
627
|
+
await confirmCurrentCommand({
|
|
628
|
+
action: resolvedCommand.command.path.join(" "),
|
|
629
|
+
target: commandConfirmationTarget(resolvedCommand),
|
|
630
|
+
details: "Canonical v2 mutation. Supply every effect-specific --allow-* flag for non-interactive execution.",
|
|
631
|
+
});
|
|
632
|
+
}
|
|
408
633
|
switch (cmd) {
|
|
409
634
|
case "whoami": {
|
|
410
635
|
await whoAmI();
|
|
@@ -1842,6 +2067,8 @@ async function main() {
|
|
|
1842
2067
|
durationMs: Date.now() - t0,
|
|
1843
2068
|
});
|
|
1844
2069
|
}
|
|
2070
|
+
clearActiveCliSession();
|
|
2071
|
+
return 0;
|
|
1845
2072
|
}
|
|
1846
2073
|
catch (err) {
|
|
1847
2074
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1854,8 +2081,21 @@ async function main() {
|
|
|
1854
2081
|
error: message,
|
|
1855
2082
|
});
|
|
1856
2083
|
}
|
|
1857
|
-
|
|
1858
|
-
|
|
2084
|
+
const exitCode = emitCommandError(normalizeDispatcherError(err));
|
|
2085
|
+
clearActiveCliSession();
|
|
2086
|
+
return exitCode;
|
|
1859
2087
|
}
|
|
1860
2088
|
}
|
|
1861
|
-
|
|
2089
|
+
const resolveExecutablePath = (value) => {
|
|
2090
|
+
try {
|
|
2091
|
+
return realpathSync(value);
|
|
2092
|
+
}
|
|
2093
|
+
catch {
|
|
2094
|
+
return resolve(value);
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
2097
|
+
const isDirectExecution = process.argv[1] !== undefined &&
|
|
2098
|
+
resolveExecutablePath(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
2099
|
+
if (isDirectExecution) {
|
|
2100
|
+
process.exitCode = await runCli();
|
|
2101
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { writeFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { commandRegistry, createCommandManifest, globalCommandOptions, } from "./index.js";
|
|
5
|
+
const packageRoot = process.cwd();
|
|
6
|
+
const manifestPath = resolve(packageRoot, "command-manifest.json");
|
|
7
|
+
const reference = [
|
|
8
|
+
"# Budget Builder CLI command reference",
|
|
9
|
+
"",
|
|
10
|
+
"This file is generated from the typed command registry. Do not edit it manually.",
|
|
11
|
+
"This catalog is not filtered by the authenticated user's role. The Budget Builder API authorizes every request using the current database role plus applicable resource, workflow-state, and pending-approver checks.",
|
|
12
|
+
"Command-specific arguments currently pass through the compatibility dispatcher; use `bb help --legacy` for their detailed transition reference.",
|
|
13
|
+
"",
|
|
14
|
+
"## Global options",
|
|
15
|
+
"",
|
|
16
|
+
...globalCommandOptions.map((option) => `- \`${option.name}\`: ${option.description}`),
|
|
17
|
+
"",
|
|
18
|
+
...commandRegistry.flatMap((command) => [
|
|
19
|
+
`## \`bb ${command.path.join(" ")}\``,
|
|
20
|
+
"",
|
|
21
|
+
command.summary,
|
|
22
|
+
"",
|
|
23
|
+
`Legacy aliases: ${command.legacyAliases.map((alias) => `\`${alias}\``).join(", ")}.`,
|
|
24
|
+
"",
|
|
25
|
+
`Effects: ${command.effects.length > 0 ? command.effects.join(", ") : "none"}.`,
|
|
26
|
+
"",
|
|
27
|
+
]),
|
|
28
|
+
].join("\n");
|
|
29
|
+
await Promise.all([
|
|
30
|
+
writeFile(resolve(packageRoot, "command-reference.md"), reference),
|
|
31
|
+
writeFile(manifestPath, `${JSON.stringify(createCommandManifest(), null, "\t")}\n`),
|
|
32
|
+
]);
|
|
33
|
+
execFileSync(process.execPath, ["x", "biome", "format", "--write", manifestPath], {
|
|
34
|
+
cwd: packageRoot,
|
|
35
|
+
stdio: ["ignore", "ignore", "inherit"],
|
|
36
|
+
});
|