@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 = [...["get", "create", "delete"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'get': { method: 'GET', path: '/v1/saved-views', summary: 'List saved views for a page', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/saved-views', summary: 'Create a saved view', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
- 'delete': { method: 'DELETE', path: '/v1/saved-views/{id}', summary: 'Delete a saved view (creator only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'get': { method: 'GET', path: '/v1/saved-views', summary: 'List saved views for a page', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/saved-views', summary: 'Create a saved view', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
13
+ 'delete': { method: 'DELETE', path: '/v1/saved-views/{id}', summary: 'Delete a saved view', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
14
  };
15
15
  export default class SavedViews extends Command {
16
16
  static description = 'Manage saved views';
@@ -258,10 +258,11 @@ export default class SavedViews extends Command {
258
258
  process.stderr.write('\n');
259
259
  }
260
260
  try {
261
- // Binary response: download to file
261
+ // Binary response: to -f / --file, or to stdout when it is redirected.
262
262
  if (endpoint.isBinaryResponse) {
263
- if (!flags.file) {
264
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
263
+ const toStdout = !flags.file;
264
+ if (toStdout && process.stdout.isTTY) {
265
+ 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.`);
265
266
  }
266
267
  const binResponse = await apiRequest(endpoint.method, apiPath, {
267
268
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -269,24 +270,55 @@ export default class SavedViews extends Command {
269
270
  contextName: flags.context,
270
271
  binary: true,
271
272
  });
272
- const fsModule = await import('node:fs');
273
273
  const binData = binResponse.data;
274
274
  const buffer = Buffer.isBuffer(binData)
275
275
  ? binData
276
276
  : Buffer.from(binData);
277
+ if (toStdout) {
278
+ // Nothing else may reach stdout here — it is the file.
279
+ process.stdout.write(buffer);
280
+ return;
281
+ }
282
+ const fsModule = await import('node:fs');
277
283
  fsModule.writeFileSync(flags.file, buffer);
278
284
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
279
285
  return;
280
286
  }
281
- const response = await apiRequest(endpoint.method, apiPath, {
282
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
283
- data,
284
- contextName: flags.context,
285
- acceptAny: true,
286
- });
287
+ let response;
288
+ if (endpoint.hasBinaryVariant) {
289
+ // This route answers JSON or a file depending on the request body, so the
290
+ // bytes are read first and the response's own content-type decides. Asking
291
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
292
+ const raw = await apiRequest(endpoint.method, apiPath, {
293
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
294
+ data,
295
+ contextName: flags.context,
296
+ binary: true,
297
+ });
298
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
299
+ const rawData = raw.data;
300
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
301
+ if (!isTextualMediaType(rawType)) {
302
+ if (process.stdout.isTTY) {
303
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
304
+ }
305
+ // Nothing else may reach stdout here — it is the file.
306
+ process.stdout.write(buffer);
307
+ return;
308
+ }
309
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
310
+ }
311
+ else {
312
+ response = await apiRequest(endpoint.method, apiPath, {
313
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
314
+ data,
315
+ contextName: flags.context,
316
+ acceptAny: true,
317
+ });
318
+ }
287
319
  // Check content-type for non-JSON responses (XML, text, etc.)
288
320
  const contentType = (response.headers['content-type'] || '').toLowerCase();
289
- if (contentType.includes('xml') || contentType.includes('text/')) {
321
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
290
322
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
291
323
  this.log(String(response.data));
292
324
  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/scheduled-processor-mappings', summary: 'List all scheduled processor mappings for the tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/scheduled-processor-mappings', summary: 'Schedule a processor to run on a cron expression', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
- 'get': { method: 'GET', path: '/v1/scheduled-processor-mappings/{id}', summary: 'Get a scheduled processor mapping by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'update': { method: 'PATCH', path: '/v1/scheduled-processor-mappings/{id}', summary: 'Update a scheduled processor mapping', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
- 'delete': { method: 'DELETE', path: '/v1/scheduled-processor-mappings/{id}', summary: 'Delete a scheduled processor mapping and its schedule', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list': { method: 'GET', path: '/v1/scheduled-processor-mappings', summary: 'List all scheduled processor mappings for the tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/scheduled-processor-mappings', summary: 'Schedule a processor to run on a cron expression', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
13
+ 'get': { method: 'GET', path: '/v1/scheduled-processor-mappings/{id}', summary: 'Get a scheduled processor mapping by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
+ 'update': { method: 'PATCH', path: '/v1/scheduled-processor-mappings/{id}', summary: 'Update a scheduled processor mapping', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'delete': { method: 'DELETE', path: '/v1/scheduled-processor-mappings/{id}', summary: 'Delete a scheduled processor mapping and its schedule', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
16
  };
17
17
  export default class ScheduledProcessorMappings extends Command {
18
18
  static description = 'Manage scheduled processor mappings';
@@ -264,10 +264,11 @@ export default class ScheduledProcessorMappings 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 ScheduledProcessorMappings 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 = true;
@@ -8,15 +8,15 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "runs", "pause", "resume", "trigger"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/schedules', summary: 'List schedules with pagination (admin only)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/schedules', summary: 'Create a schedule (admin only)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
- 'get': { method: 'GET', path: '/v1/schedules/{id}', summary: 'Get a schedule by ID (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'update': { method: 'PATCH', path: '/v1/schedules/{id}', summary: 'Update a schedule (admin only)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
- 'delete': { method: 'DELETE', path: '/v1/schedules/{id}', summary: 'Delete a schedule (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
- 'runs': { method: 'GET', path: '/v1/schedules/{id}/runs', summary: 'List a schedule\'s execution history with pagination (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
17
- 'pause': { method: 'POST', path: '/v1/schedules/{id}/pause', summary: 'Pause a schedule so it no longer runs (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
18
- 'resume': { method: 'POST', path: '/v1/schedules/{id}/resume', summary: 'Resume a paused schedule (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
19
- 'trigger': { method: 'POST', path: '/v1/schedules/{id}/trigger', summary: 'Manually run a schedule right now, rate-limited to one accepted trigger per schedule per 60 seconds (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list': { method: 'GET', path: '/v1/schedules', summary: 'List the caller\'s tenant\'s schedules with pagination (admin only)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/schedules', summary: 'Create a schedule (admin only)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
13
+ 'get': { method: 'GET', path: '/v1/schedules/{id}', summary: 'Get a schedule by ID (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
+ 'update': { method: 'PATCH', path: '/v1/schedules/{id}', summary: 'Update a schedule (admin only)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'delete': { method: 'DELETE', path: '/v1/schedules/{id}', summary: 'Delete a schedule (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
+ 'runs': { method: 'GET', path: '/v1/schedules/{id}/runs', summary: 'List a schedule\'s execution history with pagination (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
+ 'pause': { method: 'POST', path: '/v1/schedules/{id}/pause', summary: 'Pause a schedule so it no longer runs (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
+ 'resume': { method: 'POST', path: '/v1/schedules/{id}/resume', summary: 'Resume a paused schedule (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
+ 'trigger': { method: 'POST', path: '/v1/schedules/{id}/trigger', summary: 'Manually run a schedule right now, rate-limited to one accepted trigger per schedule per 60 seconds (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
20
  };
21
21
  export default class Schedules extends Command {
22
22
  static description = 'Manage schedules';
@@ -268,10 +268,11 @@ export default class Schedules extends Command {
268
268
  process.stderr.write('\n');
269
269
  }
270
270
  try {
271
- // Binary response: download to file
271
+ // Binary response: to -f / --file, or to stdout when it is redirected.
272
272
  if (endpoint.isBinaryResponse) {
273
- if (!flags.file) {
274
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
273
+ const toStdout = !flags.file;
274
+ if (toStdout && process.stdout.isTTY) {
275
+ 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.`);
275
276
  }
276
277
  const binResponse = await apiRequest(endpoint.method, apiPath, {
277
278
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -279,24 +280,55 @@ export default class Schedules extends Command {
279
280
  contextName: flags.context,
280
281
  binary: true,
281
282
  });
282
- const fsModule = await import('node:fs');
283
283
  const binData = binResponse.data;
284
284
  const buffer = Buffer.isBuffer(binData)
285
285
  ? binData
286
286
  : Buffer.from(binData);
287
+ if (toStdout) {
288
+ // Nothing else may reach stdout here — it is the file.
289
+ process.stdout.write(buffer);
290
+ return;
291
+ }
292
+ const fsModule = await import('node:fs');
287
293
  fsModule.writeFileSync(flags.file, buffer);
288
294
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
289
295
  return;
290
296
  }
291
- const response = await apiRequest(endpoint.method, apiPath, {
292
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
293
- data,
294
- contextName: flags.context,
295
- acceptAny: true,
296
- });
297
+ let response;
298
+ if (endpoint.hasBinaryVariant) {
299
+ // This route answers JSON or a file depending on the request body, so the
300
+ // bytes are read first and the response's own content-type decides. Asking
301
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
302
+ const raw = await apiRequest(endpoint.method, apiPath, {
303
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
304
+ data,
305
+ contextName: flags.context,
306
+ binary: true,
307
+ });
308
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
309
+ const rawData = raw.data;
310
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
311
+ if (!isTextualMediaType(rawType)) {
312
+ if (process.stdout.isTTY) {
313
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
314
+ }
315
+ // Nothing else may reach stdout here — it is the file.
316
+ process.stdout.write(buffer);
317
+ return;
318
+ }
319
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
320
+ }
321
+ else {
322
+ response = await apiRequest(endpoint.method, apiPath, {
323
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
324
+ data,
325
+ contextName: flags.context,
326
+ acceptAny: true,
327
+ });
328
+ }
297
329
  // Check content-type for non-JSON responses (XML, text, etc.)
298
330
  const contentType = (response.headers['content-type'] || '').toLowerCase();
299
- if (contentType.includes('xml') || contentType.includes('text/')) {
331
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
300
332
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
301
333
  this.log(String(response.data));
302
334
  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,12 +8,12 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "journal-coverage"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/tag-configurations', summary: 'Get all tag configurations with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/tag-configurations', summary: 'Create a new tag configuration', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['resourceTypes', 'accountTypes', 'tagIds', 'tagCodes'] },
13
- 'get': { method: 'GET', path: '/v1/tag-configurations/{id}', summary: 'Get a tag configuration by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'update': { method: 'PATCH', path: '/v1/tag-configurations/{id}', summary: 'Update a tag configuration', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['resourceTypes', 'accountTypes', 'tagIds', 'tagCodes'] },
15
- 'delete': { method: 'DELETE', path: '/v1/tag-configurations/{id}', summary: 'Delete a tag configuration', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
- 'journal-coverage': { method: 'GET', path: '/v1/tag-configurations/{id}/journal-coverage', summary: 'How much of the journal-line amount in scope carries one of this configuration\'s tags', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list': { method: 'GET', path: '/v1/tag-configurations', summary: 'Get all tag configurations with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/tag-configurations', summary: 'Create a new tag configuration', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['resourceTypes', 'accountTypes', 'tagIds', 'tagCodes'] },
13
+ 'get': { method: 'GET', path: '/v1/tag-configurations/{id}', summary: 'Get a tag configuration by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
+ 'update': { method: 'PATCH', path: '/v1/tag-configurations/{id}', summary: 'Update a tag configuration', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['resourceTypes', 'accountTypes', 'tagIds', 'tagCodes'] },
15
+ 'delete': { method: 'DELETE', path: '/v1/tag-configurations/{id}', summary: 'Delete a tag configuration', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
+ 'journal-coverage': { method: 'GET', path: '/v1/tag-configurations/{id}/journal-coverage', summary: 'How much of the journal-line amount in scope carries one of this configuration\'s tags', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
17
  };
18
18
  export default class TagConfigurations extends Command {
19
19
  static description = 'Manage tag configurations';
@@ -265,10 +265,11 @@ export default class TagConfigurations 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 TagConfigurations 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 = 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/tags', summary: 'Get all tags with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/tags', summary: 'Create a new tag', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
- 'get': { method: 'GET', path: '/v1/tags/{id}', summary: 'Get a tag by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'update': { method: 'PATCH', path: '/v1/tags/{id}', summary: 'Update a tag', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
- 'delete': { method: 'DELETE', path: '/v1/tags/{id}', summary: 'Delete a tag', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list': { method: 'GET', path: '/v1/tags', summary: 'Get all tags with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/tags', summary: 'Create a new tag', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
13
+ 'get': { method: 'GET', path: '/v1/tags/{id}', summary: 'Get a tag by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
+ 'update': { method: 'PATCH', path: '/v1/tags/{id}', summary: 'Update a tag', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'delete': { method: 'DELETE', path: '/v1/tags/{id}', summary: 'Delete a tag', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
16
  };
17
17
  export default class Tags extends Command {
18
18
  static description = 'Manage tags';
@@ -264,10 +264,11 @@ export default class Tags 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 Tags 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;