@aderis-erp/cli 1.0.17 → 1.1.1

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 (105) 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/{tag-configurations.d.ts → imports.d.ts} +1 -1
  31. package/dist/commands/{tag-configurations.js → imports.js} +59 -31
  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 +96 -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/{nango-integrations.js → roles.js} +63 -26
  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/support/end.d.ts +9 -0
  68. package/dist/commands/support/end.js +29 -0
  69. package/dist/commands/support/start.d.ts +14 -0
  70. package/dist/commands/support/start.js +50 -0
  71. package/dist/commands/support/status.d.ts +10 -0
  72. package/dist/commands/support/status.js +45 -0
  73. package/dist/commands/support/tenants.d.ts +10 -0
  74. package/dist/commands/support/tenants.js +39 -0
  75. package/dist/commands/tags.js +51 -18
  76. package/dist/commands/tax-codes.js +52 -20
  77. package/dist/commands/tax-matrix.js +49 -17
  78. package/dist/commands/templates.d.ts +39 -0
  79. package/dist/commands/templates.js +1157 -0
  80. package/dist/commands/tenants.js +52 -20
  81. package/dist/commands/tools.js +46 -14
  82. package/dist/commands/users.d.ts +42 -0
  83. package/dist/commands/users.js +1174 -0
  84. package/dist/commands/vat-declarations.d.ts +2 -0
  85. package/dist/commands/vat-declarations.js +72 -36
  86. package/dist/commands/vendor-invoices.js +81 -49
  87. package/dist/commands/webhooks.js +51 -19
  88. package/dist/commands/workflows.js +47 -15
  89. package/dist/lib/api-client.d.ts +6 -0
  90. package/dist/lib/api-client.js +13 -0
  91. package/dist/lib/auth/constants.d.ts +12 -0
  92. package/dist/lib/auth/constants.js +12 -0
  93. package/dist/lib/auth/index.d.ts +11 -1
  94. package/dist/lib/auth/index.js +105 -6
  95. package/dist/lib/auth/jwks.d.ts +19 -0
  96. package/dist/lib/auth/jwks.js +112 -0
  97. package/dist/lib/auth/keycloak-provider.d.ts +45 -7
  98. package/dist/lib/auth/keycloak-provider.js +161 -27
  99. package/dist/lib/auth/tokens.d.ts +8 -3
  100. package/dist/lib/auth/tokens.js +8 -7
  101. package/dist/lib/auth/types.d.ts +5 -1
  102. package/dist/lib/config.d.ts +18 -0
  103. package/dist/lib/config.js +10 -0
  104. package/oclif.manifest.json +2182 -744
  105. package/package.json +7 -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;
@@ -0,0 +1,9 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class SupportEnd extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ context: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ };
8
+ run(): Promise<void>;
9
+ }
@@ -0,0 +1,29 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { endSupportSession } from '../../lib/auth/index.js';
3
+ import { getCurrentContext } from '../../lib/config.js';
4
+ export default class SupportEnd extends Command {
5
+ static description = 'Stop acting as Aderis Support and return to your own account';
6
+ static examples = ['<%= config.bin %> support end', '<%= config.bin %> support end --context prod'];
7
+ static flags = {
8
+ context: Flags.string({
9
+ aliases: ['config'],
10
+ char: 'c',
11
+ description: 'Context to use, overriding the current context (alias: --config)',
12
+ env: 'ADERIS_CONTEXT',
13
+ }),
14
+ };
15
+ async run() {
16
+ const { flags } = await this.parse(SupportEnd);
17
+ const contextName = flags.context ?? getCurrentContext()?.name;
18
+ if (!contextName) {
19
+ this.error('No context configured. Run: aderis auth login --new');
20
+ }
21
+ const ended = await endSupportSession(contextName);
22
+ if (!ended) {
23
+ this.log(`Context ${contextName} was not acting as Aderis Support.`);
24
+ return;
25
+ }
26
+ this.log(`Stopped acting as Aderis Support in ${ended.tenant_name}.`);
27
+ this.log(`Context ${contextName} is your own account again; no new login was needed.`);
28
+ }
29
+ }
@@ -0,0 +1,14 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class SupportStart extends Command {
3
+ static args: {
4
+ tenantId: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ context: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<void>;
12
+ /** Without a tenant id, offer the customers Keycloak says this operator may act for. */
13
+ private chooseTenant;
14
+ }
@@ -0,0 +1,50 @@
1
+ import { select } from '@inquirer/prompts';
2
+ import { Args, Command, Flags } from '@oclif/core';
3
+ import { listSupportTenants, startSupportSession, tenantLabel } from '../../lib/auth/index.js';
4
+ import { getCurrentContext } from '../../lib/config.js';
5
+ export default class SupportStart extends Command {
6
+ static args = {
7
+ tenantId: Args.string({ description: 'Customer to act for; prompted for if omitted', required: false }),
8
+ };
9
+ static description = 'Act as Aderis Support in a customer tenant';
10
+ static examples = [
11
+ '<%= config.bin %> support start',
12
+ '<%= config.bin %> support start 11839c8b-7ec3-4cc1-9dae-396ced09b928',
13
+ '<%= config.bin %> support start <tenant-id> --context prod',
14
+ ];
15
+ static flags = {
16
+ context: Flags.string({
17
+ aliases: ['config'],
18
+ char: 'c',
19
+ description: 'Context to use, overriding the current context (alias: --config)',
20
+ env: 'ADERIS_CONTEXT',
21
+ }),
22
+ };
23
+ async run() {
24
+ const { args, flags } = await this.parse(SupportStart);
25
+ const contextName = flags.context ?? getCurrentContext()?.name;
26
+ if (!contextName) {
27
+ this.error('No context configured. Run: aderis auth login --new');
28
+ }
29
+ const tenantId = args.tenantId ?? (await this.chooseTenant(contextName));
30
+ const support = await startSupportSession(contextName, tenantId);
31
+ this.log(`Acting as Aderis Support in ${support.tenant_name} (${support.tenant_id})`);
32
+ this.log(`Context: ${contextName}`);
33
+ this.warn('Every call from this context now goes out as Aderis Support, and every write is recorded against you. ' +
34
+ 'End with: aderis support end');
35
+ }
36
+ /** Without a tenant id, offer the customers Keycloak says this operator may act for. */
37
+ async chooseTenant(contextName) {
38
+ const tenants = await listSupportTenants(contextName);
39
+ if (tenants.length === 0) {
40
+ this.error('There are no customers you may act for.');
41
+ }
42
+ if (!process.stdin.isTTY) {
43
+ this.error(`Name the customer to act for. List them with: aderis support tenants --context ${contextName}`);
44
+ }
45
+ return select({
46
+ choices: tenants.map((t) => ({ name: tenantLabel(t), value: t.tenantId })),
47
+ message: 'Act as Aderis Support in which customer?',
48
+ });
49
+ }
50
+ }
@@ -0,0 +1,10 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class SupportStatus extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ context: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ };
9
+ run(): Promise<void>;
10
+ }
@@ -0,0 +1,45 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { getContext, getCurrentContext, getSupportSession } from '../../lib/config.js';
3
+ export default class SupportStatus extends Command {
4
+ static description = 'Show whether this context is acting as Aderis Support';
5
+ static examples = ['<%= config.bin %> support status', '<%= config.bin %> support status --context prod'];
6
+ static flags = {
7
+ context: Flags.string({
8
+ aliases: ['config'],
9
+ char: 'c',
10
+ description: 'Context to check, overriding the current context (alias: --config)',
11
+ env: 'ADERIS_CONTEXT',
12
+ }),
13
+ json: Flags.boolean({ default: false, description: 'Output as JSON' }),
14
+ };
15
+ async run() {
16
+ const { flags } = await this.parse(SupportStatus);
17
+ const contextName = flags.context ?? getCurrentContext()?.name;
18
+ if (!contextName) {
19
+ this.error('No context configured. Run: aderis auth login --new');
20
+ }
21
+ const context = getContext(contextName);
22
+ if (!context) {
23
+ this.error(`Context "${contextName}" not found.`);
24
+ }
25
+ const support = getSupportSession(context);
26
+ if (flags.json) {
27
+ this.log(JSON.stringify({ context: contextName, supportSession: support ? supportJson(support) : null }, null, 2));
28
+ return;
29
+ }
30
+ if (!support) {
31
+ this.log(`Context ${contextName} is acting as yourself.`);
32
+ return;
33
+ }
34
+ this.log(`Acting as Aderis Support in ${support.tenant_name} (${support.tenant_id})`);
35
+ this.log(`Context: ${contextName}`);
36
+ if (support.tokens.expires_at) {
37
+ const expires = new Date(support.tokens.expires_at);
38
+ this.log(`Token expires: ${expires.toLocaleString()}${expires < new Date() ? ' (refreshes on next call)' : ''}`);
39
+ }
40
+ this.log('\nEverything you do is recorded against you. End with: aderis support end');
41
+ }
42
+ }
43
+ function supportJson(support) {
44
+ return { expiresAt: support.tokens.expires_at ?? null, tenantId: support.tenant_id, tenantName: support.tenant_name };
45
+ }
@@ -0,0 +1,10 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class SupportTenants extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ context: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ };
9
+ run(): Promise<void>;
10
+ }
@@ -0,0 +1,39 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { listSupportTenants, tenantLabel } from '../../lib/auth/index.js';
3
+ import { getCurrentContext } from '../../lib/config.js';
4
+ export default class SupportTenants extends Command {
5
+ static description = 'List the customers you may act for as Aderis Support';
6
+ static examples = [
7
+ '<%= config.bin %> support tenants',
8
+ '<%= config.bin %> support tenants --context prod --json',
9
+ ];
10
+ static flags = {
11
+ context: Flags.string({
12
+ aliases: ['config'],
13
+ char: 'c',
14
+ description: 'Context to use, overriding the current context (alias: --config)',
15
+ env: 'ADERIS_CONTEXT',
16
+ }),
17
+ json: Flags.boolean({ default: false, description: 'Output as JSON' }),
18
+ };
19
+ async run() {
20
+ const { flags } = await this.parse(SupportTenants);
21
+ const contextName = flags.context ?? getCurrentContext()?.name;
22
+ if (!contextName) {
23
+ this.error('No context configured. Run: aderis auth login --new');
24
+ }
25
+ const tenants = await listSupportTenants(contextName);
26
+ if (flags.json) {
27
+ this.log(JSON.stringify(tenants, null, 2));
28
+ return;
29
+ }
30
+ if (tenants.length === 0) {
31
+ this.log('No customers available to act for.');
32
+ return;
33
+ }
34
+ this.log('Customers you may act for:\n');
35
+ for (const tenant of tenants)
36
+ this.log(` ${tenantLabel(tenant)}`);
37
+ this.log(`\nStart with: aderis support start <tenant-id> --context ${contextName}`);
38
+ }
39
+ }