@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
@@ -0,0 +1,95 @@
1
+ import { withAuth } from '../wrapper.js';
2
+ import { outputJson } from '../../output/json.js';
3
+ import { outputDryRun } from '../../output/dry-run.js';
4
+ import { outputTable } from '../../output/table.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 location get/update/delete commands using singular /location/{id} path.
19
+ *
20
+ * The Bukku API uses /locations (plural) for list/create but /location/{id} (singular)
21
+ * for get/update/delete. The factory only generates list and create for locations,
22
+ * so these custom commands handle the singular-path operations.
23
+ */
24
+ export function registerLocationWriteCommands(program) {
25
+ // Find existing control-panel group and locations resource
26
+ const groupCmd = program.commands.find((c) => c.name() === 'control-panel');
27
+ if (!groupCmd)
28
+ return;
29
+ const resourceCmd = groupCmd.commands.find((c) => c.name() === 'locations');
30
+ if (!resourceCmd)
31
+ return;
32
+ // Add get subcommand
33
+ const getHandler = withAuth(async ({ client, opts }) => {
34
+ const id = opts._entityId;
35
+ const data = await client.get(`/location/${id}`);
36
+ const format = opts.format;
37
+ if (format === 'table') {
38
+ const response = data;
39
+ const item = response['location'];
40
+ outputTable(item ? [item] : [], ['location']);
41
+ }
42
+ else {
43
+ outputJson(data);
44
+ }
45
+ });
46
+ resourceCmd
47
+ .command('get <id>')
48
+ .description('Get a single location by ID')
49
+ .option('--format <format>', 'Output format (json, table)', 'json')
50
+ .action(function (idArg, ...rest) {
51
+ const id = parseId(idArg);
52
+ this.setOptionValue('_entityId', id);
53
+ return getHandler.call(this, idArg, ...rest);
54
+ });
55
+ // Add update subcommand
56
+ const updateHandler = withAuth(async ({ client, opts, auth }) => {
57
+ const id = opts._entityId;
58
+ const body = await readJsonInput(opts);
59
+ if (opts.dryRun) {
60
+ outputDryRun({ method: 'PUT', path: `/location/${id}`, token: auth.apiToken, subdomain: auth.companySubdomain, body });
61
+ return;
62
+ }
63
+ const data = await client.put(`/location/${id}`, body);
64
+ outputJson(data);
65
+ });
66
+ resourceCmd
67
+ .command('update <id>')
68
+ .description('Update a location')
69
+ .option('--data <json>', 'JSON data (or pipe to stdin)')
70
+ .option('--dry-run', 'Show request details without executing', false)
71
+ .action(function (idArg, ...rest) {
72
+ const id = parseId(idArg);
73
+ this.setOptionValue('_entityId', id);
74
+ return updateHandler.call(this, idArg, ...rest);
75
+ });
76
+ // Add delete subcommand
77
+ const deleteHandler = withAuth(async ({ client, opts, auth }) => {
78
+ const id = opts._entityId;
79
+ if (opts.dryRun) {
80
+ outputDryRun({ method: 'DELETE', path: `/location/${id}`, token: auth.apiToken, subdomain: auth.companySubdomain });
81
+ return;
82
+ }
83
+ await client.delete(`/location/${id}`);
84
+ outputJson({});
85
+ });
86
+ resourceCmd
87
+ .command('delete <id>')
88
+ .description('Delete a location')
89
+ .option('--dry-run', 'Show request details without executing', false)
90
+ .action(function (idArg, ...rest) {
91
+ const id = parseId(idArg);
92
+ this.setOptionValue('_entityId', id);
93
+ return deleteHandler.call(this, idArg, ...rest);
94
+ });
95
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Reference data CLI commands.
3
+ *
4
+ * Reference data (tax codes, currencies, payment methods, etc.) is accessed
5
+ * via Bukku's unified POST /v2/lists endpoint, not standard GET CRUD endpoints.
6
+ * These are short-lived CLI commands, so no caching is needed.
7
+ */
8
+ import { Command } from 'commander';
9
+ /**
10
+ * Register all reference data list commands under a `ref-data` top-level group.
11
+ */
12
+ export declare function registerReferenceDataCommands(program: Command): void;
13
+ //# sourceMappingURL=reference-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reference-data.d.ts","sourceRoot":"","sources":["../../../src/commands/custom/reference-data.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA6DpC;;GAEG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA6BpE"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Reference data CLI commands.
3
+ *
4
+ * Reference data (tax codes, currencies, payment methods, etc.) is accessed
5
+ * via Bukku's unified POST /v2/lists endpoint, not standard GET CRUD endpoints.
6
+ * These are short-lived CLI commands, so no caching is needed.
7
+ */
8
+ import { withAuth } from '../wrapper.js';
9
+ import { outputJson } from '../../output/json.js';
10
+ import { outputTable } from '../../output/table.js';
11
+ /**
12
+ * Reference data types available from POST /v2/lists endpoint.
13
+ */
14
+ const REFERENCE_TYPES = [
15
+ {
16
+ type: 'tax_codes',
17
+ commandName: 'tax-codes',
18
+ description: 'List tax codes (tax rate definitions)',
19
+ },
20
+ {
21
+ type: 'currencies',
22
+ commandName: 'currencies',
23
+ description: 'List activated currencies',
24
+ },
25
+ {
26
+ type: 'payment_methods',
27
+ commandName: 'payment-methods',
28
+ description: 'List payment methods',
29
+ },
30
+ {
31
+ type: 'terms',
32
+ commandName: 'terms',
33
+ description: 'List payment terms',
34
+ },
35
+ {
36
+ type: 'accounts',
37
+ commandName: 'accounts',
38
+ description: 'List accounts from chart of accounts (quick lookup)',
39
+ },
40
+ {
41
+ type: 'price_levels',
42
+ commandName: 'price-levels',
43
+ description: 'List price levels',
44
+ },
45
+ {
46
+ type: 'countries',
47
+ commandName: 'countries',
48
+ description: 'List countries',
49
+ },
50
+ {
51
+ type: 'classification_code_list',
52
+ commandName: 'classification-codes',
53
+ description: 'List product classification codes (Malaysia LHDN e-Invoice)',
54
+ },
55
+ {
56
+ type: 'numberings',
57
+ commandName: 'numberings',
58
+ description: 'List document numbering schemes',
59
+ },
60
+ {
61
+ type: 'state_list',
62
+ commandName: 'states',
63
+ description: 'List geographic states/provinces',
64
+ },
65
+ ];
66
+ /**
67
+ * Register all reference data list commands under a `ref-data` top-level group.
68
+ */
69
+ export function registerReferenceDataCommands(program) {
70
+ const refDataCmd = program
71
+ .command('ref-data')
72
+ .description('Reference data lookups (tax codes, currencies, terms, etc.)');
73
+ for (const { type, commandName, description } of REFERENCE_TYPES) {
74
+ refDataCmd
75
+ .command(commandName)
76
+ .description(description)
77
+ .option('--format <format>', 'Output format (json, table)', 'json')
78
+ .action(withAuth(async ({ client, opts }) => {
79
+ const result = await client.post('/v2/lists', {
80
+ lists: [type],
81
+ params: [],
82
+ });
83
+ const format = opts.format;
84
+ if (format === 'table') {
85
+ // Reference data results are keyed by type name
86
+ const response = result;
87
+ const items = response[type] || [];
88
+ outputTable(items);
89
+ }
90
+ else {
91
+ outputJson(result);
92
+ }
93
+ }));
94
+ }
95
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Search accounts custom command.
3
+ *
4
+ * Provides filtered search of the chart of accounts via GET /accounts.
5
+ * Registered under the `accounting` group command.
6
+ */
7
+ import type { Command } from 'commander';
8
+ /**
9
+ * Register the search-accounts command under the existing `accounting` group.
10
+ */
11
+ export declare function registerSearchAccountsCommand(program: Command): void;
12
+ //# sourceMappingURL=search-accounts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-accounts.d.ts","sourceRoot":"","sources":["../../../src/commands/custom/search-accounts.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKzC;;GAEG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA4CpE"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Search accounts custom command.
3
+ *
4
+ * Provides filtered search of the chart of accounts via GET /accounts.
5
+ * Registered under the `accounting` group command.
6
+ */
7
+ import { withAuth } from '../wrapper.js';
8
+ import { outputJson } from '../../output/json.js';
9
+ import { outputTable } from '../../output/table.js';
10
+ /**
11
+ * Register the search-accounts command under the existing `accounting` group.
12
+ */
13
+ export function registerSearchAccountsCommand(program) {
14
+ // Find the accounting group (created by registerEntityCommands)
15
+ let accountingCmd = program.commands.find((c) => c.name() === 'accounting');
16
+ if (!accountingCmd) {
17
+ accountingCmd = program
18
+ .command('accounting')
19
+ .description('Chart of accounts and journal entries');
20
+ }
21
+ accountingCmd
22
+ .command('search-accounts')
23
+ .description('Search and filter accounts from the chart of accounts')
24
+ .option('--search <text>', 'Search by name, code, or description')
25
+ .option('--category <cat>', 'Filter by category (assets, liabilities, equity, income, expenses)')
26
+ .option('--is-archived', 'Include archived accounts', false)
27
+ .option('--sort-by <field>', 'Sort by field (code, name, balance)')
28
+ .option('--sort-dir <dir>', 'Sort direction (asc, desc)')
29
+ .option('--page <n>', 'Page number')
30
+ .option('--limit <n>', 'Items per page')
31
+ .option('--format <format>', 'Output format (json, table)', 'json')
32
+ .action(withAuth(async ({ client, opts }) => {
33
+ const params = {};
34
+ if (opts.search != null)
35
+ params.search = opts.search;
36
+ if (opts.category != null)
37
+ params.category = opts.category;
38
+ if (opts.isArchived)
39
+ params.is_archived = 'true';
40
+ if (opts.sortBy != null)
41
+ params.sort_by = opts.sortBy;
42
+ if (opts.sortDir != null)
43
+ params.sort_dir = opts.sortDir;
44
+ if (opts.page != null)
45
+ params.page = Number(opts.page);
46
+ if (opts.limit != null)
47
+ params.page_size = Number(opts.limit);
48
+ const result = await client.get('/accounts', params);
49
+ const format = opts.format;
50
+ if (format === 'table') {
51
+ const response = result;
52
+ const items = response['accounts'] || [];
53
+ outputTable(items, ['account']);
54
+ }
55
+ else {
56
+ outputJson(result);
57
+ }
58
+ }));
59
+ }
@@ -0,0 +1,9 @@
1
+ import { Command } from 'commander';
2
+ /**
3
+ * Register all entity CRUD commands on the Commander program.
4
+ *
5
+ * Iterates allEntityConfigs from core, creates group and resource subcommands,
6
+ * and adds list/get subcommands based on each entity's supported operations.
7
+ */
8
+ export declare function registerEntityCommands(program: Command): void;
9
+ //# sourceMappingURL=factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/commands/factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA6WpC;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmD7D"}
@@ -0,0 +1,371 @@
1
+ import { allEntityConfigs } from 'core';
2
+ import { withAuth } from './wrapper.js';
3
+ import { outputJson } from '../output/json.js';
4
+ import { outputTable } from '../output/table.js';
5
+ import { readJsonInput } from '../input/json.js';
6
+ import { outputDryRun } from '../output/dry-run.js';
7
+ /**
8
+ * Map entity name to CLI resource subcommand name.
9
+ * Explicit mapping is more maintainable than prefix-stripping heuristics.
10
+ */
11
+ const RESOURCE_NAME_MAP = {
12
+ 'sales-invoice': 'invoices',
13
+ 'sales-quote': 'quotes',
14
+ 'sales-order': 'orders',
15
+ 'sales-credit-note': 'credit-notes',
16
+ 'sales-payment': 'payments',
17
+ 'sales-refund': 'refunds',
18
+ 'delivery-order': 'delivery-orders',
19
+ 'purchase-bill': 'bills',
20
+ 'purchase-order': 'orders',
21
+ 'purchase-credit-note': 'credit-notes',
22
+ 'purchase-payment': 'payments',
23
+ 'purchase-refund': 'refunds',
24
+ 'goods-received-note': 'goods-received-notes',
25
+ 'bank-money-in': 'money-in',
26
+ 'bank-money-out': 'money-out',
27
+ 'bank-transfer': 'transfers',
28
+ 'contact': 'contacts',
29
+ 'contact-group': 'groups',
30
+ 'product': 'products',
31
+ 'product-bundle': 'bundles',
32
+ 'product-group': 'groups',
33
+ 'journal-entry': 'journal-entries',
34
+ 'account': 'accounts',
35
+ 'file': 'files',
36
+ 'location': 'locations',
37
+ 'tag': 'tags',
38
+ 'tag-group': 'tag-groups',
39
+ };
40
+ /**
41
+ * Group descriptions for top-level CLI command groups.
42
+ */
43
+ const GROUP_DESCRIPTIONS = {
44
+ sales: 'Sales invoices, quotes, orders, credit notes, payments, refunds',
45
+ purchases: 'Purchase bills, orders, credit notes, payments, refunds',
46
+ banking: 'Bank transactions and transfers',
47
+ contacts: 'Customers, suppliers, and contacts',
48
+ products: 'Products, bundles, and product groups',
49
+ accounting: 'Chart of accounts and journal entries',
50
+ files: 'File uploads and attachments',
51
+ 'control-panel': 'Locations, tags, and tag groups',
52
+ };
53
+ /**
54
+ * Simple pluralization for entity descriptions.
55
+ * Handles common cases: "entry" -> "entries", "note" -> "notes".
56
+ */
57
+ function pluralize(desc) {
58
+ if (desc.endsWith('y') && !desc.endsWith('ay') && !desc.endsWith('ey') && !desc.endsWith('oy') && !desc.endsWith('uy')) {
59
+ return desc.slice(0, -1) + 'ies';
60
+ }
61
+ if (desc.endsWith('s') || desc.endsWith('x') || desc.endsWith('z') || desc.endsWith('ch') || desc.endsWith('sh')) {
62
+ return desc + 'es';
63
+ }
64
+ return desc + 's';
65
+ }
66
+ /**
67
+ * Convert a flag name with hyphens to underscore parameter name.
68
+ * e.g., "contact-id" -> "contact_id"
69
+ */
70
+ function toParamName(flag) {
71
+ return flag.replace(/-/g, '_');
72
+ }
73
+ /**
74
+ * Convert an underscore parameter name to a hyphenated flag name.
75
+ * e.g., "contact_id" -> "contact-id"
76
+ */
77
+ function toFlagName(param) {
78
+ return param.replace(/_/g, '-');
79
+ }
80
+ /**
81
+ * Add the `list` subcommand to a resource command.
82
+ */
83
+ function addListCommand(resourceCmd, config) {
84
+ const listCmd = resourceCmd
85
+ .command('list')
86
+ .description(`List ${pluralize(config.description)}`);
87
+ // Standard options
88
+ listCmd
89
+ .option('--limit <n>', 'Maximum items per page', undefined)
90
+ .option('--page <n>', 'Page number', undefined)
91
+ .option('--search <text>', 'Search text', undefined)
92
+ .option('--date-from <date>', 'Filter from date (YYYY-MM-DD)', undefined)
93
+ .option('--date-to <date>', 'Filter to date (YYYY-MM-DD)', undefined)
94
+ .option('--status <status>', 'Filter by status', undefined)
95
+ .option('--sort-by <field>', 'Sort by field', undefined)
96
+ .option('--sort-dir <dir>', 'Sort direction (asc, desc)', undefined)
97
+ .option('--all', 'Fetch all pages', false)
98
+ .option('--format <format>', 'Output format (json, table)', 'json');
99
+ // Entity-specific filter options
100
+ if (config.listFilters) {
101
+ for (const filter of config.listFilters) {
102
+ const flagName = toFlagName(filter);
103
+ // Skip filters that duplicate standard options
104
+ if (['search', 'status'].includes(filter))
105
+ continue;
106
+ listCmd.option(`--${flagName} <value>`, `Filter by ${filter}`, undefined);
107
+ }
108
+ }
109
+ listCmd.action(withAuth(async ({ client, opts }) => {
110
+ // Build query params
111
+ const params = {};
112
+ if (opts.limit != null)
113
+ params.page_size = Number(opts.limit);
114
+ if (opts.page != null)
115
+ params.page = Number(opts.page);
116
+ if (opts.search != null)
117
+ params.search = opts.search;
118
+ if (opts.dateFrom != null)
119
+ params.date_from = opts.dateFrom;
120
+ if (opts.dateTo != null)
121
+ params.date_to = opts.dateTo;
122
+ if (opts.status != null)
123
+ params.status = opts.status;
124
+ if (opts.sortBy != null)
125
+ params.sort_by = opts.sortBy;
126
+ if (opts.sortDir != null)
127
+ params.sort_dir = opts.sortDir;
128
+ // Entity-specific filters
129
+ if (config.listFilters) {
130
+ for (const filter of config.listFilters) {
131
+ if (['search', 'status'].includes(filter))
132
+ continue;
133
+ // Commander camelCases the flag: "contact-id" -> "contactId"
134
+ const camelKey = toFlagName(filter)
135
+ .replace(/-([a-z])/g, (_, c) => c.toUpperCase());
136
+ if (opts[camelKey] != null) {
137
+ params[filter] = opts[camelKey];
138
+ }
139
+ }
140
+ }
141
+ const format = opts.format;
142
+ if (opts.all) {
143
+ // Auto-pagination: fetch all pages
144
+ const pageSize = params.page_size ?? 100;
145
+ params.page_size = pageSize;
146
+ params.page = 1;
147
+ const firstResponse = await client.get(config.apiBasePath, params);
148
+ const paging = firstResponse.paging;
149
+ const allItems = [...(firstResponse[config.pluralKey] || [])];
150
+ const totalPages = Math.ceil(paging.total / paging.per_page);
151
+ if (totalPages > 1) {
152
+ process.stderr.write(`Fetching page 1/${totalPages}...\n`);
153
+ }
154
+ for (let page = 2; page <= totalPages; page++) {
155
+ process.stderr.write(`Fetching page ${page}/${totalPages}...\n`);
156
+ params.page = page;
157
+ const response = await client.get(config.apiBasePath, params);
158
+ const items = response[config.pluralKey] || [];
159
+ allItems.push(...items);
160
+ }
161
+ if (format === 'table') {
162
+ outputTable(allItems, [config.entity]);
163
+ }
164
+ else {
165
+ outputJson(allItems);
166
+ }
167
+ }
168
+ else {
169
+ const data = await client.get(config.apiBasePath, params);
170
+ if (format === 'table') {
171
+ const response = data;
172
+ const items = response[config.pluralKey] || [];
173
+ outputTable(items, [config.entity]);
174
+ }
175
+ else {
176
+ outputJson(data);
177
+ }
178
+ }
179
+ }));
180
+ }
181
+ /**
182
+ * Add the `get` subcommand to a resource command.
183
+ */
184
+ function addGetCommand(resourceCmd, config) {
185
+ const getCmd = resourceCmd
186
+ .command('get <id>')
187
+ .description(`Get a single ${config.description} by ID`);
188
+ getCmd.option('--format <format>', 'Output format (json, table)', 'json');
189
+ // Commander passes (id, options, command) to action.
190
+ // withAuth ignores positional args, so we wrap with a thin function
191
+ // that captures the id and stores it before delegating to withAuth.
192
+ const wrappedHandler = withAuth(async ({ client, opts }) => {
193
+ const parsedId = opts._entityId;
194
+ const data = await client.get(`${config.apiBasePath}/${parsedId}`);
195
+ const format = opts.format;
196
+ if (format === 'table') {
197
+ const response = data;
198
+ const item = response[config.singularKey];
199
+ outputTable(item ? [item] : [], [config.entity]);
200
+ }
201
+ else {
202
+ outputJson(data);
203
+ }
204
+ });
205
+ getCmd.action(function (idArg, ...rest) {
206
+ const parsedId = parseInt(idArg, 10);
207
+ if (isNaN(parsedId) || parsedId <= 0) {
208
+ process.stderr.write(JSON.stringify({ error: 'ID must be a positive integer', code: 'VALIDATION_ERROR' }) + '\n');
209
+ process.exit(4);
210
+ }
211
+ // Stash parsed id into the command's opts so withAuth handler can access it
212
+ this.setOptionValue('_entityId', parsedId);
213
+ return wrappedHandler.call(this, idArg, ...rest);
214
+ });
215
+ }
216
+ /**
217
+ * Add the `create` subcommand to a resource command.
218
+ */
219
+ function addCreateCommand(resourceCmd, config) {
220
+ const createCmd = resourceCmd
221
+ .command('create')
222
+ .description(`Create a new ${config.description}`)
223
+ .option('--data <json>', 'JSON data (or pipe to stdin)')
224
+ .option('--dry-run', 'Show request details without executing', false);
225
+ createCmd.action(withAuth(async ({ client, opts, auth }) => {
226
+ const body = await readJsonInput(opts);
227
+ if (opts.dryRun) {
228
+ outputDryRun({ method: 'POST', path: config.apiBasePath, token: auth.apiToken, subdomain: auth.companySubdomain, body });
229
+ return;
230
+ }
231
+ const data = await client.post(config.apiBasePath, body);
232
+ outputJson(data);
233
+ }));
234
+ }
235
+ /**
236
+ * Add the `update` subcommand to a resource command.
237
+ */
238
+ function addUpdateCommand(resourceCmd, config) {
239
+ const updateCmd = resourceCmd
240
+ .command('update <id>')
241
+ .description(`Update a ${config.description}`)
242
+ .option('--data <json>', 'JSON data (or pipe to stdin)')
243
+ .option('--dry-run', 'Show request details without executing', false);
244
+ const wrappedHandler = withAuth(async ({ client, opts, auth }) => {
245
+ const parsedId = opts._entityId;
246
+ const body = await readJsonInput(opts);
247
+ if (opts.dryRun) {
248
+ outputDryRun({ method: 'PUT', path: `${config.apiBasePath}/${parsedId}`, token: auth.apiToken, subdomain: auth.companySubdomain, body });
249
+ return;
250
+ }
251
+ const data = await client.put(`${config.apiBasePath}/${parsedId}`, body);
252
+ outputJson(data);
253
+ });
254
+ updateCmd.action(function (idArg, ...rest) {
255
+ const parsedId = parseInt(idArg, 10);
256
+ if (isNaN(parsedId) || parsedId <= 0) {
257
+ process.stderr.write(JSON.stringify({ error: 'ID must be a positive integer', code: 'VALIDATION_ERROR' }) + '\n');
258
+ process.exit(4);
259
+ }
260
+ this.setOptionValue('_entityId', parsedId);
261
+ return wrappedHandler.call(this, idArg, ...rest);
262
+ });
263
+ }
264
+ /**
265
+ * Add the `delete` subcommand to a resource command.
266
+ */
267
+ function addDeleteCommand(resourceCmd, config) {
268
+ const deleteCmd = resourceCmd
269
+ .command('delete <id>')
270
+ .description(`Delete a ${config.description}`)
271
+ .option('--dry-run', 'Show request details without executing', false);
272
+ const wrappedHandler = withAuth(async ({ client, opts, auth }) => {
273
+ const parsedId = opts._entityId;
274
+ if (opts.dryRun) {
275
+ outputDryRun({ method: 'DELETE', path: `${config.apiBasePath}/${parsedId}`, token: auth.apiToken, subdomain: auth.companySubdomain });
276
+ return;
277
+ }
278
+ await client.delete(`${config.apiBasePath}/${parsedId}`);
279
+ outputJson({});
280
+ });
281
+ deleteCmd.action(function (idArg, ...rest) {
282
+ const parsedId = parseInt(idArg, 10);
283
+ if (isNaN(parsedId) || parsedId <= 0) {
284
+ process.stderr.write(JSON.stringify({ error: 'ID must be a positive integer', code: 'VALIDATION_ERROR' }) + '\n');
285
+ process.exit(4);
286
+ }
287
+ this.setOptionValue('_entityId', parsedId);
288
+ return wrappedHandler.call(this, idArg, ...rest);
289
+ });
290
+ }
291
+ /**
292
+ * Add the `status` subcommand to a resource command.
293
+ * Used for entities that support status transitions (e.g., draft -> posted).
294
+ */
295
+ function addStatusCommand(resourceCmd, config) {
296
+ const statusCmd = resourceCmd
297
+ .command('status <id>')
298
+ .description(`Update status of a ${config.description}`)
299
+ .requiredOption('--status <status>', 'New status value')
300
+ .option('--dry-run', 'Show request details without executing', false);
301
+ const wrappedHandler = withAuth(async ({ client, opts, auth }) => {
302
+ const parsedId = opts._entityId;
303
+ const status = opts.status;
304
+ if (opts.dryRun) {
305
+ outputDryRun({ method: 'PATCH', path: `${config.apiBasePath}/${parsedId}`, token: auth.apiToken, subdomain: auth.companySubdomain, body: { status } });
306
+ return;
307
+ }
308
+ const data = await client.patch(`${config.apiBasePath}/${parsedId}`, { status });
309
+ outputJson(data);
310
+ });
311
+ statusCmd.action(function (idArg, ...rest) {
312
+ const parsedId = parseInt(idArg, 10);
313
+ if (isNaN(parsedId) || parsedId <= 0) {
314
+ process.stderr.write(JSON.stringify({ error: 'ID must be a positive integer', code: 'VALIDATION_ERROR' }) + '\n');
315
+ process.exit(4);
316
+ }
317
+ this.setOptionValue('_entityId', parsedId);
318
+ return wrappedHandler.call(this, idArg, ...rest);
319
+ });
320
+ }
321
+ /**
322
+ * Register all entity CRUD commands on the Commander program.
323
+ *
324
+ * Iterates allEntityConfigs from core, creates group and resource subcommands,
325
+ * and adds list/get subcommands based on each entity's supported operations.
326
+ */
327
+ export function registerEntityCommands(program) {
328
+ for (const config of allEntityConfigs) {
329
+ if (!config.cliGroup)
330
+ continue;
331
+ const groupName = config.cliGroup;
332
+ const resourceName = RESOURCE_NAME_MAP[config.entity];
333
+ if (!resourceName)
334
+ continue;
335
+ // Find or create the group command
336
+ let groupCmd = program.commands.find((c) => c.name() === groupName);
337
+ if (!groupCmd) {
338
+ groupCmd = program
339
+ .command(groupName)
340
+ .description(GROUP_DESCRIPTIONS[groupName] || groupName);
341
+ }
342
+ // Create resource subcommand under the group
343
+ const resourceCmd = groupCmd
344
+ .command(resourceName)
345
+ .description(`Manage ${pluralize(config.description)}`);
346
+ // Add list subcommand if supported
347
+ if (config.operations.includes('list')) {
348
+ addListCommand(resourceCmd, config);
349
+ }
350
+ // Add get subcommand if supported
351
+ if (config.operations.includes('get')) {
352
+ addGetCommand(resourceCmd, config);
353
+ }
354
+ // Add create subcommand if supported
355
+ if (config.operations.includes('create')) {
356
+ addCreateCommand(resourceCmd, config);
357
+ }
358
+ // Add update subcommand if supported
359
+ if (config.operations.includes('update')) {
360
+ addUpdateCommand(resourceCmd, config);
361
+ }
362
+ // Add delete subcommand if supported
363
+ if (config.operations.includes('delete')) {
364
+ addDeleteCommand(resourceCmd, config);
365
+ }
366
+ // Add status subcommand if entity supports status updates
367
+ if (config.hasStatusUpdate) {
368
+ addStatusCommand(resourceCmd, config);
369
+ }
370
+ }
371
+ }