@centry-digital/bukku-cli 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.
Files changed (53) hide show
  1. package/README.md +266 -0
  2. package/build/commands/config.d.ts +3 -0
  3. package/build/commands/config.d.ts.map +1 -0
  4. package/build/commands/config.js +68 -0
  5. package/build/commands/custom/archive.d.ts +9 -0
  6. package/build/commands/custom/archive.d.ts.map +1 -0
  7. package/build/commands/custom/archive.js +82 -0
  8. package/build/commands/custom/file-upload.d.ts +9 -0
  9. package/build/commands/custom/file-upload.d.ts.map +1 -0
  10. package/build/commands/custom/file-upload.js +47 -0
  11. package/build/commands/custom/journal-entry.d.ts +10 -0
  12. package/build/commands/custom/journal-entry.d.ts.map +1 -0
  13. package/build/commands/custom/journal-entry.js +82 -0
  14. package/build/commands/custom/location-write.d.ts +10 -0
  15. package/build/commands/custom/location-write.d.ts.map +1 -0
  16. package/build/commands/custom/location-write.js +95 -0
  17. package/build/commands/custom/reference-data.d.ts +13 -0
  18. package/build/commands/custom/reference-data.d.ts.map +1 -0
  19. package/build/commands/custom/reference-data.js +95 -0
  20. package/build/commands/custom/search-accounts.d.ts +12 -0
  21. package/build/commands/custom/search-accounts.d.ts.map +1 -0
  22. package/build/commands/custom/search-accounts.js +59 -0
  23. package/build/commands/factory.d.ts +9 -0
  24. package/build/commands/factory.d.ts.map +1 -0
  25. package/build/commands/factory.js +371 -0
  26. package/build/commands/wrapper.d.ts +23 -0
  27. package/build/commands/wrapper.d.ts.map +1 -0
  28. package/build/commands/wrapper.js +60 -0
  29. package/build/config/auth.d.ts +23 -0
  30. package/build/config/auth.d.ts.map +1 -0
  31. package/build/config/auth.js +69 -0
  32. package/build/config/rc.d.ts +20 -0
  33. package/build/config/rc.d.ts.map +1 -0
  34. package/build/config/rc.js +65 -0
  35. package/build/index.d.ts +2 -0
  36. package/build/index.d.ts.map +1 -0
  37. package/build/index.js +1685 -0
  38. package/build/input/json.d.ts +10 -0
  39. package/build/input/json.d.ts.map +1 -0
  40. package/build/input/json.js +42 -0
  41. package/build/output/dry-run.d.ts +21 -0
  42. package/build/output/dry-run.d.ts.map +1 -0
  43. package/build/output/dry-run.js +23 -0
  44. package/build/output/error.d.ts +27 -0
  45. package/build/output/error.d.ts.map +1 -0
  46. package/build/output/error.js +33 -0
  47. package/build/output/json.d.ts +6 -0
  48. package/build/output/json.d.ts.map +1 -0
  49. package/build/output/json.js +7 -0
  50. package/build/output/table.d.ts +13 -0
  51. package/build/output/table.d.ts.map +1 -0
  52. package/build/output/table.js +123 -0
  53. package/package.json +37 -0
package/build/index.js ADDED
@@ -0,0 +1,1685 @@
1
+ #!/usr/bin/env node
2
+
3
+ // packages/cli/src/index.ts
4
+ import { Command as Command2 } from "commander";
5
+ import { readFileSync } from "node:fs";
6
+ import { fileURLToPath } from "node:url";
7
+ import { dirname, join as join2 } from "node:path";
8
+
9
+ // packages/cli/src/commands/config.ts
10
+ import { Command } from "commander";
11
+
12
+ // packages/cli/src/config/rc.ts
13
+ import { readFile, writeFile, stat } from "node:fs/promises";
14
+ import { join } from "node:path";
15
+ import { homedir } from "node:os";
16
+ var RC_PATH = join(homedir(), ".bukkurc");
17
+ async function readRc() {
18
+ let content;
19
+ try {
20
+ content = await readFile(RC_PATH, "utf-8");
21
+ } catch (err) {
22
+ if (err.code === "ENOENT") {
23
+ return {};
24
+ }
25
+ throw err;
26
+ }
27
+ const result = {};
28
+ for (const line of content.split("\n")) {
29
+ const trimmed = line.trim();
30
+ if (!trimmed || trimmed.startsWith("#")) continue;
31
+ const eqIndex = trimmed.indexOf("=");
32
+ if (eqIndex === -1) continue;
33
+ const key = trimmed.slice(0, eqIndex).trim();
34
+ const value = trimmed.slice(eqIndex + 1).trim();
35
+ if (key) result[key] = value;
36
+ }
37
+ return result;
38
+ }
39
+ async function writeRc(key, value) {
40
+ const existing = await readRc();
41
+ existing[key] = value;
42
+ const lines = Object.entries(existing).map(([k, v]) => `${k} = ${v}`);
43
+ await writeFile(RC_PATH, lines.join("\n") + "\n", { mode: 384 });
44
+ }
45
+ async function checkPermissions() {
46
+ if (process.platform === "win32") {
47
+ return { ok: true, mode: "0600" };
48
+ }
49
+ try {
50
+ const st = await stat(RC_PATH);
51
+ const mode = st.mode & 511;
52
+ const modeStr = "0" + mode.toString(8);
53
+ return { ok: mode === 384, mode: modeStr };
54
+ } catch (err) {
55
+ if (err.code === "ENOENT") {
56
+ return { ok: true, mode: "0000" };
57
+ }
58
+ throw err;
59
+ }
60
+ }
61
+
62
+ // packages/cli/src/config/auth.ts
63
+ var AuthMissingError = class extends Error {
64
+ code = "AUTH_MISSING";
65
+ missingFields;
66
+ constructor(missingFields) {
67
+ super(`Missing required auth: ${missingFields.join(", ")}. Set via --api-token/--company-subdomain flags, BUKKU_API_TOKEN/BUKKU_COMPANY_SUBDOMAIN env vars, or ~/.bukkurc config file.`);
68
+ this.name = "AuthMissingError";
69
+ this.missingFields = missingFields;
70
+ }
71
+ };
72
+ async function resolveAuth(flags) {
73
+ const rc = await readRc();
74
+ const source = {};
75
+ let apiToken;
76
+ if (flags.apiToken) {
77
+ apiToken = flags.apiToken;
78
+ source["apiToken"] = "flags";
79
+ } else if (process.env["BUKKU_API_TOKEN"]) {
80
+ apiToken = process.env["BUKKU_API_TOKEN"];
81
+ source["apiToken"] = "env";
82
+ } else if (rc["api_token"]) {
83
+ apiToken = rc["api_token"];
84
+ source["apiToken"] = "rc";
85
+ }
86
+ let companySubdomain;
87
+ if (flags.companySubdomain) {
88
+ companySubdomain = flags.companySubdomain;
89
+ source["companySubdomain"] = "flags";
90
+ } else if (process.env["BUKKU_COMPANY_SUBDOMAIN"]) {
91
+ companySubdomain = process.env["BUKKU_COMPANY_SUBDOMAIN"];
92
+ source["companySubdomain"] = "env";
93
+ } else if (rc["company_subdomain"]) {
94
+ companySubdomain = rc["company_subdomain"];
95
+ source["companySubdomain"] = "rc";
96
+ }
97
+ const missing = [];
98
+ if (!apiToken) missing.push("apiToken");
99
+ if (!companySubdomain) missing.push("companySubdomain");
100
+ if (missing.length > 0) {
101
+ throw new AuthMissingError(missing);
102
+ }
103
+ return {
104
+ apiToken,
105
+ companySubdomain,
106
+ source
107
+ };
108
+ }
109
+ function maskToken(token) {
110
+ if (token.length >= 8) {
111
+ return token.slice(0, 4) + "..." + token.slice(-4);
112
+ }
113
+ return "****";
114
+ }
115
+
116
+ // packages/cli/src/commands/config.ts
117
+ var VALID_KEYS = ["api_token", "company_subdomain"];
118
+ function isValidKey(key) {
119
+ return VALID_KEYS.includes(key);
120
+ }
121
+ var configCommand = new Command("config").description("Manage CLI configuration");
122
+ configCommand.command("set").description("Set a config value").argument("<key>", "Config key (api_token, company_subdomain)").argument("<value>", "Config value").action(async (key, value) => {
123
+ if (!isValidKey(key)) {
124
+ console.error(JSON.stringify({
125
+ error: "Invalid config key",
126
+ code: "VALIDATION_ERROR",
127
+ details: { valid_keys: [...VALID_KEYS] }
128
+ }));
129
+ process.exit(4);
130
+ }
131
+ await writeRc(key, value);
132
+ console.log(JSON.stringify({ ok: true, key, message: "Config updated" }));
133
+ const perms = await checkPermissions();
134
+ if (!perms.ok) {
135
+ console.error(`Warning: ~/.bukkurc has permissions ${perms.mode}, expected 0600. Run: chmod 600 ~/.bukkurc`);
136
+ }
137
+ });
138
+ configCommand.command("show").description("Show resolved configuration").action(async function() {
139
+ const rc = await readRc();
140
+ const parentOpts = this.parent?.parent?.opts() ?? {};
141
+ const config = {};
142
+ if (parentOpts.apiToken) {
143
+ config["api_token"] = { value: maskToken(parentOpts.apiToken), source: "flags" };
144
+ } else if (process.env["BUKKU_API_TOKEN"]) {
145
+ config["api_token"] = { value: maskToken(process.env["BUKKU_API_TOKEN"]), source: "env" };
146
+ } else if (rc["api_token"]) {
147
+ config["api_token"] = { value: maskToken(rc["api_token"]), source: "rc" };
148
+ } else {
149
+ config["api_token"] = { value: null, source: "not set" };
150
+ }
151
+ if (parentOpts.companySubdomain) {
152
+ config["company_subdomain"] = { value: parentOpts.companySubdomain, source: "flags" };
153
+ } else if (process.env["BUKKU_COMPANY_SUBDOMAIN"]) {
154
+ config["company_subdomain"] = { value: process.env["BUKKU_COMPANY_SUBDOMAIN"], source: "env" };
155
+ } else if (rc["company_subdomain"]) {
156
+ config["company_subdomain"] = { value: rc["company_subdomain"], source: "rc" };
157
+ } else {
158
+ config["company_subdomain"] = { value: null, source: "not set" };
159
+ }
160
+ console.log(JSON.stringify({ config }));
161
+ });
162
+
163
+ // packages/core/build/utils/logger.js
164
+ function createLogger(prefix) {
165
+ return (message, ...args) => {
166
+ console.error(`[${prefix}] ${message}`, ...args);
167
+ };
168
+ }
169
+
170
+ // packages/core/build/client/bukku-client.js
171
+ import { readFile as readFile2 } from "node:fs/promises";
172
+ import { basename, extname } from "node:path";
173
+ var log = createLogger("bukku");
174
+ var BukkuClient = class {
175
+ baseUrl = "https://api.bukku.my";
176
+ token;
177
+ subdomain;
178
+ constructor(config) {
179
+ this.token = config.apiToken;
180
+ this.subdomain = config.companySubdomain;
181
+ }
182
+ /**
183
+ * Build headers for all requests.
184
+ * CRITICAL: Never log the actual token value - use "Bearer ***" for debugging.
185
+ */
186
+ getHeaders(includeContentType = false) {
187
+ const headers = {
188
+ Authorization: `Bearer ${this.token}`,
189
+ "Company-Subdomain": this.subdomain,
190
+ Accept: "application/json"
191
+ };
192
+ if (includeContentType) {
193
+ headers["Content-Type"] = "application/json";
194
+ }
195
+ return headers;
196
+ }
197
+ /**
198
+ * Map file extensions to MIME types for common file types.
199
+ * Returns null for unknown extensions.
200
+ */
201
+ getMimeType(extension) {
202
+ const mimeMap = {
203
+ ".pdf": "application/pdf",
204
+ ".png": "image/png",
205
+ ".jpg": "image/jpeg",
206
+ ".jpeg": "image/jpeg",
207
+ ".gif": "image/gif",
208
+ ".txt": "text/plain",
209
+ ".csv": "text/csv",
210
+ ".json": "application/json",
211
+ ".xml": "application/xml",
212
+ ".zip": "application/zip"
213
+ };
214
+ return mimeMap[extension.toLowerCase()] || null;
215
+ }
216
+ /**
217
+ * Build URL with query parameters.
218
+ */
219
+ buildUrl(path, params) {
220
+ const url = new URL(path, this.baseUrl);
221
+ if (params) {
222
+ for (const [key, value] of Object.entries(params)) {
223
+ if (value !== void 0) {
224
+ url.searchParams.append(key, String(value));
225
+ }
226
+ }
227
+ }
228
+ return url.toString();
229
+ }
230
+ /**
231
+ * GET request with optional query parameters.
232
+ */
233
+ async get(path, params) {
234
+ const url = this.buildUrl(path, params);
235
+ const response = await fetch(url, {
236
+ method: "GET",
237
+ headers: this.getHeaders()
238
+ });
239
+ if (!response.ok) {
240
+ throw response;
241
+ }
242
+ return response.json();
243
+ }
244
+ /**
245
+ * POST request with JSON body.
246
+ */
247
+ async post(path, body) {
248
+ const url = this.buildUrl(path);
249
+ const response = await fetch(url, {
250
+ method: "POST",
251
+ headers: this.getHeaders(true),
252
+ body: JSON.stringify(body)
253
+ });
254
+ if (!response.ok) {
255
+ throw response;
256
+ }
257
+ return response.json();
258
+ }
259
+ /**
260
+ * PUT request with JSON body.
261
+ */
262
+ async put(path, body) {
263
+ const url = this.buildUrl(path);
264
+ const response = await fetch(url, {
265
+ method: "PUT",
266
+ headers: this.getHeaders(true),
267
+ body: JSON.stringify(body)
268
+ });
269
+ if (!response.ok) {
270
+ throw response;
271
+ }
272
+ return response.json();
273
+ }
274
+ /**
275
+ * PATCH request with JSON body (for status updates).
276
+ */
277
+ async patch(path, body) {
278
+ const url = this.buildUrl(path);
279
+ const response = await fetch(url, {
280
+ method: "PATCH",
281
+ headers: this.getHeaders(true),
282
+ body: JSON.stringify(body)
283
+ });
284
+ if (!response.ok) {
285
+ throw response;
286
+ }
287
+ return response.json();
288
+ }
289
+ /**
290
+ * DELETE request.
291
+ */
292
+ async delete(path) {
293
+ const url = this.buildUrl(path);
294
+ const response = await fetch(url, {
295
+ method: "DELETE",
296
+ headers: this.getHeaders()
297
+ });
298
+ if (!response.ok) {
299
+ throw response;
300
+ }
301
+ }
302
+ /**
303
+ * POST multipart/form-data request for file uploads.
304
+ * Reads file from disk and sends as multipart form data.
305
+ * CRITICAL: Does NOT manually set Content-Type - fetch sets it automatically with boundary.
306
+ *
307
+ * @param path - API endpoint path
308
+ * @param filePath - Absolute path to file on disk
309
+ * @returns API response
310
+ */
311
+ async postMultipart(path, filePath) {
312
+ const url = this.buildUrl(path);
313
+ const fileBuffer = await readFile2(filePath);
314
+ const fileName = basename(filePath);
315
+ const fileExtension = extname(filePath);
316
+ const mimeType = this.getMimeType(fileExtension) || "application/octet-stream";
317
+ const file = new File([fileBuffer], fileName, { type: mimeType });
318
+ const form = new FormData();
319
+ form.append("file", file);
320
+ const headers = this.getHeaders(false);
321
+ const response = await fetch(url, {
322
+ method: "POST",
323
+ headers,
324
+ body: form
325
+ });
326
+ if (!response.ok) {
327
+ throw response;
328
+ }
329
+ return response.json();
330
+ }
331
+ /**
332
+ * Validate token on startup by making a lightweight API call.
333
+ * Uses GET /contacts with page_size=1 to verify authentication.
334
+ * Exits process if token is invalid (401).
335
+ */
336
+ async validateToken() {
337
+ try {
338
+ await this.get("/contacts", { page_size: 1 });
339
+ log("Token validated successfully");
340
+ } catch (error) {
341
+ if (error instanceof Response && error.status === 401) {
342
+ log("Authentication Error\n");
343
+ log("The provided BUKKU_API_TOKEN is invalid or expired.\n");
344
+ log("Please check:");
345
+ log(" 1. Token is copied correctly (no extra spaces)");
346
+ log(" 2. API Access is enabled in Bukku Control Panel -> Integrations");
347
+ log(" 3. Token has not been revoked or regenerated\n");
348
+ process.exit(1);
349
+ }
350
+ log("Failed to validate token:", error);
351
+ process.exit(1);
352
+ }
353
+ }
354
+ };
355
+
356
+ // packages/core/build/validation/double-entry.js
357
+ function validateDoubleEntry(lines, minLines = 2) {
358
+ if (lines.length < minLines) {
359
+ const lineWord = lines.length === 1 ? "line" : "lines";
360
+ return {
361
+ valid: false,
362
+ error: `Journal entries require at least ${minLines} line items (minimum one debit and one credit). This entry has ${lines.length} ${lineWord}.`
363
+ };
364
+ }
365
+ let totalDebits = 0;
366
+ let totalCredits = 0;
367
+ for (const line of lines) {
368
+ totalDebits += line.debit_amount || 0;
369
+ totalCredits += line.credit_amount || 0;
370
+ }
371
+ const difference = Math.abs(totalDebits - totalCredits);
372
+ const EPSILON = 0.01;
373
+ if (difference >= EPSILON) {
374
+ return {
375
+ valid: false,
376
+ error: `Journal entry is unbalanced. Total debits: ${totalDebits.toFixed(2)}, Total credits: ${totalCredits.toFixed(2)}. Difference: ${difference.toFixed(2)}. Debits must equal credits in double-entry accounting.`
377
+ };
378
+ }
379
+ return { valid: true };
380
+ }
381
+
382
+ // packages/core/build/entities/sales-invoice.js
383
+ var salesInvoiceConfig = {
384
+ entity: "sales-invoice",
385
+ apiBasePath: "/sales/invoices",
386
+ singularKey: "transaction",
387
+ pluralKey: "transactions",
388
+ description: "sales invoice",
389
+ operations: ["list", "get", "create", "update", "delete"],
390
+ hasStatusUpdate: true,
391
+ listFilters: ["contact_id", "email_status", "transfer_status", "payment_status"],
392
+ businessRules: {
393
+ delete: "Only draft and void invoices can be deleted. Ready or pending approval invoices cannot be deleted \u2014 use update-sales-invoice-status to void a ready invoice instead.",
394
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void invoice is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
395
+ },
396
+ cliGroup: "sales"
397
+ };
398
+
399
+ // packages/core/build/entities/sales-quote.js
400
+ var salesQuoteConfig = {
401
+ entity: "sales-quote",
402
+ apiBasePath: "/sales/quotes",
403
+ singularKey: "transaction",
404
+ pluralKey: "transactions",
405
+ description: "sales quote",
406
+ operations: ["list", "get", "create", "update", "delete"],
407
+ hasStatusUpdate: true,
408
+ listFilters: ["contact_id", "email_status", "transfer_status"],
409
+ businessRules: {
410
+ delete: "Only draft and void quotes can be deleted. Ready or pending approval quotes cannot be deleted \u2014 use update-sales-quote-status to void a ready quote instead.",
411
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void quote is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
412
+ },
413
+ cliGroup: "sales"
414
+ };
415
+
416
+ // packages/core/build/entities/sales-order.js
417
+ var salesOrderConfig = {
418
+ entity: "sales-order",
419
+ apiBasePath: "/sales/orders",
420
+ singularKey: "transaction",
421
+ pluralKey: "transactions",
422
+ description: "sales order",
423
+ operations: ["list", "get", "create", "update", "delete"],
424
+ hasStatusUpdate: true,
425
+ listFilters: ["contact_id", "email_status", "transfer_status"],
426
+ businessRules: {
427
+ delete: "Only draft and void orders can be deleted. Ready or pending approval orders cannot be deleted \u2014 use update-sales-order-status to void a ready order instead.",
428
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void order is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
429
+ },
430
+ cliGroup: "sales"
431
+ };
432
+
433
+ // packages/core/build/entities/sales-credit-note.js
434
+ var salesCreditNoteConfig = {
435
+ entity: "sales-credit-note",
436
+ apiBasePath: "/sales/credit_notes",
437
+ singularKey: "transaction",
438
+ pluralKey: "transactions",
439
+ description: "sales credit note",
440
+ operations: ["list", "get", "create", "update", "delete"],
441
+ hasStatusUpdate: true,
442
+ listFilters: ["contact_id", "email_status"],
443
+ businessRules: {
444
+ delete: "Only draft and void credit notes can be deleted. Ready or pending approval credit notes cannot be deleted \u2014 use update-sales-credit-note-status to void a ready credit note instead.",
445
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void credit note is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
446
+ },
447
+ cliGroup: "sales"
448
+ };
449
+
450
+ // packages/core/build/entities/sales-payment.js
451
+ var salesPaymentConfig = {
452
+ entity: "sales-payment",
453
+ apiBasePath: "/sales/payments",
454
+ singularKey: "transaction",
455
+ pluralKey: "transactions",
456
+ description: "sales payment",
457
+ operations: ["list", "get", "create", "update", "delete"],
458
+ hasStatusUpdate: true,
459
+ listFilters: ["contact_id", "payment_mode"],
460
+ businessRules: {
461
+ delete: "Only draft and void payments can be deleted. Ready or pending approval payments cannot be deleted \u2014 use update-sales-payment-status to void a ready payment instead.",
462
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void payment is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
463
+ },
464
+ cliGroup: "sales"
465
+ };
466
+
467
+ // packages/core/build/entities/sales-refund.js
468
+ var salesRefundConfig = {
469
+ entity: "sales-refund",
470
+ apiBasePath: "/sales/refunds",
471
+ singularKey: "transaction",
472
+ pluralKey: "transactions",
473
+ description: "sales refund",
474
+ operations: ["list", "get", "create", "update", "delete"],
475
+ hasStatusUpdate: true,
476
+ listFilters: ["contact_id"],
477
+ businessRules: {
478
+ delete: "Only draft and void refunds can be deleted. Ready or pending approval refunds cannot be deleted \u2014 use update-sales-refund-status to void a ready refund instead.",
479
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void refund is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
480
+ },
481
+ cliGroup: "sales"
482
+ };
483
+
484
+ // packages/core/build/entities/delivery-order.js
485
+ var deliveryOrderConfig = {
486
+ entity: "delivery-order",
487
+ apiBasePath: "/sales/delivery_orders",
488
+ singularKey: "transaction",
489
+ pluralKey: "transactions",
490
+ description: "delivery order",
491
+ operations: ["list", "get", "create", "update", "delete"],
492
+ hasStatusUpdate: true,
493
+ listFilters: ["contact_id", "email_status", "transfer_status"],
494
+ businessRules: {
495
+ delete: "Only draft and void delivery orders can be deleted. Ready or pending approval delivery orders cannot be deleted \u2014 use update-delivery-order-status to void a ready delivery order instead.",
496
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void delivery order is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
497
+ },
498
+ cliGroup: "sales"
499
+ };
500
+
501
+ // packages/core/build/entities/purchase-order.js
502
+ var purchaseOrderConfig = {
503
+ entity: "purchase-order",
504
+ apiBasePath: "/purchases/orders",
505
+ singularKey: "transaction",
506
+ pluralKey: "transactions",
507
+ description: "purchase order",
508
+ operations: ["list", "get", "create", "update", "delete"],
509
+ hasStatusUpdate: true,
510
+ listFilters: ["contact_id", "email_status", "transfer_status"],
511
+ businessRules: {
512
+ delete: "Only draft and void purchase orders can be deleted. Ready or pending approval purchase orders cannot be deleted \u2014 use update-purchase-order-status to void a ready purchase order instead.",
513
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void purchase order is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
514
+ },
515
+ cliGroup: "purchases"
516
+ };
517
+
518
+ // packages/core/build/entities/purchase-bill.js
519
+ var purchaseBillConfig = {
520
+ entity: "purchase-bill",
521
+ apiBasePath: "/purchases/bills",
522
+ singularKey: "transaction",
523
+ pluralKey: "transactions",
524
+ description: "purchase bill",
525
+ operations: ["list", "get", "create", "update", "delete"],
526
+ hasStatusUpdate: true,
527
+ listFilters: ["contact_id", "payment_status", "payment_mode"],
528
+ businessRules: {
529
+ delete: "Only draft and void bills can be deleted. Ready or pending approval bills cannot be deleted \u2014 use update-purchase-bill-status to void a ready bill instead.",
530
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void bill is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
531
+ },
532
+ cliGroup: "purchases"
533
+ };
534
+
535
+ // packages/core/build/entities/purchase-credit-note.js
536
+ var purchaseCreditNoteConfig = {
537
+ entity: "purchase-credit-note",
538
+ apiBasePath: "/purchases/credit_notes",
539
+ singularKey: "transaction",
540
+ pluralKey: "transactions",
541
+ description: "purchase credit note",
542
+ operations: ["list", "get", "create", "update", "delete"],
543
+ hasStatusUpdate: true,
544
+ listFilters: ["contact_id", "payment_status"],
545
+ businessRules: {
546
+ delete: "Only draft and void credit notes can be deleted. Ready or pending approval credit notes cannot be deleted \u2014 use update-purchase-credit-note-status to void a ready credit note instead.",
547
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void credit note is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
548
+ },
549
+ cliGroup: "purchases"
550
+ };
551
+
552
+ // packages/core/build/entities/purchase-payment.js
553
+ var purchasePaymentConfig = {
554
+ entity: "purchase-payment",
555
+ apiBasePath: "/purchases/payments",
556
+ singularKey: "transaction",
557
+ pluralKey: "transactions",
558
+ description: "purchase payment",
559
+ operations: ["list", "get", "create", "update", "delete"],
560
+ hasStatusUpdate: true,
561
+ listFilters: ["contact_id", "email_status", "payment_status", "account_id"],
562
+ businessRules: {
563
+ delete: "Only draft and void payments can be deleted. Ready or pending approval payments cannot be deleted \u2014 use update-purchase-payment-status to void a ready payment instead.",
564
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void payment is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
565
+ },
566
+ cliGroup: "purchases"
567
+ };
568
+
569
+ // packages/core/build/entities/purchase-refund.js
570
+ var purchaseRefundConfig = {
571
+ entity: "purchase-refund",
572
+ apiBasePath: "/purchases/refunds",
573
+ singularKey: "transaction",
574
+ pluralKey: "transactions",
575
+ description: "purchase refund",
576
+ operations: ["list", "get", "create", "update", "delete"],
577
+ hasStatusUpdate: true,
578
+ listFilters: ["contact_id", "email_status", "payment_status", "account_id"],
579
+ businessRules: {
580
+ delete: "Only draft and void refunds can be deleted. Ready or pending approval refunds cannot be deleted \u2014 use update-purchase-refund-status to void a ready refund instead.",
581
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void refund is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
582
+ },
583
+ cliGroup: "purchases"
584
+ };
585
+
586
+ // packages/core/build/entities/goods-received-note.js
587
+ var goodsReceivedNoteConfig = {
588
+ entity: "goods-received-note",
589
+ apiBasePath: "/purchases/goods_received_notes",
590
+ singularKey: "transaction",
591
+ pluralKey: "transactions",
592
+ description: "goods received note",
593
+ operations: ["list", "get", "create", "update", "delete"],
594
+ hasStatusUpdate: true,
595
+ listFilters: ["contact_id", "email_status", "transfer_status"],
596
+ businessRules: {
597
+ delete: "Only draft and void goods received notes can be deleted. Ready or pending approval goods received notes cannot be deleted \u2014 use update-goods-received-note-status to void a ready goods received note instead.",
598
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void goods received note is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
599
+ },
600
+ cliGroup: "purchases"
601
+ };
602
+
603
+ // packages/core/build/entities/bank-money-in.js
604
+ var bankMoneyInConfig = {
605
+ entity: "bank-money-in",
606
+ apiBasePath: "/banking/incomes",
607
+ singularKey: "transaction",
608
+ pluralKey: "transactions",
609
+ description: "bank money in transaction",
610
+ operations: ["list", "get", "create", "update", "delete"],
611
+ hasStatusUpdate: true,
612
+ listFilters: ["contact_id", "account_id", "email_status"],
613
+ businessRules: {
614
+ delete: "Only draft and void money in transactions can be deleted. Ready or pending approval transactions cannot be deleted \u2014 use update-bank-money-in-status to void a ready transaction instead.",
615
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void transaction is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
616
+ },
617
+ cliGroup: "banking"
618
+ };
619
+
620
+ // packages/core/build/entities/bank-money-out.js
621
+ var bankMoneyOutConfig = {
622
+ entity: "bank-money-out",
623
+ apiBasePath: "/banking/expenses",
624
+ singularKey: "transaction",
625
+ pluralKey: "transactions",
626
+ description: "bank money out transaction",
627
+ operations: ["list", "get", "create", "update", "delete"],
628
+ hasStatusUpdate: true,
629
+ listFilters: ["contact_id", "account_id", "email_status"],
630
+ businessRules: {
631
+ delete: "Only draft and void money out transactions can be deleted. Ready or pending approval transactions cannot be deleted \u2014 use update-bank-money-out-status to void a ready transaction instead.",
632
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void transaction is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
633
+ },
634
+ cliGroup: "banking"
635
+ };
636
+
637
+ // packages/core/build/entities/bank-transfer.js
638
+ var bankTransferConfig = {
639
+ entity: "bank-transfer",
640
+ apiBasePath: "/banking/transfers",
641
+ singularKey: "transaction",
642
+ pluralKey: "transactions",
643
+ description: "bank transfer",
644
+ operations: ["list", "get", "create", "update", "delete"],
645
+ hasStatusUpdate: true,
646
+ listFilters: ["account_id"],
647
+ businessRules: {
648
+ delete: "Only draft and void transfers can be deleted. Ready or pending approval transfers cannot be deleted \u2014 use update-bank-transfer-status to void a ready transfer instead.",
649
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void transfer is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
650
+ },
651
+ cliGroup: "banking"
652
+ };
653
+
654
+ // packages/core/build/entities/contact.js
655
+ var contactConfig = {
656
+ entity: "contact",
657
+ apiBasePath: "/contacts",
658
+ singularKey: "contact",
659
+ pluralKey: "contacts",
660
+ description: "contact",
661
+ operations: ["list", "get", "create", "update", "delete"],
662
+ hasStatusUpdate: false,
663
+ listFilters: ["group_id", "status", "type", "is_myinvois_ready"],
664
+ businessRules: {
665
+ delete: "Only contacts with no linked transactions can be deleted. Archive instead if the contact has transaction history."
666
+ },
667
+ cliGroup: "contacts"
668
+ };
669
+
670
+ // packages/core/build/entities/contact-group.js
671
+ var contactGroupConfig = {
672
+ entity: "contact-group",
673
+ apiBasePath: "/contacts/groups",
674
+ singularKey: "group",
675
+ pluralKey: "groups",
676
+ description: "contact group",
677
+ operations: ["list", "get", "create", "update", "delete"],
678
+ hasStatusUpdate: false,
679
+ listFilters: [],
680
+ cliGroup: "contacts"
681
+ };
682
+
683
+ // packages/core/build/entities/product.js
684
+ var productConfig = {
685
+ entity: "product",
686
+ apiBasePath: "/products",
687
+ singularKey: "product",
688
+ pluralKey: "products",
689
+ description: "product. Use list-tax-codes to find valid tax code IDs for sale_tax_code_id and purchase_tax_code_id. Use list-accounts to find valid account IDs for sale_account_id, purchase_account_id, and inventory_account_id. Use list-product-groups to find group IDs for group_ids.",
690
+ operations: ["list", "get", "create", "update", "delete"],
691
+ hasStatusUpdate: false,
692
+ listFilters: ["search", "stock_level", "mode", "type", "include_archived"],
693
+ businessRules: {
694
+ delete: "Only products that are not used in any transactions can be deleted. Archive instead if the product has transaction history."
695
+ },
696
+ cliGroup: "products"
697
+ };
698
+
699
+ // packages/core/build/entities/product-bundle.js
700
+ var productBundleConfig = {
701
+ entity: "product-bundle",
702
+ apiBasePath: "/products/bundles",
703
+ singularKey: "bundle",
704
+ pluralKey: "bundles",
705
+ description: "product bundle. Bundles aggregate multiple products with optional discounts. To list bundles, use list-products with type=bundle. Use list-products to find product IDs for bundle items.",
706
+ operations: ["get", "create", "update", "delete"],
707
+ hasStatusUpdate: false,
708
+ listFilters: [],
709
+ businessRules: {
710
+ delete: "Only bundles that are not used in any transactions can be deleted."
711
+ },
712
+ cliGroup: "products"
713
+ };
714
+
715
+ // packages/core/build/entities/product-group.js
716
+ var productGroupConfig = {
717
+ entity: "product-group",
718
+ apiBasePath: "/products/groups",
719
+ singularKey: "group",
720
+ pluralKey: "groups",
721
+ description: "product group. Groups organize products into categories. Use list-products to find product IDs for the product_ids array.",
722
+ operations: ["list", "get", "create", "update", "delete"],
723
+ hasStatusUpdate: false,
724
+ listFilters: [],
725
+ cliGroup: "products"
726
+ };
727
+
728
+ // packages/core/build/entities/journal-entry.js
729
+ var journalEntryConfig = {
730
+ entity: "journal-entry",
731
+ apiBasePath: "/journal_entries",
732
+ singularKey: "transaction",
733
+ pluralKey: "transactions",
734
+ description: "journal entry",
735
+ operations: ["list", "get", "delete"],
736
+ hasStatusUpdate: true,
737
+ listFilters: [],
738
+ businessRules: {
739
+ delete: "Only draft and void journal entries can be deleted. Ready journal entries cannot be deleted -- use update-journal-entry-status to void a ready entry instead.",
740
+ statusTransitions: "Valid transitions: draft -> ready, ready -> void. A void entry is final and cannot be changed."
741
+ },
742
+ cliGroup: "accounting"
743
+ };
744
+
745
+ // packages/core/build/entities/account.js
746
+ var accountConfig = {
747
+ entity: "account",
748
+ apiBasePath: "/accounts",
749
+ singularKey: "account",
750
+ pluralKey: "accounts",
751
+ description: "account",
752
+ operations: ["get", "create", "update", "delete"],
753
+ hasStatusUpdate: false,
754
+ listFilters: [],
755
+ businessRules: {
756
+ delete: "Only accounts that are not assigned to locked system type, have no children, and are not used in transactions can be deleted. Use archive-account instead if the account has transaction history."
757
+ },
758
+ cliGroup: "accounting"
759
+ };
760
+
761
+ // packages/core/build/entities/file.js
762
+ var fileConfig = {
763
+ entity: "file",
764
+ apiBasePath: "/files",
765
+ singularKey: "file",
766
+ pluralKey: "files",
767
+ description: "file. Files are typically attached to sales and purchase transactions using file_ids arrays. Use upload-file to add new files.",
768
+ operations: ["list", "get"],
769
+ hasStatusUpdate: false,
770
+ listFilters: [],
771
+ cliGroup: "files"
772
+ };
773
+
774
+ // packages/core/build/entities/location.js
775
+ var locationConfig = {
776
+ entity: "location",
777
+ apiBasePath: "/locations",
778
+ singularKey: "location",
779
+ pluralKey: "locations",
780
+ description: "location for multi-branch accounting. Use locations to track which branch or office a transaction belongs to.",
781
+ operations: ["list", "create"],
782
+ hasStatusUpdate: false,
783
+ listFilters: ["include_archived"],
784
+ cliGroup: "control-panel"
785
+ };
786
+
787
+ // packages/core/build/entities/tag.js
788
+ var tagConfig = {
789
+ entity: "tag",
790
+ apiBasePath: "/tags",
791
+ singularKey: "tag",
792
+ pluralKey: "tags",
793
+ description: "tag for categorizing transactions and documents. Tags must belong to a tag group \u2014 use list-tag-groups to find available groups and their tag_group_id before creating a tag.",
794
+ operations: ["list", "get", "create", "update", "delete"],
795
+ hasStatusUpdate: false,
796
+ listFilters: [],
797
+ businessRules: {
798
+ delete: "API may restrict deletion if tag is referenced by transactions. Archive the tag instead if deletion fails."
799
+ },
800
+ cliGroup: "control-panel"
801
+ };
802
+
803
+ // packages/core/build/entities/tag-group.js
804
+ var tagGroupConfig = {
805
+ entity: "tag-group",
806
+ apiBasePath: "/tags/groups",
807
+ singularKey: "tag_group",
808
+ pluralKey: "tag_groups",
809
+ description: "tag group for organizing tags into categories. Tag groups contain tags as children \u2014 the list response includes nested tag arrays for each group.",
810
+ operations: ["list", "get", "create", "update", "delete"],
811
+ hasStatusUpdate: false,
812
+ listFilters: ["include_archived"],
813
+ businessRules: {
814
+ delete: "API may restrict deletion if tag group contains tags or is referenced by transactions. Archive instead, or manually delete child tags first."
815
+ },
816
+ cliGroup: "control-panel"
817
+ };
818
+
819
+ // packages/core/build/entities/index.js
820
+ var allEntityConfigs = [
821
+ salesInvoiceConfig,
822
+ salesQuoteConfig,
823
+ salesOrderConfig,
824
+ salesCreditNoteConfig,
825
+ salesPaymentConfig,
826
+ salesRefundConfig,
827
+ deliveryOrderConfig,
828
+ purchaseOrderConfig,
829
+ purchaseBillConfig,
830
+ purchaseCreditNoteConfig,
831
+ purchasePaymentConfig,
832
+ purchaseRefundConfig,
833
+ goodsReceivedNoteConfig,
834
+ bankMoneyInConfig,
835
+ bankMoneyOutConfig,
836
+ bankTransferConfig,
837
+ contactConfig,
838
+ contactGroupConfig,
839
+ productConfig,
840
+ productBundleConfig,
841
+ productGroupConfig,
842
+ journalEntryConfig,
843
+ accountConfig,
844
+ fileConfig,
845
+ locationConfig,
846
+ tagConfig,
847
+ tagGroupConfig
848
+ ];
849
+
850
+ // packages/cli/src/output/error.ts
851
+ var ExitCode = {
852
+ SUCCESS: 0,
853
+ GENERAL: 1,
854
+ AUTH: 2,
855
+ API: 3,
856
+ VALIDATION: 4
857
+ };
858
+ function outputError(err, exitCode) {
859
+ process.stderr.write(JSON.stringify(err) + "\n");
860
+ process.exit(exitCode);
861
+ }
862
+
863
+ // packages/cli/src/commands/wrapper.ts
864
+ function withAuth(handler) {
865
+ return async function() {
866
+ const mergedOpts = this.optsWithGlobals();
867
+ let auth;
868
+ try {
869
+ auth = await resolveAuth({
870
+ apiToken: mergedOpts.apiToken,
871
+ companySubdomain: mergedOpts.companySubdomain
872
+ });
873
+ } catch (err) {
874
+ if (err instanceof AuthMissingError) {
875
+ outputError(
876
+ {
877
+ error: "Authentication required. Set credentials via --api-token flag, BUKKU_API_TOKEN env var, or bukku config set",
878
+ code: "AUTH_MISSING",
879
+ details: null
880
+ },
881
+ ExitCode.AUTH
882
+ );
883
+ }
884
+ throw err;
885
+ }
886
+ const client = new BukkuClient({
887
+ apiToken: auth.apiToken,
888
+ companySubdomain: auth.companySubdomain
889
+ });
890
+ try {
891
+ await handler({ client, opts: mergedOpts, auth });
892
+ } catch (err) {
893
+ if (err instanceof Response) {
894
+ let body = {};
895
+ try {
896
+ body = await err.json();
897
+ } catch {
898
+ }
899
+ outputError(
900
+ {
901
+ error: body["message"] || "API error",
902
+ code: "API_ERROR",
903
+ details: body["errors"] || null
904
+ },
905
+ ExitCode.API
906
+ );
907
+ }
908
+ outputError(
909
+ {
910
+ error: err instanceof Error ? err.message : "Unknown error",
911
+ code: "GENERAL_ERROR"
912
+ },
913
+ ExitCode.GENERAL
914
+ );
915
+ }
916
+ };
917
+ }
918
+
919
+ // packages/cli/src/output/json.ts
920
+ function outputJson(data) {
921
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
922
+ }
923
+
924
+ // packages/cli/src/output/table.ts
925
+ function col(key, header, width, align = "left") {
926
+ return { key, header, width, align };
927
+ }
928
+ function id(width = 6) {
929
+ return col("id", "ID", width, "right");
930
+ }
931
+ var paymentColumns = [
932
+ id(6),
933
+ col("date", "Date", 12),
934
+ col("number", "Number", 15),
935
+ col("contact_name", "Contact", 25),
936
+ col("total", "Amount", 12, "right")
937
+ ];
938
+ function transactionColumns() {
939
+ return [
940
+ id(6),
941
+ col("date", "Date", 12),
942
+ col("number", "Number", 15),
943
+ col("contact_name", "Contact", 25),
944
+ col("total", "Total", 12, "right"),
945
+ col("status", "Status", 12)
946
+ ];
947
+ }
948
+ var DEFAULT_COLUMNS = [
949
+ id(8),
950
+ col("name", "Name", 30),
951
+ col("description", "Description", 40)
952
+ ];
953
+ var TABLE_COLUMNS = {
954
+ // Sales / Purchase transaction types
955
+ "sales-invoice": transactionColumns(),
956
+ "sales-quote": transactionColumns(),
957
+ "sales-order": transactionColumns(),
958
+ "sales-credit-note": transactionColumns(),
959
+ "sales-payment": paymentColumns,
960
+ "sales-refund": paymentColumns,
961
+ "delivery-order": [id(6), col("date", "Date", 12), col("number", "Number", 15), col("contact_name", "Contact", 25), col("status", "Status", 12)],
962
+ "purchase-bill": transactionColumns(),
963
+ "purchase-order": transactionColumns(),
964
+ "purchase-credit-note": transactionColumns(),
965
+ "purchase-payment": paymentColumns,
966
+ "purchase-refund": paymentColumns,
967
+ "goods-received-note": [id(6), col("date", "Date", 12), col("number", "Number", 15), col("contact_name", "Contact", 25), col("status", "Status", 12)],
968
+ // Banking
969
+ "bank-money-in": transactionColumns(),
970
+ "bank-money-out": transactionColumns(),
971
+ "bank-transfer": [id(6), col("date", "Date", 12), col("number", "Number", 15), col("total", "Amount", 12, "right"), col("status", "Status", 12)],
972
+ // Contacts
973
+ "contact": [id(6), col("name", "Name", 30), col("email", "Email", 25), col("type", "Type", 10), col("phone", "Phone", 15)],
974
+ "contact-group": [id(6), col("name", "Name", 30), col("contacts_count", "Contacts", 10, "right")],
975
+ // Products
976
+ "product": [id(6), col("name", "Name", 30), col("code", "Code", 12), col("type", "Type", 10), col("sale_price", "Price", 12, "right")],
977
+ "product-bundle": [id(6), col("name", "Name", 30), col("code", "Code", 12), col("sale_price", "Price", 12, "right")],
978
+ "product-group": [id(6), col("name", "Name", 30)],
979
+ // Accounting
980
+ "journal-entry": [id(6), col("date", "Date", 12), col("number", "Number", 15), col("description", "Description", 30), col("status", "Status", 12)],
981
+ "account": [id(6), col("code", "Code", 8), col("name", "Name", 30), col("category", "Category", 12), col("balance", "Balance", 12, "right")],
982
+ // Files
983
+ "file": [id(6), col("name", "Name", 35), col("content_type", "Type", 20), col("size", "Size", 10, "right")],
984
+ // Control Panel
985
+ "location": [id(6), col("name", "Name", 30), col("is_archived", "Archived", 10)],
986
+ "tag": [id(6), col("name", "Name", 30), col("tag_group_id", "Group ID", 10, "right")],
987
+ "tag-group": [id(6), col("name", "Name", 30)]
988
+ };
989
+ function toStr(value) {
990
+ if (value == null) return "";
991
+ if (typeof value === "boolean") return value ? "yes" : "no";
992
+ return String(value);
993
+ }
994
+ function fit(text, width, align) {
995
+ if (text.length > width) {
996
+ return width > 3 ? text.slice(0, width - 3) + "..." : text.slice(0, width);
997
+ }
998
+ return align === "right" ? text.padStart(width) : text.padEnd(width);
999
+ }
1000
+ function formatTable(rows, columns) {
1001
+ const gap = " ";
1002
+ const lines = [];
1003
+ lines.push(columns.map((c) => fit(c.header, c.width, c.align)).join(gap));
1004
+ lines.push(columns.map((c) => "-".repeat(c.width)).join(gap));
1005
+ for (const row of rows) {
1006
+ lines.push(
1007
+ columns.map((c) => fit(toStr(row[c.key]), c.width, c.align)).join(gap)
1008
+ );
1009
+ }
1010
+ return lines.join("\n") + "\n";
1011
+ }
1012
+ function outputTable(data, columns) {
1013
+ const items = Array.isArray(data) ? data : [data];
1014
+ if (items.length === 0) {
1015
+ process.stdout.write("(no results)\n");
1016
+ return;
1017
+ }
1018
+ const entityName = columns?.[0];
1019
+ const colDefs = entityName && TABLE_COLUMNS[entityName] || DEFAULT_COLUMNS;
1020
+ const rows = items;
1021
+ process.stdout.write(formatTable(rows, colDefs));
1022
+ }
1023
+
1024
+ // packages/cli/src/input/json.ts
1025
+ async function readJsonInput(opts) {
1026
+ if (opts.data != null) {
1027
+ try {
1028
+ return JSON.parse(opts.data);
1029
+ } catch (err) {
1030
+ outputError(
1031
+ {
1032
+ error: "Invalid JSON input: " + (err instanceof SyntaxError ? err.message : String(err)),
1033
+ code: "VALIDATION_ERROR"
1034
+ },
1035
+ ExitCode.VALIDATION
1036
+ );
1037
+ }
1038
+ }
1039
+ if (!process.stdin.isTTY) {
1040
+ const chunks = [];
1041
+ for await (const chunk of process.stdin) {
1042
+ chunks.push(chunk);
1043
+ }
1044
+ const raw = Buffer.concat(chunks).toString("utf-8");
1045
+ try {
1046
+ return JSON.parse(raw);
1047
+ } catch (err) {
1048
+ outputError(
1049
+ {
1050
+ error: "Invalid JSON input: " + (err instanceof SyntaxError ? err.message : String(err)),
1051
+ code: "VALIDATION_ERROR"
1052
+ },
1053
+ ExitCode.VALIDATION
1054
+ );
1055
+ }
1056
+ }
1057
+ outputError(
1058
+ {
1059
+ error: "JSON input required. Use --data flag or pipe JSON to stdin",
1060
+ code: "VALIDATION_ERROR"
1061
+ },
1062
+ ExitCode.VALIDATION
1063
+ );
1064
+ }
1065
+
1066
+ // packages/cli/src/output/dry-run.ts
1067
+ function outputDryRun(opts) {
1068
+ const maskedToken = opts.token.length > 6 ? opts.token.slice(0, 3) + "****" : "****";
1069
+ const output = {
1070
+ dry_run: true,
1071
+ method: opts.method,
1072
+ url: `https://api.bukku.my${opts.path}`,
1073
+ headers: {
1074
+ Authorization: `Bearer ${maskedToken}`,
1075
+ "Company-Subdomain": opts.subdomain
1076
+ }
1077
+ };
1078
+ if (opts.body !== void 0) {
1079
+ output.body = opts.body;
1080
+ }
1081
+ outputJson(output);
1082
+ }
1083
+
1084
+ // packages/cli/src/commands/factory.ts
1085
+ var RESOURCE_NAME_MAP = {
1086
+ "sales-invoice": "invoices",
1087
+ "sales-quote": "quotes",
1088
+ "sales-order": "orders",
1089
+ "sales-credit-note": "credit-notes",
1090
+ "sales-payment": "payments",
1091
+ "sales-refund": "refunds",
1092
+ "delivery-order": "delivery-orders",
1093
+ "purchase-bill": "bills",
1094
+ "purchase-order": "orders",
1095
+ "purchase-credit-note": "credit-notes",
1096
+ "purchase-payment": "payments",
1097
+ "purchase-refund": "refunds",
1098
+ "goods-received-note": "goods-received-notes",
1099
+ "bank-money-in": "money-in",
1100
+ "bank-money-out": "money-out",
1101
+ "bank-transfer": "transfers",
1102
+ "contact": "contacts",
1103
+ "contact-group": "groups",
1104
+ "product": "products",
1105
+ "product-bundle": "bundles",
1106
+ "product-group": "groups",
1107
+ "journal-entry": "journal-entries",
1108
+ "account": "accounts",
1109
+ "file": "files",
1110
+ "location": "locations",
1111
+ "tag": "tags",
1112
+ "tag-group": "tag-groups"
1113
+ };
1114
+ var GROUP_DESCRIPTIONS = {
1115
+ sales: "Sales invoices, quotes, orders, credit notes, payments, refunds",
1116
+ purchases: "Purchase bills, orders, credit notes, payments, refunds",
1117
+ banking: "Bank transactions and transfers",
1118
+ contacts: "Customers, suppliers, and contacts",
1119
+ products: "Products, bundles, and product groups",
1120
+ accounting: "Chart of accounts and journal entries",
1121
+ files: "File uploads and attachments",
1122
+ "control-panel": "Locations, tags, and tag groups"
1123
+ };
1124
+ function pluralize(desc) {
1125
+ if (desc.endsWith("y") && !desc.endsWith("ay") && !desc.endsWith("ey") && !desc.endsWith("oy") && !desc.endsWith("uy")) {
1126
+ return desc.slice(0, -1) + "ies";
1127
+ }
1128
+ if (desc.endsWith("s") || desc.endsWith("x") || desc.endsWith("z") || desc.endsWith("ch") || desc.endsWith("sh")) {
1129
+ return desc + "es";
1130
+ }
1131
+ return desc + "s";
1132
+ }
1133
+ function toFlagName(param) {
1134
+ return param.replace(/_/g, "-");
1135
+ }
1136
+ function addListCommand(resourceCmd, config) {
1137
+ const listCmd = resourceCmd.command("list").description(`List ${pluralize(config.description)}`);
1138
+ listCmd.option("--limit <n>", "Maximum items per page", void 0).option("--page <n>", "Page number", void 0).option("--search <text>", "Search text", void 0).option("--date-from <date>", "Filter from date (YYYY-MM-DD)", void 0).option("--date-to <date>", "Filter to date (YYYY-MM-DD)", void 0).option("--status <status>", "Filter by status", void 0).option("--sort-by <field>", "Sort by field", void 0).option("--sort-dir <dir>", "Sort direction (asc, desc)", void 0).option("--all", "Fetch all pages", false).option("--format <format>", "Output format (json, table)", "json");
1139
+ if (config.listFilters) {
1140
+ for (const filter of config.listFilters) {
1141
+ const flagName = toFlagName(filter);
1142
+ if (["search", "status"].includes(filter)) continue;
1143
+ listCmd.option(`--${flagName} <value>`, `Filter by ${filter}`, void 0);
1144
+ }
1145
+ }
1146
+ listCmd.action(
1147
+ withAuth(async ({ client, opts }) => {
1148
+ const params = {};
1149
+ if (opts.limit != null) params.page_size = Number(opts.limit);
1150
+ if (opts.page != null) params.page = Number(opts.page);
1151
+ if (opts.search != null) params.search = opts.search;
1152
+ if (opts.dateFrom != null) params.date_from = opts.dateFrom;
1153
+ if (opts.dateTo != null) params.date_to = opts.dateTo;
1154
+ if (opts.status != null) params.status = opts.status;
1155
+ if (opts.sortBy != null) params.sort_by = opts.sortBy;
1156
+ if (opts.sortDir != null) params.sort_dir = opts.sortDir;
1157
+ if (config.listFilters) {
1158
+ for (const filter of config.listFilters) {
1159
+ if (["search", "status"].includes(filter)) continue;
1160
+ const camelKey = toFlagName(filter).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1161
+ if (opts[camelKey] != null) {
1162
+ params[filter] = opts[camelKey];
1163
+ }
1164
+ }
1165
+ }
1166
+ const format = opts.format;
1167
+ if (opts.all) {
1168
+ const pageSize = params.page_size ?? 100;
1169
+ params.page_size = pageSize;
1170
+ params.page = 1;
1171
+ const firstResponse = await client.get(config.apiBasePath, params);
1172
+ const paging = firstResponse.paging;
1173
+ const allItems = [...firstResponse[config.pluralKey] || []];
1174
+ const totalPages = Math.ceil(paging.total / paging.per_page);
1175
+ if (totalPages > 1) {
1176
+ process.stderr.write(`Fetching page 1/${totalPages}...
1177
+ `);
1178
+ }
1179
+ for (let page = 2; page <= totalPages; page++) {
1180
+ process.stderr.write(`Fetching page ${page}/${totalPages}...
1181
+ `);
1182
+ params.page = page;
1183
+ const response = await client.get(config.apiBasePath, params);
1184
+ const items = response[config.pluralKey] || [];
1185
+ allItems.push(...items);
1186
+ }
1187
+ if (format === "table") {
1188
+ outputTable(allItems, [config.entity]);
1189
+ } else {
1190
+ outputJson(allItems);
1191
+ }
1192
+ } else {
1193
+ const data = await client.get(config.apiBasePath, params);
1194
+ if (format === "table") {
1195
+ const response = data;
1196
+ const items = response[config.pluralKey] || [];
1197
+ outputTable(items, [config.entity]);
1198
+ } else {
1199
+ outputJson(data);
1200
+ }
1201
+ }
1202
+ })
1203
+ );
1204
+ }
1205
+ function addGetCommand(resourceCmd, config) {
1206
+ const getCmd = resourceCmd.command("get <id>").description(`Get a single ${config.description} by ID`);
1207
+ getCmd.option("--format <format>", "Output format (json, table)", "json");
1208
+ const wrappedHandler = withAuth(async ({ client, opts }) => {
1209
+ const parsedId = opts._entityId;
1210
+ const data = await client.get(`${config.apiBasePath}/${parsedId}`);
1211
+ const format = opts.format;
1212
+ if (format === "table") {
1213
+ const response = data;
1214
+ const item = response[config.singularKey];
1215
+ outputTable(item ? [item] : [], [config.entity]);
1216
+ } else {
1217
+ outputJson(data);
1218
+ }
1219
+ });
1220
+ getCmd.action(function(idArg, ...rest) {
1221
+ const parsedId = parseInt(idArg, 10);
1222
+ if (isNaN(parsedId) || parsedId <= 0) {
1223
+ process.stderr.write(
1224
+ JSON.stringify({ error: "ID must be a positive integer", code: "VALIDATION_ERROR" }) + "\n"
1225
+ );
1226
+ process.exit(4);
1227
+ }
1228
+ this.setOptionValue("_entityId", parsedId);
1229
+ return wrappedHandler.call(this, idArg, ...rest);
1230
+ });
1231
+ }
1232
+ function addCreateCommand(resourceCmd, config) {
1233
+ const createCmd = resourceCmd.command("create").description(`Create a new ${config.description}`).option("--data <json>", "JSON data (or pipe to stdin)").option("--dry-run", "Show request details without executing", false);
1234
+ createCmd.action(
1235
+ withAuth(async ({ client, opts, auth }) => {
1236
+ const body = await readJsonInput(opts);
1237
+ if (opts.dryRun) {
1238
+ outputDryRun({ method: "POST", path: config.apiBasePath, token: auth.apiToken, subdomain: auth.companySubdomain, body });
1239
+ return;
1240
+ }
1241
+ const data = await client.post(config.apiBasePath, body);
1242
+ outputJson(data);
1243
+ })
1244
+ );
1245
+ }
1246
+ function addUpdateCommand(resourceCmd, config) {
1247
+ const updateCmd = resourceCmd.command("update <id>").description(`Update a ${config.description}`).option("--data <json>", "JSON data (or pipe to stdin)").option("--dry-run", "Show request details without executing", false);
1248
+ const wrappedHandler = withAuth(async ({ client, opts, auth }) => {
1249
+ const parsedId = opts._entityId;
1250
+ const body = await readJsonInput(opts);
1251
+ if (opts.dryRun) {
1252
+ outputDryRun({ method: "PUT", path: `${config.apiBasePath}/${parsedId}`, token: auth.apiToken, subdomain: auth.companySubdomain, body });
1253
+ return;
1254
+ }
1255
+ const data = await client.put(`${config.apiBasePath}/${parsedId}`, body);
1256
+ outputJson(data);
1257
+ });
1258
+ updateCmd.action(function(idArg, ...rest) {
1259
+ const parsedId = parseInt(idArg, 10);
1260
+ if (isNaN(parsedId) || parsedId <= 0) {
1261
+ process.stderr.write(
1262
+ JSON.stringify({ error: "ID must be a positive integer", code: "VALIDATION_ERROR" }) + "\n"
1263
+ );
1264
+ process.exit(4);
1265
+ }
1266
+ this.setOptionValue("_entityId", parsedId);
1267
+ return wrappedHandler.call(this, idArg, ...rest);
1268
+ });
1269
+ }
1270
+ function addDeleteCommand(resourceCmd, config) {
1271
+ const deleteCmd = resourceCmd.command("delete <id>").description(`Delete a ${config.description}`).option("--dry-run", "Show request details without executing", false);
1272
+ const wrappedHandler = withAuth(async ({ client, opts, auth }) => {
1273
+ const parsedId = opts._entityId;
1274
+ if (opts.dryRun) {
1275
+ outputDryRun({ method: "DELETE", path: `${config.apiBasePath}/${parsedId}`, token: auth.apiToken, subdomain: auth.companySubdomain });
1276
+ return;
1277
+ }
1278
+ await client.delete(`${config.apiBasePath}/${parsedId}`);
1279
+ outputJson({});
1280
+ });
1281
+ deleteCmd.action(function(idArg, ...rest) {
1282
+ const parsedId = parseInt(idArg, 10);
1283
+ if (isNaN(parsedId) || parsedId <= 0) {
1284
+ process.stderr.write(
1285
+ JSON.stringify({ error: "ID must be a positive integer", code: "VALIDATION_ERROR" }) + "\n"
1286
+ );
1287
+ process.exit(4);
1288
+ }
1289
+ this.setOptionValue("_entityId", parsedId);
1290
+ return wrappedHandler.call(this, idArg, ...rest);
1291
+ });
1292
+ }
1293
+ function addStatusCommand(resourceCmd, config) {
1294
+ const statusCmd = resourceCmd.command("status <id>").description(`Update status of a ${config.description}`).requiredOption("--status <status>", "New status value").option("--dry-run", "Show request details without executing", false);
1295
+ const wrappedHandler = withAuth(async ({ client, opts, auth }) => {
1296
+ const parsedId = opts._entityId;
1297
+ const status = opts.status;
1298
+ if (opts.dryRun) {
1299
+ outputDryRun({ method: "PATCH", path: `${config.apiBasePath}/${parsedId}`, token: auth.apiToken, subdomain: auth.companySubdomain, body: { status } });
1300
+ return;
1301
+ }
1302
+ const data = await client.patch(`${config.apiBasePath}/${parsedId}`, { status });
1303
+ outputJson(data);
1304
+ });
1305
+ statusCmd.action(function(idArg, ...rest) {
1306
+ const parsedId = parseInt(idArg, 10);
1307
+ if (isNaN(parsedId) || parsedId <= 0) {
1308
+ process.stderr.write(
1309
+ JSON.stringify({ error: "ID must be a positive integer", code: "VALIDATION_ERROR" }) + "\n"
1310
+ );
1311
+ process.exit(4);
1312
+ }
1313
+ this.setOptionValue("_entityId", parsedId);
1314
+ return wrappedHandler.call(this, idArg, ...rest);
1315
+ });
1316
+ }
1317
+ function registerEntityCommands(program2) {
1318
+ for (const config of allEntityConfigs) {
1319
+ if (!config.cliGroup) continue;
1320
+ const groupName = config.cliGroup;
1321
+ const resourceName = RESOURCE_NAME_MAP[config.entity];
1322
+ if (!resourceName) continue;
1323
+ let groupCmd = program2.commands.find((c) => c.name() === groupName);
1324
+ if (!groupCmd) {
1325
+ groupCmd = program2.command(groupName).description(GROUP_DESCRIPTIONS[groupName] || groupName);
1326
+ }
1327
+ const resourceCmd = groupCmd.command(resourceName).description(`Manage ${pluralize(config.description)}`);
1328
+ if (config.operations.includes("list")) {
1329
+ addListCommand(resourceCmd, config);
1330
+ }
1331
+ if (config.operations.includes("get")) {
1332
+ addGetCommand(resourceCmd, config);
1333
+ }
1334
+ if (config.operations.includes("create")) {
1335
+ addCreateCommand(resourceCmd, config);
1336
+ }
1337
+ if (config.operations.includes("update")) {
1338
+ addUpdateCommand(resourceCmd, config);
1339
+ }
1340
+ if (config.operations.includes("delete")) {
1341
+ addDeleteCommand(resourceCmd, config);
1342
+ }
1343
+ if (config.hasStatusUpdate) {
1344
+ addStatusCommand(resourceCmd, config);
1345
+ }
1346
+ }
1347
+ }
1348
+
1349
+ // packages/cli/src/commands/custom/reference-data.ts
1350
+ var REFERENCE_TYPES = [
1351
+ {
1352
+ type: "tax_codes",
1353
+ commandName: "tax-codes",
1354
+ description: "List tax codes (tax rate definitions)"
1355
+ },
1356
+ {
1357
+ type: "currencies",
1358
+ commandName: "currencies",
1359
+ description: "List activated currencies"
1360
+ },
1361
+ {
1362
+ type: "payment_methods",
1363
+ commandName: "payment-methods",
1364
+ description: "List payment methods"
1365
+ },
1366
+ {
1367
+ type: "terms",
1368
+ commandName: "terms",
1369
+ description: "List payment terms"
1370
+ },
1371
+ {
1372
+ type: "accounts",
1373
+ commandName: "accounts",
1374
+ description: "List accounts from chart of accounts (quick lookup)"
1375
+ },
1376
+ {
1377
+ type: "price_levels",
1378
+ commandName: "price-levels",
1379
+ description: "List price levels"
1380
+ },
1381
+ {
1382
+ type: "countries",
1383
+ commandName: "countries",
1384
+ description: "List countries"
1385
+ },
1386
+ {
1387
+ type: "classification_code_list",
1388
+ commandName: "classification-codes",
1389
+ description: "List product classification codes (Malaysia LHDN e-Invoice)"
1390
+ },
1391
+ {
1392
+ type: "numberings",
1393
+ commandName: "numberings",
1394
+ description: "List document numbering schemes"
1395
+ },
1396
+ {
1397
+ type: "state_list",
1398
+ commandName: "states",
1399
+ description: "List geographic states/provinces"
1400
+ }
1401
+ ];
1402
+ function registerReferenceDataCommands(program2) {
1403
+ const refDataCmd = program2.command("ref-data").description("Reference data lookups (tax codes, currencies, terms, etc.)");
1404
+ for (const { type, commandName, description } of REFERENCE_TYPES) {
1405
+ refDataCmd.command(commandName).description(description).option("--format <format>", "Output format (json, table)", "json").action(
1406
+ withAuth(async ({ client, opts }) => {
1407
+ const result = await client.post("/v2/lists", {
1408
+ lists: [type],
1409
+ params: []
1410
+ });
1411
+ const format = opts.format;
1412
+ if (format === "table") {
1413
+ const response = result;
1414
+ const items = response[type] || [];
1415
+ outputTable(items);
1416
+ } else {
1417
+ outputJson(result);
1418
+ }
1419
+ })
1420
+ );
1421
+ }
1422
+ }
1423
+
1424
+ // packages/cli/src/commands/custom/search-accounts.ts
1425
+ function registerSearchAccountsCommand(program2) {
1426
+ let accountingCmd = program2.commands.find((c) => c.name() === "accounting");
1427
+ if (!accountingCmd) {
1428
+ accountingCmd = program2.command("accounting").description("Chart of accounts and journal entries");
1429
+ }
1430
+ accountingCmd.command("search-accounts").description("Search and filter accounts from the chart of accounts").option("--search <text>", "Search by name, code, or description").option("--category <cat>", "Filter by category (assets, liabilities, equity, income, expenses)").option("--is-archived", "Include archived accounts", false).option("--sort-by <field>", "Sort by field (code, name, balance)").option("--sort-dir <dir>", "Sort direction (asc, desc)").option("--page <n>", "Page number").option("--limit <n>", "Items per page").option("--format <format>", "Output format (json, table)", "json").action(
1431
+ withAuth(async ({ client, opts }) => {
1432
+ const params = {};
1433
+ if (opts.search != null) params.search = opts.search;
1434
+ if (opts.category != null) params.category = opts.category;
1435
+ if (opts.isArchived) params.is_archived = "true";
1436
+ if (opts.sortBy != null) params.sort_by = opts.sortBy;
1437
+ if (opts.sortDir != null) params.sort_dir = opts.sortDir;
1438
+ if (opts.page != null) params.page = Number(opts.page);
1439
+ if (opts.limit != null) params.page_size = Number(opts.limit);
1440
+ const result = await client.get("/accounts", params);
1441
+ const format = opts.format;
1442
+ if (format === "table") {
1443
+ const response = result;
1444
+ const items = response["accounts"] || [];
1445
+ outputTable(items, ["account"]);
1446
+ } else {
1447
+ outputJson(result);
1448
+ }
1449
+ })
1450
+ );
1451
+ }
1452
+
1453
+ // packages/cli/src/commands/custom/archive.ts
1454
+ var ARCHIVE_CONFIGS = [
1455
+ { group: "contacts", resource: "contacts", apiPath: "/contacts", description: "contact" },
1456
+ { group: "products", resource: "products", apiPath: "/products", description: "product" },
1457
+ { group: "products", resource: "bundles", apiPath: "/products/bundles", description: "product bundle" },
1458
+ { group: "accounting", resource: "accounts", apiPath: "/accounts", description: "account" },
1459
+ { group: "control-panel", resource: "locations", apiPath: "/location", description: "location" }
1460
+ ];
1461
+ function parseId(idArg) {
1462
+ const parsed = parseInt(idArg, 10);
1463
+ if (isNaN(parsed) || parsed <= 0) {
1464
+ outputError(
1465
+ { error: "ID must be a positive integer", code: "VALIDATION_ERROR" },
1466
+ ExitCode.VALIDATION
1467
+ );
1468
+ }
1469
+ return parsed;
1470
+ }
1471
+ function registerArchiveCommands(program2) {
1472
+ for (const config of ARCHIVE_CONFIGS) {
1473
+ const groupCmd = program2.commands.find((c) => c.name() === config.group);
1474
+ if (!groupCmd) continue;
1475
+ const resourceCmd = groupCmd.commands.find((c) => c.name() === config.resource);
1476
+ if (!resourceCmd) continue;
1477
+ const archiveHandler = withAuth(async ({ client, opts, auth }) => {
1478
+ const id2 = opts._entityId;
1479
+ if (opts.dryRun) {
1480
+ outputDryRun({ method: "PATCH", path: `${config.apiPath}/${id2}`, token: auth.apiToken, subdomain: auth.companySubdomain, body: { is_archived: true } });
1481
+ return;
1482
+ }
1483
+ const data = await client.patch(`${config.apiPath}/${id2}`, { is_archived: true });
1484
+ outputJson(data);
1485
+ });
1486
+ resourceCmd.command("archive <id>").description(`Archive a ${config.description}`).option("--dry-run", "Show request details without executing", false).action(function(idArg, ...rest) {
1487
+ const id2 = parseId(idArg);
1488
+ this.setOptionValue("_entityId", id2);
1489
+ return archiveHandler.call(this, idArg, ...rest);
1490
+ });
1491
+ const unarchiveHandler = withAuth(async ({ client, opts, auth }) => {
1492
+ const id2 = opts._entityId;
1493
+ if (opts.dryRun) {
1494
+ outputDryRun({ method: "PATCH", path: `${config.apiPath}/${id2}`, token: auth.apiToken, subdomain: auth.companySubdomain, body: { is_archived: false } });
1495
+ return;
1496
+ }
1497
+ const data = await client.patch(`${config.apiPath}/${id2}`, { is_archived: false });
1498
+ outputJson(data);
1499
+ });
1500
+ resourceCmd.command("unarchive <id>").description(`Unarchive a ${config.description}`).option("--dry-run", "Show request details without executing", false).action(function(idArg, ...rest) {
1501
+ const id2 = parseId(idArg);
1502
+ this.setOptionValue("_entityId", id2);
1503
+ return unarchiveHandler.call(this, idArg, ...rest);
1504
+ });
1505
+ }
1506
+ }
1507
+
1508
+ // packages/cli/src/commands/custom/location-write.ts
1509
+ function parseId2(idArg) {
1510
+ const parsed = parseInt(idArg, 10);
1511
+ if (isNaN(parsed) || parsed <= 0) {
1512
+ outputError(
1513
+ { error: "ID must be a positive integer", code: "VALIDATION_ERROR" },
1514
+ ExitCode.VALIDATION
1515
+ );
1516
+ }
1517
+ return parsed;
1518
+ }
1519
+ function registerLocationWriteCommands(program2) {
1520
+ const groupCmd = program2.commands.find((c) => c.name() === "control-panel");
1521
+ if (!groupCmd) return;
1522
+ const resourceCmd = groupCmd.commands.find((c) => c.name() === "locations");
1523
+ if (!resourceCmd) return;
1524
+ const getHandler = withAuth(async ({ client, opts }) => {
1525
+ const id2 = opts._entityId;
1526
+ const data = await client.get(`/location/${id2}`);
1527
+ const format = opts.format;
1528
+ if (format === "table") {
1529
+ const response = data;
1530
+ const item = response["location"];
1531
+ outputTable(item ? [item] : [], ["location"]);
1532
+ } else {
1533
+ outputJson(data);
1534
+ }
1535
+ });
1536
+ resourceCmd.command("get <id>").description("Get a single location by ID").option("--format <format>", "Output format (json, table)", "json").action(function(idArg, ...rest) {
1537
+ const id2 = parseId2(idArg);
1538
+ this.setOptionValue("_entityId", id2);
1539
+ return getHandler.call(this, idArg, ...rest);
1540
+ });
1541
+ const updateHandler = withAuth(async ({ client, opts, auth }) => {
1542
+ const id2 = opts._entityId;
1543
+ const body = await readJsonInput(opts);
1544
+ if (opts.dryRun) {
1545
+ outputDryRun({ method: "PUT", path: `/location/${id2}`, token: auth.apiToken, subdomain: auth.companySubdomain, body });
1546
+ return;
1547
+ }
1548
+ const data = await client.put(`/location/${id2}`, body);
1549
+ outputJson(data);
1550
+ });
1551
+ resourceCmd.command("update <id>").description("Update a location").option("--data <json>", "JSON data (or pipe to stdin)").option("--dry-run", "Show request details without executing", false).action(function(idArg, ...rest) {
1552
+ const id2 = parseId2(idArg);
1553
+ this.setOptionValue("_entityId", id2);
1554
+ return updateHandler.call(this, idArg, ...rest);
1555
+ });
1556
+ const deleteHandler = withAuth(async ({ client, opts, auth }) => {
1557
+ const id2 = opts._entityId;
1558
+ if (opts.dryRun) {
1559
+ outputDryRun({ method: "DELETE", path: `/location/${id2}`, token: auth.apiToken, subdomain: auth.companySubdomain });
1560
+ return;
1561
+ }
1562
+ await client.delete(`/location/${id2}`);
1563
+ outputJson({});
1564
+ });
1565
+ resourceCmd.command("delete <id>").description("Delete a location").option("--dry-run", "Show request details without executing", false).action(function(idArg, ...rest) {
1566
+ const id2 = parseId2(idArg);
1567
+ this.setOptionValue("_entityId", id2);
1568
+ return deleteHandler.call(this, idArg, ...rest);
1569
+ });
1570
+ }
1571
+
1572
+ // packages/cli/src/commands/custom/journal-entry.ts
1573
+ function parseId3(idArg) {
1574
+ const parsed = parseInt(idArg, 10);
1575
+ if (isNaN(parsed) || parsed <= 0) {
1576
+ outputError(
1577
+ { error: "ID must be a positive integer", code: "VALIDATION_ERROR" },
1578
+ ExitCode.VALIDATION
1579
+ );
1580
+ }
1581
+ return parsed;
1582
+ }
1583
+ function registerJournalEntryCommands(program2) {
1584
+ const groupCmd = program2.commands.find((c) => c.name() === "accounting");
1585
+ if (!groupCmd) return;
1586
+ const resourceCmd = groupCmd.commands.find((c) => c.name() === "journal-entries");
1587
+ if (!resourceCmd) return;
1588
+ resourceCmd.command("create").description("Create a new journal entry").option("--data <json>", "JSON data (or pipe to stdin)").option("--dry-run", "Show request details without executing", false).action(
1589
+ withAuth(async ({ client, opts, auth }) => {
1590
+ const body = await readJsonInput(opts);
1591
+ if (body.journal_items && Array.isArray(body.journal_items)) {
1592
+ const validation = validateDoubleEntry(body.journal_items);
1593
+ if (!validation.valid) {
1594
+ outputError(
1595
+ { error: validation.error, code: "VALIDATION_ERROR" },
1596
+ ExitCode.VALIDATION
1597
+ );
1598
+ }
1599
+ }
1600
+ if (opts.dryRun) {
1601
+ outputDryRun({ method: "POST", path: "/journal_entries", token: auth.apiToken, subdomain: auth.companySubdomain, body });
1602
+ return;
1603
+ }
1604
+ const data = await client.post("/journal_entries", body);
1605
+ outputJson(data);
1606
+ })
1607
+ );
1608
+ const updateHandler = withAuth(async ({ client, opts, auth }) => {
1609
+ const id2 = opts._entityId;
1610
+ const body = await readJsonInput(opts);
1611
+ if (body.journal_items && Array.isArray(body.journal_items)) {
1612
+ const validation = validateDoubleEntry(body.journal_items);
1613
+ if (!validation.valid) {
1614
+ outputError(
1615
+ { error: validation.error, code: "VALIDATION_ERROR" },
1616
+ ExitCode.VALIDATION
1617
+ );
1618
+ }
1619
+ }
1620
+ if (opts.dryRun) {
1621
+ outputDryRun({ method: "PUT", path: `/journal_entries/${id2}`, token: auth.apiToken, subdomain: auth.companySubdomain, body });
1622
+ return;
1623
+ }
1624
+ const data = await client.put(`/journal_entries/${id2}`, body);
1625
+ outputJson(data);
1626
+ });
1627
+ resourceCmd.command("update <id>").description("Update a journal entry").option("--data <json>", "JSON data (or pipe to stdin)").option("--dry-run", "Show request details without executing", false).action(function(idArg, ...rest) {
1628
+ const id2 = parseId3(idArg);
1629
+ this.setOptionValue("_entityId", id2);
1630
+ return updateHandler.call(this, idArg, ...rest);
1631
+ });
1632
+ }
1633
+
1634
+ // packages/cli/src/commands/custom/file-upload.ts
1635
+ import { basename as basename2 } from "node:path";
1636
+ import { access } from "node:fs/promises";
1637
+ function registerFileUploadCommand(program2) {
1638
+ let groupCmd = program2.commands.find((c) => c.name() === "files");
1639
+ if (!groupCmd) {
1640
+ groupCmd = program2.command("files").description("File uploads and attachments");
1641
+ }
1642
+ const wrappedHandler = withAuth(async ({ client, opts, auth }) => {
1643
+ const filePath = opts._filePath;
1644
+ try {
1645
+ await access(filePath);
1646
+ } catch {
1647
+ outputError(
1648
+ { error: "File not found: " + filePath, code: "VALIDATION_ERROR" },
1649
+ ExitCode.VALIDATION
1650
+ );
1651
+ }
1652
+ if (opts.dryRun) {
1653
+ outputDryRun({ method: "POST", path: "/files", token: auth.apiToken, subdomain: auth.companySubdomain, body: { file: basename2(filePath) } });
1654
+ return;
1655
+ }
1656
+ const data = await client.postMultipart("/files", filePath);
1657
+ outputJson(data);
1658
+ });
1659
+ groupCmd.command("upload <path>").description("Upload a file to Bukku").option("--dry-run", "Show request details without executing", false).action(function(pathArg, ...rest) {
1660
+ this.setOptionValue("_filePath", pathArg);
1661
+ return wrappedHandler.call(this, pathArg, ...rest);
1662
+ });
1663
+ }
1664
+
1665
+ // packages/cli/src/index.ts
1666
+ var __filename = fileURLToPath(import.meta.url);
1667
+ var __dirname = dirname(__filename);
1668
+ var pkg = JSON.parse(
1669
+ readFileSync(join2(__dirname, "..", "package.json"), "utf-8")
1670
+ );
1671
+ var program = new Command2();
1672
+ program.name("bukku").version(pkg.version).description("Bukku accounting CLI");
1673
+ program.option("--api-token <token>", "API token").option("--company-subdomain <subdomain>", "Company subdomain");
1674
+ registerEntityCommands(program);
1675
+ registerReferenceDataCommands(program);
1676
+ registerSearchAccountsCommand(program);
1677
+ registerArchiveCommands(program);
1678
+ registerLocationWriteCommands(program);
1679
+ registerJournalEntryCommands(program);
1680
+ registerFileUploadCommand(program);
1681
+ program.addCommand(configCommand);
1682
+ program.parseAsync(process.argv).catch((err) => {
1683
+ console.error(err instanceof Error ? err.message : String(err));
1684
+ process.exit(1);
1685
+ });