@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.
- package/README.md +266 -0
- package/build/commands/config.d.ts +3 -0
- package/build/commands/config.d.ts.map +1 -0
- package/build/commands/config.js +68 -0
- package/build/commands/custom/archive.d.ts +9 -0
- package/build/commands/custom/archive.d.ts.map +1 -0
- package/build/commands/custom/archive.js +82 -0
- package/build/commands/custom/file-upload.d.ts +9 -0
- package/build/commands/custom/file-upload.d.ts.map +1 -0
- package/build/commands/custom/file-upload.js +47 -0
- package/build/commands/custom/journal-entry.d.ts +10 -0
- package/build/commands/custom/journal-entry.d.ts.map +1 -0
- package/build/commands/custom/journal-entry.js +82 -0
- package/build/commands/custom/location-write.d.ts +10 -0
- package/build/commands/custom/location-write.d.ts.map +1 -0
- package/build/commands/custom/location-write.js +95 -0
- package/build/commands/custom/reference-data.d.ts +13 -0
- package/build/commands/custom/reference-data.d.ts.map +1 -0
- package/build/commands/custom/reference-data.js +95 -0
- package/build/commands/custom/search-accounts.d.ts +12 -0
- package/build/commands/custom/search-accounts.d.ts.map +1 -0
- package/build/commands/custom/search-accounts.js +59 -0
- package/build/commands/factory.d.ts +9 -0
- package/build/commands/factory.d.ts.map +1 -0
- package/build/commands/factory.js +371 -0
- package/build/commands/wrapper.d.ts +23 -0
- package/build/commands/wrapper.d.ts.map +1 -0
- package/build/commands/wrapper.js +60 -0
- package/build/config/auth.d.ts +23 -0
- package/build/config/auth.d.ts.map +1 -0
- package/build/config/auth.js +69 -0
- package/build/config/rc.d.ts +20 -0
- package/build/config/rc.d.ts.map +1 -0
- package/build/config/rc.js +65 -0
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +1685 -0
- package/build/input/json.d.ts +10 -0
- package/build/input/json.d.ts.map +1 -0
- package/build/input/json.js +42 -0
- package/build/output/dry-run.d.ts +21 -0
- package/build/output/dry-run.d.ts.map +1 -0
- package/build/output/dry-run.js +23 -0
- package/build/output/error.d.ts +27 -0
- package/build/output/error.d.ts.map +1 -0
- package/build/output/error.js +33 -0
- package/build/output/json.d.ts +6 -0
- package/build/output/json.d.ts.map +1 -0
- package/build/output/json.js +7 -0
- package/build/output/table.d.ts +13 -0
- package/build/output/table.d.ts.map +1 -0
- package/build/output/table.js +123 -0
- 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
|
+
}
|