@aderis-erp/cli 1.0.17 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/commands/access-groups.js +49 -17
  2. package/dist/commands/account-transaction-allocations.js +46 -14
  3. package/dist/commands/account-transactions.js +75 -35
  4. package/dist/commands/accounts.js +54 -22
  5. package/dist/commands/agent-platform.js +53 -21
  6. package/dist/commands/{nango-integrations.d.ts → agent-runs.d.ts} +1 -1
  7. package/dist/commands/agent-runs.js +1155 -0
  8. package/dist/commands/analytics.js +47 -15
  9. package/dist/commands/api-keys.js +49 -16
  10. package/dist/commands/{text-templates.d.ts → async-jobs.d.ts} +1 -1
  11. package/dist/commands/async-jobs.js +1158 -0
  12. package/dist/commands/audit-log.js +46 -14
  13. package/dist/commands/bank-accounts.js +63 -30
  14. package/dist/commands/bank-integrations.d.ts +1 -0
  15. package/dist/commands/bank-integrations.js +66 -31
  16. package/dist/commands/business-partners.js +77 -44
  17. package/dist/commands/chat.js +52 -19
  18. package/dist/commands/credit-card-accounts.d.ts +1 -0
  19. package/dist/commands/credit-card-accounts.js +67 -29
  20. package/dist/commands/delivery-notes.js +78 -44
  21. package/dist/commands/dunning-configs.js +49 -17
  22. package/dist/commands/dunning-reminders.js +52 -20
  23. package/dist/commands/dunning-runs.js +45 -13
  24. package/dist/commands/email-transactions.js +47 -15
  25. package/dist/commands/employees.js +76 -44
  26. package/dist/commands/entity-configurations.js +47 -15
  27. package/dist/commands/event-processor-mappings.js +49 -17
  28. package/dist/commands/files.js +56 -24
  29. package/dist/commands/health.js +49 -17
  30. package/dist/commands/imports.d.ts +39 -0
  31. package/dist/commands/{nango-integrations.js → imports.js} +61 -25
  32. package/dist/commands/inbox.js +52 -19
  33. package/dist/commands/integration-identities.d.ts +40 -0
  34. package/dist/commands/integration-identities.js +1151 -0
  35. package/dist/commands/integrations.js +47 -15
  36. package/dist/commands/item-categories.js +49 -17
  37. package/dist/commands/items.js +49 -17
  38. package/dist/commands/journal-lines.js +45 -13
  39. package/dist/commands/journals.js +68 -36
  40. package/dist/commands/ledgers.d.ts +3 -1
  41. package/dist/commands/ledgers.js +95 -34
  42. package/dist/commands/my-account.d.ts +39 -0
  43. package/dist/commands/my-account.js +1160 -0
  44. package/dist/commands/office-locations.js +49 -17
  45. package/dist/commands/payment-methods.js +54 -22
  46. package/dist/commands/payment-runs.js +61 -29
  47. package/dist/commands/payment-terms.js +50 -18
  48. package/dist/commands/payments.js +51 -19
  49. package/dist/commands/payroll-runs.d.ts +43 -0
  50. package/dist/commands/payroll-runs.js +1188 -0
  51. package/dist/commands/permissions.d.ts +39 -0
  52. package/dist/commands/{text-templates.js → permissions.js} +54 -30
  53. package/dist/commands/price-lists.js +54 -22
  54. package/dist/commands/processor-mappings.js +48 -16
  55. package/dist/commands/processors.js +55 -24
  56. package/dist/commands/purchase-order-types.js +49 -17
  57. package/dist/commands/purchase-orders.js +69 -36
  58. package/dist/commands/reports.js +49 -17
  59. package/dist/commands/roles.d.ts +39 -0
  60. package/dist/commands/roles.js +1152 -0
  61. package/dist/commands/salary-types.js +49 -17
  62. package/dist/commands/sales-invoices.js +84 -51
  63. package/dist/commands/sales-orders.js +78 -44
  64. package/dist/commands/saved-views.js +47 -15
  65. package/dist/commands/scheduled-processor-mappings.js +49 -17
  66. package/dist/commands/schedules.js +53 -21
  67. package/dist/commands/tag-configurations.js +50 -18
  68. package/dist/commands/tags.js +49 -17
  69. package/dist/commands/tax-codes.js +52 -20
  70. package/dist/commands/tax-matrix.js +49 -17
  71. package/dist/commands/templates.d.ts +39 -0
  72. package/dist/commands/templates.js +1157 -0
  73. package/dist/commands/tenants.js +52 -20
  74. package/dist/commands/tools.js +46 -14
  75. package/dist/commands/users.d.ts +42 -0
  76. package/dist/commands/users.js +1174 -0
  77. package/dist/commands/vat-declarations.d.ts +2 -0
  78. package/dist/commands/vat-declarations.js +72 -36
  79. package/dist/commands/vendor-invoices.js +81 -49
  80. package/dist/commands/webhooks.js +51 -19
  81. package/dist/commands/workflows.js +47 -15
  82. package/dist/lib/api-client.d.ts +6 -0
  83. package/dist/lib/api-client.js +13 -0
  84. package/dist/lib/auth/index.js +15 -4
  85. package/dist/lib/auth/jwks.d.ts +19 -0
  86. package/dist/lib/auth/jwks.js +112 -0
  87. package/dist/lib/auth/keycloak-provider.d.ts +23 -7
  88. package/dist/lib/auth/keycloak-provider.js +53 -24
  89. package/dist/lib/auth/tokens.d.ts +8 -3
  90. package/dist/lib/auth/tokens.js +8 -7
  91. package/dist/lib/auth/types.d.ts +5 -1
  92. package/oclif.manifest.json +2152 -715
  93. package/package.json +4 -4
@@ -0,0 +1,39 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Permissions extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ action: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ id: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
8
+ };
9
+ static flags: {
10
+ context: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ table: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ data: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ set: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ page: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
+ limit: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ include: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
+ search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
+ 'sort-by': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
22
+ filter: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
23
+ query: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
24
+ fields: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
25
+ sanitize: import("@oclif/core/interfaces").BooleanFlag<boolean>;
26
+ 'show-http-request': import("@oclif/core/interfaces").BooleanFlag<boolean>;
27
+ };
28
+ run(): Promise<void>;
29
+ private runImport;
30
+ private runExport;
31
+ private runCamt053Import;
32
+ private runNestedImport;
33
+ private parseFilterValue;
34
+ private sanitizeOutput;
35
+ private getColumns;
36
+ private getValue;
37
+ private printTable;
38
+ private printCsv;
39
+ }
@@ -1,28 +1,20 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
- import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
2
+ import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
4
  // Add 'import' action if both create and update exist
5
- const HAS_IMPORT = true;
5
+ const HAS_IMPORT = false;
6
6
  const HAS_EXPORT = true;
7
7
  const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
- const SUBCOMMANDS = [...["list", "create", "get", "update", "delete"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
9
+ const SUBCOMMANDS = [...["list"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/text-templates', summary: 'List all text templates', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/text-templates', summary: 'Create a new text template', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
- 'get': { method: 'GET', path: '/v1/text-templates/{id}', summary: 'Get a single text template by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'update': { method: 'PATCH', path: '/v1/text-templates/{id}', summary: 'Update a text template', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
- 'delete': { method: 'DELETE', path: '/v1/text-templates/{id}', summary: 'Delete a text template', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list': { method: 'GET', path: '/v1/permissions', summary: 'List permissions selectable for tenant-created roles', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
12
  };
17
- export default class TextTemplates extends Command {
18
- static description = 'Manage text templates';
13
+ export default class Permissions extends Command {
14
+ static description = 'Manage permissions';
19
15
  static examples = [
20
- '<%= config.bin %> text-templates list',
21
- '<%= config.bin %> text-templates get <id>',
22
- '<%= config.bin %> text-templates create --data \'{...}\'',
23
- '<%= config.bin %> text-templates update <id> --set field=value',
24
- '<%= config.bin %> text-templates import --file data.json',
25
- '<%= config.bin %> text-templates export --csv > text-templates.csv',
16
+ '<%= config.bin %> permissions list',
17
+ '<%= config.bin %> permissions export --csv > permissions.csv',
26
18
  ];
27
19
  static args = {
28
20
  action: Args.string({ description: 'Action: ' + SUBCOMMANDS.join(', '), required: true }),
@@ -48,7 +40,7 @@ export default class TextTemplates extends Command {
48
40
  'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
49
41
  };
50
42
  async run() {
51
- const { args, flags } = await this.parse(TextTemplates);
43
+ const { args, flags } = await this.parse(Permissions);
52
44
  // Handle import action specially
53
45
  if (args.action === 'import') {
54
46
  if (!HAS_IMPORT)
@@ -89,7 +81,7 @@ export default class TextTemplates extends Command {
89
81
  // First path param always comes from positional argument
90
82
  value = args.id;
91
83
  if (!value)
92
- this.error('Missing required argument: id. Usage: aderis text-templates <action> <id>');
84
+ this.error('Missing required argument: id. Usage: aderis permissions <action> <id>');
93
85
  }
94
86
  else {
95
87
  const flagName = param.replace(/\./g, '-').replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
@@ -264,10 +256,11 @@ export default class TextTemplates extends Command {
264
256
  process.stderr.write('\n');
265
257
  }
266
258
  try {
267
- // Binary response: download to file
259
+ // Binary response: to -f / --file, or to stdout when it is redirected.
268
260
  if (endpoint.isBinaryResponse) {
269
- if (!flags.file) {
270
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
261
+ const toStdout = !flags.file;
262
+ if (toStdout && process.stdout.isTTY) {
263
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
271
264
  }
272
265
  const binResponse = await apiRequest(endpoint.method, apiPath, {
273
266
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -275,24 +268,55 @@ export default class TextTemplates extends Command {
275
268
  contextName: flags.context,
276
269
  binary: true,
277
270
  });
278
- const fsModule = await import('node:fs');
279
271
  const binData = binResponse.data;
280
272
  const buffer = Buffer.isBuffer(binData)
281
273
  ? binData
282
274
  : Buffer.from(binData);
275
+ if (toStdout) {
276
+ // Nothing else may reach stdout here — it is the file.
277
+ process.stdout.write(buffer);
278
+ return;
279
+ }
280
+ const fsModule = await import('node:fs');
283
281
  fsModule.writeFileSync(flags.file, buffer);
284
282
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
285
283
  return;
286
284
  }
287
- const response = await apiRequest(endpoint.method, apiPath, {
288
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
289
- data,
290
- contextName: flags.context,
291
- acceptAny: true,
292
- });
285
+ let response;
286
+ if (endpoint.hasBinaryVariant) {
287
+ // This route answers JSON or a file depending on the request body, so the
288
+ // bytes are read first and the response's own content-type decides. Asking
289
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
290
+ const raw = await apiRequest(endpoint.method, apiPath, {
291
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
292
+ data,
293
+ contextName: flags.context,
294
+ binary: true,
295
+ });
296
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
297
+ const rawData = raw.data;
298
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
299
+ if (!isTextualMediaType(rawType)) {
300
+ if (process.stdout.isTTY) {
301
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
302
+ }
303
+ // Nothing else may reach stdout here — it is the file.
304
+ process.stdout.write(buffer);
305
+ return;
306
+ }
307
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
308
+ }
309
+ else {
310
+ response = await apiRequest(endpoint.method, apiPath, {
311
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
312
+ data,
313
+ contextName: flags.context,
314
+ acceptAny: true,
315
+ });
316
+ }
293
317
  // Check content-type for non-JSON responses (XML, text, etc.)
294
318
  const contentType = (response.headers['content-type'] || '').toLowerCase();
295
- if (contentType.includes('xml') || contentType.includes('text/')) {
319
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
296
320
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
297
321
  this.log(String(response.data));
298
322
  return;
@@ -732,7 +756,7 @@ export default class TextTemplates extends Command {
732
756
  }
733
757
  async runNestedImport(suffix, parentId, flags) {
734
758
  if (!parentId) {
735
- this.error(`Missing required argument: id. Usage: aderis text-templates import-${suffix} <parent-id> --file data.json`);
759
+ this.error(`Missing required argument: id. Usage: aderis permissions import-${suffix} <parent-id> --file data.json`);
736
760
  }
737
761
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
738
762
  if (!context)
@@ -1,5 +1,5 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
- import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
2
+ import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
4
  // Add 'import' action if both create and update exist
5
5
  const HAS_IMPORT = true;
@@ -8,16 +8,16 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = ["prices"];
9
9
  const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "list-prices", "create-prices", "get-prices", "update-prices", "delete-prices"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/price-lists', summary: 'Get all price lists with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/price-lists', summary: 'Create a new price list', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
- 'get': { method: 'GET', path: '/v1/price-lists/{id}', summary: 'Get a price list by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'update': { method: 'PATCH', path: '/v1/price-lists/{id}', summary: 'Update a price list', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
- 'delete': { method: 'DELETE', path: '/v1/price-lists/{id}', summary: 'Delete a price list', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
- 'list-prices': { method: 'GET', path: '/v1/price-lists/{id}/prices', summary: 'Get all prices in a price list with pagination', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
17
- 'create-prices': { method: 'POST', path: '/v1/price-lists/{id}/prices', summary: 'Create a new price in a price list', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
18
- 'get-prices': { method: 'GET', path: '/v1/price-lists/{id}/prices/{priceId}', summary: 'Get a price by ID', pathParams: ['id', 'priceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
19
- 'update-prices': { method: 'PATCH', path: '/v1/price-lists/{id}/prices/{priceId}', summary: 'Update a price', pathParams: ['id', 'priceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
20
- 'delete-prices': { method: 'DELETE', path: '/v1/price-lists/{id}/prices/{priceId}', summary: 'Delete a price', pathParams: ['id', 'priceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list': { method: 'GET', path: '/v1/price-lists', summary: 'Get all price lists with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/price-lists', summary: 'Create a new price list', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
13
+ 'get': { method: 'GET', path: '/v1/price-lists/{id}', summary: 'Get a price list by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
+ 'update': { method: 'PATCH', path: '/v1/price-lists/{id}', summary: 'Update a price list', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'delete': { method: 'DELETE', path: '/v1/price-lists/{id}', summary: 'Delete a price list', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
+ 'list-prices': { method: 'GET', path: '/v1/price-lists/{id}/prices', summary: 'Get all prices in a price list with pagination', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
+ 'create-prices': { method: 'POST', path: '/v1/price-lists/{id}/prices', summary: 'Create a new price in a price list', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
+ 'get-prices': { method: 'GET', path: '/v1/price-lists/{id}/prices/{priceId}', summary: 'Get a price by ID', pathParams: ['id', 'priceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
+ 'update-prices': { method: 'PATCH', path: '/v1/price-lists/{id}/prices/{priceId}', summary: 'Update a price', pathParams: ['id', 'priceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
+ 'delete-prices': { method: 'DELETE', path: '/v1/price-lists/{id}/prices/{priceId}', summary: 'Delete a price', pathParams: ['id', 'priceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
21
  };
22
22
  export default class PriceLists extends Command {
23
23
  static description = 'Manage price lists';
@@ -270,10 +270,11 @@ export default class PriceLists extends Command {
270
270
  process.stderr.write('\n');
271
271
  }
272
272
  try {
273
- // Binary response: download to file
273
+ // Binary response: to -f / --file, or to stdout when it is redirected.
274
274
  if (endpoint.isBinaryResponse) {
275
- if (!flags.file) {
276
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
275
+ const toStdout = !flags.file;
276
+ if (toStdout && process.stdout.isTTY) {
277
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
277
278
  }
278
279
  const binResponse = await apiRequest(endpoint.method, apiPath, {
279
280
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -281,24 +282,55 @@ export default class PriceLists extends Command {
281
282
  contextName: flags.context,
282
283
  binary: true,
283
284
  });
284
- const fsModule = await import('node:fs');
285
285
  const binData = binResponse.data;
286
286
  const buffer = Buffer.isBuffer(binData)
287
287
  ? binData
288
288
  : Buffer.from(binData);
289
+ if (toStdout) {
290
+ // Nothing else may reach stdout here — it is the file.
291
+ process.stdout.write(buffer);
292
+ return;
293
+ }
294
+ const fsModule = await import('node:fs');
289
295
  fsModule.writeFileSync(flags.file, buffer);
290
296
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
291
297
  return;
292
298
  }
293
- const response = await apiRequest(endpoint.method, apiPath, {
294
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
295
- data,
296
- contextName: flags.context,
297
- acceptAny: true,
298
- });
299
+ let response;
300
+ if (endpoint.hasBinaryVariant) {
301
+ // This route answers JSON or a file depending on the request body, so the
302
+ // bytes are read first and the response's own content-type decides. Asking
303
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
304
+ const raw = await apiRequest(endpoint.method, apiPath, {
305
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
306
+ data,
307
+ contextName: flags.context,
308
+ binary: true,
309
+ });
310
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
311
+ const rawData = raw.data;
312
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
313
+ if (!isTextualMediaType(rawType)) {
314
+ if (process.stdout.isTTY) {
315
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
316
+ }
317
+ // Nothing else may reach stdout here — it is the file.
318
+ process.stdout.write(buffer);
319
+ return;
320
+ }
321
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
322
+ }
323
+ else {
324
+ response = await apiRequest(endpoint.method, apiPath, {
325
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
326
+ data,
327
+ contextName: flags.context,
328
+ acceptAny: true,
329
+ });
330
+ }
299
331
  // Check content-type for non-JSON responses (XML, text, etc.)
300
332
  const contentType = (response.headers['content-type'] || '').toLowerCase();
301
- if (contentType.includes('xml') || contentType.includes('text/')) {
333
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
302
334
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
303
335
  this.log(String(response.data));
304
336
  return;
@@ -1,5 +1,5 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
- import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
2
+ import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
4
  // Add 'import' action if both create and update exist
5
5
  const HAS_IMPORT = true;
@@ -8,10 +8,10 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["list", "create", "update", "delete"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/processor-mappings', summary: 'List all document type processor mappings for the tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/processor-mappings', summary: 'Create a new document type processor mapping', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['documentTypes'] },
13
- 'update': { method: 'PATCH', path: '/v1/processor-mappings/{id}', summary: 'Update a document type processor mapping', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['documentTypes'] },
14
- 'delete': { method: 'DELETE', path: '/v1/processor-mappings/{id}', summary: 'Delete a document type processor mapping', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list': { method: 'GET', path: '/v1/processor-mappings', summary: 'List all document type processor mappings for the tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/processor-mappings', summary: 'Create a new document type processor mapping', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['documentTypes'] },
13
+ 'update': { method: 'PATCH', path: '/v1/processor-mappings/{id}', summary: 'Update a document type processor mapping', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['documentTypes'] },
14
+ 'delete': { method: 'DELETE', path: '/v1/processor-mappings/{id}', summary: 'Delete a document type processor mapping', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
15
  };
16
16
  export default class ProcessorMappings extends Command {
17
17
  static description = 'Manage processor mappings';
@@ -262,10 +262,11 @@ export default class ProcessorMappings extends Command {
262
262
  process.stderr.write('\n');
263
263
  }
264
264
  try {
265
- // Binary response: download to file
265
+ // Binary response: to -f / --file, or to stdout when it is redirected.
266
266
  if (endpoint.isBinaryResponse) {
267
- if (!flags.file) {
268
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
267
+ const toStdout = !flags.file;
268
+ if (toStdout && process.stdout.isTTY) {
269
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
269
270
  }
270
271
  const binResponse = await apiRequest(endpoint.method, apiPath, {
271
272
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -273,24 +274,55 @@ export default class ProcessorMappings extends Command {
273
274
  contextName: flags.context,
274
275
  binary: true,
275
276
  });
276
- const fsModule = await import('node:fs');
277
277
  const binData = binResponse.data;
278
278
  const buffer = Buffer.isBuffer(binData)
279
279
  ? binData
280
280
  : Buffer.from(binData);
281
+ if (toStdout) {
282
+ // Nothing else may reach stdout here — it is the file.
283
+ process.stdout.write(buffer);
284
+ return;
285
+ }
286
+ const fsModule = await import('node:fs');
281
287
  fsModule.writeFileSync(flags.file, buffer);
282
288
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
283
289
  return;
284
290
  }
285
- const response = await apiRequest(endpoint.method, apiPath, {
286
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
287
- data,
288
- contextName: flags.context,
289
- acceptAny: true,
290
- });
291
+ let response;
292
+ if (endpoint.hasBinaryVariant) {
293
+ // This route answers JSON or a file depending on the request body, so the
294
+ // bytes are read first and the response's own content-type decides. Asking
295
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
296
+ const raw = await apiRequest(endpoint.method, apiPath, {
297
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
298
+ data,
299
+ contextName: flags.context,
300
+ binary: true,
301
+ });
302
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
303
+ const rawData = raw.data;
304
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
305
+ if (!isTextualMediaType(rawType)) {
306
+ if (process.stdout.isTTY) {
307
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
308
+ }
309
+ // Nothing else may reach stdout here — it is the file.
310
+ process.stdout.write(buffer);
311
+ return;
312
+ }
313
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
314
+ }
315
+ else {
316
+ response = await apiRequest(endpoint.method, apiPath, {
317
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
318
+ data,
319
+ contextName: flags.context,
320
+ acceptAny: true,
321
+ });
322
+ }
291
323
  // Check content-type for non-JSON responses (XML, text, etc.)
292
324
  const contentType = (response.headers['content-type'] || '').toLowerCase();
293
- if (contentType.includes('xml') || contentType.includes('text/')) {
325
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
294
326
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
295
327
  this.log(String(response.data));
296
328
  return;
@@ -1,24 +1,23 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
- import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
2
+ import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
4
  // Add 'import' action if both create and update exist
5
5
  const HAS_IMPORT = true;
6
6
  const HAS_EXPORT = true;
7
7
  const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
- const SUBCOMMANDS = [...["list-tools", "list", "create", "resolve", "resolve-event", "get", "update", "delete", "template", "update-config", "delete-config"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
9
+ const SUBCOMMANDS = [...["list-tools", "list", "create", "resolve", "resolve-event", "get", "update", "delete", "update-config", "delete-config"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list-tools': { method: 'GET', path: '/v1/processors/tools', summary: 'List all available tools', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'list': { method: 'GET', path: '/v1/processors', summary: 'List processors available to the tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
- 'create': { method: 'POST', path: '/v1/processors', summary: 'Create a new tenant-scoped processor', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['processorTools', 'options'] },
14
- 'resolve': { method: 'GET', path: '/v1/processors/resolve/{documentType}', summary: 'Resolve processor for a document type', pathParams: ['documentType'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
- 'resolve-event': { method: 'GET', path: '/v1/processors/resolve/event/{eventType}', summary: 'Resolve processors for an event type', pathParams: ['eventType'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
- 'get': { method: 'GET', path: '/v1/processors/{id}', summary: 'Get a single processor by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
17
- 'update': { method: 'PATCH', path: '/v1/processors/{id}', summary: 'Update a tenant-scoped processor', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['processorTools', 'options'] },
18
- 'delete': { method: 'DELETE', path: '/v1/processors/{id}', summary: 'Delete a tenant-scoped processor', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
19
- 'template': { method: 'POST', path: '/v1/processors/{id}/template', summary: 'Upload a PDF template for a processor', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
20
- 'update-config': { method: 'PUT', path: '/v1/processors/{id}/config', summary: 'Set or replace a tenant prompt overlay on a system processor', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
21
- 'delete-config': { method: 'DELETE', path: '/v1/processors/{id}/config', summary: 'Remove the tenant prompt overlay on a system processor', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list-tools': { method: 'GET', path: '/v1/processors/tools', summary: 'List all available tools', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'list': { method: 'GET', path: '/v1/processors', summary: 'List processors available to the tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
13
+ 'create': { method: 'POST', path: '/v1/processors', summary: 'Create a new tenant-scoped processor', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['processorTools', 'options'] },
14
+ 'resolve': { method: 'GET', path: '/v1/processors/resolve/{documentType}', summary: 'Resolve processor for a document type', pathParams: ['documentType'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'resolve-event': { method: 'GET', path: '/v1/processors/resolve/event/{eventType}', summary: 'Resolve processors for an event type', pathParams: ['eventType'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
+ 'get': { method: 'GET', path: '/v1/processors/{id}', summary: 'Get a single processor by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
+ 'update': { method: 'PATCH', path: '/v1/processors/{id}', summary: 'Update a tenant-scoped processor', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['processorTools', 'options'] },
18
+ 'delete': { method: 'DELETE', path: '/v1/processors/{id}', summary: 'Delete a tenant-scoped processor', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
+ 'update-config': { method: 'PUT', path: '/v1/processors/{id}/config', summary: 'Set or replace the tenant\'s overwrite on a system processor', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
+ 'delete-config': { method: 'DELETE', path: '/v1/processors/{id}/config', summary: 'Remove the tenant\'s overwrite on a system processor', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
22
21
  };
23
22
  export default class Processors extends Command {
24
23
  static description = 'Manage processors';
@@ -270,10 +269,11 @@ export default class Processors extends Command {
270
269
  process.stderr.write('\n');
271
270
  }
272
271
  try {
273
- // Binary response: download to file
272
+ // Binary response: to -f / --file, or to stdout when it is redirected.
274
273
  if (endpoint.isBinaryResponse) {
275
- if (!flags.file) {
276
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
274
+ const toStdout = !flags.file;
275
+ if (toStdout && process.stdout.isTTY) {
276
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
277
277
  }
278
278
  const binResponse = await apiRequest(endpoint.method, apiPath, {
279
279
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -281,24 +281,55 @@ export default class Processors extends Command {
281
281
  contextName: flags.context,
282
282
  binary: true,
283
283
  });
284
- const fsModule = await import('node:fs');
285
284
  const binData = binResponse.data;
286
285
  const buffer = Buffer.isBuffer(binData)
287
286
  ? binData
288
287
  : Buffer.from(binData);
288
+ if (toStdout) {
289
+ // Nothing else may reach stdout here — it is the file.
290
+ process.stdout.write(buffer);
291
+ return;
292
+ }
293
+ const fsModule = await import('node:fs');
289
294
  fsModule.writeFileSync(flags.file, buffer);
290
295
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
291
296
  return;
292
297
  }
293
- const response = await apiRequest(endpoint.method, apiPath, {
294
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
295
- data,
296
- contextName: flags.context,
297
- acceptAny: true,
298
- });
298
+ let response;
299
+ if (endpoint.hasBinaryVariant) {
300
+ // This route answers JSON or a file depending on the request body, so the
301
+ // bytes are read first and the response's own content-type decides. Asking
302
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
303
+ const raw = await apiRequest(endpoint.method, apiPath, {
304
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
305
+ data,
306
+ contextName: flags.context,
307
+ binary: true,
308
+ });
309
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
310
+ const rawData = raw.data;
311
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
312
+ if (!isTextualMediaType(rawType)) {
313
+ if (process.stdout.isTTY) {
314
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
315
+ }
316
+ // Nothing else may reach stdout here — it is the file.
317
+ process.stdout.write(buffer);
318
+ return;
319
+ }
320
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
321
+ }
322
+ else {
323
+ response = await apiRequest(endpoint.method, apiPath, {
324
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
325
+ data,
326
+ contextName: flags.context,
327
+ acceptAny: true,
328
+ });
329
+ }
299
330
  // Check content-type for non-JSON responses (XML, text, etc.)
300
331
  const contentType = (response.headers['content-type'] || '').toLowerCase();
301
- if (contentType.includes('xml') || contentType.includes('text/')) {
332
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
302
333
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
303
334
  this.log(String(response.data));
304
335
  return;