@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
@@ -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 = false;
@@ -8,9 +8,9 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["event-webhook", "list", "get"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'event-webhook': { method: 'POST', path: '/v1/email-transactions/event-webhook', summary: 'SendGrid delivery-event webhook (ECDSA signature-verified, no bearer token)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'list': { method: 'GET', path: '/v1/email-transactions', summary: 'List email transactions for the current tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
- 'get': { method: 'GET', path: '/v1/email-transactions/{id}', summary: 'Get a single email transaction (404 if not in this tenant)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'event-webhook': { method: 'POST', path: '/v1/email-transactions/event-webhook', summary: 'SendGrid delivery-event webhook (ECDSA signature-verified, no bearer token)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'list': { method: 'GET', path: '/v1/email-transactions', summary: 'List email transactions for the current tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
13
+ 'get': { method: 'GET', path: '/v1/email-transactions/{id}', summary: 'Get a single email transaction (404 if not in this tenant)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
14
  };
15
15
  export default class EmailTransactions extends Command {
16
16
  static description = 'Manage email transactions';
@@ -259,10 +259,11 @@ export default class EmailTransactions extends Command {
259
259
  process.stderr.write('\n');
260
260
  }
261
261
  try {
262
- // Binary response: download to file
262
+ // Binary response: to -f / --file, or to stdout when it is redirected.
263
263
  if (endpoint.isBinaryResponse) {
264
- if (!flags.file) {
265
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
264
+ const toStdout = !flags.file;
265
+ if (toStdout && process.stdout.isTTY) {
266
+ 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.`);
266
267
  }
267
268
  const binResponse = await apiRequest(endpoint.method, apiPath, {
268
269
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -270,24 +271,55 @@ export default class EmailTransactions extends Command {
270
271
  contextName: flags.context,
271
272
  binary: true,
272
273
  });
273
- const fsModule = await import('node:fs');
274
274
  const binData = binResponse.data;
275
275
  const buffer = Buffer.isBuffer(binData)
276
276
  ? binData
277
277
  : Buffer.from(binData);
278
+ if (toStdout) {
279
+ // Nothing else may reach stdout here — it is the file.
280
+ process.stdout.write(buffer);
281
+ return;
282
+ }
283
+ const fsModule = await import('node:fs');
278
284
  fsModule.writeFileSync(flags.file, buffer);
279
285
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
280
286
  return;
281
287
  }
282
- const response = await apiRequest(endpoint.method, apiPath, {
283
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
284
- data,
285
- contextName: flags.context,
286
- acceptAny: true,
287
- });
288
+ let response;
289
+ if (endpoint.hasBinaryVariant) {
290
+ // This route answers JSON or a file depending on the request body, so the
291
+ // bytes are read first and the response's own content-type decides. Asking
292
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
293
+ const raw = await apiRequest(endpoint.method, apiPath, {
294
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
295
+ data,
296
+ contextName: flags.context,
297
+ binary: true,
298
+ });
299
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
300
+ const rawData = raw.data;
301
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
302
+ if (!isTextualMediaType(rawType)) {
303
+ if (process.stdout.isTTY) {
304
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
305
+ }
306
+ // Nothing else may reach stdout here — it is the file.
307
+ process.stdout.write(buffer);
308
+ return;
309
+ }
310
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
311
+ }
312
+ else {
313
+ response = await apiRequest(endpoint.method, apiPath, {
314
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
315
+ data,
316
+ contextName: flags.context,
317
+ acceptAny: true,
318
+ });
319
+ }
288
320
  // Check content-type for non-JSON responses (XML, text, etc.)
289
321
  const contentType = (response.headers['content-type'] || '').toLowerCase();
290
- if (contentType.includes('xml') || contentType.includes('text/')) {
322
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
291
323
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
292
324
  this.log(String(response.data));
293
325
  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,38 +8,38 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = ["salaries", "payments", "notes", "files", "salary-payment-files"];
9
9
  const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "get-salaries", "create-salaries", "get-salaries-salaryId", "update-salaries", "delete-salaries", "get-payments", "create-payments", "get-payments-paymentId", "update-payments", "delete-payments", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download", "list-salary-payment-files", "create-salary-payment-files", "get-salary-payment-files", "update-salary-payment-files", "delete-salary-payment-files", "download-salary-payment-files"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/employees', summary: 'Get all employees with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/employees', summary: 'Create a new employee', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['beneficiaries', 'children', 'salaries', 'files', 'notes', 'tagIds', 'tagCodes'] },
13
- 'get': { method: 'GET', path: '/v1/employees/{id}', summary: 'Get an employee by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'update': { method: 'PATCH', path: '/v1/employees/{id}', summary: 'Update an employee', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['beneficiaries', 'children', 'salaries', 'files', 'notes', 'tagIds', 'tagCodes'] },
15
- 'delete': { method: 'DELETE', path: '/v1/employees/{id}', summary: 'Delete an employee', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
- 'get-salaries': { method: 'GET', path: '/v1/employees/{id}/salaries', summary: 'Get all salaries for an employee', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
17
- 'create-salaries': { method: 'POST', path: '/v1/employees/{id}/salaries', summary: 'Add a salary configuration to an employee', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
18
- 'get-salaries-salaryId': { method: 'GET', path: '/v1/employees/{id}/salaries/{salaryId}', summary: 'Get a specific salary by ID', pathParams: ['id', 'salaryId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
19
- 'update-salaries': { method: 'PATCH', path: '/v1/employees/{id}/salaries/{salaryId}', summary: 'Update a salary configuration', pathParams: ['id', 'salaryId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
20
- 'delete-salaries': { method: 'DELETE', path: '/v1/employees/{id}/salaries/{salaryId}', summary: 'Delete a salary configuration', pathParams: ['id', 'salaryId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
21
- 'get-payments': { method: 'GET', path: '/v1/employees/{id}/payments', summary: 'Get all payments for an employee', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
22
- 'create-payments': { method: 'POST', path: '/v1/employees/{id}/payments', summary: 'Add a payment to an employee', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['salaryComponents', 'files'] },
23
- 'get-payments-paymentId': { method: 'GET', path: '/v1/employees/{id}/payments/{paymentId}', summary: 'Get a specific payment by ID', pathParams: ['id', 'paymentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
24
- 'update-payments': { method: 'PATCH', path: '/v1/employees/{id}/payments/{paymentId}', summary: 'Update a payment', pathParams: ['id', 'paymentId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['salaryComponents', 'files'] },
25
- 'delete-payments': { method: 'DELETE', path: '/v1/employees/{id}/payments/{paymentId}', summary: 'Delete a payment', pathParams: ['id', 'paymentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
26
- 'list-notes': { method: 'GET', path: '/v1/employees/{employeeId}/notes', summary: 'Get all notes for an employee', pathParams: ['employeeId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
27
- 'create-notes': { method: 'POST', path: '/v1/employees/{employeeId}/notes', summary: 'Create a note for an employee', pathParams: ['employeeId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
28
- 'get-notes': { method: 'GET', path: '/v1/employees/{employeeId}/notes/{noteId}', summary: 'Get a specific note for an employee', pathParams: ['employeeId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
29
- 'update-notes': { method: 'PATCH', path: '/v1/employees/{employeeId}/notes/{noteId}', summary: 'Update a note for an employee', pathParams: ['employeeId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
30
- 'delete-notes': { method: 'DELETE', path: '/v1/employees/{employeeId}/notes/{noteId}', summary: 'Delete a note from an employee', pathParams: ['employeeId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
31
- 'list-files': { method: 'GET', path: '/v1/employees/{parentId}/files', summary: 'List file attachments for a employee', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
32
- 'create-files': { method: 'POST', path: '/v1/employees/{parentId}/files', summary: 'Upload a file attachment to a employee', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, bodyArrayFields: [] },
33
- 'get-files': { method: 'GET', path: '/v1/employees/{parentId}/files/{fileId}', summary: 'Get a file attachment for a employee', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
34
- 'update-files': { method: 'PATCH', path: '/v1/employees/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a employee', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
35
- 'delete-files': { method: 'DELETE', path: '/v1/employees/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a employee', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
36
- 'files-download': { method: 'GET', path: '/v1/employees/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a employee', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
37
- 'list-salary-payment-files': { method: 'GET', path: '/v1/salary-payments/{parentId}/files', summary: 'List file attachments for a salary payment', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
38
- 'create-salary-payment-files': { method: 'POST', path: '/v1/salary-payments/{parentId}/files', summary: 'Upload a file attachment to a salary payment', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, bodyArrayFields: [] },
39
- 'get-salary-payment-files': { method: 'GET', path: '/v1/salary-payments/{parentId}/files/{fileId}', summary: 'Get a file attachment for a salary payment', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
40
- 'update-salary-payment-files': { method: 'PATCH', path: '/v1/salary-payments/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a salary payment', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
41
- 'delete-salary-payment-files': { method: 'DELETE', path: '/v1/salary-payments/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a salary payment', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
42
- 'download-salary-payment-files': { method: 'GET', path: '/v1/salary-payments/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a salary payment', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list': { method: 'GET', path: '/v1/employees', summary: 'Get all employees with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/employees', summary: 'Create a new employee', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['beneficiaries', 'children', 'salaries', 'files', 'notes', 'tagIds', 'tagCodes'] },
13
+ 'get': { method: 'GET', path: '/v1/employees/{id}', summary: 'Get an employee by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
+ 'update': { method: 'PATCH', path: '/v1/employees/{id}', summary: 'Update an employee', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['beneficiaries', 'children', 'salaries', 'files', 'notes', 'tagIds', 'tagCodes'] },
15
+ 'delete': { method: 'DELETE', path: '/v1/employees/{id}', summary: 'Delete an employee', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
+ 'get-salaries': { method: 'GET', path: '/v1/employees/{id}/salaries', summary: 'Get all salaries for an employee', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
+ 'create-salaries': { method: 'POST', path: '/v1/employees/{id}/salaries', summary: 'Add a salary configuration to an employee', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
+ 'get-salaries-salaryId': { method: 'GET', path: '/v1/employees/{id}/salaries/{salaryId}', summary: 'Get a specific salary by ID', pathParams: ['id', 'salaryId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
+ 'update-salaries': { method: 'PATCH', path: '/v1/employees/{id}/salaries/{salaryId}', summary: 'Update a salary configuration', pathParams: ['id', 'salaryId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
+ 'delete-salaries': { method: 'DELETE', path: '/v1/employees/{id}/salaries/{salaryId}', summary: 'Delete a salary configuration', pathParams: ['id', 'salaryId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
+ 'get-payments': { method: 'GET', path: '/v1/employees/{id}/payments', summary: 'Get all payments for an employee', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
22
+ 'create-payments': { method: 'POST', path: '/v1/employees/{id}/payments', summary: 'Add a payment to an employee', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['salaryComponents', 'files'] },
23
+ 'get-payments-paymentId': { method: 'GET', path: '/v1/employees/{id}/payments/{paymentId}', summary: 'Get a specific payment by ID', pathParams: ['id', 'paymentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
24
+ 'update-payments': { method: 'PATCH', path: '/v1/employees/{id}/payments/{paymentId}', summary: 'Update a payment', pathParams: ['id', 'paymentId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['salaryComponents', 'files'] },
25
+ 'delete-payments': { method: 'DELETE', path: '/v1/employees/{id}/payments/{paymentId}', summary: 'Delete a payment', pathParams: ['id', 'paymentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
26
+ 'list-notes': { method: 'GET', path: '/v1/employees/{employeeId}/notes', summary: 'Get all notes for an employee', pathParams: ['employeeId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
27
+ 'create-notes': { method: 'POST', path: '/v1/employees/{employeeId}/notes', summary: 'Create a note for an employee', pathParams: ['employeeId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
28
+ 'get-notes': { method: 'GET', path: '/v1/employees/{employeeId}/notes/{noteId}', summary: 'Get a specific note for an employee', pathParams: ['employeeId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
29
+ 'update-notes': { method: 'PATCH', path: '/v1/employees/{employeeId}/notes/{noteId}', summary: 'Update a note for an employee', pathParams: ['employeeId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
30
+ 'delete-notes': { method: 'DELETE', path: '/v1/employees/{employeeId}/notes/{noteId}', summary: 'Delete a note from an employee', pathParams: ['employeeId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
31
+ 'list-files': { method: 'GET', path: '/v1/employees/{parentId}/files', summary: 'List file attachments for a employee', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
32
+ 'create-files': { method: 'POST', path: '/v1/employees/{parentId}/files', summary: 'Upload a file attachment to a employee', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
33
+ 'get-files': { method: 'GET', path: '/v1/employees/{parentId}/files/{fileId}', summary: 'Get a file attachment for a employee', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
34
+ 'update-files': { method: 'PATCH', path: '/v1/employees/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a employee', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
35
+ 'delete-files': { method: 'DELETE', path: '/v1/employees/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a employee', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
36
+ 'files-download': { method: 'GET', path: '/v1/employees/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a employee', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [] },
37
+ 'list-salary-payment-files': { method: 'GET', path: '/v1/salary-payments/{parentId}/files', summary: 'List file attachments for a salary payment', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
38
+ 'create-salary-payment-files': { method: 'POST', path: '/v1/salary-payments/{parentId}/files', summary: 'Upload a file attachment to a salary payment', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
39
+ 'get-salary-payment-files': { method: 'GET', path: '/v1/salary-payments/{parentId}/files/{fileId}', summary: 'Get a file attachment for a salary payment', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
40
+ 'update-salary-payment-files': { method: 'PATCH', path: '/v1/salary-payments/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a salary payment', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
41
+ 'delete-salary-payment-files': { method: 'DELETE', path: '/v1/salary-payments/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a salary payment', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
42
+ 'download-salary-payment-files': { method: 'GET', path: '/v1/salary-payments/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a salary payment', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [] },
43
43
  };
44
44
  export default class Employees extends Command {
45
45
  static description = 'Manage employees';
@@ -295,10 +295,11 @@ export default class Employees extends Command {
295
295
  process.stderr.write('\n');
296
296
  }
297
297
  try {
298
- // Binary response: download to file
298
+ // Binary response: to -f / --file, or to stdout when it is redirected.
299
299
  if (endpoint.isBinaryResponse) {
300
- if (!flags.file) {
301
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
300
+ const toStdout = !flags.file;
301
+ if (toStdout && process.stdout.isTTY) {
302
+ 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.`);
302
303
  }
303
304
  const binResponse = await apiRequest(endpoint.method, apiPath, {
304
305
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -306,24 +307,55 @@ export default class Employees extends Command {
306
307
  contextName: flags.context,
307
308
  binary: true,
308
309
  });
309
- const fsModule = await import('node:fs');
310
310
  const binData = binResponse.data;
311
311
  const buffer = Buffer.isBuffer(binData)
312
312
  ? binData
313
313
  : Buffer.from(binData);
314
+ if (toStdout) {
315
+ // Nothing else may reach stdout here — it is the file.
316
+ process.stdout.write(buffer);
317
+ return;
318
+ }
319
+ const fsModule = await import('node:fs');
314
320
  fsModule.writeFileSync(flags.file, buffer);
315
321
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
316
322
  return;
317
323
  }
318
- const response = await apiRequest(endpoint.method, apiPath, {
319
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
320
- data,
321
- contextName: flags.context,
322
- acceptAny: true,
323
- });
324
+ let response;
325
+ if (endpoint.hasBinaryVariant) {
326
+ // This route answers JSON or a file depending on the request body, so the
327
+ // bytes are read first and the response's own content-type decides. Asking
328
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
329
+ const raw = await apiRequest(endpoint.method, apiPath, {
330
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
331
+ data,
332
+ contextName: flags.context,
333
+ binary: true,
334
+ });
335
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
336
+ const rawData = raw.data;
337
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
338
+ if (!isTextualMediaType(rawType)) {
339
+ if (process.stdout.isTTY) {
340
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
341
+ }
342
+ // Nothing else may reach stdout here — it is the file.
343
+ process.stdout.write(buffer);
344
+ return;
345
+ }
346
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
347
+ }
348
+ else {
349
+ response = await apiRequest(endpoint.method, apiPath, {
350
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
351
+ data,
352
+ contextName: flags.context,
353
+ acceptAny: true,
354
+ });
355
+ }
324
356
  // Check content-type for non-JSON responses (XML, text, etc.)
325
357
  const contentType = (response.headers['content-type'] || '').toLowerCase();
326
- if (contentType.includes('xml') || contentType.includes('text/')) {
358
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
327
359
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
328
360
  this.log(String(response.data));
329
361
  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 = false;
@@ -8,9 +8,9 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["list-entities", "get-entities", "update-entities"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list-entities': { method: 'GET', path: '/v1/config/entities', summary: 'List all entity-type configs', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'get-entities': { method: 'GET', path: '/v1/config/entities/{entityType}', summary: 'Get one entity-type config', pathParams: ['entityType'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
- 'update-entities': { method: 'PATCH', path: '/v1/config/entities/{entityType}', summary: 'Adjust one entity-type config (admin only)', pathParams: ['entityType'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list-entities': { method: 'GET', path: '/v1/config/entities', summary: 'List all entity-type configs', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'get-entities': { method: 'GET', path: '/v1/config/entities/{entityType}', summary: 'Get one entity-type config', pathParams: ['entityType'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
13
+ 'update-entities': { method: 'PATCH', path: '/v1/config/entities/{entityType}', summary: 'Adjust one entity-type config (admin only)', pathParams: ['entityType'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
14
  };
15
15
  export default class EntityConfigurations extends Command {
16
16
  static description = 'Manage entity configurations';
@@ -255,10 +255,11 @@ export default class EntityConfigurations extends Command {
255
255
  process.stderr.write('\n');
256
256
  }
257
257
  try {
258
- // Binary response: download to file
258
+ // Binary response: to -f / --file, or to stdout when it is redirected.
259
259
  if (endpoint.isBinaryResponse) {
260
- if (!flags.file) {
261
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
260
+ const toStdout = !flags.file;
261
+ if (toStdout && process.stdout.isTTY) {
262
+ 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.`);
262
263
  }
263
264
  const binResponse = await apiRequest(endpoint.method, apiPath, {
264
265
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -266,24 +267,55 @@ export default class EntityConfigurations extends Command {
266
267
  contextName: flags.context,
267
268
  binary: true,
268
269
  });
269
- const fsModule = await import('node:fs');
270
270
  const binData = binResponse.data;
271
271
  const buffer = Buffer.isBuffer(binData)
272
272
  ? binData
273
273
  : Buffer.from(binData);
274
+ if (toStdout) {
275
+ // Nothing else may reach stdout here — it is the file.
276
+ process.stdout.write(buffer);
277
+ return;
278
+ }
279
+ const fsModule = await import('node:fs');
274
280
  fsModule.writeFileSync(flags.file, buffer);
275
281
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
276
282
  return;
277
283
  }
278
- const response = await apiRequest(endpoint.method, apiPath, {
279
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
280
- data,
281
- contextName: flags.context,
282
- acceptAny: true,
283
- });
284
+ let response;
285
+ if (endpoint.hasBinaryVariant) {
286
+ // This route answers JSON or a file depending on the request body, so the
287
+ // bytes are read first and the response's own content-type decides. Asking
288
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
289
+ const raw = await apiRequest(endpoint.method, apiPath, {
290
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
291
+ data,
292
+ contextName: flags.context,
293
+ binary: true,
294
+ });
295
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
296
+ const rawData = raw.data;
297
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
298
+ if (!isTextualMediaType(rawType)) {
299
+ if (process.stdout.isTTY) {
300
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
301
+ }
302
+ // Nothing else may reach stdout here — it is the file.
303
+ process.stdout.write(buffer);
304
+ return;
305
+ }
306
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
307
+ }
308
+ else {
309
+ response = await apiRequest(endpoint.method, apiPath, {
310
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
311
+ data,
312
+ contextName: flags.context,
313
+ acceptAny: true,
314
+ });
315
+ }
284
316
  // Check content-type for non-JSON responses (XML, text, etc.)
285
317
  const contentType = (response.headers['content-type'] || '').toLowerCase();
286
- if (contentType.includes('xml') || contentType.includes('text/')) {
318
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
287
319
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
288
320
  this.log(String(response.data));
289
321
  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,11 +8,11 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["events", "list", "create", "update", "delete"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'events': { method: 'GET', path: '/v1/event-processor-mappings/events', summary: 'List all valid event types', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'list': { method: 'GET', path: '/v1/event-processor-mappings', summary: 'List all event type processor mappings for the tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
- 'create': { method: 'POST', path: '/v1/event-processor-mappings', summary: 'Create event type processor mappings', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['eventTypes', 'includes'] },
14
- 'update': { method: 'PATCH', path: '/v1/event-processor-mappings/{id}', summary: 'Update an event type processor mapping', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['includes'] },
15
- 'delete': { method: 'DELETE', path: '/v1/event-processor-mappings/{id}', summary: 'Delete an event type processor mapping', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'events': { method: 'GET', path: '/v1/event-processor-mappings/events', summary: 'List all valid event types', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'list': { method: 'GET', path: '/v1/event-processor-mappings', summary: 'List all event type processor mappings for the tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
13
+ 'create': { method: 'POST', path: '/v1/event-processor-mappings', summary: 'Create event type processor mappings', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['eventTypes', 'includes'] },
14
+ 'update': { method: 'PATCH', path: '/v1/event-processor-mappings/{id}', summary: 'Update an event type processor mapping', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['includes'] },
15
+ 'delete': { method: 'DELETE', path: '/v1/event-processor-mappings/{id}', summary: 'Delete an event type processor mapping', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
16
  };
17
17
  export default class EventProcessorMappings extends Command {
18
18
  static description = 'Manage event processor mappings';
@@ -263,10 +263,11 @@ export default class EventProcessorMappings extends Command {
263
263
  process.stderr.write('\n');
264
264
  }
265
265
  try {
266
- // Binary response: download to file
266
+ // Binary response: to -f / --file, or to stdout when it is redirected.
267
267
  if (endpoint.isBinaryResponse) {
268
- if (!flags.file) {
269
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
268
+ const toStdout = !flags.file;
269
+ if (toStdout && process.stdout.isTTY) {
270
+ 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.`);
270
271
  }
271
272
  const binResponse = await apiRequest(endpoint.method, apiPath, {
272
273
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -274,24 +275,55 @@ export default class EventProcessorMappings extends Command {
274
275
  contextName: flags.context,
275
276
  binary: true,
276
277
  });
277
- const fsModule = await import('node:fs');
278
278
  const binData = binResponse.data;
279
279
  const buffer = Buffer.isBuffer(binData)
280
280
  ? binData
281
281
  : Buffer.from(binData);
282
+ if (toStdout) {
283
+ // Nothing else may reach stdout here — it is the file.
284
+ process.stdout.write(buffer);
285
+ return;
286
+ }
287
+ const fsModule = await import('node:fs');
282
288
  fsModule.writeFileSync(flags.file, buffer);
283
289
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
284
290
  return;
285
291
  }
286
- const response = await apiRequest(endpoint.method, apiPath, {
287
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
288
- data,
289
- contextName: flags.context,
290
- acceptAny: true,
291
- });
292
+ let response;
293
+ if (endpoint.hasBinaryVariant) {
294
+ // This route answers JSON or a file depending on the request body, so the
295
+ // bytes are read first and the response's own content-type decides. Asking
296
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
297
+ const raw = await apiRequest(endpoint.method, apiPath, {
298
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
299
+ data,
300
+ contextName: flags.context,
301
+ binary: true,
302
+ });
303
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
304
+ const rawData = raw.data;
305
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
306
+ if (!isTextualMediaType(rawType)) {
307
+ if (process.stdout.isTTY) {
308
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
309
+ }
310
+ // Nothing else may reach stdout here — it is the file.
311
+ process.stdout.write(buffer);
312
+ return;
313
+ }
314
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
315
+ }
316
+ else {
317
+ response = await apiRequest(endpoint.method, apiPath, {
318
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
319
+ data,
320
+ contextName: flags.context,
321
+ acceptAny: true,
322
+ });
323
+ }
292
324
  // Check content-type for non-JSON responses (XML, text, etc.)
293
325
  const contentType = (response.headers['content-type'] || '').toLowerCase();
294
- if (contentType.includes('xml') || contentType.includes('text/')) {
326
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
295
327
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
296
328
  this.log(String(response.data));
297
329
  return;