@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,12 +12,12 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "journal-coverage"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/tags', summary: 'Get all tags with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create': { method: 'POST', path: '/v1/tags', summary: 'Create a new tag', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['rules'] },
16
- 'get': { method: 'GET', path: '/v1/tags/{id}', summary: 'Get a tag by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'update': { method: 'PATCH', path: '/v1/tags/{id}', summary: 'Update a tag', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['rules'] },
18
- 'delete': { method: 'DELETE', path: '/v1/tags/{id}', summary: 'Delete a tag', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
- 'journal-coverage': { method: 'GET', path: '/v1/tags/{id}/journal-coverage', summary: 'How much of the journal-line amount in scope carries one of the tags below this tag', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/tags', summary: 'Get all tags with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create': { method: 'POST', path: '/v1/tags', summary: 'Create a new tag', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['rules'], bodyMediaType: '', specParams: [] },
17
+ 'get': { method: 'GET', path: '/v1/tags/{id}', summary: 'Get a tag by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'update': { method: 'PATCH', path: '/v1/tags/{id}', summary: 'Update a tag', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['rules'], bodyMediaType: '', specParams: [] },
19
+ 'delete': { method: 'DELETE', path: '/v1/tags/{id}', summary: 'Delete a tag', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
20
+ 'journal-coverage': { method: 'GET', path: '/v1/tags/{id}/journal-coverage', summary: 'How much of the journal-line amount in scope carries one of the tags below this tag', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
20
21
  };
21
22
  export default class Tags extends Command {
22
23
  static description = 'Manage tags';
@@ -38,7 +39,7 @@ export default class Tags extends Command {
38
39
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
39
40
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
40
41
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
41
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
42
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
42
43
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
43
44
  page: Flags.string({ description: 'Page number' }),
44
45
  limit: Flags.string({ description: 'Items per page' }),
@@ -169,6 +170,17 @@ export default class Tags extends Command {
169
170
  }
170
171
  }
171
172
  }
173
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
174
+ // have them: their body is not JSON, so --set cannot carry a value there.
175
+ for (const param of endpoint.specParams) {
176
+ const value = flags[param.flag];
177
+ if (value === undefined || value === '') {
178
+ if (param.required)
179
+ this.error(`Missing required flag: --${param.flag}`);
180
+ continue;
181
+ }
182
+ queryParams[param.name] = value;
183
+ }
172
184
  let data;
173
185
  if (endpoint.isMultipart) {
174
186
  if (!flags.file)
@@ -189,6 +201,25 @@ export default class Tags extends Command {
189
201
  }
190
202
  data = form;
191
203
  }
204
+ else if (endpoint.bodyMediaType) {
205
+ // The body is not JSON (today only application/x-ndjson): it is sent
206
+ // verbatim, under the content type the endpoint asks for.
207
+ if (flags.file) {
208
+ const fsModule = await import('node:fs');
209
+ try {
210
+ data = fsModule.readFileSync(flags.file, 'utf-8');
211
+ }
212
+ catch {
213
+ this.error(`Failed to read file: ${flags.file}`);
214
+ }
215
+ }
216
+ else if (flags.data) {
217
+ data = flags.data;
218
+ }
219
+ else {
220
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
221
+ }
222
+ }
192
223
  else if (endpoint.hasBody) {
193
224
  // For binary-response endpoints, --file is the output path, not a JSON
194
225
  // input file — skip the read-as-body branch.
@@ -284,7 +315,7 @@ export default class Tags extends Command {
284
315
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
285
316
  }
286
317
  else if (data) {
287
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
318
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
288
319
  }
289
320
  process.stderr.write('\n');
290
321
  }
@@ -298,6 +329,7 @@ export default class Tags extends Command {
298
329
  const binResponse = await apiRequest(endpoint.method, apiPath, {
299
330
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
300
331
  data,
332
+ contentType: endpoint.bodyMediaType || undefined,
301
333
  contextName: flags.context,
302
334
  binary: true,
303
335
  });
@@ -323,6 +355,7 @@ export default class Tags extends Command {
323
355
  const raw = await apiRequest(endpoint.method, apiPath, {
324
356
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
325
357
  data,
358
+ contentType: endpoint.bodyMediaType || undefined,
326
359
  contextName: flags.context,
327
360
  binary: true,
328
361
  });
@@ -343,6 +376,7 @@ export default class Tags extends Command {
343
376
  response = await apiRequest(endpoint.method, apiPath, {
344
377
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
345
378
  data,
379
+ contentType: endpoint.bodyMediaType || undefined,
346
380
  contextName: flags.context,
347
381
  acceptAny: true,
348
382
  });
@@ -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,14 +12,14 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["list", "create", "tax-codes-standard-rate", "get-by-country-rate", "get-by-code-at-date", "get", "update", "delete"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/tax/tax-codes', summary: 'Get all tax codes with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create': { method: 'POST', path: '/v1/tax/tax-codes', summary: 'Create a new tax code', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['directions'] },
16
- 'tax-codes-standard-rate': { method: 'GET', path: '/v1/tax/tax-codes/standard-rate', summary: 'Get the standard-rate tax code for a country at a specific date', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'get-by-country-rate': { method: 'GET', path: '/v1/tax/tax-codes/by-country-rate', summary: 'Get tax code by country and rate at a specific date', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
- 'get-by-code-at-date': { method: 'GET', path: '/v1/tax/tax-codes/by-code-at-date', summary: 'Get tax code by code at a specific date', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
- 'get': { method: 'GET', path: '/v1/tax/tax-codes/{id}', summary: 'Get a tax code by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
- 'update': { method: 'PATCH', path: '/v1/tax/tax-codes/{id}', summary: 'Update a tax code', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['directions'] },
21
- 'delete': { method: 'DELETE', path: '/v1/tax/tax-codes/{id}', summary: 'Delete a tax code', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/tax/tax-codes', summary: 'Get all tax codes with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create': { method: 'POST', path: '/v1/tax/tax-codes', summary: 'Create a new tax code', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['directions'], bodyMediaType: '', specParams: [] },
17
+ 'tax-codes-standard-rate': { method: 'GET', path: '/v1/tax/tax-codes/standard-rate', summary: 'Get the standard-rate tax code for a country at a specific date', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'get-by-country-rate': { method: 'GET', path: '/v1/tax/tax-codes/by-country-rate', summary: 'Get tax code by country and rate at a specific date', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
+ 'get-by-code-at-date': { method: 'GET', path: '/v1/tax/tax-codes/by-code-at-date', summary: 'Get tax code by code at a specific date', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
20
+ 'get': { method: 'GET', path: '/v1/tax/tax-codes/{id}', summary: 'Get a tax code by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
21
+ 'update': { method: 'PATCH', path: '/v1/tax/tax-codes/{id}', summary: 'Update a tax code', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['directions'], bodyMediaType: '', specParams: [] },
22
+ 'delete': { method: 'DELETE', path: '/v1/tax/tax-codes/{id}', summary: 'Delete a tax code', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
22
23
  };
23
24
  export default class TaxCodes extends Command {
24
25
  static description = 'Manage tax codes';
@@ -40,7 +41,7 @@ export default class TaxCodes extends Command {
40
41
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
41
42
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
42
43
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
43
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
44
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
44
45
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
45
46
  page: Flags.string({ description: 'Page number' }),
46
47
  limit: Flags.string({ description: 'Items per page' }),
@@ -171,6 +172,17 @@ export default class TaxCodes extends Command {
171
172
  }
172
173
  }
173
174
  }
175
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
176
+ // have them: their body is not JSON, so --set cannot carry a value there.
177
+ for (const param of endpoint.specParams) {
178
+ const value = flags[param.flag];
179
+ if (value === undefined || value === '') {
180
+ if (param.required)
181
+ this.error(`Missing required flag: --${param.flag}`);
182
+ continue;
183
+ }
184
+ queryParams[param.name] = value;
185
+ }
174
186
  let data;
175
187
  if (endpoint.isMultipart) {
176
188
  if (!flags.file)
@@ -191,6 +203,25 @@ export default class TaxCodes extends Command {
191
203
  }
192
204
  data = form;
193
205
  }
206
+ else if (endpoint.bodyMediaType) {
207
+ // The body is not JSON (today only application/x-ndjson): it is sent
208
+ // verbatim, under the content type the endpoint asks for.
209
+ if (flags.file) {
210
+ const fsModule = await import('node:fs');
211
+ try {
212
+ data = fsModule.readFileSync(flags.file, 'utf-8');
213
+ }
214
+ catch {
215
+ this.error(`Failed to read file: ${flags.file}`);
216
+ }
217
+ }
218
+ else if (flags.data) {
219
+ data = flags.data;
220
+ }
221
+ else {
222
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
223
+ }
224
+ }
194
225
  else if (endpoint.hasBody) {
195
226
  // For binary-response endpoints, --file is the output path, not a JSON
196
227
  // input file — skip the read-as-body branch.
@@ -286,7 +317,7 @@ export default class TaxCodes extends Command {
286
317
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
287
318
  }
288
319
  else if (data) {
289
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
320
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
290
321
  }
291
322
  process.stderr.write('\n');
292
323
  }
@@ -300,6 +331,7 @@ export default class TaxCodes extends Command {
300
331
  const binResponse = await apiRequest(endpoint.method, apiPath, {
301
332
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
302
333
  data,
334
+ contentType: endpoint.bodyMediaType || undefined,
303
335
  contextName: flags.context,
304
336
  binary: true,
305
337
  });
@@ -325,6 +357,7 @@ export default class TaxCodes extends Command {
325
357
  const raw = await apiRequest(endpoint.method, apiPath, {
326
358
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
327
359
  data,
360
+ contentType: endpoint.bodyMediaType || undefined,
328
361
  contextName: flags.context,
329
362
  binary: true,
330
363
  });
@@ -345,6 +378,7 @@ export default class TaxCodes extends Command {
345
378
  response = await apiRequest(endpoint.method, apiPath, {
346
379
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
347
380
  data,
381
+ contentType: endpoint.bodyMediaType || undefined,
348
382
  contextName: flags.context,
349
383
  acceptAny: true,
350
384
  });
@@ -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,11 +12,11 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["list", "create", "get", "update", "delete"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/tax/tax-matrix', summary: 'Get all tax matrix mappings with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create': { method: 'POST', path: '/v1/tax/tax-matrix', summary: 'Create a new tax matrix mapping', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
- 'get': { method: 'GET', path: '/v1/tax/tax-matrix/{id}', summary: 'Get a tax matrix mapping by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'update': { method: 'PATCH', path: '/v1/tax/tax-matrix/{id}', summary: 'Update a tax matrix mapping', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
- 'delete': { method: 'DELETE', path: '/v1/tax/tax-matrix/{id}', summary: 'Delete a tax matrix mapping', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/tax/tax-matrix', summary: 'Get all tax matrix mappings with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create': { method: 'POST', path: '/v1/tax/tax-matrix', summary: 'Create a new tax matrix mapping', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
17
+ 'get': { method: 'GET', path: '/v1/tax/tax-matrix/{id}', summary: 'Get a tax matrix mapping by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'update': { method: 'PATCH', path: '/v1/tax/tax-matrix/{id}', summary: 'Update a tax matrix mapping', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
+ 'delete': { method: 'DELETE', path: '/v1/tax/tax-matrix/{id}', summary: 'Delete a tax matrix mapping', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
20
  };
20
21
  export default class TaxMatrix extends Command {
21
22
  static description = 'Manage tax matrix';
@@ -37,7 +38,7 @@ export default class TaxMatrix extends Command {
37
38
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
38
39
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
39
40
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
40
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
41
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
41
42
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
42
43
  page: Flags.string({ description: 'Page number' }),
43
44
  limit: Flags.string({ description: 'Items per page' }),
@@ -168,6 +169,17 @@ export default class TaxMatrix extends Command {
168
169
  }
169
170
  }
170
171
  }
172
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
173
+ // have them: their body is not JSON, so --set cannot carry a value there.
174
+ for (const param of endpoint.specParams) {
175
+ const value = flags[param.flag];
176
+ if (value === undefined || value === '') {
177
+ if (param.required)
178
+ this.error(`Missing required flag: --${param.flag}`);
179
+ continue;
180
+ }
181
+ queryParams[param.name] = value;
182
+ }
171
183
  let data;
172
184
  if (endpoint.isMultipart) {
173
185
  if (!flags.file)
@@ -188,6 +200,25 @@ export default class TaxMatrix extends Command {
188
200
  }
189
201
  data = form;
190
202
  }
203
+ else if (endpoint.bodyMediaType) {
204
+ // The body is not JSON (today only application/x-ndjson): it is sent
205
+ // verbatim, under the content type the endpoint asks for.
206
+ if (flags.file) {
207
+ const fsModule = await import('node:fs');
208
+ try {
209
+ data = fsModule.readFileSync(flags.file, 'utf-8');
210
+ }
211
+ catch {
212
+ this.error(`Failed to read file: ${flags.file}`);
213
+ }
214
+ }
215
+ else if (flags.data) {
216
+ data = flags.data;
217
+ }
218
+ else {
219
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
220
+ }
221
+ }
191
222
  else if (endpoint.hasBody) {
192
223
  // For binary-response endpoints, --file is the output path, not a JSON
193
224
  // input file — skip the read-as-body branch.
@@ -283,7 +314,7 @@ export default class TaxMatrix extends Command {
283
314
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
284
315
  }
285
316
  else if (data) {
286
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
317
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
287
318
  }
288
319
  process.stderr.write('\n');
289
320
  }
@@ -297,6 +328,7 @@ export default class TaxMatrix extends Command {
297
328
  const binResponse = await apiRequest(endpoint.method, apiPath, {
298
329
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
299
330
  data,
331
+ contentType: endpoint.bodyMediaType || undefined,
300
332
  contextName: flags.context,
301
333
  binary: true,
302
334
  });
@@ -322,6 +354,7 @@ export default class TaxMatrix extends Command {
322
354
  const raw = await apiRequest(endpoint.method, apiPath, {
323
355
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
324
356
  data,
357
+ contentType: endpoint.bodyMediaType || undefined,
325
358
  contextName: flags.context,
326
359
  binary: true,
327
360
  });
@@ -342,6 +375,7 @@ export default class TaxMatrix extends Command {
342
375
  response = await apiRequest(endpoint.method, apiPath, {
343
376
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
344
377
  data,
378
+ contentType: endpoint.bodyMediaType || undefined,
345
379
  contextName: flags.context,
346
380
  acceptAny: true,
347
381
  });
@@ -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.
@@ -9,20 +10,21 @@ const HAS_EXPORT = true;
9
10
  const HAS_CAMT053 = false;
10
11
  const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
- const SUBCOMMANDS = [...["rule-fields", "rule-order", "replace-rule", "delete-rule", "list", "create", "get", "update", "delete", "usage", "duplicate", "preview"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
+ const SUBCOMMANDS = [...["rule-fields", "rule-order", "replace-rule", "delete-rule", "pdf-fields", "list", "create", "get", "update", "delete", "usage", "duplicate", "preview"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'rule-fields': { method: 'GET', path: '/v1/templates/rule-fields', summary: 'List the fields a TEXT module’s template rules may test', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'rule-order': { method: 'POST', path: '/v1/templates/rule-order', summary: 'Order the template rules of a TEXT module', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['templateIds'] },
16
- 'replace-rule': { method: 'PUT', path: '/v1/templates/{id}/rule', summary: 'Set the rule of a TEXT template', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'delete-rule': { method: 'DELETE', path: '/v1/templates/{id}/rule', summary: 'Remove the rule of a template', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
- 'list': { method: 'GET', path: '/v1/templates', summary: 'Get all templates with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
- 'create': { method: 'POST', path: '/v1/templates', summary: 'Create a new template', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['includes'] },
20
- 'get': { method: 'GET', path: '/v1/templates/{id}', summary: 'Get a template by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
- 'update': { method: 'PATCH', path: '/v1/templates/{id}', summary: 'Update a template', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['includes'] },
22
- 'delete': { method: 'DELETE', path: '/v1/templates/{id}', summary: 'Delete a template', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
23
- 'usage': { method: 'GET', path: '/v1/templates/{id}/usage', summary: 'Show which processor uses a template', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
24
- 'duplicate': { method: 'POST', path: '/v1/templates/{id}/duplicate', summary: 'Duplicate a template', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
25
- 'preview': { method: 'POST', path: '/v1/templates/{id}/preview', summary: 'Preview an EMAIL or TEXT template against a real document', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'rule-fields': { method: 'GET', path: '/v1/templates/rule-fields', summary: 'List the fields a TEXT module’s template rules may test', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'rule-order': { method: 'POST', path: '/v1/templates/rule-order', summary: 'Order the template rules of a TEXT module', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['templateIds'], bodyMediaType: '', specParams: [] },
17
+ 'replace-rule': { method: 'PUT', path: '/v1/templates/{id}/rule', summary: 'Set the rule of a TEXT template', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'delete-rule': { method: 'DELETE', path: '/v1/templates/{id}/rule', summary: 'Remove the rule of a template', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
+ 'pdf-fields': { method: 'GET', path: '/v1/templates/pdf-fields', summary: 'List the values a PDF template of a document type can print', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
20
+ 'list': { method: 'GET', path: '/v1/templates', summary: 'Get all templates with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
21
+ 'create': { method: 'POST', path: '/v1/templates', summary: 'Create a new template', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['includes'], bodyMediaType: '', specParams: [] },
22
+ 'get': { method: 'GET', path: '/v1/templates/{id}', summary: 'Get a template by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
23
+ 'update': { method: 'PATCH', path: '/v1/templates/{id}', summary: 'Update a template', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['includes'], bodyMediaType: '', specParams: [] },
24
+ 'delete': { method: 'DELETE', path: '/v1/templates/{id}', summary: 'Delete a template', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
25
+ 'usage': { method: 'GET', path: '/v1/templates/{id}/usage', summary: 'Show which processor uses a template', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
26
+ 'duplicate': { method: 'POST', path: '/v1/templates/{id}/duplicate', summary: 'Duplicate a template', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
27
+ 'preview': { method: 'POST', path: '/v1/templates/{id}/preview', summary: 'Preview an EMAIL or TEXT template against a real document', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
26
28
  };
27
29
  export default class Templates extends Command {
28
30
  static description = 'Manage templates';
@@ -44,7 +46,7 @@ export default class Templates extends Command {
44
46
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
45
47
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
46
48
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
47
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
49
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
48
50
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
49
51
  page: Flags.string({ description: 'Page number' }),
50
52
  limit: Flags.string({ description: 'Items per page' }),
@@ -175,6 +177,17 @@ export default class Templates extends Command {
175
177
  }
176
178
  }
177
179
  }
180
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
181
+ // have them: their body is not JSON, so --set cannot carry a value there.
182
+ for (const param of endpoint.specParams) {
183
+ const value = flags[param.flag];
184
+ if (value === undefined || value === '') {
185
+ if (param.required)
186
+ this.error(`Missing required flag: --${param.flag}`);
187
+ continue;
188
+ }
189
+ queryParams[param.name] = value;
190
+ }
178
191
  let data;
179
192
  if (endpoint.isMultipart) {
180
193
  if (!flags.file)
@@ -195,6 +208,25 @@ export default class Templates extends Command {
195
208
  }
196
209
  data = form;
197
210
  }
211
+ else if (endpoint.bodyMediaType) {
212
+ // The body is not JSON (today only application/x-ndjson): it is sent
213
+ // verbatim, under the content type the endpoint asks for.
214
+ if (flags.file) {
215
+ const fsModule = await import('node:fs');
216
+ try {
217
+ data = fsModule.readFileSync(flags.file, 'utf-8');
218
+ }
219
+ catch {
220
+ this.error(`Failed to read file: ${flags.file}`);
221
+ }
222
+ }
223
+ else if (flags.data) {
224
+ data = flags.data;
225
+ }
226
+ else {
227
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
228
+ }
229
+ }
198
230
  else if (endpoint.hasBody) {
199
231
  // For binary-response endpoints, --file is the output path, not a JSON
200
232
  // input file — skip the read-as-body branch.
@@ -290,7 +322,7 @@ export default class Templates extends Command {
290
322
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
291
323
  }
292
324
  else if (data) {
293
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
325
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
294
326
  }
295
327
  process.stderr.write('\n');
296
328
  }
@@ -304,6 +336,7 @@ export default class Templates extends Command {
304
336
  const binResponse = await apiRequest(endpoint.method, apiPath, {
305
337
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
306
338
  data,
339
+ contentType: endpoint.bodyMediaType || undefined,
307
340
  contextName: flags.context,
308
341
  binary: true,
309
342
  });
@@ -329,6 +362,7 @@ export default class Templates extends Command {
329
362
  const raw = await apiRequest(endpoint.method, apiPath, {
330
363
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
331
364
  data,
365
+ contentType: endpoint.bodyMediaType || undefined,
332
366
  contextName: flags.context,
333
367
  binary: true,
334
368
  });
@@ -349,6 +383,7 @@ export default class Templates extends Command {
349
383
  response = await apiRequest(endpoint.method, apiPath, {
350
384
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
351
385
  data,
386
+ contentType: endpoint.bodyMediaType || undefined,
352
387
  contextName: flags.context,
353
388
  acceptAny: true,
354
389
  });
@@ -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,14 +12,14 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "logo", "icon", "update-identity-provider"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/tenants', summary: 'Get all tenants with pagination (admin only)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create': { method: 'POST', path: '/v1/tenants', summary: 'Create a new tenant (admin only)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['supportedLocales', 'allowedSenders', 'salesInvoiceRecipientCc', 'salesInvoiceRecipientBcc', 'salesOrderRecipientCc', 'salesOrderRecipientBcc', 'deliveryNoteRecipientCc', 'deliveryNoteRecipientBcc'] },
16
- 'get': { method: 'GET', path: '/v1/tenants/{id}', summary: 'Get a tenant by ID (own tenant or admin)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'update': { method: 'PATCH', path: '/v1/tenants/{id}', summary: 'Update a tenant (own tenant or admin)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['supportedLocales', 'allowedSenders', 'salesInvoiceRecipientCc', 'salesInvoiceRecipientBcc', 'salesOrderRecipientCc', 'salesOrderRecipientBcc', 'deliveryNoteRecipientCc', 'deliveryNoteRecipientBcc'] },
18
- 'delete': { method: 'DELETE', path: '/v1/tenants/{id}', summary: 'Delete a tenant (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
- 'logo': { method: 'POST', path: '/v1/tenants/{id}/logo', summary: 'Upload the tenant logo to the public bucket and set logoUrl (own tenant or admin)', pathParams: ['id'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
- 'icon': { method: 'POST', path: '/v1/tenants/{id}/icon', summary: 'Upload the tenant icon to the public bucket and set iconUrl (own tenant or admin)', pathParams: ['id'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
- 'update-identity-provider': { method: 'PUT', path: '/v1/tenants/{id}/identity-provider', summary: 'Synchronize a tenant with the identity provider (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/tenants', summary: 'Get all tenants with pagination (admin only)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create': { method: 'POST', path: '/v1/tenants', summary: 'Create a new tenant (admin only)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['supportedLocales', 'allowedSenders', 'salesInvoiceRecipientCc', 'salesInvoiceRecipientBcc', 'salesOrderRecipientCc', 'salesOrderRecipientBcc', 'deliveryNoteRecipientCc', 'deliveryNoteRecipientBcc'], bodyMediaType: '', specParams: [] },
17
+ 'get': { method: 'GET', path: '/v1/tenants/{id}', summary: 'Get a tenant by ID (own tenant or admin)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'update': { method: 'PATCH', path: '/v1/tenants/{id}', summary: 'Update a tenant (own tenant or admin)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['supportedLocales', 'allowedSenders', 'salesInvoiceRecipientCc', 'salesInvoiceRecipientBcc', 'salesOrderRecipientCc', 'salesOrderRecipientBcc', 'deliveryNoteRecipientCc', 'deliveryNoteRecipientBcc'], bodyMediaType: '', specParams: [] },
19
+ 'delete': { method: 'DELETE', path: '/v1/tenants/{id}', summary: 'Delete a tenant (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
20
+ 'logo': { method: 'POST', path: '/v1/tenants/{id}/logo', summary: 'Upload the tenant logo to the public bucket and set logoUrl (own tenant or admin)', pathParams: ['id'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
21
+ 'icon': { method: 'POST', path: '/v1/tenants/{id}/icon', summary: 'Upload the tenant icon to the public bucket and set iconUrl (own tenant or admin)', pathParams: ['id'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
22
+ 'update-identity-provider': { method: 'PUT', path: '/v1/tenants/{id}/identity-provider', summary: 'Synchronize a tenant with the identity provider (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
22
23
  };
23
24
  export default class Tenants extends Command {
24
25
  static description = 'Manage tenants';
@@ -40,7 +41,7 @@ export default class Tenants extends Command {
40
41
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
41
42
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
42
43
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
43
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
44
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
44
45
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
45
46
  page: Flags.string({ description: 'Page number' }),
46
47
  limit: Flags.string({ description: 'Items per page' }),
@@ -171,6 +172,17 @@ export default class Tenants extends Command {
171
172
  }
172
173
  }
173
174
  }
175
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
176
+ // have them: their body is not JSON, so --set cannot carry a value there.
177
+ for (const param of endpoint.specParams) {
178
+ const value = flags[param.flag];
179
+ if (value === undefined || value === '') {
180
+ if (param.required)
181
+ this.error(`Missing required flag: --${param.flag}`);
182
+ continue;
183
+ }
184
+ queryParams[param.name] = value;
185
+ }
174
186
  let data;
175
187
  if (endpoint.isMultipart) {
176
188
  if (!flags.file)
@@ -191,6 +203,25 @@ export default class Tenants extends Command {
191
203
  }
192
204
  data = form;
193
205
  }
206
+ else if (endpoint.bodyMediaType) {
207
+ // The body is not JSON (today only application/x-ndjson): it is sent
208
+ // verbatim, under the content type the endpoint asks for.
209
+ if (flags.file) {
210
+ const fsModule = await import('node:fs');
211
+ try {
212
+ data = fsModule.readFileSync(flags.file, 'utf-8');
213
+ }
214
+ catch {
215
+ this.error(`Failed to read file: ${flags.file}`);
216
+ }
217
+ }
218
+ else if (flags.data) {
219
+ data = flags.data;
220
+ }
221
+ else {
222
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
223
+ }
224
+ }
194
225
  else if (endpoint.hasBody) {
195
226
  // For binary-response endpoints, --file is the output path, not a JSON
196
227
  // input file — skip the read-as-body branch.
@@ -286,7 +317,7 @@ export default class Tenants extends Command {
286
317
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
287
318
  }
288
319
  else if (data) {
289
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
320
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
290
321
  }
291
322
  process.stderr.write('\n');
292
323
  }
@@ -300,6 +331,7 @@ export default class Tenants extends Command {
300
331
  const binResponse = await apiRequest(endpoint.method, apiPath, {
301
332
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
302
333
  data,
334
+ contentType: endpoint.bodyMediaType || undefined,
303
335
  contextName: flags.context,
304
336
  binary: true,
305
337
  });
@@ -325,6 +357,7 @@ export default class Tenants extends Command {
325
357
  const raw = await apiRequest(endpoint.method, apiPath, {
326
358
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
327
359
  data,
360
+ contentType: endpoint.bodyMediaType || undefined,
328
361
  contextName: flags.context,
329
362
  binary: true,
330
363
  });
@@ -345,6 +378,7 @@ export default class Tenants extends Command {
345
378
  response = await apiRequest(endpoint.method, apiPath, {
346
379
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
347
380
  data,
381
+ contentType: endpoint.bodyMediaType || undefined,
348
382
  contextName: flags.context,
349
383
  acceptAny: true,
350
384
  });