@aderis-erp/cli 1.1.3 → 1.1.5

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 (99) hide show
  1. package/README.md +1 -0
  2. package/dist/commands/access-groups.js +42 -8
  3. package/dist/commands/account-transaction-allocations.js +38 -4
  4. package/dist/commands/account-transactions.d.ts +2 -0
  5. package/dist/commands/account-transactions.js +68 -32
  6. package/dist/commands/accounts.js +46 -12
  7. package/dist/commands/agent-platform.js +37 -3
  8. package/dist/commands/agent-runs.js +49 -15
  9. package/dist/commands/analytics.js +39 -5
  10. package/dist/commands/api-clients.d.ts +58 -0
  11. package/dist/commands/api-clients.js +1375 -0
  12. package/dist/commands/api-keys.js +40 -6
  13. package/dist/commands/async-jobs.js +45 -11
  14. package/dist/commands/audit-log.js +38 -4
  15. package/dist/commands/bank-accounts.js +54 -20
  16. package/dist/commands/bank-integrations.js +56 -22
  17. package/dist/commands/business-partners.js +77 -43
  18. package/dist/commands/chat.js +43 -9
  19. package/dist/commands/credit-card-accounts.d.ts +4 -0
  20. package/dist/commands/credit-card-accounts.js +64 -25
  21. package/dist/commands/currencies.d.ts +56 -0
  22. package/dist/commands/currencies.js +1361 -0
  23. package/dist/commands/custody-accounts.d.ts +56 -0
  24. package/dist/commands/custody-accounts.js +1369 -0
  25. package/dist/commands/{integration-identities.d.ts → dashboards.d.ts} +3 -3
  26. package/dist/commands/dashboards.js +1381 -0
  27. package/dist/commands/delivery-notes.js +69 -35
  28. package/dist/commands/document-checksums.d.ts +57 -0
  29. package/dist/commands/{integration-identities.js → document-checksums.js} +48 -21
  30. package/dist/commands/dunning-configs.js +41 -7
  31. package/dist/commands/dunning-reminders.js +44 -10
  32. package/dist/commands/dunning-runs.js +37 -3
  33. package/dist/commands/email-transactions.js +39 -5
  34. package/dist/commands/employees.js +77 -43
  35. package/dist/commands/entity-configurations.js +39 -5
  36. package/dist/commands/event-mapping-rules.js +37 -3
  37. package/dist/commands/event-processor-mappings.js +41 -7
  38. package/dist/commands/files.js +48 -14
  39. package/dist/commands/financial-instrument-transactions.d.ts +56 -0
  40. package/dist/commands/financial-instrument-transactions.js +1369 -0
  41. package/dist/commands/financial-instrument-types.d.ts +56 -0
  42. package/dist/commands/financial-instrument-types.js +1369 -0
  43. package/dist/commands/financial-instruments.d.ts +56 -0
  44. package/dist/commands/financial-instruments.js +1369 -0
  45. package/dist/commands/health.js +41 -7
  46. package/dist/commands/imports.js +39 -5
  47. package/dist/commands/inbox.js +42 -8
  48. package/dist/commands/integrations.js +39 -5
  49. package/dist/commands/item-categories.js +41 -7
  50. package/dist/commands/items.js +42 -8
  51. package/dist/commands/journal-lines.js +37 -3
  52. package/dist/commands/journals.js +61 -27
  53. package/dist/commands/{workflows.d.ts → kpis.d.ts} +1 -1
  54. package/dist/commands/kpis.js +1371 -0
  55. package/dist/commands/ledgers.js +83 -49
  56. package/dist/commands/my-account.js +54 -20
  57. package/dist/commands/notifications.d.ts +56 -0
  58. package/dist/commands/notifications.js +1360 -0
  59. package/dist/commands/office-locations.js +41 -7
  60. package/dist/commands/payment-methods.js +46 -12
  61. package/dist/commands/payment-runs.js +54 -20
  62. package/dist/commands/payment-terms.js +42 -8
  63. package/dist/commands/payments.js +43 -9
  64. package/dist/commands/payroll-runs.js +71 -37
  65. package/dist/commands/permissions.js +37 -3
  66. package/dist/commands/price-lists.js +48 -14
  67. package/dist/commands/processor-mappings.js +40 -6
  68. package/dist/commands/processors.js +46 -12
  69. package/dist/commands/purchase-order-types.js +41 -7
  70. package/dist/commands/purchase-orders.js +60 -26
  71. package/dist/commands/reports.js +43 -8
  72. package/dist/commands/roles.js +40 -6
  73. package/dist/commands/salary-types.js +41 -7
  74. package/dist/commands/sales-invoices.js +85 -51
  75. package/dist/commands/sales-orders.js +69 -35
  76. package/dist/commands/saved-views.js +39 -5
  77. package/dist/commands/scheduled-processor-mappings.js +41 -7
  78. package/dist/commands/schedules.js +45 -11
  79. package/dist/commands/settings.d.ts +56 -0
  80. package/dist/commands/settings.js +1361 -0
  81. package/dist/commands/tags.js +42 -8
  82. package/dist/commands/tax-codes.js +44 -10
  83. package/dist/commands/tax-matrix.js +41 -7
  84. package/dist/commands/templates.js +50 -15
  85. package/dist/commands/tenants.js +44 -10
  86. package/dist/commands/tools.js +38 -4
  87. package/dist/commands/users.js +58 -24
  88. package/dist/commands/vat-declarations.js +61 -27
  89. package/dist/commands/vendor-invoices.js +82 -47
  90. package/dist/commands/webhooks.js +43 -9
  91. package/dist/commands/work-summary.d.ts +56 -0
  92. package/dist/commands/{workflows.js → work-summary.js} +49 -18
  93. package/dist/lib/auth/index.js +5 -0
  94. package/dist/lib/paths.d.ts +8 -0
  95. package/dist/lib/paths.js +16 -0
  96. package/dist/lib/production-guard.d.ts +42 -0
  97. package/dist/lib/production-guard.js +94 -0
  98. package/oclif.manifest.json +2142 -333
  99. package/package.json +9 -3
@@ -1,6 +1,7 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
+ import { resolveUserPath } from '../lib/paths.js';
4
5
  // 'upsert' loops create/update from the CLI, one request per record. It exists
5
6
  // only where the API has no import endpoint of its own; those keep the name
6
7
  // 'import' and are listed in SERVER_IMPORTS.
@@ -11,10 +12,10 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["list", "create", "api-keys-rotate", "delete"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/auth/api-keys', summary: 'List the tenant\'s API keys', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create': { method: 'POST', path: '/v1/auth/api-keys', summary: 'Create an API key for the tenant', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['permissions'] },
16
- 'api-keys-rotate': { method: 'POST', path: '/v1/auth/api-keys/{apiKeyId}/rotate', summary: 'Rotate an API key', pathParams: ['apiKeyId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'delete': { method: 'DELETE', path: '/v1/auth/api-keys/{apiKeyId}', summary: 'Revoke an API key', pathParams: ['apiKeyId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/auth/api-keys', summary: 'List the tenant\'s API keys', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create': { method: 'POST', path: '/v1/auth/api-keys', summary: 'Create an API key for the tenant', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['permissions'], bodyMediaType: '', specParams: [] },
17
+ 'api-keys-rotate': { method: 'POST', path: '/v1/auth/api-keys/{apiKeyId}/rotate', summary: 'Rotate an API key', pathParams: ['apiKeyId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'delete': { method: 'DELETE', path: '/v1/auth/api-keys/{apiKeyId}', summary: 'Revoke an API key', pathParams: ['apiKeyId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
19
  };
19
20
  export default class ApiKeys extends Command {
20
21
  static description = 'Manage api keys';
@@ -33,7 +34,7 @@ export default class ApiKeys extends Command {
33
34
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
34
35
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
35
36
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
36
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
37
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
37
38
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
38
39
  page: Flags.string({ description: 'Page number' }),
39
40
  limit: Flags.string({ description: 'Items per page' }),
@@ -164,6 +165,17 @@ export default class ApiKeys extends Command {
164
165
  }
165
166
  }
166
167
  }
168
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
169
+ // have them: their body is not JSON, so --set cannot carry a value there.
170
+ for (const param of endpoint.specParams) {
171
+ const value = flags[param.flag];
172
+ if (value === undefined || value === '') {
173
+ if (param.required)
174
+ this.error(`Missing required flag: --${param.flag}`);
175
+ continue;
176
+ }
177
+ queryParams[param.name] = value;
178
+ }
167
179
  let data;
168
180
  if (endpoint.isMultipart) {
169
181
  if (!flags.file)
@@ -184,6 +196,25 @@ export default class ApiKeys extends Command {
184
196
  }
185
197
  data = form;
186
198
  }
199
+ else if (endpoint.bodyMediaType) {
200
+ // The body is not JSON (today only application/x-ndjson): it is sent
201
+ // verbatim, under the content type the endpoint asks for.
202
+ if (flags.file) {
203
+ const fsModule = await import('node:fs');
204
+ try {
205
+ data = fsModule.readFileSync(flags.file, 'utf-8');
206
+ }
207
+ catch {
208
+ this.error(`Failed to read file: ${flags.file}`);
209
+ }
210
+ }
211
+ else if (flags.data) {
212
+ data = flags.data;
213
+ }
214
+ else {
215
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
216
+ }
217
+ }
187
218
  else if (endpoint.hasBody) {
188
219
  // For binary-response endpoints, --file is the output path, not a JSON
189
220
  // input file — skip the read-as-body branch.
@@ -279,7 +310,7 @@ export default class ApiKeys extends Command {
279
310
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
280
311
  }
281
312
  else if (data) {
282
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
313
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
283
314
  }
284
315
  process.stderr.write('\n');
285
316
  }
@@ -293,6 +324,7 @@ export default class ApiKeys extends Command {
293
324
  const binResponse = await apiRequest(endpoint.method, apiPath, {
294
325
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
295
326
  data,
327
+ contentType: endpoint.bodyMediaType || undefined,
296
328
  contextName: flags.context,
297
329
  binary: true,
298
330
  });
@@ -318,6 +350,7 @@ export default class ApiKeys extends Command {
318
350
  const raw = await apiRequest(endpoint.method, apiPath, {
319
351
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
320
352
  data,
353
+ contentType: endpoint.bodyMediaType || undefined,
321
354
  contextName: flags.context,
322
355
  binary: true,
323
356
  });
@@ -338,6 +371,7 @@ export default class ApiKeys extends Command {
338
371
  response = await apiRequest(endpoint.method, apiPath, {
339
372
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
340
373
  data,
374
+ contentType: endpoint.bodyMediaType || undefined,
341
375
  contextName: flags.context,
342
376
  acceptAny: true,
343
377
  });
@@ -1,6 +1,7 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
+ import { resolveUserPath } from '../lib/paths.js';
4
5
  // 'upsert' loops create/update from the CLI, one request per record. It exists
5
6
  // only where the API has no import endpoint of its own; those keep the name
6
7
  // 'import' and are listed in SERVER_IMPORTS.
@@ -11,15 +12,15 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "submit", "list-attempts", "list-items", "payload"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/async-jobs', summary: 'List the jobs the caller may see', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create': { method: 'POST', path: '/v1/async-jobs', summary: 'Create a draft job', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
- 'get': { method: 'GET', path: '/v1/async-jobs/{id}', summary: 'Read one job back', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'update': { method: 'PATCH', path: '/v1/async-jobs/{id}', summary: 'Replace the input of a draft', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
- 'delete': { method: 'DELETE', path: '/v1/async-jobs/{id}', summary: 'Throw a job away', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
- 'submit': { method: 'POST', path: '/v1/async-jobs/{id}/submit', summary: 'Ask for the work of a draft', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
- 'list-attempts': { method: 'GET', path: '/v1/async-jobs/{id}/attempts', summary: 'List the runs of one job', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
- 'list-items': { method: 'GET', path: '/v1/async-jobs/{id}/items', summary: 'List the records of one job and how the work on each ended', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
22
- 'payload': { method: 'GET', path: '/v1/async-jobs/{id}/payload', summary: 'Read the input a job holds', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/async-jobs', summary: 'List the jobs the caller may see', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create': { method: 'POST', path: '/v1/async-jobs', summary: 'Create a draft job', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
17
+ 'get': { method: 'GET', path: '/v1/async-jobs/{id}', summary: 'Read one job back', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'update': { method: 'PATCH', path: '/v1/async-jobs/{id}', summary: 'Replace the input of a draft', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
+ 'delete': { method: 'DELETE', path: '/v1/async-jobs/{id}', summary: 'Throw a job away', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
20
+ 'submit': { method: 'POST', path: '/v1/async-jobs/{id}/submit', summary: 'Ask for the work of a draft', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
21
+ 'list-attempts': { method: 'GET', path: '/v1/async-jobs/{id}/attempts', summary: 'List the runs of one job', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
22
+ 'list-items': { method: 'GET', path: '/v1/async-jobs/{id}/items', summary: 'List the records of one job and how the work on each ended', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
23
+ 'payload': { method: 'GET', path: '/v1/async-jobs/{id}/payload', summary: 'Read the input a job holds', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
23
24
  };
24
25
  export default class AsyncJobs extends Command {
25
26
  static description = 'Manage async jobs';
@@ -41,7 +42,7 @@ export default class AsyncJobs extends Command {
41
42
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
42
43
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
43
44
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
44
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
45
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
45
46
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
46
47
  page: Flags.string({ description: 'Page number' }),
47
48
  limit: Flags.string({ description: 'Items per page' }),
@@ -172,6 +173,17 @@ export default class AsyncJobs extends Command {
172
173
  }
173
174
  }
174
175
  }
176
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
177
+ // have them: their body is not JSON, so --set cannot carry a value there.
178
+ for (const param of endpoint.specParams) {
179
+ const value = flags[param.flag];
180
+ if (value === undefined || value === '') {
181
+ if (param.required)
182
+ this.error(`Missing required flag: --${param.flag}`);
183
+ continue;
184
+ }
185
+ queryParams[param.name] = value;
186
+ }
175
187
  let data;
176
188
  if (endpoint.isMultipart) {
177
189
  if (!flags.file)
@@ -192,6 +204,25 @@ export default class AsyncJobs extends Command {
192
204
  }
193
205
  data = form;
194
206
  }
207
+ else if (endpoint.bodyMediaType) {
208
+ // The body is not JSON (today only application/x-ndjson): it is sent
209
+ // verbatim, under the content type the endpoint asks for.
210
+ if (flags.file) {
211
+ const fsModule = await import('node:fs');
212
+ try {
213
+ data = fsModule.readFileSync(flags.file, 'utf-8');
214
+ }
215
+ catch {
216
+ this.error(`Failed to read file: ${flags.file}`);
217
+ }
218
+ }
219
+ else if (flags.data) {
220
+ data = flags.data;
221
+ }
222
+ else {
223
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
224
+ }
225
+ }
195
226
  else if (endpoint.hasBody) {
196
227
  // For binary-response endpoints, --file is the output path, not a JSON
197
228
  // input file — skip the read-as-body branch.
@@ -287,7 +318,7 @@ export default class AsyncJobs extends Command {
287
318
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
288
319
  }
289
320
  else if (data) {
290
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
321
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
291
322
  }
292
323
  process.stderr.write('\n');
293
324
  }
@@ -301,6 +332,7 @@ export default class AsyncJobs extends Command {
301
332
  const binResponse = await apiRequest(endpoint.method, apiPath, {
302
333
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
303
334
  data,
335
+ contentType: endpoint.bodyMediaType || undefined,
304
336
  contextName: flags.context,
305
337
  binary: true,
306
338
  });
@@ -326,6 +358,7 @@ export default class AsyncJobs extends Command {
326
358
  const raw = await apiRequest(endpoint.method, apiPath, {
327
359
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
328
360
  data,
361
+ contentType: endpoint.bodyMediaType || undefined,
329
362
  contextName: flags.context,
330
363
  binary: true,
331
364
  });
@@ -346,6 +379,7 @@ export default class AsyncJobs extends Command {
346
379
  response = await apiRequest(endpoint.method, apiPath, {
347
380
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
348
381
  data,
382
+ contentType: endpoint.bodyMediaType || undefined,
349
383
  contextName: flags.context,
350
384
  acceptAny: true,
351
385
  });
@@ -1,6 +1,7 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
+ import { resolveUserPath } from '../lib/paths.js';
4
5
  // 'upsert' loops create/update from the CLI, one request per record. It exists
5
6
  // only where the API has no import endpoint of its own; those keep the name
6
7
  // 'import' and are listed in SERVER_IMPORTS.
@@ -11,8 +12,8 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["list", "get"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/audit-logs', summary: 'Get audit log entries', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'get': { method: 'GET', path: '/v1/audit-logs/{id}', summary: 'Get a single audit log entry', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/audit-logs', summary: 'Get audit log entries', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'get': { method: 'GET', path: '/v1/audit-logs/{id}', summary: 'Get a single audit log entry', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
17
  };
17
18
  export default class AuditLog extends Command {
18
19
  static description = 'Manage audit log';
@@ -31,7 +32,7 @@ export default class AuditLog extends Command {
31
32
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
32
33
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
33
34
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
34
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
35
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
35
36
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
36
37
  page: Flags.string({ description: 'Page number' }),
37
38
  limit: Flags.string({ description: 'Items per page' }),
@@ -162,6 +163,17 @@ export default class AuditLog extends Command {
162
163
  }
163
164
  }
164
165
  }
166
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
167
+ // have them: their body is not JSON, so --set cannot carry a value there.
168
+ for (const param of endpoint.specParams) {
169
+ const value = flags[param.flag];
170
+ if (value === undefined || value === '') {
171
+ if (param.required)
172
+ this.error(`Missing required flag: --${param.flag}`);
173
+ continue;
174
+ }
175
+ queryParams[param.name] = value;
176
+ }
165
177
  let data;
166
178
  if (endpoint.isMultipart) {
167
179
  if (!flags.file)
@@ -182,6 +194,25 @@ export default class AuditLog extends Command {
182
194
  }
183
195
  data = form;
184
196
  }
197
+ else if (endpoint.bodyMediaType) {
198
+ // The body is not JSON (today only application/x-ndjson): it is sent
199
+ // verbatim, under the content type the endpoint asks for.
200
+ if (flags.file) {
201
+ const fsModule = await import('node:fs');
202
+ try {
203
+ data = fsModule.readFileSync(flags.file, 'utf-8');
204
+ }
205
+ catch {
206
+ this.error(`Failed to read file: ${flags.file}`);
207
+ }
208
+ }
209
+ else if (flags.data) {
210
+ data = flags.data;
211
+ }
212
+ else {
213
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
214
+ }
215
+ }
185
216
  else if (endpoint.hasBody) {
186
217
  // For binary-response endpoints, --file is the output path, not a JSON
187
218
  // input file — skip the read-as-body branch.
@@ -277,7 +308,7 @@ export default class AuditLog extends Command {
277
308
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
278
309
  }
279
310
  else if (data) {
280
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
311
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
281
312
  }
282
313
  process.stderr.write('\n');
283
314
  }
@@ -291,6 +322,7 @@ export default class AuditLog extends Command {
291
322
  const binResponse = await apiRequest(endpoint.method, apiPath, {
292
323
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
293
324
  data,
325
+ contentType: endpoint.bodyMediaType || undefined,
294
326
  contextName: flags.context,
295
327
  binary: true,
296
328
  });
@@ -316,6 +348,7 @@ export default class AuditLog extends Command {
316
348
  const raw = await apiRequest(endpoint.method, apiPath, {
317
349
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
318
350
  data,
351
+ contentType: endpoint.bodyMediaType || undefined,
319
352
  contextName: flags.context,
320
353
  binary: true,
321
354
  });
@@ -336,6 +369,7 @@ export default class AuditLog extends Command {
336
369
  response = await apiRequest(endpoint.method, apiPath, {
337
370
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
338
371
  data,
372
+ contentType: endpoint.bodyMediaType || undefined,
339
373
  contextName: flags.context,
340
374
  acceptAny: true,
341
375
  });
@@ -1,6 +1,7 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
+ import { resolveUserPath } from '../lib/paths.js';
4
5
  // 'upsert' loops create/update from the CLI, one request per record. It exists
5
6
  // only where the API has no import endpoint of its own; those keep the name
6
7
  // 'import' and are listed in SERVER_IMPORTS.
@@ -11,24 +12,24 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = ["ledger-mappings", "notes"];
12
13
  const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "get-balances", "get-balances-latest", "list-ledger-mappings", "create-ledger-mappings", "get-ledger-mappings", "update-ledger-mappings", "delete-ledger-mappings", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-daily-balances"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/bank-accounts', summary: 'Get all bank accounts with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create': { method: 'POST', path: '/v1/bank-accounts', summary: 'Create a new bank account (with optional notes)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['paymentRunDays', 'notes'] },
16
- 'get': { method: 'GET', path: '/v1/bank-accounts/{id}', summary: 'Get a bank account by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'update': { method: 'PATCH', path: '/v1/bank-accounts/{id}', summary: 'Update a bank account', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['paymentRunDays', 'notes'] },
18
- 'delete': { method: 'DELETE', path: '/v1/bank-accounts/{id}', summary: 'Delete a bank account', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
- 'get-balances': { method: 'GET', path: '/v1/bank-accounts/{id}/balances', summary: 'Get balance history for a bank account', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
- 'get-balances-latest': { method: 'GET', path: '/v1/bank-accounts/{id}/balances/latest', summary: 'Get latest balances (OPBD, CLBD, CLAV) for a bank account', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
- 'list-ledger-mappings': { method: 'GET', path: '/v1/bank-accounts/{bankAccountId}/ledger-mappings', summary: 'Get all ledger mappings for a bank account', pathParams: ['bankAccountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
22
- 'create-ledger-mappings': { method: 'POST', path: '/v1/bank-accounts/{bankAccountId}/ledger-mappings', summary: 'Create a ledger mapping for a bank account', pathParams: ['bankAccountId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
23
- 'get-ledger-mappings': { method: 'GET', path: '/v1/bank-accounts/{bankAccountId}/ledger-mappings/{mappingId}', summary: 'Get a ledger mapping by ID', pathParams: ['bankAccountId', 'mappingId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
24
- 'update-ledger-mappings': { method: 'PATCH', path: '/v1/bank-accounts/{bankAccountId}/ledger-mappings/{mappingId}', summary: 'Update a ledger mapping', pathParams: ['bankAccountId', 'mappingId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
25
- 'delete-ledger-mappings': { method: 'DELETE', path: '/v1/bank-accounts/{bankAccountId}/ledger-mappings/{mappingId}', summary: 'Delete a ledger mapping', pathParams: ['bankAccountId', 'mappingId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
26
- 'list-notes': { method: 'GET', path: '/v1/bank-accounts/{bankAccountId}/notes', summary: 'Get all notes for a bank account', pathParams: ['bankAccountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
27
- 'create-notes': { method: 'POST', path: '/v1/bank-accounts/{bankAccountId}/notes', summary: 'Create a note for a bank account', pathParams: ['bankAccountId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
28
- 'get-notes': { method: 'GET', path: '/v1/bank-accounts/{bankAccountId}/notes/{noteId}', summary: 'Get a specific note for a bank account', pathParams: ['bankAccountId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
29
- 'update-notes': { method: 'PATCH', path: '/v1/bank-accounts/{bankAccountId}/notes/{noteId}', summary: 'Update a note for a bank account', pathParams: ['bankAccountId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
30
- 'delete-notes': { method: 'DELETE', path: '/v1/bank-accounts/{bankAccountId}/notes/{noteId}', summary: 'Delete a note from a bank account', pathParams: ['bankAccountId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
31
- 'list-daily-balances': { method: 'GET', path: '/v1/bank-accounts/{bankAccountId}/daily-balances', summary: 'Get a bank account\'s daily closing balances for a date range', pathParams: ['bankAccountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/bank-accounts', summary: 'Get all bank accounts with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create': { method: 'POST', path: '/v1/bank-accounts', summary: 'Create a new bank account (with optional notes)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['paymentRunDays', 'notes'], bodyMediaType: '', specParams: [] },
17
+ 'get': { method: 'GET', path: '/v1/bank-accounts/{id}', summary: 'Get a bank account by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'update': { method: 'PATCH', path: '/v1/bank-accounts/{id}', summary: 'Update a bank account', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['paymentRunDays', 'notes'], bodyMediaType: '', specParams: [] },
19
+ 'delete': { method: 'DELETE', path: '/v1/bank-accounts/{id}', summary: 'Delete a bank account', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
20
+ 'get-balances': { method: 'GET', path: '/v1/bank-accounts/{id}/balances', summary: 'Get balance history for a bank account', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
21
+ 'get-balances-latest': { method: 'GET', path: '/v1/bank-accounts/{id}/balances/latest', summary: 'Get latest balances (OPBD, CLBD, CLAV) for a bank account', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
22
+ 'list-ledger-mappings': { method: 'GET', path: '/v1/bank-accounts/{bankAccountId}/ledger-mappings', summary: 'Get all ledger mappings for a bank account', pathParams: ['bankAccountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
23
+ 'create-ledger-mappings': { method: 'POST', path: '/v1/bank-accounts/{bankAccountId}/ledger-mappings', summary: 'Create a ledger mapping for a bank account', pathParams: ['bankAccountId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
24
+ 'get-ledger-mappings': { method: 'GET', path: '/v1/bank-accounts/{bankAccountId}/ledger-mappings/{mappingId}', summary: 'Get a ledger mapping by ID', pathParams: ['bankAccountId', 'mappingId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
25
+ 'update-ledger-mappings': { method: 'PATCH', path: '/v1/bank-accounts/{bankAccountId}/ledger-mappings/{mappingId}', summary: 'Update a ledger mapping', pathParams: ['bankAccountId', 'mappingId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
26
+ 'delete-ledger-mappings': { method: 'DELETE', path: '/v1/bank-accounts/{bankAccountId}/ledger-mappings/{mappingId}', summary: 'Delete a ledger mapping', pathParams: ['bankAccountId', 'mappingId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
27
+ 'list-notes': { method: 'GET', path: '/v1/bank-accounts/{bankAccountId}/notes', summary: 'Get all notes for a bank account', pathParams: ['bankAccountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
28
+ 'create-notes': { method: 'POST', path: '/v1/bank-accounts/{bankAccountId}/notes', summary: 'Create a note for a bank account', pathParams: ['bankAccountId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
29
+ 'get-notes': { method: 'GET', path: '/v1/bank-accounts/{bankAccountId}/notes/{noteId}', summary: 'Get a specific note for a bank account', pathParams: ['bankAccountId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
30
+ 'update-notes': { method: 'PATCH', path: '/v1/bank-accounts/{bankAccountId}/notes/{noteId}', summary: 'Update a note for a bank account', pathParams: ['bankAccountId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
31
+ 'delete-notes': { method: 'DELETE', path: '/v1/bank-accounts/{bankAccountId}/notes/{noteId}', summary: 'Delete a note from a bank account', pathParams: ['bankAccountId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
32
+ 'list-daily-balances': { method: 'GET', path: '/v1/bank-accounts/{bankAccountId}/daily-balances', summary: 'Get a bank account\'s daily closing balances for a date range, one row per day and currency', pathParams: ['bankAccountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
32
33
  };
33
34
  export default class BankAccounts extends Command {
34
35
  static description = 'Manage bank accounts';
@@ -50,7 +51,7 @@ export default class BankAccounts extends Command {
50
51
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
51
52
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
52
53
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
53
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
54
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
54
55
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
55
56
  'mapping-id': Flags.string({ description: 'Path param: mappingId' }),
56
57
  'note-id': Flags.string({ description: 'Path param: noteId' }),
@@ -183,6 +184,17 @@ export default class BankAccounts extends Command {
183
184
  }
184
185
  }
185
186
  }
187
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
188
+ // have them: their body is not JSON, so --set cannot carry a value there.
189
+ for (const param of endpoint.specParams) {
190
+ const value = flags[param.flag];
191
+ if (value === undefined || value === '') {
192
+ if (param.required)
193
+ this.error(`Missing required flag: --${param.flag}`);
194
+ continue;
195
+ }
196
+ queryParams[param.name] = value;
197
+ }
186
198
  let data;
187
199
  if (endpoint.isMultipart) {
188
200
  if (!flags.file)
@@ -203,6 +215,25 @@ export default class BankAccounts extends Command {
203
215
  }
204
216
  data = form;
205
217
  }
218
+ else if (endpoint.bodyMediaType) {
219
+ // The body is not JSON (today only application/x-ndjson): it is sent
220
+ // verbatim, under the content type the endpoint asks for.
221
+ if (flags.file) {
222
+ const fsModule = await import('node:fs');
223
+ try {
224
+ data = fsModule.readFileSync(flags.file, 'utf-8');
225
+ }
226
+ catch {
227
+ this.error(`Failed to read file: ${flags.file}`);
228
+ }
229
+ }
230
+ else if (flags.data) {
231
+ data = flags.data;
232
+ }
233
+ else {
234
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
235
+ }
236
+ }
206
237
  else if (endpoint.hasBody) {
207
238
  // For binary-response endpoints, --file is the output path, not a JSON
208
239
  // input file — skip the read-as-body branch.
@@ -298,7 +329,7 @@ export default class BankAccounts extends Command {
298
329
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
299
330
  }
300
331
  else if (data) {
301
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
332
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
302
333
  }
303
334
  process.stderr.write('\n');
304
335
  }
@@ -312,6 +343,7 @@ export default class BankAccounts extends Command {
312
343
  const binResponse = await apiRequest(endpoint.method, apiPath, {
313
344
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
314
345
  data,
346
+ contentType: endpoint.bodyMediaType || undefined,
315
347
  contextName: flags.context,
316
348
  binary: true,
317
349
  });
@@ -337,6 +369,7 @@ export default class BankAccounts extends Command {
337
369
  const raw = await apiRequest(endpoint.method, apiPath, {
338
370
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
339
371
  data,
372
+ contentType: endpoint.bodyMediaType || undefined,
340
373
  contextName: flags.context,
341
374
  binary: true,
342
375
  });
@@ -357,6 +390,7 @@ export default class BankAccounts extends Command {
357
390
  response = await apiRequest(endpoint.method, apiPath, {
358
391
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
359
392
  data,
393
+ contentType: endpoint.bodyMediaType || undefined,
360
394
  contextName: flags.context,
361
395
  acceptAny: true,
362
396
  });