@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 = true;
@@ -8,18 +8,18 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["list", "create", "download-all", "download", "get", "references", "update", "discard", "move", "retention", "hold", "reclassify"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/files', summary: 'List files (paginated, permission-scoped)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/files', summary: 'Upload a file (streaming, memory-efficient)', pathParams: [], hasBody: false, isMultipart: true, isBinaryResponse: false, bodyArrayFields: [] },
13
- 'download-all': { method: 'GET', path: '/v1/files/download/*', summary: 'Download file directly by storage key', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'download': { method: 'GET', path: '/v1/files/{id}/download', summary: 'Get a signed download URL for a file', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
- 'get': { method: 'GET', path: '/v1/files/{id}', summary: 'Get a file by id (permission-scoped)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
- 'references': { method: 'POST', path: '/v1/files/references', summary: 'Register a file reference to an existing storage object', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
17
- 'update': { method: 'PATCH', path: '/v1/files/{fileId}', summary: 'Update file metadata (type, parent, sortOrder)', pathParams: ['fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['tags', 'references'] },
18
- 'discard': { method: 'POST', path: '/v1/files/{fileId}/discard', summary: 'Discard a file (sets status DISCARDED; stays readable by id)', pathParams: ['fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
19
- 'move': { method: 'POST', path: '/v1/files/{fileId}/move', summary: 'Move file between storage zones', pathParams: ['fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
20
- 'retention': { method: 'POST', path: '/v1/files/{fileId}/retention', summary: 'Set or override retention period on a file', pathParams: ['fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
21
- 'hold': { method: 'POST', path: '/v1/files/{fileId}/hold', summary: 'Set or release legal hold on a file', pathParams: ['fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
22
- 'reclassify': { method: 'POST', path: '/v1/files/{fileId}/reclassify', summary: 'Reclassify a file and reprocess it through the inbox pipeline', pathParams: ['fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list': { method: 'GET', path: '/v1/files', summary: 'List files (paginated, permission-scoped)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/files', summary: 'Upload a file (streaming, memory-efficient)', pathParams: [], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
13
+ 'download-all': { method: 'GET', path: '/v1/files/download/*', summary: 'Download file directly by storage key', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [] },
14
+ 'download': { method: 'GET', path: '/v1/files/{id}/download', summary: 'Get a signed download URL for a file', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'get': { method: 'GET', path: '/v1/files/{id}', summary: 'Get a file by id (permission-scoped)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
+ 'references': { method: 'POST', path: '/v1/files/references', summary: 'Register a file reference to an existing storage object', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
+ 'update': { method: 'PATCH', path: '/v1/files/{fileId}', summary: 'Update file metadata (type, parent, sortOrder)', pathParams: ['fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['tags', 'references'] },
18
+ 'discard': { method: 'POST', path: '/v1/files/{fileId}/discard', summary: 'Discard a file (sets status DISCARDED; stays readable by id)', pathParams: ['fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
+ 'move': { method: 'POST', path: '/v1/files/{fileId}/move', summary: 'Move file between storage zones', pathParams: ['fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
+ 'retention': { method: 'POST', path: '/v1/files/{fileId}/retention', summary: 'Set or override retention period on a file', pathParams: ['fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
+ 'hold': { method: 'POST', path: '/v1/files/{fileId}/hold', summary: 'Set or release legal hold on a file', pathParams: ['fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
22
+ 'reclassify': { method: 'POST', path: '/v1/files/{fileId}/reclassify', summary: 'Reclassify a file and reprocess it through the inbox pipeline', pathParams: ['fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
23
23
  };
24
24
  export default class Files extends Command {
25
25
  static description = 'Manage files';
@@ -271,10 +271,11 @@ export default class Files extends Command {
271
271
  process.stderr.write('\n');
272
272
  }
273
273
  try {
274
- // Binary response: download to file
274
+ // Binary response: to -f / --file, or to stdout when it is redirected.
275
275
  if (endpoint.isBinaryResponse) {
276
- if (!flags.file) {
277
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
276
+ const toStdout = !flags.file;
277
+ if (toStdout && process.stdout.isTTY) {
278
+ 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.`);
278
279
  }
279
280
  const binResponse = await apiRequest(endpoint.method, apiPath, {
280
281
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -282,24 +283,55 @@ export default class Files extends Command {
282
283
  contextName: flags.context,
283
284
  binary: true,
284
285
  });
285
- const fsModule = await import('node:fs');
286
286
  const binData = binResponse.data;
287
287
  const buffer = Buffer.isBuffer(binData)
288
288
  ? binData
289
289
  : Buffer.from(binData);
290
+ if (toStdout) {
291
+ // Nothing else may reach stdout here — it is the file.
292
+ process.stdout.write(buffer);
293
+ return;
294
+ }
295
+ const fsModule = await import('node:fs');
290
296
  fsModule.writeFileSync(flags.file, buffer);
291
297
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
292
298
  return;
293
299
  }
294
- const response = await apiRequest(endpoint.method, apiPath, {
295
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
296
- data,
297
- contextName: flags.context,
298
- acceptAny: true,
299
- });
300
+ let response;
301
+ if (endpoint.hasBinaryVariant) {
302
+ // This route answers JSON or a file depending on the request body, so the
303
+ // bytes are read first and the response's own content-type decides. Asking
304
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
305
+ const raw = await apiRequest(endpoint.method, apiPath, {
306
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
307
+ data,
308
+ contextName: flags.context,
309
+ binary: true,
310
+ });
311
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
312
+ const rawData = raw.data;
313
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
314
+ if (!isTextualMediaType(rawType)) {
315
+ if (process.stdout.isTTY) {
316
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
317
+ }
318
+ // Nothing else may reach stdout here — it is the file.
319
+ process.stdout.write(buffer);
320
+ return;
321
+ }
322
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
323
+ }
324
+ else {
325
+ response = await apiRequest(endpoint.method, apiPath, {
326
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
327
+ data,
328
+ contextName: flags.context,
329
+ acceptAny: true,
330
+ });
331
+ }
300
332
  // Check content-type for non-JSON responses (XML, text, etc.)
301
333
  const contentType = (response.headers['content-type'] || '').toLowerCase();
302
- if (contentType.includes('xml') || contentType.includes('text/')) {
334
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
303
335
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
304
336
  this.log(String(response.data));
305
337
  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,11 +8,11 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["get", "get-all", "ready", "get-3", "v1"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'get': { method: 'GET', path: '/', summary: 'GET /', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'get-all': { method: 'GET', path: '/health', summary: 'GET /health', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
- 'ready': { method: 'GET', path: '/ready', summary: 'GET /ready', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'get-3': { method: 'GET', path: '/v1/status', summary: 'GET /v1/status', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
- 'v1': { method: 'GET', path: '/v1', summary: 'GET /v1', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'get': { method: 'GET', path: '/', summary: 'GET /', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'get-all': { method: 'GET', path: '/health', summary: 'GET /health', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
13
+ 'ready': { method: 'GET', path: '/ready', summary: 'GET /ready', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
+ 'get-3': { method: 'GET', path: '/v1/status', summary: 'GET /v1/status', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'v1': { method: 'GET', path: '/v1', summary: 'GET /v1', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
16
  };
17
17
  export default class Health extends Command {
18
18
  static description = 'Manage health';
@@ -259,10 +259,11 @@ export default class Health extends Command {
259
259
  process.stderr.write('\n');
260
260
  }
261
261
  try {
262
- // Binary response: download to file
262
+ // Binary response: to -f / --file, or to stdout when it is redirected.
263
263
  if (endpoint.isBinaryResponse) {
264
- if (!flags.file) {
265
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
264
+ const toStdout = !flags.file;
265
+ if (toStdout && process.stdout.isTTY) {
266
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
266
267
  }
267
268
  const binResponse = await apiRequest(endpoint.method, apiPath, {
268
269
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -270,24 +271,55 @@ export default class Health extends Command {
270
271
  contextName: flags.context,
271
272
  binary: true,
272
273
  });
273
- const fsModule = await import('node:fs');
274
274
  const binData = binResponse.data;
275
275
  const buffer = Buffer.isBuffer(binData)
276
276
  ? binData
277
277
  : Buffer.from(binData);
278
+ if (toStdout) {
279
+ // Nothing else may reach stdout here — it is the file.
280
+ process.stdout.write(buffer);
281
+ return;
282
+ }
283
+ const fsModule = await import('node:fs');
278
284
  fsModule.writeFileSync(flags.file, buffer);
279
285
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
280
286
  return;
281
287
  }
282
- const response = await apiRequest(endpoint.method, apiPath, {
283
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
284
- data,
285
- contextName: flags.context,
286
- acceptAny: true,
287
- });
288
+ let response;
289
+ if (endpoint.hasBinaryVariant) {
290
+ // This route answers JSON or a file depending on the request body, so the
291
+ // bytes are read first and the response's own content-type decides. Asking
292
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
293
+ const raw = await apiRequest(endpoint.method, apiPath, {
294
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
295
+ data,
296
+ contextName: flags.context,
297
+ binary: true,
298
+ });
299
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
300
+ const rawData = raw.data;
301
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
302
+ if (!isTextualMediaType(rawType)) {
303
+ if (process.stdout.isTTY) {
304
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
305
+ }
306
+ // Nothing else may reach stdout here — it is the file.
307
+ process.stdout.write(buffer);
308
+ return;
309
+ }
310
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
311
+ }
312
+ else {
313
+ response = await apiRequest(endpoint.method, apiPath, {
314
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
315
+ data,
316
+ contextName: flags.context,
317
+ acceptAny: true,
318
+ });
319
+ }
288
320
  // Check content-type for non-JSON responses (XML, text, etc.)
289
321
  const contentType = (response.headers['content-type'] || '').toLowerCase();
290
- if (contentType.includes('xml') || contentType.includes('text/')) {
322
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
291
323
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
292
324
  this.log(String(response.data));
293
325
  return;
@@ -0,0 +1,39 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Imports extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ action: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ id: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
8
+ };
9
+ static flags: {
10
+ context: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ table: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ data: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ set: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ page: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
+ limit: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ include: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
+ search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
+ 'sort-by': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
22
+ filter: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
23
+ query: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
24
+ fields: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
25
+ sanitize: import("@oclif/core/interfaces").BooleanFlag<boolean>;
26
+ 'show-http-request': import("@oclif/core/interfaces").BooleanFlag<boolean>;
27
+ };
28
+ run(): Promise<void>;
29
+ private runImport;
30
+ private runExport;
31
+ private runCamt053Import;
32
+ private runNestedImport;
33
+ private parseFilterValue;
34
+ private sanitizeOutput;
35
+ private getColumns;
36
+ private getValue;
37
+ private printTable;
38
+ private printCsv;
39
+ }
@@ -1,22 +1,26 @@
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;
6
- const HAS_EXPORT = false;
6
+ const HAS_EXPORT = true;
7
7
  const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
- const SUBCOMMANDS = [...["providers", "connect-sessions", "list-connections", "delete-connections", "webhooks"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
9
+ const SUBCOMMANDS = [...["list", "create", "get", "delete"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'providers': { method: 'GET', path: '/v1/nango/providers', summary: 'List the providers a tenant can connect', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'connect-sessions': { method: 'POST', path: '/v1/nango/connect-sessions', summary: 'Create a Nango Connect session so the user can connect an external account', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
- 'list-connections': { method: 'GET', path: '/v1/nango/connections', summary: 'List the tenant\'s Nango connections', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'delete-connections': { method: 'DELETE', path: '/v1/nango/connections/{id}', summary: 'Disconnect: delete the connection in Nango (discarding the stored credentials) and mark it REVOKED', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
- 'webhooks': { method: 'POST', path: '/v1/nango/webhooks', summary: 'Receive Nango webhooks (auth and sync)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list': { method: 'GET', path: '/v1/imports', summary: 'List the tenant\'s import runs', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/imports', summary: 'Import records of one resource type', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
13
+ 'get': { method: 'GET', path: '/v1/imports/{id}', summary: 'Read one import run back', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
+ 'delete': { method: 'DELETE', path: '/v1/imports/{id}', summary: 'Throw one import run away', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
15
  };
17
- export default class NangoIntegrations extends Command {
18
- static description = 'Manage nango integrations';
19
- static examples = [];
16
+ export default class Imports extends Command {
17
+ static description = 'Manage imports';
18
+ static examples = [
19
+ '<%= config.bin %> imports list',
20
+ '<%= config.bin %> imports get <id>',
21
+ '<%= config.bin %> imports create --data \'{...}\'',
22
+ '<%= config.bin %> imports export --csv > imports.csv',
23
+ ];
20
24
  static args = {
21
25
  action: Args.string({ description: 'Action: ' + SUBCOMMANDS.join(', '), required: true }),
22
26
  id: Args.string({ description: 'Resource ID (for get, update, delete operations)', required: false }),
@@ -41,7 +45,7 @@ export default class NangoIntegrations extends Command {
41
45
  'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
42
46
  };
43
47
  async run() {
44
- const { args, flags } = await this.parse(NangoIntegrations);
48
+ const { args, flags } = await this.parse(Imports);
45
49
  // Handle import action specially
46
50
  if (args.action === 'import') {
47
51
  if (!HAS_IMPORT)
@@ -82,7 +86,7 @@ export default class NangoIntegrations extends Command {
82
86
  // First path param always comes from positional argument
83
87
  value = args.id;
84
88
  if (!value)
85
- this.error('Missing required argument: id. Usage: aderis nango-integrations <action> <id>');
89
+ this.error('Missing required argument: id. Usage: aderis imports <action> <id>');
86
90
  }
87
91
  else {
88
92
  const flagName = param.replace(/\./g, '-').replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
@@ -257,10 +261,11 @@ export default class NangoIntegrations extends Command {
257
261
  process.stderr.write('\n');
258
262
  }
259
263
  try {
260
- // Binary response: download to file
264
+ // Binary response: to -f / --file, or to stdout when it is redirected.
261
265
  if (endpoint.isBinaryResponse) {
262
- if (!flags.file) {
263
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
266
+ const toStdout = !flags.file;
267
+ if (toStdout && process.stdout.isTTY) {
268
+ 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.`);
264
269
  }
265
270
  const binResponse = await apiRequest(endpoint.method, apiPath, {
266
271
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -268,24 +273,55 @@ export default class NangoIntegrations extends Command {
268
273
  contextName: flags.context,
269
274
  binary: true,
270
275
  });
271
- const fsModule = await import('node:fs');
272
276
  const binData = binResponse.data;
273
277
  const buffer = Buffer.isBuffer(binData)
274
278
  ? binData
275
279
  : Buffer.from(binData);
280
+ if (toStdout) {
281
+ // Nothing else may reach stdout here — it is the file.
282
+ process.stdout.write(buffer);
283
+ return;
284
+ }
285
+ const fsModule = await import('node:fs');
276
286
  fsModule.writeFileSync(flags.file, buffer);
277
287
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
278
288
  return;
279
289
  }
280
- const response = await apiRequest(endpoint.method, apiPath, {
281
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
282
- data,
283
- contextName: flags.context,
284
- acceptAny: true,
285
- });
290
+ let response;
291
+ if (endpoint.hasBinaryVariant) {
292
+ // This route answers JSON or a file depending on the request body, so the
293
+ // bytes are read first and the response's own content-type decides. Asking
294
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
295
+ const raw = await apiRequest(endpoint.method, apiPath, {
296
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
297
+ data,
298
+ contextName: flags.context,
299
+ binary: true,
300
+ });
301
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
302
+ const rawData = raw.data;
303
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
304
+ if (!isTextualMediaType(rawType)) {
305
+ if (process.stdout.isTTY) {
306
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
307
+ }
308
+ // Nothing else may reach stdout here — it is the file.
309
+ process.stdout.write(buffer);
310
+ return;
311
+ }
312
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
313
+ }
314
+ else {
315
+ response = await apiRequest(endpoint.method, apiPath, {
316
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
317
+ data,
318
+ contextName: flags.context,
319
+ acceptAny: true,
320
+ });
321
+ }
286
322
  // Check content-type for non-JSON responses (XML, text, etc.)
287
323
  const contentType = (response.headers['content-type'] || '').toLowerCase();
288
- if (contentType.includes('xml') || contentType.includes('text/')) {
324
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
289
325
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
290
326
  this.log(String(response.data));
291
327
  return;
@@ -725,7 +761,7 @@ export default class NangoIntegrations extends Command {
725
761
  }
726
762
  async runNestedImport(suffix, parentId, flags) {
727
763
  if (!parentId) {
728
- this.error(`Missing required argument: id. Usage: aderis nango-integrations import-${suffix} <parent-id> --file data.json`);
764
+ this.error(`Missing required argument: id. Usage: aderis imports import-${suffix} <parent-id> --file data.json`);
729
765
  }
730
766
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
731
767
  if (!context)
@@ -1,19 +1,20 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
- import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
2
+ import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
4
  // Add 'import' action if both create and update exist
5
5
  const HAS_IMPORT = false;
6
6
  const HAS_EXPORT = false;
7
7
  const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
- const SUBCOMMANDS = [...["create-upload", "email-webhook", "document-types", "uploads-files-content", "list-processing-logs", "processing-logs"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
9
+ const SUBCOMMANDS = [...["create-upload", "email-webhook", "document-types", "uploads-files-content", "import", "list-processing-logs", "processing-logs"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'create-upload': { method: 'POST', path: '/v1/inbox/upload', summary: 'Upload document(s) to inbox for processing', pathParams: [], hasBody: false, isMultipart: true, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'email-webhook': { method: 'POST', path: '/v1/inbox/email-webhook/{secret}', summary: 'Email webhook for document ingestion (SendGrid inbound-parse)', pathParams: ['secret'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
- 'document-types': { method: 'GET', path: '/v1/inbox/document-types', summary: 'Get available document types and per-parent allowed types', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'uploads-files-content': { method: 'GET', path: '/v1/inbox/uploads/{processingId}/files/{fileId}/content', summary: 'Stream the original bytes of an uploaded inbox file', pathParams: ['fileId', 'processingId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
- 'list-processing-logs': { method: 'GET', path: '/v1/inbox/processing-logs', summary: 'List recent inbox processing logs (newest first)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
- 'processing-logs': { method: 'GET', path: '/v1/inbox/processing-logs/{processingId}', summary: 'Get a processing run by its processing identifier', pathParams: ['processingId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'create-upload': { method: 'POST', path: '/v1/inbox/upload', summary: 'Upload document(s) to inbox for processing', pathParams: [], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'email-webhook': { method: 'POST', path: '/v1/inbox/email-webhook/{secret}', summary: 'Email webhook for document ingestion (SendGrid inbound-parse)', pathParams: ['secret'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
13
+ 'document-types': { method: 'GET', path: '/v1/inbox/document-types', summary: 'Get available document types and per-parent allowed types', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
+ 'uploads-files-content': { method: 'GET', path: '/v1/inbox/uploads/{processingId}/files/{fileId}/content', summary: 'Stream the original bytes of an uploaded inbox file', pathParams: ['fileId', 'processingId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'import': { method: 'POST', path: '/v1/inbox/import', summary: 'Import a provider-identified document into the inbox, refusing duplicates', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
+ 'list-processing-logs': { method: 'GET', path: '/v1/inbox/processing-logs', summary: 'List recent inbox processing logs (newest first)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
+ 'processing-logs': { method: 'GET', path: '/v1/inbox/processing-logs/{processingId}', summary: 'Get a processing run by its processing identifier', pathParams: ['processingId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
18
  };
18
19
  export default class Inbox extends Command {
19
20
  static description = 'Manage inbox';
@@ -259,10 +260,11 @@ export default class Inbox extends Command {
259
260
  process.stderr.write('\n');
260
261
  }
261
262
  try {
262
- // Binary response: download to file
263
+ // Binary response: to -f / --file, or to stdout when it is redirected.
263
264
  if (endpoint.isBinaryResponse) {
264
- if (!flags.file) {
265
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
265
+ const toStdout = !flags.file;
266
+ if (toStdout && process.stdout.isTTY) {
267
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
266
268
  }
267
269
  const binResponse = await apiRequest(endpoint.method, apiPath, {
268
270
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -270,24 +272,55 @@ export default class Inbox extends Command {
270
272
  contextName: flags.context,
271
273
  binary: true,
272
274
  });
273
- const fsModule = await import('node:fs');
274
275
  const binData = binResponse.data;
275
276
  const buffer = Buffer.isBuffer(binData)
276
277
  ? binData
277
278
  : Buffer.from(binData);
279
+ if (toStdout) {
280
+ // Nothing else may reach stdout here — it is the file.
281
+ process.stdout.write(buffer);
282
+ return;
283
+ }
284
+ const fsModule = await import('node:fs');
278
285
  fsModule.writeFileSync(flags.file, buffer);
279
286
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
280
287
  return;
281
288
  }
282
- const response = await apiRequest(endpoint.method, apiPath, {
283
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
284
- data,
285
- contextName: flags.context,
286
- acceptAny: true,
287
- });
289
+ let response;
290
+ if (endpoint.hasBinaryVariant) {
291
+ // This route answers JSON or a file depending on the request body, so the
292
+ // bytes are read first and the response's own content-type decides. Asking
293
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
294
+ const raw = await apiRequest(endpoint.method, apiPath, {
295
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
296
+ data,
297
+ contextName: flags.context,
298
+ binary: true,
299
+ });
300
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
301
+ const rawData = raw.data;
302
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
303
+ if (!isTextualMediaType(rawType)) {
304
+ if (process.stdout.isTTY) {
305
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
306
+ }
307
+ // Nothing else may reach stdout here — it is the file.
308
+ process.stdout.write(buffer);
309
+ return;
310
+ }
311
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
312
+ }
313
+ else {
314
+ response = await apiRequest(endpoint.method, apiPath, {
315
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
316
+ data,
317
+ contextName: flags.context,
318
+ acceptAny: true,
319
+ });
320
+ }
288
321
  // Check content-type for non-JSON responses (XML, text, etc.)
289
322
  const contentType = (response.headers['content-type'] || '').toLowerCase();
290
- if (contentType.includes('xml') || contentType.includes('text/')) {
323
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
291
324
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
292
325
  this.log(String(response.data));
293
326
  return;
@@ -0,0 +1,40 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class IntegrationIdentities extends Command {
3
+ static description: string;
4
+ static examples: never[];
5
+ static args: {
6
+ action: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ id: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
8
+ };
9
+ static flags: {
10
+ context: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ table: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ data: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ set: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ 'secret-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
+ page: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ limit: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
+ include: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
+ search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
22
+ 'sort-by': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
23
+ filter: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
24
+ query: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
25
+ fields: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
26
+ sanitize: import("@oclif/core/interfaces").BooleanFlag<boolean>;
27
+ 'show-http-request': import("@oclif/core/interfaces").BooleanFlag<boolean>;
28
+ };
29
+ run(): Promise<void>;
30
+ private runImport;
31
+ private runExport;
32
+ private runCamt053Import;
33
+ private runNestedImport;
34
+ private parseFilterValue;
35
+ private sanitizeOutput;
36
+ private getColumns;
37
+ private getValue;
38
+ private printTable;
39
+ private printCsv;
40
+ }