@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/README.md ADDED
@@ -0,0 +1,266 @@
1
+ # @centry-digital/bukku-cli
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@centry-digital/bukku-cli)](https://www.npmjs.com/package/@centry-digital/bukku-cli)
4
+
5
+ A command-line interface for [Bukku](https://bukku.my), a Malaysian accounting platform. Manage invoices, contacts, products, and more from your terminal.
6
+
7
+ ## What can it do?
8
+
9
+ The CLI exposes **169 commands** across 9 categories, covering the full Bukku API:
10
+
11
+ | Category | Commands | What you can do |
12
+ |----------|----------|-----------------|
13
+ | **Sales** | 42 | Quotes, orders, delivery orders, invoices, credit notes, payments, refunds |
14
+ | **Purchases** | 36 | Purchase orders, bills, credit notes, goods received notes, payments, refunds |
15
+ | **Banking** | 18 | Money in, money out, bank transfers |
16
+ | **Contacts** | 12 | Customers, suppliers, contact groups |
17
+ | **Products** | 18 | Products, bundles, product groups |
18
+ | **Accounting** | 13 | Chart of accounts, journal entries |
19
+ | **Files** | 3 | Upload and manage file attachments |
20
+ | **Control Panel** | 21 | Locations, tags, tag groups |
21
+ | **Reference Data** | 10 | Tax codes, currencies, payment methods, terms, and more |
22
+
23
+ Every list/get/create/update/delete operation available in the Bukku web UI is available from the command line.
24
+
25
+ ## Quick Start
26
+
27
+ Try it without installing:
28
+
29
+ ```bash
30
+ npx @centry-digital/bukku-cli --help
31
+ ```
32
+
33
+ Or install globally:
34
+
35
+ ```bash
36
+ npm install -g @centry-digital/bukku-cli
37
+ bukku --help
38
+ ```
39
+
40
+ ## Configuration
41
+
42
+ The CLI needs two things: your API token and company subdomain. There are three ways to provide them, listed in order of precedence:
43
+
44
+ ### 1. CLI flags (highest precedence)
45
+
46
+ ```bash
47
+ bukku --api-token YOUR_TOKEN --company-subdomain YOUR_SUB sales invoices list
48
+ ```
49
+
50
+ ### 2. Environment variables
51
+
52
+ ```bash
53
+ export BUKKU_API_TOKEN=your-token-here
54
+ export BUKKU_COMPANY_SUBDOMAIN=your-subdomain
55
+ bukku sales invoices list
56
+ ```
57
+
58
+ ### 3. Config file (~/.bukkurc)
59
+
60
+ ```bash
61
+ bukku config set api_token your-token-here
62
+ bukku config set company_subdomain your-subdomain
63
+ ```
64
+
65
+ Verify your configuration:
66
+
67
+ ```bash
68
+ bukku config show
69
+ ```
70
+
71
+ This shows the resolved value for each field and where it came from (flag, environment variable, or config file).
72
+
73
+ ## Usage Examples
74
+
75
+ ### Sales
76
+
77
+ ```bash
78
+ # List recent invoices
79
+ bukku sales invoices list --limit 5
80
+
81
+ # Get a specific invoice
82
+ bukku sales invoices get 123
83
+
84
+ # Create an invoice
85
+ bukku sales invoices create --data '{
86
+ "contact_id": 1,
87
+ "date": "2025-01-15",
88
+ "currency_code": "MYR",
89
+ "exchange_rate": 1,
90
+ "tax_mode": "exclusive",
91
+ "payment_mode": "credit",
92
+ "status": "draft",
93
+ "form_items": [{"account_id": 100, "description": "Consulting", "amount": 5000}],
94
+ "term_items": [{"payment_due": "100%", "date": "2025-02-15"}]
95
+ }'
96
+
97
+ # List quotes in table format
98
+ bukku sales quotes list --format table
99
+ ```
100
+
101
+ ### Purchases
102
+
103
+ ```bash
104
+ # List bills
105
+ bukku purchases bills list --format table
106
+
107
+ # Get a specific bill
108
+ bukku purchases bills get 456
109
+
110
+ # List purchase orders
111
+ bukku purchases orders list --limit 10
112
+ ```
113
+
114
+ ### Banking
115
+
116
+ ```bash
117
+ # List money-in transactions
118
+ bukku banking money-in list --limit 10
119
+
120
+ # List money-out transactions
121
+ bukku banking money-out list --format table
122
+
123
+ # List bank transfers
124
+ bukku banking transfers list
125
+ ```
126
+
127
+ ### Contacts
128
+
129
+ ```bash
130
+ # List all contacts
131
+ bukku contacts contacts list --format table
132
+
133
+ # Get a specific contact
134
+ bukku contacts contacts get 789
135
+
136
+ # List contact groups
137
+ bukku contacts contact-groups list
138
+ ```
139
+
140
+ ### Products
141
+
142
+ ```bash
143
+ # List products
144
+ bukku products products list --format table
145
+
146
+ # Get a specific product
147
+ bukku products products get 123
148
+
149
+ # List product groups
150
+ bukku products product-groups list
151
+ ```
152
+
153
+ ### Accounting
154
+
155
+ ```bash
156
+ # List journal entries
157
+ bukku accounting journal-entries list
158
+
159
+ # Search chart of accounts
160
+ bukku accounting accounts list --format table
161
+ ```
162
+
163
+ ### Files
164
+
165
+ ```bash
166
+ # Upload a file
167
+ bukku files upload /path/to/receipt.pdf
168
+
169
+ # List uploaded files
170
+ bukku files files list
171
+ ```
172
+
173
+ ### Control Panel
174
+
175
+ ```bash
176
+ # List locations
177
+ bukku control-panel locations list
178
+
179
+ # List tags
180
+ bukku control-panel tags list --format table
181
+
182
+ # List tag groups
183
+ bukku control-panel tag-groups list
184
+ ```
185
+
186
+ ### Reference Data
187
+
188
+ ```bash
189
+ # List tax codes
190
+ bukku ref-data tax-codes
191
+
192
+ # List currencies
193
+ bukku ref-data currencies
194
+
195
+ # List payment methods
196
+ bukku ref-data payment-methods
197
+
198
+ # List classification codes (Malaysia e-Invoice)
199
+ bukku ref-data classification-codes
200
+ ```
201
+
202
+ ## Input Formats
203
+
204
+ For create and update commands, provide JSON data in one of these ways:
205
+
206
+ **Inline with `--data` flag:**
207
+
208
+ ```bash
209
+ bukku sales invoices create --data '{"contact_id": 1, "date": "2025-01-15", ...}'
210
+ ```
211
+
212
+ **Piped from a file:**
213
+
214
+ ```bash
215
+ cat invoice.json | bukku sales invoices create
216
+ ```
217
+
218
+ **Dry run (preview without sending):**
219
+
220
+ ```bash
221
+ bukku sales invoices create --data '{"contact_id": 1, ...}' --dry-run
222
+ ```
223
+
224
+ Dry run shows the HTTP method, URL, and request body that would be sent, without making the API call.
225
+
226
+ ## Output Formats
227
+
228
+ **JSON (default):** Machine-readable output, easy to pipe to `jq`:
229
+
230
+ ```bash
231
+ bukku sales invoices list | jq '.[0].total'
232
+ ```
233
+
234
+ **Table:** Human-readable table format:
235
+
236
+ ```bash
237
+ bukku sales invoices list --format table
238
+ ```
239
+
240
+ Errors are written to stderr as structured JSON, so they don't interfere with piped output.
241
+
242
+ ## Exit Codes
243
+
244
+ | Code | Meaning |
245
+ |------|---------|
246
+ | 0 | Success |
247
+ | 1 | General error |
248
+ | 2 | Authentication error (missing or invalid credentials) |
249
+ | 3 | API error (Bukku returned an error) |
250
+ | 4 | Validation error (invalid input or config) |
251
+
252
+ ## Getting Your API Token
253
+
254
+ 1. Log into your [Bukku](https://bukku.my) account
255
+ 2. Go to **Control Panel > Integrations > API Access**
256
+ 3. Generate a new API token (or copy your existing one)
257
+ 4. Note your company subdomain from the URL (e.g. `mycompany` from `mycompany.bukku.my`)
258
+
259
+ ## Related
260
+
261
+ - [@centry-digital/bukku-mcp](https://www.npmjs.com/package/@centry-digital/bukku-mcp) -- MCP server for connecting AI assistants (Claude, etc.) to Bukku
262
+ - [GitHub repository](https://github.com/centry-digital/bukku)
263
+
264
+ ## License
265
+
266
+ MIT
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare const configCommand: Command;
3
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/commands/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,eAAO,MAAM,aAAa,SACgB,CAAC"}
@@ -0,0 +1,68 @@
1
+ import { Command } from 'commander';
2
+ import { readRc, writeRc, checkPermissions } from '../config/rc.js';
3
+ import { maskToken } from '../config/auth.js';
4
+ const VALID_KEYS = ['api_token', 'company_subdomain'];
5
+ function isValidKey(key) {
6
+ return VALID_KEYS.includes(key);
7
+ }
8
+ export const configCommand = new Command('config')
9
+ .description('Manage CLI configuration');
10
+ // config set <key> <value>
11
+ configCommand
12
+ .command('set')
13
+ .description('Set a config value')
14
+ .argument('<key>', 'Config key (api_token, company_subdomain)')
15
+ .argument('<value>', 'Config value')
16
+ .action(async (key, value) => {
17
+ if (!isValidKey(key)) {
18
+ console.error(JSON.stringify({
19
+ error: 'Invalid config key',
20
+ code: 'VALIDATION_ERROR',
21
+ details: { valid_keys: [...VALID_KEYS] },
22
+ }));
23
+ process.exit(4);
24
+ }
25
+ await writeRc(key, value);
26
+ console.log(JSON.stringify({ ok: true, key, message: 'Config updated' }));
27
+ const perms = await checkPermissions();
28
+ if (!perms.ok) {
29
+ console.error(`Warning: ~/.bukkurc has permissions ${perms.mode}, expected 0600. Run: chmod 600 ~/.bukkurc`);
30
+ }
31
+ });
32
+ // config show
33
+ configCommand
34
+ .command('show')
35
+ .description('Show resolved configuration')
36
+ .action(async function () {
37
+ const rc = await readRc();
38
+ // Resolve each field with precedence: flags > env > rc
39
+ const parentOpts = this.parent?.parent?.opts() ?? {};
40
+ const config = {};
41
+ // api_token
42
+ if (parentOpts.apiToken) {
43
+ config['api_token'] = { value: maskToken(parentOpts.apiToken), source: 'flags' };
44
+ }
45
+ else if (process.env['BUKKU_API_TOKEN']) {
46
+ config['api_token'] = { value: maskToken(process.env['BUKKU_API_TOKEN']), source: 'env' };
47
+ }
48
+ else if (rc['api_token']) {
49
+ config['api_token'] = { value: maskToken(rc['api_token']), source: 'rc' };
50
+ }
51
+ else {
52
+ config['api_token'] = { value: null, source: 'not set' };
53
+ }
54
+ // company_subdomain
55
+ if (parentOpts.companySubdomain) {
56
+ config['company_subdomain'] = { value: parentOpts.companySubdomain, source: 'flags' };
57
+ }
58
+ else if (process.env['BUKKU_COMPANY_SUBDOMAIN']) {
59
+ config['company_subdomain'] = { value: process.env['BUKKU_COMPANY_SUBDOMAIN'], source: 'env' };
60
+ }
61
+ else if (rc['company_subdomain']) {
62
+ config['company_subdomain'] = { value: rc['company_subdomain'], source: 'rc' };
63
+ }
64
+ else {
65
+ config['company_subdomain'] = { value: null, source: 'not set' };
66
+ }
67
+ console.log(JSON.stringify({ config }));
68
+ });
@@ -0,0 +1,9 @@
1
+ import type { Command } from 'commander';
2
+ /**
3
+ * Register archive and unarchive commands for contacts, products, bundles, accounts, locations.
4
+ *
5
+ * These commands are added as subcommands under existing resource commands
6
+ * created by the factory (registerEntityCommands must run first).
7
+ */
8
+ export declare function registerArchiveCommands(program: Command): void;
9
+ //# sourceMappingURL=archive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../../../src/commands/custom/archive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiCzC;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAoD9D"}
@@ -0,0 +1,82 @@
1
+ import { withAuth } from '../wrapper.js';
2
+ import { outputJson } from '../../output/json.js';
3
+ import { outputDryRun } from '../../output/dry-run.js';
4
+ import { outputError, ExitCode } from '../../output/error.js';
5
+ /**
6
+ * Archive/unarchive configs for entities that support is_archived toggling.
7
+ *
8
+ * Note: locations use singular /location/{id} path (API inconsistency).
9
+ */
10
+ const ARCHIVE_CONFIGS = [
11
+ { group: 'contacts', resource: 'contacts', apiPath: '/contacts', description: 'contact' },
12
+ { group: 'products', resource: 'products', apiPath: '/products', description: 'product' },
13
+ { group: 'products', resource: 'bundles', apiPath: '/products/bundles', description: 'product bundle' },
14
+ { group: 'accounting', resource: 'accounts', apiPath: '/accounts', description: 'account' },
15
+ { group: 'control-panel', resource: 'locations', apiPath: '/location', description: 'location' },
16
+ ];
17
+ /**
18
+ * Parse and validate a positional ID argument.
19
+ */
20
+ function parseId(idArg) {
21
+ const parsed = parseInt(idArg, 10);
22
+ if (isNaN(parsed) || parsed <= 0) {
23
+ outputError({ error: 'ID must be a positive integer', code: 'VALIDATION_ERROR' }, ExitCode.VALIDATION);
24
+ }
25
+ return parsed;
26
+ }
27
+ /**
28
+ * Register archive and unarchive commands for contacts, products, bundles, accounts, locations.
29
+ *
30
+ * These commands are added as subcommands under existing resource commands
31
+ * created by the factory (registerEntityCommands must run first).
32
+ */
33
+ export function registerArchiveCommands(program) {
34
+ for (const config of ARCHIVE_CONFIGS) {
35
+ // Find the group command (already created by factory)
36
+ const groupCmd = program.commands.find((c) => c.name() === config.group);
37
+ if (!groupCmd)
38
+ continue;
39
+ // Find the resource subcommand (already created by factory)
40
+ const resourceCmd = groupCmd.commands.find((c) => c.name() === config.resource);
41
+ if (!resourceCmd)
42
+ continue;
43
+ // Add archive subcommand
44
+ const archiveHandler = withAuth(async ({ client, opts, auth }) => {
45
+ const id = opts._entityId;
46
+ if (opts.dryRun) {
47
+ outputDryRun({ method: 'PATCH', path: `${config.apiPath}/${id}`, token: auth.apiToken, subdomain: auth.companySubdomain, body: { is_archived: true } });
48
+ return;
49
+ }
50
+ const data = await client.patch(`${config.apiPath}/${id}`, { is_archived: true });
51
+ outputJson(data);
52
+ });
53
+ resourceCmd
54
+ .command('archive <id>')
55
+ .description(`Archive a ${config.description}`)
56
+ .option('--dry-run', 'Show request details without executing', false)
57
+ .action(function (idArg, ...rest) {
58
+ const id = parseId(idArg);
59
+ this.setOptionValue('_entityId', id);
60
+ return archiveHandler.call(this, idArg, ...rest);
61
+ });
62
+ // Add unarchive subcommand
63
+ const unarchiveHandler = withAuth(async ({ client, opts, auth }) => {
64
+ const id = opts._entityId;
65
+ if (opts.dryRun) {
66
+ outputDryRun({ method: 'PATCH', path: `${config.apiPath}/${id}`, token: auth.apiToken, subdomain: auth.companySubdomain, body: { is_archived: false } });
67
+ return;
68
+ }
69
+ const data = await client.patch(`${config.apiPath}/${id}`, { is_archived: false });
70
+ outputJson(data);
71
+ });
72
+ resourceCmd
73
+ .command('unarchive <id>')
74
+ .description(`Unarchive a ${config.description}`)
75
+ .option('--dry-run', 'Show request details without executing', false)
76
+ .action(function (idArg, ...rest) {
77
+ const id = parseId(idArg);
78
+ this.setOptionValue('_entityId', id);
79
+ return unarchiveHandler.call(this, idArg, ...rest);
80
+ });
81
+ }
82
+ }
@@ -0,0 +1,9 @@
1
+ import type { Command } from 'commander';
2
+ /**
3
+ * Register the file upload command.
4
+ *
5
+ * Uploads a file to Bukku via multipart/form-data using BukkuClient.postMultipart.
6
+ * Validates the file exists before uploading.
7
+ */
8
+ export declare function registerFileUploadCommand(program: Command): void;
9
+ //# sourceMappingURL=file-upload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-upload.d.ts","sourceRoot":"","sources":["../../../src/commands/custom/file-upload.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQzC;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAyChE"}
@@ -0,0 +1,47 @@
1
+ import { basename } from 'node:path';
2
+ import { access } from 'node:fs/promises';
3
+ import { withAuth } from '../wrapper.js';
4
+ import { outputJson } from '../../output/json.js';
5
+ import { outputDryRun } from '../../output/dry-run.js';
6
+ import { outputError, ExitCode } from '../../output/error.js';
7
+ /**
8
+ * Register the file upload command.
9
+ *
10
+ * Uploads a file to Bukku via multipart/form-data using BukkuClient.postMultipart.
11
+ * Validates the file exists before uploading.
12
+ */
13
+ export function registerFileUploadCommand(program) {
14
+ // Find or create the files group command.
15
+ // The factory creates a 'files' group for the file entity (list only),
16
+ // so it should already exist. If not, create it.
17
+ let groupCmd = program.commands.find((c) => c.name() === 'files');
18
+ if (!groupCmd) {
19
+ groupCmd = program
20
+ .command('files')
21
+ .description('File uploads and attachments');
22
+ }
23
+ const wrappedHandler = withAuth(async ({ client, opts, auth }) => {
24
+ const filePath = opts._filePath;
25
+ // Validate file exists
26
+ try {
27
+ await access(filePath);
28
+ }
29
+ catch {
30
+ outputError({ error: 'File not found: ' + filePath, code: 'VALIDATION_ERROR' }, ExitCode.VALIDATION);
31
+ }
32
+ if (opts.dryRun) {
33
+ outputDryRun({ method: 'POST', path: '/files', token: auth.apiToken, subdomain: auth.companySubdomain, body: { file: basename(filePath) } });
34
+ return;
35
+ }
36
+ const data = await client.postMultipart('/files', filePath);
37
+ outputJson(data);
38
+ });
39
+ groupCmd
40
+ .command('upload <path>')
41
+ .description('Upload a file to Bukku')
42
+ .option('--dry-run', 'Show request details without executing', false)
43
+ .action(function (pathArg, ...rest) {
44
+ this.setOptionValue('_filePath', pathArg);
45
+ return wrappedHandler.call(this, pathArg, ...rest);
46
+ });
47
+ }
@@ -0,0 +1,10 @@
1
+ import type { Command } from 'commander';
2
+ /**
3
+ * Register journal entry create and update commands with double-entry validation.
4
+ *
5
+ * The factory generates list, get, and delete for journal entries.
6
+ * Create and update need custom handling to validate that debits equal credits
7
+ * before submitting to the API.
8
+ */
9
+ export declare function registerJournalEntryCommands(program: Command): void;
10
+ //# sourceMappingURL=journal-entry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journal-entry.d.ts","sourceRoot":"","sources":["../../../src/commands/custom/journal-entry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsBzC;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0EnE"}
@@ -0,0 +1,82 @@
1
+ import { validateDoubleEntry } from 'core';
2
+ import { withAuth } from '../wrapper.js';
3
+ import { outputJson } from '../../output/json.js';
4
+ import { outputDryRun } from '../../output/dry-run.js';
5
+ import { outputError, ExitCode } from '../../output/error.js';
6
+ import { readJsonInput } from '../../input/json.js';
7
+ /**
8
+ * Parse and validate a positional ID argument.
9
+ */
10
+ function parseId(idArg) {
11
+ const parsed = parseInt(idArg, 10);
12
+ if (isNaN(parsed) || parsed <= 0) {
13
+ outputError({ error: 'ID must be a positive integer', code: 'VALIDATION_ERROR' }, ExitCode.VALIDATION);
14
+ }
15
+ return parsed;
16
+ }
17
+ /**
18
+ * Register journal entry create and update commands with double-entry validation.
19
+ *
20
+ * The factory generates list, get, and delete for journal entries.
21
+ * Create and update need custom handling to validate that debits equal credits
22
+ * before submitting to the API.
23
+ */
24
+ export function registerJournalEntryCommands(program) {
25
+ // Find existing accounting group and journal-entries resource
26
+ const groupCmd = program.commands.find((c) => c.name() === 'accounting');
27
+ if (!groupCmd)
28
+ return;
29
+ const resourceCmd = groupCmd.commands.find((c) => c.name() === 'journal-entries');
30
+ if (!resourceCmd)
31
+ return;
32
+ // Add create subcommand
33
+ resourceCmd
34
+ .command('create')
35
+ .description('Create a new journal entry')
36
+ .option('--data <json>', 'JSON data (or pipe to stdin)')
37
+ .option('--dry-run', 'Show request details without executing', false)
38
+ .action(withAuth(async ({ client, opts, auth }) => {
39
+ const body = await readJsonInput(opts);
40
+ // Validate double-entry balance if journal_items present
41
+ if (body.journal_items && Array.isArray(body.journal_items)) {
42
+ const validation = validateDoubleEntry(body.journal_items);
43
+ if (!validation.valid) {
44
+ outputError({ error: validation.error, code: 'VALIDATION_ERROR' }, ExitCode.VALIDATION);
45
+ }
46
+ }
47
+ if (opts.dryRun) {
48
+ outputDryRun({ method: 'POST', path: '/journal_entries', token: auth.apiToken, subdomain: auth.companySubdomain, body });
49
+ return;
50
+ }
51
+ const data = await client.post('/journal_entries', body);
52
+ outputJson(data);
53
+ }));
54
+ // Add update subcommand
55
+ const updateHandler = withAuth(async ({ client, opts, auth }) => {
56
+ const id = opts._entityId;
57
+ const body = await readJsonInput(opts);
58
+ // Validate double-entry balance if journal_items present in update
59
+ if (body.journal_items && Array.isArray(body.journal_items)) {
60
+ const validation = validateDoubleEntry(body.journal_items);
61
+ if (!validation.valid) {
62
+ outputError({ error: validation.error, code: 'VALIDATION_ERROR' }, ExitCode.VALIDATION);
63
+ }
64
+ }
65
+ if (opts.dryRun) {
66
+ outputDryRun({ method: 'PUT', path: `/journal_entries/${id}`, token: auth.apiToken, subdomain: auth.companySubdomain, body });
67
+ return;
68
+ }
69
+ const data = await client.put(`/journal_entries/${id}`, body);
70
+ outputJson(data);
71
+ });
72
+ resourceCmd
73
+ .command('update <id>')
74
+ .description('Update a journal entry')
75
+ .option('--data <json>', 'JSON data (or pipe to stdin)')
76
+ .option('--dry-run', 'Show request details without executing', false)
77
+ .action(function (idArg, ...rest) {
78
+ const id = parseId(idArg);
79
+ this.setOptionValue('_entityId', id);
80
+ return updateHandler.call(this, idArg, ...rest);
81
+ });
82
+ }
@@ -0,0 +1,10 @@
1
+ import type { Command } from 'commander';
2
+ /**
3
+ * Register location get/update/delete commands using singular /location/{id} path.
4
+ *
5
+ * The Bukku API uses /locations (plural) for list/create but /location/{id} (singular)
6
+ * for get/update/delete. The factory only generates list and create for locations,
7
+ * so these custom commands handle the singular-path operations.
8
+ */
9
+ export declare function registerLocationWriteCommands(program: Command): void;
10
+ //# sourceMappingURL=location-write.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"location-write.d.ts","sourceRoot":"","sources":["../../../src/commands/custom/location-write.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsBzC;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA4EpE"}