@go-labs-sg/bb 1.2.1 → 1.2.3
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 +5 -4
- package/dist/api-client.js +12 -1
- package/dist/cli-trace.js +51 -0
- package/dist/commands.js +77 -6
- package/dist/index.js +117 -9
- package/dist/parse-args.js +3 -0
- package/dist/parse-mutation-payload.js +1 -0
- package/dist/prisma-enums.js +1 -1
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -56,8 +56,8 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
|
|
|
56
56
|
|
|
57
57
|
| Area | Commands (non-exhaustive) |
|
|
58
58
|
| --- | --- |
|
|
59
|
-
| **Budgets** | `list-budgets
|
|
60
|
-
| **Bills** | `list-bills`, `list-claims`, `create-bill` (`--payload`), `update-bill` (`--payload`), `delete-bill`, `create-bill-approval`, `update-bill-status`, `patch-bill-payment`, `patch-bill-invoice-number`, `get-bill-attachments`, `get-bill-details` |
|
|
59
|
+
| **Budgets** | `list-budgets` (full payload by default; `--summary` or `--includeDetails false` for slim list), `get-budget`, `get-budget-items`, `get-budget-details`, `get-budget-categories`, `get-budget-versions`, `update-budget-status`, `create-budget` / `update-budget` (`--payload`), `delete-budget`, `create-budget-approval`, `add-budget-items`, `update-budget-item`, `remove-budget-item`, `reorder-budget-items`, `update-budget-item-supplier`, `create-budget-category`, `update-budget-category`, `delete-budget-category`, `update-budget-commission`, `update-budget-discount` (`--payload` where noted), `upload-budget-attachment` (`<budgetId>` + local file path; uses `attachment.requestBudgetAttachmentUpload` + PUT + `attachment.confirmBudgetAttachment`) |
|
|
60
|
+
| **Bills** | `list-bills`, `list-claims`, `create-bill` (`--payload`), `update-bill` (`--payload`), `delete-bill`, `create-bill-approval`, `update-bill-status`, `patch-bill-payment` (PAID bills: `--paymentTrackingUrl`, `--paymentReference`, `--quickbooksBillId`, `--paymentDate` ISO; clear with `--clearPaymentTrackingUrl` / `--clearPaymentReference` / `--clearQuickbooksBillId` / `--clearPaymentDate` `true`), `patch-bill-invoice-number`, `get-bill-attachments`, `upload-bill-attachment` (`<billId>` + local path), `get-bill-details` |
|
|
61
61
|
| **Approvals** | `list-approvals` / `get-pending-approvals`, `approve-bill`, `reject-bill`, `approve-budget`, `reject-budget`, `approve-supplier`, `reject-supplier` |
|
|
62
62
|
| **Companies & projects** | `list-companies`, `get-company`, `create-company`, `update-company` (`--payload`), `delete-company`, `list-projects`, `get-project`, `create-project` (optional `--requestQboAccountantNotification false` to skip QBO accountant emails), `update-project` (`--payload`, optional `requestQboAccountantNotification` in JSON), `delete-project`, `update-project-status` |
|
|
63
63
|
| **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
|
|
@@ -70,6 +70,7 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
|
|
|
70
70
|
|
|
71
71
|
- **Stdout:** JSON (pretty-printed)
|
|
72
72
|
- **Stderr:** Error objects as JSON on failure
|
|
73
|
+
- **Action logs (default on):** Each run logs to **stderr**: (1) JSON lines for command start, then `ok` or `error` with `durationMs` (`--payload` and similar flags are redacted); (2) tRPC lines for every API procedure (path, timing). **Stdout** stays JSON-only for piping (e.g. `| jq`). Suppress with `--quiet`, `-q`, or `BB_CLI_QUIET=1`.
|
|
73
74
|
- **Exit code:** `0` on success, `1` on error
|
|
74
75
|
|
|
75
76
|
A `.env` file in the **current working directory** is loaded automatically (for `BB_API_KEY`, etc.).
|
|
@@ -93,9 +94,9 @@ bun run src/index.ts list-budgets
|
|
|
93
94
|
|
|
94
95
|
### Layout
|
|
95
96
|
|
|
96
|
-
- **`src/prisma-enums.ts`** — Prisma enum string values for runtime; mirror `packages/db/src/generated/prisma/enums.ts` after schema changes.
|
|
97
|
+
- **`src/prisma-enums.ts`** — Prisma enum string values for runtime; mirror `packages/budget-builder/db/src/generated/prisma/enums.ts` after schema changes.
|
|
97
98
|
- **`src/filter-enums.ts`** — “extended” filter enums (ALL + Prisma enums). Keep in line with `packages/utils/src/filter-enums.ts` when those values change.
|
|
98
99
|
|
|
99
100
|
### Publish
|
|
100
101
|
|
|
101
|
-
`prepublishOnly` strips `workspace:` / `catalog:` entries from this package’s `package.json`, runs `tsc`, then `postpack` restores the file — so the npm tarball does not contain monorepo protocol dependencies. Types from `@
|
|
102
|
+
`prepublishOnly` strips `workspace:` / `catalog:` entries from this package’s `package.json`, runs `tsc`, then `postpack` restores the file — so the npm tarball does not contain monorepo protocol dependencies. Types from `@go-labs/budget-builder-api` are compile-time only (`import type`). Prisma enum **values** used at runtime live in `src/prisma-enums.ts` (kept in sync with `packages/budget-builder/db` generated enums) so npm installs do not need `@go-labs/budget-builder-db`.
|
package/dist/api-client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
|
|
1
|
+
import { createTRPCProxyClient, httpBatchLink, loggerLink } from "@trpc/client";
|
|
2
2
|
import SuperJSON from "superjson";
|
|
3
|
+
import { shouldLogCliActions } from "./cli-trace.js";
|
|
3
4
|
const BASE_URL = "https://budget-builder.getout.events";
|
|
4
5
|
function getAuthHeader() {
|
|
5
6
|
const key = process.env.BB_API_KEY;
|
|
@@ -9,6 +10,16 @@ function getAuthHeader() {
|
|
|
9
10
|
}
|
|
10
11
|
export const api = createTRPCProxyClient({
|
|
11
12
|
links: [
|
|
13
|
+
loggerLink({
|
|
14
|
+
enabled: () => shouldLogCliActions(),
|
|
15
|
+
colorMode: "ansi",
|
|
16
|
+
withContext: false,
|
|
17
|
+
// tRPC defaults use console.log for requests; stderr keeps stdout JSON-safe for pipes.
|
|
18
|
+
console: {
|
|
19
|
+
log: (...args) => console.error(...args),
|
|
20
|
+
error: (...args) => console.error(...args),
|
|
21
|
+
},
|
|
22
|
+
}),
|
|
12
23
|
httpBatchLink({
|
|
13
24
|
url: `${BASE_URL}/api/trpc`,
|
|
14
25
|
transformer: SuperJSON,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action logging for the CLI (default on).
|
|
3
|
+
* - Lifecycle: `logCliAction` in index (command + timing).
|
|
4
|
+
* - Per API call: tRPC `loggerLink` in api-client when `shouldLogCliActions()`.
|
|
5
|
+
* Logs go to stderr so stdout stays JSON for piping. Use `--quiet` / `-q` / BB_CLI_QUIET=1 to silence.
|
|
6
|
+
*/
|
|
7
|
+
let cliQuietSession = false;
|
|
8
|
+
/** Call from main after `consumeCliQuietFlags`. */
|
|
9
|
+
export const setCliQuiet = (quiet) => {
|
|
10
|
+
cliQuietSession = quiet;
|
|
11
|
+
};
|
|
12
|
+
export const isCliQuiet = () => cliQuietSession || process.env.BB_CLI_QUIET === "1";
|
|
13
|
+
export const shouldLogCliActions = () => !isCliQuiet();
|
|
14
|
+
const SENSITIVE_FLAG = new Set([
|
|
15
|
+
"payload",
|
|
16
|
+
"apikey",
|
|
17
|
+
"bb_api_key",
|
|
18
|
+
"token",
|
|
19
|
+
"password",
|
|
20
|
+
"secret",
|
|
21
|
+
"authorization",
|
|
22
|
+
]);
|
|
23
|
+
export const sanitizeFlagsForTrace = (flags) => {
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const [k, v] of Object.entries(flags)) {
|
|
26
|
+
const lower = k.toLowerCase();
|
|
27
|
+
if (SENSITIVE_FLAG.has(lower) ||
|
|
28
|
+
lower.includes("secret") ||
|
|
29
|
+
lower.includes("password")) {
|
|
30
|
+
out[k] =
|
|
31
|
+
typeof v === "string" ? `[redacted ${v.length} chars]` : "[redacted]";
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
out[k] = v;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
};
|
|
39
|
+
export const consumeCliQuietFlags = (flags) => {
|
|
40
|
+
const fromEnv = process.env.BB_CLI_QUIET === "1";
|
|
41
|
+
const fromFlags = flags.quiet === true || flags.q === true;
|
|
42
|
+
delete flags.quiet;
|
|
43
|
+
delete flags.q;
|
|
44
|
+
return fromEnv || fromFlags;
|
|
45
|
+
};
|
|
46
|
+
/** stderr so stdout remains JSON-only for pipes (same intent as “log what the CLI is doing”). */
|
|
47
|
+
export const logCliAction = (record) => {
|
|
48
|
+
if (!shouldLogCliActions())
|
|
49
|
+
return;
|
|
50
|
+
console.error("[bb]", JSON.stringify({ ...record, ts: new Date().toISOString() }));
|
|
51
|
+
};
|
package/dist/commands.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { contentType } from "mime-types";
|
|
1
4
|
import { api } from "./api-client.js";
|
|
2
5
|
import { BudgetRole, Deals, ExtendedApprovalStatus, ExtendedApprovalType, ExtendedBudgetStatus, TimeFrame, } from "./filter-enums.js";
|
|
3
6
|
import { billStatusesForApi, } from "./parse-cli-enums.js";
|
|
@@ -11,6 +14,11 @@ function isPendingRequest(n) {
|
|
|
11
14
|
const out = (data) => {
|
|
12
15
|
console.log(JSON.stringify(data, null, 2));
|
|
13
16
|
};
|
|
17
|
+
/** Extension-based MIME, like browser `File.type` when the OS provides it */
|
|
18
|
+
const contentTypeHeaderForFileName = (fileName) => {
|
|
19
|
+
const ct = contentType(fileName);
|
|
20
|
+
return typeof ct === "string" ? ct : "application/octet-stream";
|
|
21
|
+
};
|
|
14
22
|
export async function listBudgets(opts) {
|
|
15
23
|
const input = {
|
|
16
24
|
projectId: opts.projectId,
|
|
@@ -23,6 +31,7 @@ export async function listBudgets(opts) {
|
|
|
23
31
|
sortDir: opts.sortDir,
|
|
24
32
|
page: opts.page ?? 1,
|
|
25
33
|
perPage: opts.perPage ?? 20,
|
|
34
|
+
includeDetails: opts.includeDetails ?? true,
|
|
26
35
|
};
|
|
27
36
|
const result = await api.budget.getAllBudgets.query(input);
|
|
28
37
|
out(result);
|
|
@@ -141,12 +150,22 @@ export async function updateBillStatus(opts) {
|
|
|
141
150
|
out(result);
|
|
142
151
|
}
|
|
143
152
|
export async function patchBillPayment(opts) {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
paymentTrackingUrl
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
153
|
+
const input = { id: opts.id };
|
|
154
|
+
if (opts.paymentTrackingUrl !== undefined)
|
|
155
|
+
input.paymentTrackingUrl = opts.paymentTrackingUrl;
|
|
156
|
+
if (opts.paymentReference !== undefined)
|
|
157
|
+
input.paymentReference = opts.paymentReference;
|
|
158
|
+
if (opts.quickbooksBillId !== undefined)
|
|
159
|
+
input.quickbooksBillId = opts.quickbooksBillId;
|
|
160
|
+
if (opts.paymentDate !== undefined) {
|
|
161
|
+
input.paymentDate =
|
|
162
|
+
opts.paymentDate === null
|
|
163
|
+
? null
|
|
164
|
+
: typeof opts.paymentDate === "string"
|
|
165
|
+
? new Date(opts.paymentDate)
|
|
166
|
+
: opts.paymentDate;
|
|
167
|
+
}
|
|
168
|
+
const result = await api.bill.patchPayment.mutate(input);
|
|
150
169
|
out(result);
|
|
151
170
|
}
|
|
152
171
|
export async function patchBillInvoiceNumber(id, invoiceNumber) {
|
|
@@ -160,6 +179,56 @@ export async function getBillAttachments(id) {
|
|
|
160
179
|
const result = await api.bill.getAttachments.query({ id });
|
|
161
180
|
out(result);
|
|
162
181
|
}
|
|
182
|
+
export async function uploadBillAttachmentFromPath(billId, filePath) {
|
|
183
|
+
const buf = await readFile(filePath);
|
|
184
|
+
const fileName = basename(filePath);
|
|
185
|
+
const size = buf.byteLength;
|
|
186
|
+
const { uploadUrl, key } = await api.attachment.requestBillAttachmentUpload.mutate({
|
|
187
|
+
billId,
|
|
188
|
+
fileName,
|
|
189
|
+
size,
|
|
190
|
+
});
|
|
191
|
+
const res = await fetch(uploadUrl, {
|
|
192
|
+
method: "PUT",
|
|
193
|
+
body: buf,
|
|
194
|
+
headers: { "Content-Type": contentTypeHeaderForFileName(fileName) },
|
|
195
|
+
});
|
|
196
|
+
if (!res.ok) {
|
|
197
|
+
throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
|
|
198
|
+
}
|
|
199
|
+
const confirmed = await api.attachment.confirmBillAttachment.mutate({
|
|
200
|
+
billId,
|
|
201
|
+
key,
|
|
202
|
+
name: fileName,
|
|
203
|
+
size,
|
|
204
|
+
});
|
|
205
|
+
out(confirmed);
|
|
206
|
+
}
|
|
207
|
+
export async function uploadBudgetAttachmentFromPath(budgetId, filePath) {
|
|
208
|
+
const buf = await readFile(filePath);
|
|
209
|
+
const fileName = basename(filePath);
|
|
210
|
+
const size = buf.byteLength;
|
|
211
|
+
const { uploadUrl, key } = await api.attachment.requestBudgetAttachmentUpload.mutate({
|
|
212
|
+
budgetId,
|
|
213
|
+
fileName,
|
|
214
|
+
size,
|
|
215
|
+
});
|
|
216
|
+
const res = await fetch(uploadUrl, {
|
|
217
|
+
method: "PUT",
|
|
218
|
+
body: buf,
|
|
219
|
+
headers: { "Content-Type": contentTypeHeaderForFileName(fileName) },
|
|
220
|
+
});
|
|
221
|
+
if (!res.ok) {
|
|
222
|
+
throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
|
|
223
|
+
}
|
|
224
|
+
const confirmed = await api.attachment.confirmBudgetAttachment.mutate({
|
|
225
|
+
budgetId,
|
|
226
|
+
key,
|
|
227
|
+
name: fileName,
|
|
228
|
+
size,
|
|
229
|
+
});
|
|
230
|
+
out(confirmed);
|
|
231
|
+
}
|
|
163
232
|
export async function getBillDetails(id) {
|
|
164
233
|
const result = await api.bill.getById.query({ id });
|
|
165
234
|
out(result);
|
|
@@ -477,6 +546,7 @@ export async function createProject(opts) {
|
|
|
477
546
|
description: opts.description,
|
|
478
547
|
companyId: opts.companyId,
|
|
479
548
|
contactPersonId: opts.contactPersonId,
|
|
549
|
+
asanaTaskId: opts.asanaTaskId,
|
|
480
550
|
dateRange: {
|
|
481
551
|
from: new Date(opts.startDate),
|
|
482
552
|
to: opts.endDate ? new Date(opts.endDate) : undefined,
|
|
@@ -619,6 +689,7 @@ export async function getApprovedBudgets(opts) {
|
|
|
619
689
|
],
|
|
620
690
|
page: 1,
|
|
621
691
|
perPage,
|
|
692
|
+
includeDetails: false,
|
|
622
693
|
});
|
|
623
694
|
const items = (budgets ?? []).map((b) => {
|
|
624
695
|
const calc = b;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "./load-env.js";
|
|
3
3
|
import { requireApiKey } from "./api-client.js";
|
|
4
|
-
import {
|
|
4
|
+
import { consumeCliQuietFlags, logCliAction, sanitizeFlagsForTrace, setCliQuiet, shouldLogCliActions, } from "./cli-trace.js";
|
|
5
|
+
import { addBudgetItems, approveBill, approveBudget, approveSupplier, createBillApproval, createBillFromPayload, createBudgetApproval, createBudgetCategory, createBudgetFromPayload, createCompany, createContactPersonFromPayload, createItemCategory, createItemFromPayload, createProject, createSupplierCertification, createSupplierFromPayload, createSupplierPaymentMethod, createSupplierRoleOption, createSupplierTagOption, deleteBillById, deleteBudgetById, deleteBudgetCategory, deleteCompanyById, deleteItemById, deleteItemCategoriesByIds, deleteProjectById, deleteSuppliersByIds, getApprovedBudgets, getBillAttachments, getBillDetails, getBudget, getBudgetCategories, getBudgetCategoryBenchmarks, getBudgetDetails, getBudgetItemsOnly, getBudgetVersions, getCompany, getDashboard, getErrorMetrics, getEstimatePerformance, getFinancialOverview, getItem, getItemPricingHistory, getMonthlyMetrics, getProject, getRecentErrors, getSupplierAnalytics, getSupplierDetails, getSupplierPricingHistory, getSystemOverview, getUserPerformance, listApprovals, listBills, listBudgets, listCompanies, listContacts, listItemCategories, listItems, listProjects, listSuppliers, listUsers, patchBillInvoiceNumber, patchBillPayment, rejectBill, rejectBudget, rejectSupplier, removeBudgetItem, reorderBudgetItemsCli, updateBillFromPayload, updateBillStatus, updateBudgetCategory, updateBudgetCommissionFromPayload, updateBudgetDiscountFromPayload, updateBudgetFromPayload, updateBudgetItem, updateBudgetItemSupplierCli, updateBudgetStatus, updateCompanyFromPayload, updateContactPersonFromPayload, updateItemCategory, updateItemFromPayload, updateProjectFromPayload, updateProjectStatus, updateSupplierFromPayload, uploadBillAttachmentFromPath, uploadBudgetAttachmentFromPath, } from "./commands.js";
|
|
5
6
|
import { getFlag, parseArgs } from "./parse-args.js";
|
|
6
7
|
import { billStatusesForUpdateHelp, budgetStatusesForHelp, isBudgetStatusUpdate, parseApprovalTypeFlag, parseBillStatusForUpdate, parseCommaSeparatedBillStatuses, parseCommaSeparatedBudgetStatuses, parseCommaSeparatedIds, parseOptionalBillListSortBy, parseOptionalBillListSortDir, parseOptionalDashboardRole, parseOptionalDeals, parseOptionalErrorSeverity, parseOptionalErrorStatus, parseOptionalExtendedProjectStatus, parseOptionalFinancialRole, parseOptionalSupplierAnalyticsTimeFrame, parseOptionalTimeFrame, parseProjectStatusForUpdate, projectStatusesForHelp, } from "./parse-cli-enums.js";
|
|
7
8
|
import { parseJsonFlag, parseOptionalNumber as parseOptNum, } from "./parse-json-flag.js";
|
|
@@ -62,10 +63,12 @@ bb — Budget Builder CLI (parity with MCP tools)
|
|
|
62
63
|
|
|
63
64
|
Usage: bb <command> [options] [args]
|
|
64
65
|
|
|
66
|
+
Global: --quiet | -q | BB_CLI_QUIET=1 — hide action logs (default: log command + each tRPC call to stderr; stdout JSON unchanged).
|
|
67
|
+
|
|
65
68
|
Auth: Set BB_API_KEY in the environment (same API key as MCP / Goracle).
|
|
66
69
|
|
|
67
70
|
Budgets
|
|
68
|
-
list-budgets [--projectId] [--name] [--status CSV] [--createdBy] [--dateFrom] [--dateTo] [--sortBy] [--sortDir] [--page] [--perPage]
|
|
71
|
+
list-budgets [--projectId] [--name] [--status CSV] [--createdBy] [--dateFrom] [--dateTo] [--sortBy] [--sortDir] [--page] [--perPage] [--summary]
|
|
69
72
|
get-budget <id> Budget + line items
|
|
70
73
|
get-budget-items <id> Line items only
|
|
71
74
|
get-budget-details <id> Full detail (margins, suppliers, etc.)
|
|
@@ -87,19 +90,22 @@ Budgets
|
|
|
87
90
|
delete-budget-category <id>
|
|
88
91
|
update-budget-commission --payload '<json>' (budget.updateBudgetCommission)
|
|
89
92
|
update-budget-discount --payload '<json>' (budget.updateBudgetDiscount; type percentage|amount)
|
|
93
|
+
upload-budget-attachment <budgetId> <filePath> Presigned S3 upload + confirm
|
|
90
94
|
|
|
91
95
|
Bills
|
|
92
96
|
list-bills [--projectId] [--budgetId] [--status CSV] [--search <text>] [--isClaimable true|false] [--createdByIds <csv>] [--sortBy createdAt|amount|status] [--sortDir asc|desc] [--page] [--pageSize]
|
|
93
97
|
list-claims same flags as list-bills; only reimbursable claims (ignores --isClaimable)
|
|
94
98
|
create-bill-approval <billId>
|
|
95
|
-
create-bill --payload '<json>' (bill.create;
|
|
99
|
+
create-bill --payload '<json>' (bill.create; use upload-bill-attachment after create to add files)
|
|
96
100
|
update-bill --payload '<json>' (bill.update; must include id)
|
|
97
101
|
delete-bill <billId>
|
|
98
102
|
update-bill-status <id> <status> [--rejectionReason] [--paymentTrackingUrl] [--paymentReference]
|
|
99
103
|
status (${billStatusesForUpdateHelp.join(", ")}) — not PENDING_APPROVAL; use create-bill-approval
|
|
100
|
-
patch-bill-payment <billId> [--paymentTrackingUrl] [--paymentReference] [--quickbooksBillId]
|
|
104
|
+
patch-bill-payment <billId> [--paymentTrackingUrl] [--paymentReference] [--quickbooksBillId] [--paymentDate <ISO>]
|
|
105
|
+
Clear a field: --clearPaymentTrackingUrl true | --clearPaymentReference true | --clearQuickbooksBillId true | --clearPaymentDate true
|
|
101
106
|
patch-bill-invoice-number <billId> <invoiceNumber>
|
|
102
107
|
get-bill-attachments <billId>
|
|
108
|
+
upload-bill-attachment <billId> <filePath> Presigned S3 upload + confirm (same flow as web UI)
|
|
103
109
|
get-bill-details <billId>
|
|
104
110
|
|
|
105
111
|
Approvals
|
|
@@ -188,12 +194,23 @@ function normalizeCliCommand(raw) {
|
|
|
188
194
|
}
|
|
189
195
|
async function main() {
|
|
190
196
|
const { command, positional, flags } = parseArgs(process.argv);
|
|
197
|
+
setCliQuiet(consumeCliQuietFlags(flags));
|
|
191
198
|
const cmd = normalizeCliCommand(command ?? "");
|
|
192
199
|
if (!cmd || cmd === "help" || flags.help === true) {
|
|
193
200
|
printHelp();
|
|
194
201
|
process.exit(0);
|
|
195
202
|
}
|
|
196
203
|
requireApiKey();
|
|
204
|
+
const t0 = Date.now();
|
|
205
|
+
if (shouldLogCliActions()) {
|
|
206
|
+
logCliAction({
|
|
207
|
+
event: "cli.command",
|
|
208
|
+
phase: "start",
|
|
209
|
+
command: cmd,
|
|
210
|
+
positional,
|
|
211
|
+
flags: sanitizeFlagsForTrace(flags),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
197
214
|
try {
|
|
198
215
|
switch (cmd) {
|
|
199
216
|
case "list-budgets": {
|
|
@@ -210,6 +227,9 @@ async function main() {
|
|
|
210
227
|
sortDir: getFlag(flags, "sortDir"),
|
|
211
228
|
page: parsePositiveIntFlag(getFlag(flags, "page"), "--page"),
|
|
212
229
|
perPage: parsePositiveIntFlag(getFlag(flags, "perPage"), "--perPage"),
|
|
230
|
+
includeDetails: flags.summary === true
|
|
231
|
+
? false
|
|
232
|
+
: (parseOptionalBoolFlag(flags, "includeDetails") ?? true),
|
|
213
233
|
});
|
|
214
234
|
break;
|
|
215
235
|
}
|
|
@@ -220,6 +240,14 @@ async function main() {
|
|
|
220
240
|
await getBudget(id);
|
|
221
241
|
break;
|
|
222
242
|
}
|
|
243
|
+
case "upload-budget-attachment": {
|
|
244
|
+
const [budgetId, filePath] = positional;
|
|
245
|
+
if (!budgetId || !filePath) {
|
|
246
|
+
throw new Error("upload-budget-attachment requires <budgetId> <filePath>");
|
|
247
|
+
}
|
|
248
|
+
await uploadBudgetAttachmentFromPath(budgetId, filePath);
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
223
251
|
case "get-budget-items": {
|
|
224
252
|
const id = positional[0];
|
|
225
253
|
if (!id)
|
|
@@ -450,11 +478,60 @@ async function main() {
|
|
|
450
478
|
const id = positional[0];
|
|
451
479
|
if (!id)
|
|
452
480
|
throw new Error("patch-bill-payment requires <billId>");
|
|
481
|
+
const paymentTrackingUrl = (() => {
|
|
482
|
+
const clear = parseOptionalBoolFlag(flags, "clearPaymentTrackingUrl");
|
|
483
|
+
const raw = getFlag(flags, "paymentTrackingUrl");
|
|
484
|
+
if (clear === true && raw !== undefined) {
|
|
485
|
+
throw new Error("Use either --paymentTrackingUrl or --clearPaymentTrackingUrl true, not both");
|
|
486
|
+
}
|
|
487
|
+
if (clear === true)
|
|
488
|
+
return null;
|
|
489
|
+
if (raw !== undefined)
|
|
490
|
+
return String(raw);
|
|
491
|
+
return undefined;
|
|
492
|
+
})();
|
|
493
|
+
const paymentReference = (() => {
|
|
494
|
+
const clear = parseOptionalBoolFlag(flags, "clearPaymentReference");
|
|
495
|
+
const raw = getFlag(flags, "paymentReference");
|
|
496
|
+
if (clear === true && raw !== undefined) {
|
|
497
|
+
throw new Error("Use either --paymentReference or --clearPaymentReference true, not both");
|
|
498
|
+
}
|
|
499
|
+
if (clear === true)
|
|
500
|
+
return null;
|
|
501
|
+
if (raw !== undefined)
|
|
502
|
+
return String(raw);
|
|
503
|
+
return undefined;
|
|
504
|
+
})();
|
|
505
|
+
const quickbooksBillId = (() => {
|
|
506
|
+
const clear = parseOptionalBoolFlag(flags, "clearQuickbooksBillId");
|
|
507
|
+
const raw = getFlag(flags, "quickbooksBillId");
|
|
508
|
+
if (clear === true && raw !== undefined) {
|
|
509
|
+
throw new Error("Use either --quickbooksBillId or --clearQuickbooksBillId true, not both");
|
|
510
|
+
}
|
|
511
|
+
if (clear === true)
|
|
512
|
+
return null;
|
|
513
|
+
if (raw !== undefined)
|
|
514
|
+
return String(raw);
|
|
515
|
+
return undefined;
|
|
516
|
+
})();
|
|
517
|
+
const paymentDate = (() => {
|
|
518
|
+
const clear = parseOptionalBoolFlag(flags, "clearPaymentDate");
|
|
519
|
+
const raw = getFlag(flags, "paymentDate");
|
|
520
|
+
if (clear === true && raw !== undefined) {
|
|
521
|
+
throw new Error("Use either --paymentDate or --clearPaymentDate true, not both");
|
|
522
|
+
}
|
|
523
|
+
if (clear === true)
|
|
524
|
+
return null;
|
|
525
|
+
if (raw !== undefined)
|
|
526
|
+
return String(raw);
|
|
527
|
+
return undefined;
|
|
528
|
+
})();
|
|
453
529
|
await patchBillPayment({
|
|
454
530
|
id,
|
|
455
|
-
paymentTrackingUrl
|
|
456
|
-
paymentReference
|
|
457
|
-
quickbooksBillId
|
|
531
|
+
paymentTrackingUrl,
|
|
532
|
+
paymentReference,
|
|
533
|
+
quickbooksBillId,
|
|
534
|
+
paymentDate,
|
|
458
535
|
});
|
|
459
536
|
break;
|
|
460
537
|
}
|
|
@@ -473,6 +550,14 @@ async function main() {
|
|
|
473
550
|
await getBillAttachments(id);
|
|
474
551
|
break;
|
|
475
552
|
}
|
|
553
|
+
case "upload-bill-attachment": {
|
|
554
|
+
const [billId, filePath] = positional;
|
|
555
|
+
if (!billId || !filePath) {
|
|
556
|
+
throw new Error("upload-bill-attachment requires <billId> <filePath>");
|
|
557
|
+
}
|
|
558
|
+
await uploadBillAttachmentFromPath(billId, filePath);
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
476
561
|
case "get-bill-details": {
|
|
477
562
|
const id = positional[0];
|
|
478
563
|
if (!id)
|
|
@@ -606,14 +691,20 @@ async function main() {
|
|
|
606
691
|
const name = getFlag(flags, "name");
|
|
607
692
|
const companyId = getFlag(flags, "companyId");
|
|
608
693
|
const contactPersonId = getFlag(flags, "contactPersonId");
|
|
694
|
+
const asanaTaskId = getFlag(flags, "asanaTaskId");
|
|
609
695
|
const startDate = getFlag(flags, "startDate");
|
|
610
|
-
if (!name ||
|
|
611
|
-
|
|
696
|
+
if (!name ||
|
|
697
|
+
!companyId ||
|
|
698
|
+
!contactPersonId ||
|
|
699
|
+
!asanaTaskId ||
|
|
700
|
+
!startDate) {
|
|
701
|
+
throw new Error("create-project requires --name --companyId --contactPersonId --asanaTaskId --startDate (ISO)");
|
|
612
702
|
}
|
|
613
703
|
await createProject({
|
|
614
704
|
name,
|
|
615
705
|
companyId,
|
|
616
706
|
contactPersonId,
|
|
707
|
+
asanaTaskId,
|
|
617
708
|
startDate,
|
|
618
709
|
endDate: getFlag(flags, "endDate"),
|
|
619
710
|
description: getFlag(flags, "description"),
|
|
@@ -914,9 +1005,26 @@ async function main() {
|
|
|
914
1005
|
throw new Error(`Unknown command: ${cmd}. Run 'bb help'.`);
|
|
915
1006
|
}
|
|
916
1007
|
}
|
|
1008
|
+
if (shouldLogCliActions()) {
|
|
1009
|
+
logCliAction({
|
|
1010
|
+
event: "cli.command",
|
|
1011
|
+
phase: "ok",
|
|
1012
|
+
command: cmd,
|
|
1013
|
+
durationMs: Date.now() - t0,
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
917
1016
|
}
|
|
918
1017
|
catch (err) {
|
|
919
1018
|
const message = err instanceof Error ? err.message : String(err);
|
|
1019
|
+
if (shouldLogCliActions()) {
|
|
1020
|
+
logCliAction({
|
|
1021
|
+
event: "cli.command",
|
|
1022
|
+
phase: "error",
|
|
1023
|
+
command: cmd,
|
|
1024
|
+
durationMs: Date.now() - t0,
|
|
1025
|
+
error: message,
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
920
1028
|
console.error(JSON.stringify({ error: message }));
|
|
921
1029
|
process.exit(1);
|
|
922
1030
|
}
|
package/dist/parse-args.js
CHANGED
|
@@ -196,6 +196,7 @@ export function parseUpdateProjectPayload(raw) {
|
|
|
196
196
|
description: optionalString(o.description),
|
|
197
197
|
companyId: requiredString(o.companyId, "update-project payload.companyId"),
|
|
198
198
|
contactPersonId: requiredString(o.contactPersonId, "update-project payload.contactPersonId"),
|
|
199
|
+
asanaTaskId: requiredString(o.asanaTaskId, "update-project payload.asanaTaskId"),
|
|
199
200
|
dateRange: {
|
|
200
201
|
from: toDate(range.from, "dateRange.from"),
|
|
201
202
|
to: range.to === undefined || range.to === null
|
package/dist/prisma-enums.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Runtime Prisma enum values the CLI needs. Mirrors
|
|
3
|
-
* `packages/db/src/generated/prisma/enums.ts` — keep in sync when the schema changes.
|
|
3
|
+
* `packages/budget-builder/db/src/generated/prisma/enums.ts` — keep in sync when the schema changes.
|
|
4
4
|
*/
|
|
5
5
|
export const EstimationMode = {
|
|
6
6
|
MICRO: "MICRO",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@go-labs-sg/bb",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "Budget Builder CLI — list budgets, bills, approvals, suppliers; approve bills; change status. For AI agents (e.g. Chuck/OpenClaw).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,10 +22,12 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@trpc/client": "^11.16.0",
|
|
25
|
-
"
|
|
25
|
+
"mime-types": "^3.0.2",
|
|
26
26
|
"superjson": "^2.2.6"
|
|
27
27
|
},
|
|
28
|
-
"devDependencies": {
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/mime-types": "^3.0.1"
|
|
30
|
+
},
|
|
29
31
|
"engines": {
|
|
30
32
|
"node": ">=18"
|
|
31
33
|
},
|