@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,44 +1,46 @@
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 = ["line-items", "notes", "files"];
9
- const SUBCOMMANDS = [...["list", "create", "create-from-orders", "create-from-invoice", "create-invoice", "get", "update", "delete", "pdf", "submit", "approve", "cancel", "replace", "deliver", "ship", "send", "create-line-items", "update-line-items-reorder", "update-line-items", "delete-line-items", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
9
+ const SUBCOMMANDS = [...["list", "create", "create-from-orders", "create-from-invoice", "create-invoice", "get", "update", "delete", "get-pdf", "create-pdf", "submit", "approve", "cancel", "replace", "copy", "deliver", "ship", "send", "create-line-items", "update-line-items-reorder", "update-line-items", "delete-line-items", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/delivery-notes', summary: 'Get all delivery notes with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/delivery-notes', summary: 'Create a new delivery note', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['files', 'notes', 'tagIds', 'tagCodes', 'lineItems'] },
13
- 'create-from-orders': { method: 'POST', path: '/v1/delivery-notes/create-from-orders', summary: 'Create a DRAFT delivery note from several sales orders', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['salesOrderIds'] },
14
- 'create-from-invoice': { method: 'POST', path: '/v1/delivery-notes/create-from-invoice', summary: 'Create a DRAFT delivery note from a sales invoice', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
- 'create-invoice': { method: 'POST', path: '/v1/delivery-notes/create-invoice', summary: 'Create ONE draft sales invoice from several delivery notes (same customer)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['deliveryNoteIds'] },
16
- 'get': { method: 'GET', path: '/v1/delivery-notes/{id}', summary: 'Get a delivery note by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
17
- 'update': { method: 'PATCH', path: '/v1/delivery-notes/{id}', summary: 'Update a delivery note', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['files', 'notes', 'tagIds', 'tagCodes', 'removeFileIds'] },
18
- 'delete': { method: 'DELETE', path: '/v1/delivery-notes/{id}', summary: 'Delete a delivery note', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
19
- 'pdf': { method: 'GET', path: '/v1/delivery-notes/{id}/pdf', summary: 'Render a delivery note PDF using the configured PDF processor', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
20
- 'submit': { method: 'POST', path: '/v1/delivery-notes/{id}/submit', summary: 'Submit a delivery note for approval (DRAFT → PENDING_APPROVAL)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
21
- 'approve': { method: 'POST', path: '/v1/delivery-notes/{id}/approve', summary: 'Approve a delivery note (DRAFT or PENDING_APPROVAL → APPROVED)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
22
- 'cancel': { method: 'POST', path: '/v1/delivery-notes/{id}/cancel', summary: 'Cancel a delivery note (DRAFT / PENDING_APPROVAL / APPROVED / SHIPPED / DELIVERED → CANCELLED)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
23
- 'replace': { method: 'POST', path: '/v1/delivery-notes/{id}/replace', summary: 'Replace a delivery note (APPROVED / SHIPPED / DELIVERED / CANCELLED → REPLACED)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
24
- 'deliver': { method: 'POST', path: '/v1/delivery-notes/{id}/deliver', summary: 'Mark a delivery note as delivered (SHIPPED → DELIVERED)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
25
- 'ship': { method: 'POST', path: '/v1/delivery-notes/{id}/ship', summary: 'Ship a delivery note (APPROVED → SHIPPED)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['lineItems'] },
26
- 'send': { method: 'POST', path: '/v1/delivery-notes/{id}/send', summary: 'Send a delivery note to its recipient by email', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['emailCc', 'emailBcc'] },
27
- 'create-line-items': { method: 'POST', path: '/v1/delivery-notes/{noteId}/line-items', summary: 'Add a line item to a delivery note', pathParams: ['noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['salesOrderLineItems', 'tagIds', 'tagCodes'] },
28
- 'update-line-items-reorder': { method: 'PATCH', path: '/v1/delivery-notes/{noteId}/line-items/reorder', summary: 'Bulk reorder line items (drag-and-drop)', pathParams: ['noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['items'] },
29
- 'update-line-items': { method: 'PATCH', path: '/v1/delivery-notes/{noteId}/line-items/{lineItemId}', summary: 'Update a line item on a delivery note', pathParams: ['noteId', 'lineItemId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['salesOrderLineItems', 'tagIds', 'tagCodes'] },
30
- 'delete-line-items': { method: 'DELETE', path: '/v1/delivery-notes/{noteId}/line-items/{lineItemId}', summary: 'Remove a line item from a delivery note', pathParams: ['noteId', 'lineItemId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
31
- 'list-notes': { method: 'GET', path: '/v1/delivery-notes/{deliveryNoteId}/notes', summary: 'Get all notes for a delivery note', pathParams: ['deliveryNoteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
32
- 'create-notes': { method: 'POST', path: '/v1/delivery-notes/{deliveryNoteId}/notes', summary: 'Create a note for a delivery note', pathParams: ['deliveryNoteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
33
- 'get-notes': { method: 'GET', path: '/v1/delivery-notes/{deliveryNoteId}/notes/{noteId}', summary: 'Get a specific note for a delivery note', pathParams: ['deliveryNoteId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
34
- 'update-notes': { method: 'PATCH', path: '/v1/delivery-notes/{deliveryNoteId}/notes/{noteId}', summary: 'Update a note for a delivery note', pathParams: ['deliveryNoteId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
35
- 'delete-notes': { method: 'DELETE', path: '/v1/delivery-notes/{deliveryNoteId}/notes/{noteId}', summary: 'Delete a note from a delivery note', pathParams: ['deliveryNoteId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
36
- 'list-files': { method: 'GET', path: '/v1/delivery-notes/{parentId}/files', summary: 'List file attachments for a delivery note', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
37
- 'create-files': { method: 'POST', path: '/v1/delivery-notes/{parentId}/files', summary: 'Upload a file attachment to a delivery note', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, bodyArrayFields: [] },
38
- 'get-files': { method: 'GET', path: '/v1/delivery-notes/{parentId}/files/{fileId}', summary: 'Get a file attachment for a delivery note', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
39
- 'update-files': { method: 'PATCH', path: '/v1/delivery-notes/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a delivery note', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
40
- 'delete-files': { method: 'DELETE', path: '/v1/delivery-notes/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a delivery note', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
41
- 'files-download': { method: 'GET', path: '/v1/delivery-notes/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a delivery note', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list': { method: 'GET', path: '/v1/delivery-notes', summary: 'Get all delivery notes with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/delivery-notes', summary: 'Create a new delivery note', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['files', 'notes', 'tagIds', 'tagCodes', 'lineItems'] },
13
+ 'create-from-orders': { method: 'POST', path: '/v1/delivery-notes/create-from-orders', summary: 'Create a DRAFT delivery note from several sales orders', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['salesOrderIds'] },
14
+ 'create-from-invoice': { method: 'POST', path: '/v1/delivery-notes/create-from-invoice', summary: 'Create a DRAFT delivery note from a sales invoice', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'create-invoice': { method: 'POST', path: '/v1/delivery-notes/create-invoice', summary: 'Create ONE draft sales invoice from several delivery notes (same customer)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['deliveryNoteIds'] },
16
+ 'get': { method: 'GET', path: '/v1/delivery-notes/{id}', summary: 'Get a delivery note by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
+ 'update': { method: 'PATCH', path: '/v1/delivery-notes/{id}', summary: 'Update a delivery note', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['files', 'notes', 'tagIds', 'tagCodes', 'removeFileIds'] },
18
+ 'delete': { method: 'DELETE', path: '/v1/delivery-notes/{id}', summary: 'Delete a delivery note', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
+ 'get-pdf': { method: 'GET', path: '/v1/delivery-notes/{id}/pdf', summary: 'Render a delivery note PDF as the mapped event would, or preview a stored template', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
+ 'create-pdf': { method: 'POST', path: '/v1/delivery-notes/{id}/pdf', summary: 'Render a delivery note PDF from unsaved HTML posted in the body', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
+ 'submit': { method: 'POST', path: '/v1/delivery-notes/{id}/submit', summary: 'Submit a delivery note for approval (DRAFT → PENDING_APPROVAL)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
22
+ 'approve': { method: 'POST', path: '/v1/delivery-notes/{id}/approve', summary: 'Approve a delivery note (DRAFT or PENDING_APPROVAL → APPROVED)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
23
+ 'cancel': { method: 'POST', path: '/v1/delivery-notes/{id}/cancel', summary: 'Cancel a delivery note (DRAFT / PENDING_APPROVAL / APPROVED / SHIPPED / DELIVERED → CANCELLED)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
24
+ 'replace': { method: 'POST', path: '/v1/delivery-notes/{id}/replace', summary: 'Replace a delivery note (APPROVED / SHIPPED / DELIVERED / CANCELLED → REPLACED)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
25
+ 'copy': { method: 'POST', path: '/v1/delivery-notes/{id}/copy', summary: 'Create a copy of a delivery note as a new draft', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
26
+ 'deliver': { method: 'POST', path: '/v1/delivery-notes/{id}/deliver', summary: 'Mark a delivery note as delivered (SHIPPED → DELIVERED)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
27
+ 'ship': { method: 'POST', path: '/v1/delivery-notes/{id}/ship', summary: 'Ship a delivery note (APPROVED → SHIPPED)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lineItems'] },
28
+ 'send': { method: 'POST', path: '/v1/delivery-notes/{id}/send', summary: 'Send a delivery note to its recipient by email', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['emailCc', 'emailBcc'] },
29
+ 'create-line-items': { method: 'POST', path: '/v1/delivery-notes/{noteId}/line-items', summary: 'Add a line item to a delivery note', pathParams: ['noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['salesOrderLineItems', 'tagIds', 'tagCodes'] },
30
+ 'update-line-items-reorder': { method: 'PATCH', path: '/v1/delivery-notes/{noteId}/line-items/reorder', summary: 'Bulk reorder line items (drag-and-drop)', pathParams: ['noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['items'] },
31
+ 'update-line-items': { method: 'PATCH', path: '/v1/delivery-notes/{noteId}/line-items/{lineItemId}', summary: 'Update a line item on a delivery note', pathParams: ['noteId', 'lineItemId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['salesOrderLineItems', 'tagIds', 'tagCodes'] },
32
+ 'delete-line-items': { method: 'DELETE', path: '/v1/delivery-notes/{noteId}/line-items/{lineItemId}', summary: 'Remove a line item from a delivery note', pathParams: ['noteId', 'lineItemId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
33
+ 'list-notes': { method: 'GET', path: '/v1/delivery-notes/{deliveryNoteId}/notes', summary: 'Get all notes for a delivery note', pathParams: ['deliveryNoteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
34
+ 'create-notes': { method: 'POST', path: '/v1/delivery-notes/{deliveryNoteId}/notes', summary: 'Create a note for a delivery note', pathParams: ['deliveryNoteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
35
+ 'get-notes': { method: 'GET', path: '/v1/delivery-notes/{deliveryNoteId}/notes/{noteId}', summary: 'Get a specific note for a delivery note', pathParams: ['deliveryNoteId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
36
+ 'update-notes': { method: 'PATCH', path: '/v1/delivery-notes/{deliveryNoteId}/notes/{noteId}', summary: 'Update a note for a delivery note', pathParams: ['deliveryNoteId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
37
+ 'delete-notes': { method: 'DELETE', path: '/v1/delivery-notes/{deliveryNoteId}/notes/{noteId}', summary: 'Delete a note from a delivery note', pathParams: ['deliveryNoteId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
38
+ 'list-files': { method: 'GET', path: '/v1/delivery-notes/{parentId}/files', summary: 'List file attachments for a delivery note', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
39
+ 'create-files': { method: 'POST', path: '/v1/delivery-notes/{parentId}/files', summary: 'Upload a file attachment to a delivery note', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
40
+ 'get-files': { method: 'GET', path: '/v1/delivery-notes/{parentId}/files/{fileId}', summary: 'Get a file attachment for a delivery note', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
41
+ 'update-files': { method: 'PATCH', path: '/v1/delivery-notes/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a delivery note', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
42
+ 'delete-files': { method: 'DELETE', path: '/v1/delivery-notes/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a delivery note', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
43
+ 'files-download': { method: 'GET', path: '/v1/delivery-notes/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a delivery note', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [] },
42
44
  };
43
45
  export default class DeliveryNotes extends Command {
44
46
  static description = 'Manage delivery notes';
@@ -293,10 +295,11 @@ export default class DeliveryNotes extends Command {
293
295
  process.stderr.write('\n');
294
296
  }
295
297
  try {
296
- // Binary response: download to file
298
+ // Binary response: to -f / --file, or to stdout when it is redirected.
297
299
  if (endpoint.isBinaryResponse) {
298
- if (!flags.file) {
299
- 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.`);
300
303
  }
301
304
  const binResponse = await apiRequest(endpoint.method, apiPath, {
302
305
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -304,24 +307,55 @@ export default class DeliveryNotes extends Command {
304
307
  contextName: flags.context,
305
308
  binary: true,
306
309
  });
307
- const fsModule = await import('node:fs');
308
310
  const binData = binResponse.data;
309
311
  const buffer = Buffer.isBuffer(binData)
310
312
  ? binData
311
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');
312
320
  fsModule.writeFileSync(flags.file, buffer);
313
321
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
314
322
  return;
315
323
  }
316
- const 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
- });
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
+ }
322
356
  // Check content-type for non-JSON responses (XML, text, etc.)
323
357
  const contentType = (response.headers['content-type'] || '').toLowerCase();
324
- if (contentType.includes('xml') || contentType.includes('text/')) {
358
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
325
359
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
326
360
  this.log(String(response.data));
327
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 = true;
@@ -8,11 +8,11 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["list", "create", "get", "update", "delete"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/dunning-configs', summary: 'List all dunning run configs', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/dunning-configs', summary: 'Create a new dunning run config', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['paymentMethodIds', 'accessGroupIds'] },
13
- 'get': { method: 'GET', path: '/v1/dunning-configs/{id}', summary: 'Get a dunning run config by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'update': { method: 'PATCH', path: '/v1/dunning-configs/{id}', summary: 'Update a dunning run config', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['paymentMethodIds', 'accessGroupIds'] },
15
- 'delete': { method: 'DELETE', path: '/v1/dunning-configs/{id}', summary: 'Delete a dunning run config', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list': { method: 'GET', path: '/v1/dunning-configs', summary: 'List all dunning run configs', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/dunning-configs', summary: 'Create a new dunning run config', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['paymentMethodIds', 'accessGroupIds'] },
13
+ 'get': { method: 'GET', path: '/v1/dunning-configs/{id}', summary: 'Get a dunning run config by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
+ 'update': { method: 'PATCH', path: '/v1/dunning-configs/{id}', summary: 'Update a dunning run config', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['paymentMethodIds', 'accessGroupIds'] },
15
+ 'delete': { method: 'DELETE', path: '/v1/dunning-configs/{id}', summary: 'Delete a dunning run config', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
16
  };
17
17
  export default class DunningConfigs extends Command {
18
18
  static description = 'Manage dunning configs';
@@ -264,10 +264,11 @@ export default class DunningConfigs extends Command {
264
264
  process.stderr.write('\n');
265
265
  }
266
266
  try {
267
- // Binary response: download to file
267
+ // Binary response: to -f / --file, or to stdout when it is redirected.
268
268
  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).`);
269
+ const toStdout = !flags.file;
270
+ if (toStdout && process.stdout.isTTY) {
271
+ 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
272
  }
272
273
  const binResponse = await apiRequest(endpoint.method, apiPath, {
273
274
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -275,24 +276,55 @@ export default class DunningConfigs extends Command {
275
276
  contextName: flags.context,
276
277
  binary: true,
277
278
  });
278
- const fsModule = await import('node:fs');
279
279
  const binData = binResponse.data;
280
280
  const buffer = Buffer.isBuffer(binData)
281
281
  ? binData
282
282
  : Buffer.from(binData);
283
+ if (toStdout) {
284
+ // Nothing else may reach stdout here — it is the file.
285
+ process.stdout.write(buffer);
286
+ return;
287
+ }
288
+ const fsModule = await import('node:fs');
283
289
  fsModule.writeFileSync(flags.file, buffer);
284
290
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
285
291
  return;
286
292
  }
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
- });
293
+ let response;
294
+ if (endpoint.hasBinaryVariant) {
295
+ // This route answers JSON or a file depending on the request body, so the
296
+ // bytes are read first and the response's own content-type decides. Asking
297
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
298
+ const raw = await apiRequest(endpoint.method, apiPath, {
299
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
300
+ data,
301
+ contextName: flags.context,
302
+ binary: true,
303
+ });
304
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
305
+ const rawData = raw.data;
306
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
307
+ if (!isTextualMediaType(rawType)) {
308
+ if (process.stdout.isTTY) {
309
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
310
+ }
311
+ // Nothing else may reach stdout here — it is the file.
312
+ process.stdout.write(buffer);
313
+ return;
314
+ }
315
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
316
+ }
317
+ else {
318
+ 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
+ }
293
325
  // Check content-type for non-JSON responses (XML, text, etc.)
294
326
  const contentType = (response.headers['content-type'] || '').toLowerCase();
295
- if (contentType.includes('xml') || contentType.includes('text/')) {
327
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
296
328
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
297
329
  this.log(String(response.data));
298
330
  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,14 +8,14 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["pending", "ongoing", "cancelled", "get", "update", "list", "approve", "cancel"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'pending': { method: 'GET', path: '/v1/dunning-reminders/pending', summary: 'Get pending reminders (awaiting approval) with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'ongoing': { method: 'GET', path: '/v1/dunning-reminders/ongoing', summary: 'Get ongoing reminders (pending, sent, or failed) with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
- 'cancelled': { method: 'GET', path: '/v1/dunning-reminders/cancelled', summary: 'Get cancelled reminders with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'get': { method: 'GET', path: '/v1/dunning-reminders/{id}', summary: 'Get a single dunning reminder by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
- 'update': { method: 'PATCH', path: '/v1/dunning-reminders/{id}', summary: 'Update recipient or action on a dunning reminder', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
- 'list': { method: 'GET', path: '/v1/dunning-reminders', summary: 'Get all dunning reminders with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
17
- 'approve': { method: 'POST', path: '/v1/dunning-reminders/{reminderId}/approve', summary: 'Approve a dunning reminder for sending', pathParams: ['reminderId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
18
- 'cancel': { method: 'POST', path: '/v1/dunning-reminders/{reminderId}/cancel', summary: 'Cancel a dunning reminder', pathParams: ['reminderId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'pending': { method: 'GET', path: '/v1/dunning-reminders/pending', summary: 'Get pending reminders (awaiting approval) with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'ongoing': { method: 'GET', path: '/v1/dunning-reminders/ongoing', summary: 'Get ongoing reminders (pending, sent, or failed) with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
13
+ 'cancelled': { method: 'GET', path: '/v1/dunning-reminders/cancelled', summary: 'Get cancelled reminders with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
+ 'get': { method: 'GET', path: '/v1/dunning-reminders/{id}', summary: 'Get a single dunning reminder by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'update': { method: 'PATCH', path: '/v1/dunning-reminders/{id}', summary: 'Update recipient or action on a dunning reminder', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
+ 'list': { method: 'GET', path: '/v1/dunning-reminders', summary: 'Get all dunning reminders with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
+ 'approve': { method: 'POST', path: '/v1/dunning-reminders/{reminderId}/approve', summary: 'Approve a dunning reminder for sending', pathParams: ['reminderId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
+ 'cancel': { method: 'POST', path: '/v1/dunning-reminders/{reminderId}/cancel', summary: 'Cancel a dunning reminder', pathParams: ['reminderId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
19
  };
20
20
  export default class DunningReminders extends Command {
21
21
  static description = 'Manage dunning reminders';
@@ -265,10 +265,11 @@ export default class DunningReminders extends Command {
265
265
  process.stderr.write('\n');
266
266
  }
267
267
  try {
268
- // Binary response: download to file
268
+ // Binary response: to -f / --file, or to stdout when it is redirected.
269
269
  if (endpoint.isBinaryResponse) {
270
- if (!flags.file) {
271
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
270
+ const toStdout = !flags.file;
271
+ if (toStdout && process.stdout.isTTY) {
272
+ 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.`);
272
273
  }
273
274
  const binResponse = await apiRequest(endpoint.method, apiPath, {
274
275
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -276,24 +277,55 @@ export default class DunningReminders extends Command {
276
277
  contextName: flags.context,
277
278
  binary: true,
278
279
  });
279
- const fsModule = await import('node:fs');
280
280
  const binData = binResponse.data;
281
281
  const buffer = Buffer.isBuffer(binData)
282
282
  ? binData
283
283
  : Buffer.from(binData);
284
+ if (toStdout) {
285
+ // Nothing else may reach stdout here — it is the file.
286
+ process.stdout.write(buffer);
287
+ return;
288
+ }
289
+ const fsModule = await import('node:fs');
284
290
  fsModule.writeFileSync(flags.file, buffer);
285
291
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
286
292
  return;
287
293
  }
288
- const response = await apiRequest(endpoint.method, apiPath, {
289
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
290
- data,
291
- contextName: flags.context,
292
- acceptAny: true,
293
- });
294
+ let response;
295
+ if (endpoint.hasBinaryVariant) {
296
+ // This route answers JSON or a file depending on the request body, so the
297
+ // bytes are read first and the response's own content-type decides. Asking
298
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
299
+ const raw = await apiRequest(endpoint.method, apiPath, {
300
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
301
+ data,
302
+ contextName: flags.context,
303
+ binary: true,
304
+ });
305
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
306
+ const rawData = raw.data;
307
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
308
+ if (!isTextualMediaType(rawType)) {
309
+ if (process.stdout.isTTY) {
310
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
311
+ }
312
+ // Nothing else may reach stdout here — it is the file.
313
+ process.stdout.write(buffer);
314
+ return;
315
+ }
316
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
317
+ }
318
+ else {
319
+ response = await apiRequest(endpoint.method, apiPath, {
320
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
321
+ data,
322
+ contextName: flags.context,
323
+ acceptAny: true,
324
+ });
325
+ }
294
326
  // Check content-type for non-JSON responses (XML, text, etc.)
295
327
  const contentType = (response.headers['content-type'] || '').toLowerCase();
296
- if (contentType.includes('xml') || contentType.includes('text/')) {
328
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
297
329
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
298
330
  this.log(String(response.data));
299
331
  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,7 +8,7 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["trigger"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'trigger': { method: 'POST', path: '/v1/dunning-runs/trigger', summary: 'Manually trigger a dunning run', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'trigger': { method: 'POST', path: '/v1/dunning-runs/trigger', summary: 'Manually trigger a dunning run', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
12
  };
13
13
  export default class DunningRuns extends Command {
14
14
  static description = 'Manage dunning runs';
@@ -253,10 +253,11 @@ export default class DunningRuns extends Command {
253
253
  process.stderr.write('\n');
254
254
  }
255
255
  try {
256
- // Binary response: download to file
256
+ // Binary response: to -f / --file, or to stdout when it is redirected.
257
257
  if (endpoint.isBinaryResponse) {
258
- if (!flags.file) {
259
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
258
+ const toStdout = !flags.file;
259
+ if (toStdout && process.stdout.isTTY) {
260
+ 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.`);
260
261
  }
261
262
  const binResponse = await apiRequest(endpoint.method, apiPath, {
262
263
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -264,24 +265,55 @@ export default class DunningRuns extends Command {
264
265
  contextName: flags.context,
265
266
  binary: true,
266
267
  });
267
- const fsModule = await import('node:fs');
268
268
  const binData = binResponse.data;
269
269
  const buffer = Buffer.isBuffer(binData)
270
270
  ? binData
271
271
  : Buffer.from(binData);
272
+ if (toStdout) {
273
+ // Nothing else may reach stdout here — it is the file.
274
+ process.stdout.write(buffer);
275
+ return;
276
+ }
277
+ const fsModule = await import('node:fs');
272
278
  fsModule.writeFileSync(flags.file, buffer);
273
279
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
274
280
  return;
275
281
  }
276
- const response = await apiRequest(endpoint.method, apiPath, {
277
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
278
- data,
279
- contextName: flags.context,
280
- acceptAny: true,
281
- });
282
+ let response;
283
+ if (endpoint.hasBinaryVariant) {
284
+ // This route answers JSON or a file depending on the request body, so the
285
+ // bytes are read first and the response's own content-type decides. Asking
286
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
287
+ const raw = await apiRequest(endpoint.method, apiPath, {
288
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
289
+ data,
290
+ contextName: flags.context,
291
+ binary: true,
292
+ });
293
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
294
+ const rawData = raw.data;
295
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
296
+ if (!isTextualMediaType(rawType)) {
297
+ if (process.stdout.isTTY) {
298
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
299
+ }
300
+ // Nothing else may reach stdout here — it is the file.
301
+ process.stdout.write(buffer);
302
+ return;
303
+ }
304
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
305
+ }
306
+ else {
307
+ response = await apiRequest(endpoint.method, apiPath, {
308
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
309
+ data,
310
+ contextName: flags.context,
311
+ acceptAny: true,
312
+ });
313
+ }
282
314
  // Check content-type for non-JSON responses (XML, text, etc.)
283
315
  const contentType = (response.headers['content-type'] || '').toLowerCase();
284
- if (contentType.includes('xml') || contentType.includes('text/')) {
316
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
285
317
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
286
318
  this.log(String(response.data));
287
319
  return;