@aderis-erp/cli 1.0.15 → 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 (182) hide show
  1. package/README.md +30 -8
  2. package/dist/commands/access-groups.d.ts +0 -1
  3. package/dist/commands/access-groups.js +54 -78
  4. package/dist/commands/account-transaction-allocations.d.ts +0 -1
  5. package/dist/commands/account-transaction-allocations.js +51 -75
  6. package/dist/commands/account-transactions.d.ts +0 -1
  7. package/dist/commands/account-transactions.js +80 -94
  8. package/dist/commands/accounts.d.ts +0 -1
  9. package/dist/commands/accounts.js +59 -83
  10. package/dist/commands/agent-platform.d.ts +0 -1
  11. package/dist/commands/agent-platform.js +58 -82
  12. package/dist/commands/agent-runs.d.ts +39 -0
  13. package/dist/commands/agent-runs.js +1155 -0
  14. package/dist/commands/analytics.d.ts +0 -1
  15. package/dist/commands/analytics.js +52 -76
  16. package/dist/commands/{text-templates.d.ts → api-keys.d.ts} +1 -2
  17. package/dist/commands/api-keys.js +1150 -0
  18. package/dist/commands/async-jobs.d.ts +39 -0
  19. package/dist/commands/async-jobs.js +1158 -0
  20. package/dist/commands/audit-log.d.ts +0 -1
  21. package/dist/commands/audit-log.js +51 -75
  22. package/dist/commands/auth/login.d.ts +9 -0
  23. package/dist/commands/auth/login.js +73 -20
  24. package/dist/commands/auth/logout.d.ts +4 -0
  25. package/dist/commands/auth/logout.js +4 -0
  26. package/dist/commands/auth/status.js +26 -17
  27. package/dist/commands/auth/tenant.js +3 -6
  28. package/dist/commands/auth.d.ts +0 -1
  29. package/dist/commands/auth.js +5 -34
  30. package/dist/commands/bank-accounts.d.ts +0 -1
  31. package/dist/commands/bank-accounts.js +68 -91
  32. package/dist/commands/bank-integrations.d.ts +1 -1
  33. package/dist/commands/bank-integrations.js +71 -92
  34. package/dist/commands/business-partners.d.ts +1 -1
  35. package/dist/commands/business-partners.js +84 -100
  36. package/dist/commands/chat.d.ts +0 -1
  37. package/dist/commands/chat.js +57 -80
  38. package/dist/commands/config/add.d.ts +3 -0
  39. package/dist/commands/config/add.js +34 -16
  40. package/dist/commands/config/current.js +3 -0
  41. package/dist/commands/config/list.js +3 -0
  42. package/dist/commands/credit-card-accounts.d.ts +1 -1
  43. package/dist/commands/credit-card-accounts.js +72 -90
  44. package/dist/commands/delivery-notes.d.ts +0 -1
  45. package/dist/commands/delivery-notes.js +83 -105
  46. package/dist/commands/dunning-configs.d.ts +0 -1
  47. package/dist/commands/dunning-configs.js +54 -78
  48. package/dist/commands/dunning-reminders.d.ts +0 -1
  49. package/dist/commands/dunning-reminders.js +57 -81
  50. package/dist/commands/dunning-runs.d.ts +0 -1
  51. package/dist/commands/dunning-runs.js +50 -74
  52. package/dist/commands/email-transactions.d.ts +0 -1
  53. package/dist/commands/email-transactions.js +52 -76
  54. package/dist/commands/employees.d.ts +0 -1
  55. package/dist/commands/employees.js +81 -105
  56. package/dist/commands/entity-configurations.d.ts +0 -1
  57. package/dist/commands/entity-configurations.js +52 -76
  58. package/dist/commands/event-processor-mappings.d.ts +0 -1
  59. package/dist/commands/event-processor-mappings.js +54 -78
  60. package/dist/commands/files.d.ts +0 -1
  61. package/dist/commands/files.js +61 -85
  62. package/dist/commands/health.d.ts +0 -1
  63. package/dist/commands/health.js +54 -78
  64. package/dist/commands/imports.d.ts +39 -0
  65. package/dist/commands/imports.js +1151 -0
  66. package/dist/commands/inbox.d.ts +0 -1
  67. package/dist/commands/inbox.js +57 -80
  68. package/dist/commands/integration-identities.d.ts +40 -0
  69. package/dist/commands/integration-identities.js +1151 -0
  70. package/dist/commands/integrations.d.ts +0 -1
  71. package/dist/commands/integrations.js +52 -76
  72. package/dist/commands/item-categories.d.ts +0 -1
  73. package/dist/commands/item-categories.js +54 -78
  74. package/dist/commands/items.d.ts +0 -1
  75. package/dist/commands/items.js +54 -78
  76. package/dist/commands/journal-lines.d.ts +0 -1
  77. package/dist/commands/journal-lines.js +50 -74
  78. package/dist/commands/journals.d.ts +1 -1
  79. package/dist/commands/journals.js +75 -96
  80. package/dist/commands/ledgers.d.ts +3 -2
  81. package/dist/commands/ledgers.js +100 -95
  82. package/dist/commands/my-account.d.ts +39 -0
  83. package/dist/commands/my-account.js +1160 -0
  84. package/dist/commands/office-locations.d.ts +0 -1
  85. package/dist/commands/office-locations.js +54 -78
  86. package/dist/commands/payment-methods.d.ts +0 -1
  87. package/dist/commands/payment-methods.js +59 -83
  88. package/dist/commands/payment-runs.d.ts +0 -1
  89. package/dist/commands/payment-runs.js +66 -90
  90. package/dist/commands/payment-terms.d.ts +0 -1
  91. package/dist/commands/payment-terms.js +55 -79
  92. package/dist/commands/payments.d.ts +0 -1
  93. package/dist/commands/payments.js +56 -80
  94. package/dist/commands/payroll-runs.d.ts +43 -0
  95. package/dist/commands/payroll-runs.js +1188 -0
  96. package/dist/commands/permissions.d.ts +39 -0
  97. package/dist/commands/permissions.js +1146 -0
  98. package/dist/commands/price-lists.d.ts +0 -1
  99. package/dist/commands/price-lists.js +59 -83
  100. package/dist/commands/processor-mappings.d.ts +0 -1
  101. package/dist/commands/processor-mappings.js +53 -77
  102. package/dist/commands/processors.d.ts +0 -1
  103. package/dist/commands/processors.js +60 -85
  104. package/dist/commands/purchase-order-types.d.ts +0 -1
  105. package/dist/commands/purchase-order-types.js +54 -78
  106. package/dist/commands/purchase-orders.d.ts +0 -1
  107. package/dist/commands/purchase-orders.js +74 -97
  108. package/dist/commands/reports.d.ts +0 -1
  109. package/dist/commands/reports.js +54 -78
  110. package/dist/commands/roles.d.ts +39 -0
  111. package/dist/commands/{text-templates.js → roles.js} +64 -90
  112. package/dist/commands/salary-types.d.ts +0 -1
  113. package/dist/commands/salary-types.js +54 -78
  114. package/dist/commands/sales-invoices.d.ts +0 -1
  115. package/dist/commands/sales-invoices.js +90 -110
  116. package/dist/commands/sales-orders.d.ts +0 -1
  117. package/dist/commands/sales-orders.js +83 -105
  118. package/dist/commands/saved-views.d.ts +0 -1
  119. package/dist/commands/saved-views.js +52 -76
  120. package/dist/commands/scheduled-processor-mappings.d.ts +0 -1
  121. package/dist/commands/scheduled-processor-mappings.js +54 -78
  122. package/dist/commands/schedules.d.ts +0 -1
  123. package/dist/commands/schedules.js +58 -82
  124. package/dist/commands/switch.js +4 -4
  125. package/dist/commands/tag-configurations.d.ts +0 -1
  126. package/dist/commands/tag-configurations.js +56 -79
  127. package/dist/commands/tags.d.ts +0 -1
  128. package/dist/commands/tags.js +54 -78
  129. package/dist/commands/tax-codes.d.ts +0 -1
  130. package/dist/commands/tax-codes.js +57 -81
  131. package/dist/commands/tax-matrix.d.ts +0 -1
  132. package/dist/commands/tax-matrix.js +54 -78
  133. package/dist/commands/templates.d.ts +39 -0
  134. package/dist/commands/templates.js +1157 -0
  135. package/dist/commands/tenants.d.ts +0 -1
  136. package/dist/commands/tenants.js +58 -81
  137. package/dist/commands/tools.d.ts +0 -1
  138. package/dist/commands/tools.js +51 -75
  139. package/dist/commands/users.d.ts +42 -0
  140. package/dist/commands/users.js +1174 -0
  141. package/dist/commands/vat-declarations.d.ts +3 -1
  142. package/dist/commands/vat-declarations.js +78 -93
  143. package/dist/commands/vendor-invoices.d.ts +0 -1
  144. package/dist/commands/vendor-invoices.js +87 -110
  145. package/dist/commands/webhooks.d.ts +0 -1
  146. package/dist/commands/webhooks.js +56 -80
  147. package/dist/commands/workflows.d.ts +0 -1
  148. package/dist/commands/workflows.js +52 -76
  149. package/dist/lib/api-client.d.ts +6 -0
  150. package/dist/lib/api-client.js +16 -6
  151. package/dist/lib/auth/constants.d.ts +11 -0
  152. package/dist/lib/auth/constants.js +11 -0
  153. package/dist/lib/auth/errors.d.ts +17 -0
  154. package/dist/lib/auth/errors.js +22 -0
  155. package/dist/lib/auth/flow.d.ts +27 -0
  156. package/dist/lib/auth/flow.js +163 -0
  157. package/dist/lib/auth/guards.d.ts +5 -0
  158. package/dist/lib/auth/guards.js +14 -0
  159. package/dist/lib/auth/http.d.ts +16 -0
  160. package/dist/lib/auth/http.js +49 -0
  161. package/dist/lib/auth/index.d.ts +49 -0
  162. package/dist/lib/auth/index.js +228 -0
  163. package/dist/lib/auth/jwks.d.ts +19 -0
  164. package/dist/lib/auth/jwks.js +112 -0
  165. package/dist/lib/auth/keycloak-provider.d.ts +91 -0
  166. package/dist/lib/auth/keycloak-provider.js +244 -0
  167. package/dist/lib/auth/pages.d.ts +25 -0
  168. package/dist/lib/auth/pages.js +123 -0
  169. package/dist/lib/auth/pkce.d.ts +11 -0
  170. package/dist/lib/auth/pkce.js +17 -0
  171. package/dist/lib/auth/tokens.d.ts +59 -0
  172. package/dist/lib/auth/tokens.js +105 -0
  173. package/dist/lib/auth/types.d.ts +61 -0
  174. package/dist/lib/auth/types.js +1 -0
  175. package/dist/lib/auth/validated-url.d.ts +6 -0
  176. package/dist/lib/auth/validated-url.js +53 -0
  177. package/dist/lib/config.d.ts +13 -3
  178. package/dist/lib/config.js +24 -19
  179. package/oclif.manifest.json +2447 -641
  180. package/package.json +5 -4
  181. package/dist/lib/auth.d.ts +0 -43
  182. package/dist/lib/auth.js +0 -610
@@ -31,7 +31,6 @@ export default class Tenants extends Command {
31
31
  private runCamt053Import;
32
32
  private runNestedImport;
33
33
  private parseFilterValue;
34
- private filterFields;
35
34
  private sanitizeOutput;
36
35
  private getColumns;
37
36
  private getValue;
@@ -1,20 +1,21 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
- import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
2
+ import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
4
  // Add 'import' action if both create and update exist
5
5
  const HAS_IMPORT = true;
6
6
  const HAS_EXPORT = true;
7
7
  const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
- const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "logo", "icon"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
9
+ const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "logo", "icon", "update-identity-provider"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/tenants', summary: 'Get all tenants with pagination (admin only)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/tenants', summary: 'Create a new tenant (admin only)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['supportedLocales', 'allowedSenders', 'salesInvoiceRecipientCc', 'salesInvoiceRecipientBcc', 'salesOrderRecipientCc', 'salesOrderRecipientBcc', 'deliveryNoteRecipientCc', 'deliveryNoteRecipientBcc'] },
13
- 'get': { method: 'GET', path: '/v1/tenants/{id}', summary: 'Get a tenant by ID (own tenant or admin)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'update': { method: 'PATCH', path: '/v1/tenants/{id}', summary: 'Update a tenant (own tenant or admin)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['supportedLocales', 'allowedSenders', 'salesInvoiceRecipientCc', 'salesInvoiceRecipientBcc', 'salesOrderRecipientCc', 'salesOrderRecipientBcc', 'deliveryNoteRecipientCc', 'deliveryNoteRecipientBcc'] },
15
- 'delete': { method: 'DELETE', path: '/v1/tenants/{id}', summary: 'Delete a tenant (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
- 'logo': { method: 'POST', path: '/v1/tenants/{id}/logo', summary: 'Upload the tenant logo to the public bucket and set logoUrl (own tenant or admin)', pathParams: ['id'], hasBody: false, isMultipart: true, isBinaryResponse: false, bodyArrayFields: [] },
17
- 'icon': { method: 'POST', path: '/v1/tenants/{id}/icon', summary: 'Upload the tenant icon to the public bucket and set iconUrl (own tenant or admin)', pathParams: ['id'], hasBody: false, isMultipart: true, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list': { method: 'GET', path: '/v1/tenants', summary: 'Get all tenants with pagination (admin only)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/tenants', summary: 'Create a new tenant (admin only)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['supportedLocales', 'allowedSenders', 'salesInvoiceRecipientCc', 'salesInvoiceRecipientBcc', 'salesOrderRecipientCc', 'salesOrderRecipientBcc', 'deliveryNoteRecipientCc', 'deliveryNoteRecipientBcc'] },
13
+ 'get': { method: 'GET', path: '/v1/tenants/{id}', summary: 'Get a tenant by ID (own tenant or admin)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
+ 'update': { method: 'PATCH', path: '/v1/tenants/{id}', summary: 'Update a tenant (own tenant or admin)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['supportedLocales', 'allowedSenders', 'salesInvoiceRecipientCc', 'salesInvoiceRecipientBcc', 'salesOrderRecipientCc', 'salesOrderRecipientBcc', 'deliveryNoteRecipientCc', 'deliveryNoteRecipientBcc'] },
15
+ 'delete': { method: 'DELETE', path: '/v1/tenants/{id}', summary: 'Delete a tenant (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
+ 'logo': { method: 'POST', path: '/v1/tenants/{id}/logo', summary: 'Upload the tenant logo to the public bucket and set logoUrl (own tenant or admin)', pathParams: ['id'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
+ 'icon': { method: 'POST', path: '/v1/tenants/{id}/icon', summary: 'Upload the tenant icon to the public bucket and set iconUrl (own tenant or admin)', pathParams: ['id'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
+ 'update-identity-provider': { method: 'PUT', path: '/v1/tenants/{id}/identity-provider', summary: 'Synchronize a tenant with the identity provider (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
19
  };
19
20
  export default class Tenants extends Command {
20
21
  static description = 'Manage tenants';
@@ -45,7 +46,7 @@ export default class Tenants extends Command {
45
46
  'sort-by': Flags.string({ description: 'Sort field(s), comma-separated. Direction (:ASC/:DESC) is optional; date-like fields default to DESC, others to ASC.' }),
46
47
  filter: Flags.string({ description: 'Filter (e.g. --filter status=ACTIVE). Operators: in:A,B, like:%, gt:, lt:, null:', multiple: true }),
47
48
  query: Flags.string({ description: 'Additional query params as JSON' }),
48
- fields: Flags.string({ description: 'Comma-separated list of fields to show (e.g. --fields=id,name,status)' }),
49
+ fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
49
50
  sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
50
51
  'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
51
52
  };
@@ -117,6 +118,8 @@ export default class Tenants extends Command {
117
118
  queryParams.search = flags.search;
118
119
  if (flags['sort-by'])
119
120
  queryParams.sortBy = normalizeSortBy(flags['sort-by']);
121
+ if (flags.fields)
122
+ queryParams.fields = flags.fields;
120
123
  if (flags.query) {
121
124
  try {
122
125
  Object.assign(queryParams, JSON.parse(flags.query));
@@ -264,10 +267,11 @@ export default class Tenants extends Command {
264
267
  process.stderr.write('\n');
265
268
  }
266
269
  try {
267
- // Binary response: download to file
270
+ // Binary response: to -f / --file, or to stdout when it is redirected.
268
271
  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).`);
272
+ const toStdout = !flags.file;
273
+ if (toStdout && process.stdout.isTTY) {
274
+ 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
275
  }
272
276
  const binResponse = await apiRequest(endpoint.method, apiPath, {
273
277
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -275,24 +279,55 @@ export default class Tenants extends Command {
275
279
  contextName: flags.context,
276
280
  binary: true,
277
281
  });
278
- const fsModule = await import('node:fs');
279
282
  const binData = binResponse.data;
280
283
  const buffer = Buffer.isBuffer(binData)
281
284
  ? binData
282
285
  : Buffer.from(binData);
286
+ if (toStdout) {
287
+ // Nothing else may reach stdout here — it is the file.
288
+ process.stdout.write(buffer);
289
+ return;
290
+ }
291
+ const fsModule = await import('node:fs');
283
292
  fsModule.writeFileSync(flags.file, buffer);
284
293
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
285
294
  return;
286
295
  }
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
- });
296
+ let response;
297
+ if (endpoint.hasBinaryVariant) {
298
+ // This route answers JSON or a file depending on the request body, so the
299
+ // bytes are read first and the response's own content-type decides. Asking
300
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
301
+ const raw = await apiRequest(endpoint.method, apiPath, {
302
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
303
+ data,
304
+ contextName: flags.context,
305
+ binary: true,
306
+ });
307
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
308
+ const rawData = raw.data;
309
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
310
+ if (!isTextualMediaType(rawType)) {
311
+ if (process.stdout.isTTY) {
312
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
313
+ }
314
+ // Nothing else may reach stdout here — it is the file.
315
+ process.stdout.write(buffer);
316
+ return;
317
+ }
318
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
319
+ }
320
+ else {
321
+ response = await apiRequest(endpoint.method, apiPath, {
322
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
323
+ data,
324
+ contextName: flags.context,
325
+ acceptAny: true,
326
+ });
327
+ }
293
328
  // Check content-type for non-JSON responses (XML, text, etc.)
294
329
  const contentType = (response.headers['content-type'] || '').toLowerCase();
295
- if (contentType.includes('xml') || contentType.includes('text/')) {
330
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
296
331
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
297
332
  this.log(String(response.data));
298
333
  return;
@@ -319,11 +354,6 @@ export default class Tenants extends Command {
319
354
  output = obj[arrayKeys[0]];
320
355
  }
321
356
  }
322
- // Filter fields if --fields flag is provided
323
- if (flags.fields) {
324
- const fieldList = flags.fields.split(',').map(f => f.trim());
325
- output = this.filterFields(output, fieldList);
326
- }
327
357
  // Sanitize output if requested
328
358
  if (flags.sanitize) {
329
359
  output = this.sanitizeOutput(output);
@@ -578,6 +608,8 @@ export default class Tenants extends Command {
578
608
  baseParams.sortBy = normalizeSortBy(flags['sort-by']);
579
609
  if (flags.include)
580
610
  baseParams.include = flags.include;
611
+ if (flags.fields)
612
+ baseParams.fields = flags.fields;
581
613
  if (flags.query) {
582
614
  try {
583
615
  Object.assign(baseParams, JSON.parse(flags.query));
@@ -632,12 +664,7 @@ export default class Tenants extends Command {
632
664
  }
633
665
  }
634
666
  process.stderr.write(' done\n');
635
- // Filter fields if specified
636
667
  let output = allItems;
637
- if (flags.fields) {
638
- const fieldList = flags.fields.split(',').map(f => f.trim());
639
- output = this.filterFields(allItems, fieldList);
640
- }
641
668
  // Sanitize output if requested
642
669
  if (flags.sanitize) {
643
670
  output = this.sanitizeOutput(output);
@@ -966,56 +993,6 @@ export default class Tenants extends Command {
966
993
  return `\$${op}`;
967
994
  return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
968
995
  }
969
- filterFields(data, fields) {
970
- const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
971
- const pick = (obj, keys) => {
972
- // Group keys by their first segment so sibling sub-paths
973
- // (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
974
- // and don't clobber each other on merge.
975
- const directKeys = new Set();
976
- const nestedKeys = new Map();
977
- for (const key of keys) {
978
- if (key.split('.').some(isUnsafeKey))
979
- continue;
980
- if (key.includes('.')) {
981
- const [first, ...rest] = key.split('.');
982
- if (isUnsafeKey(first))
983
- continue;
984
- const list = nestedKeys.get(first);
985
- if (list) {
986
- list.push(rest.join('.'));
987
- }
988
- else {
989
- nestedKeys.set(first, [rest.join('.')]);
990
- }
991
- }
992
- else {
993
- directKeys.add(key);
994
- }
995
- }
996
- const result = {};
997
- for (const key of directKeys) {
998
- if (key in obj)
999
- result[key] = obj[key];
1000
- }
1001
- for (const [first, restKeys] of nestedKeys) {
1002
- const val = obj[first];
1003
- if (!val || typeof val !== 'object')
1004
- continue;
1005
- if (Array.isArray(val)) {
1006
- result[first] = val.map(item => pick(item, restKeys));
1007
- }
1008
- else {
1009
- result[first] = pick(val, restKeys);
1010
- }
1011
- }
1012
- return result;
1013
- };
1014
- if (Array.isArray(data)) {
1015
- return data.map(item => pick(item, fields));
1016
- }
1017
- return pick(data, fields);
1018
- }
1019
996
  sanitizeOutput(data) {
1020
997
  const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
1021
998
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -31,7 +31,6 @@ export default class Tools extends Command {
31
31
  private runCamt053Import;
32
32
  private runNestedImport;
33
33
  private parseFilterValue;
34
- private filterFields;
35
34
  private sanitizeOutput;
36
35
  private getColumns;
37
36
  private getValue;
@@ -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,8 +8,8 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["list", "find"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/tools', summary: 'List all available tools', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'find': { method: 'GET', path: '/v1/tools/{name}', summary: 'Get a single tool by canonical name', pathParams: ['name'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list': { method: 'GET', path: '/v1/tools', summary: 'List all available tools', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'find': { method: 'GET', path: '/v1/tools/{name}', summary: 'Get a single tool by canonical name', pathParams: ['name'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
13
13
  };
14
14
  export default class Tools extends Command {
15
15
  static description = 'Manage tools';
@@ -36,7 +36,7 @@ export default class Tools extends Command {
36
36
  'sort-by': Flags.string({ description: 'Sort field(s), comma-separated. Direction (:ASC/:DESC) is optional; date-like fields default to DESC, others to ASC.' }),
37
37
  filter: Flags.string({ description: 'Filter (e.g. --filter status=ACTIVE). Operators: in:A,B, like:%, gt:, lt:, null:', multiple: true }),
38
38
  query: Flags.string({ description: 'Additional query params as JSON' }),
39
- fields: Flags.string({ description: 'Comma-separated list of fields to show (e.g. --fields=id,name,status)' }),
39
+ fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
40
40
  sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
41
41
  'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
42
42
  };
@@ -108,6 +108,8 @@ export default class Tools extends Command {
108
108
  queryParams.search = flags.search;
109
109
  if (flags['sort-by'])
110
110
  queryParams.sortBy = normalizeSortBy(flags['sort-by']);
111
+ if (flags.fields)
112
+ queryParams.fields = flags.fields;
111
113
  if (flags.query) {
112
114
  try {
113
115
  Object.assign(queryParams, JSON.parse(flags.query));
@@ -255,10 +257,11 @@ export default class Tools extends Command {
255
257
  process.stderr.write('\n');
256
258
  }
257
259
  try {
258
- // Binary response: download to file
260
+ // Binary response: to -f / --file, or to stdout when it is redirected.
259
261
  if (endpoint.isBinaryResponse) {
260
- if (!flags.file) {
261
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
262
+ const toStdout = !flags.file;
263
+ if (toStdout && process.stdout.isTTY) {
264
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
262
265
  }
263
266
  const binResponse = await apiRequest(endpoint.method, apiPath, {
264
267
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -266,24 +269,55 @@ export default class Tools extends Command {
266
269
  contextName: flags.context,
267
270
  binary: true,
268
271
  });
269
- const fsModule = await import('node:fs');
270
272
  const binData = binResponse.data;
271
273
  const buffer = Buffer.isBuffer(binData)
272
274
  ? binData
273
275
  : Buffer.from(binData);
276
+ if (toStdout) {
277
+ // Nothing else may reach stdout here — it is the file.
278
+ process.stdout.write(buffer);
279
+ return;
280
+ }
281
+ const fsModule = await import('node:fs');
274
282
  fsModule.writeFileSync(flags.file, buffer);
275
283
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
276
284
  return;
277
285
  }
278
- const response = await apiRequest(endpoint.method, apiPath, {
279
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
280
- data,
281
- contextName: flags.context,
282
- acceptAny: true,
283
- });
286
+ let response;
287
+ if (endpoint.hasBinaryVariant) {
288
+ // This route answers JSON or a file depending on the request body, so the
289
+ // bytes are read first and the response's own content-type decides. Asking
290
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
291
+ const raw = await apiRequest(endpoint.method, apiPath, {
292
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
293
+ data,
294
+ contextName: flags.context,
295
+ binary: true,
296
+ });
297
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
298
+ const rawData = raw.data;
299
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
300
+ if (!isTextualMediaType(rawType)) {
301
+ if (process.stdout.isTTY) {
302
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
303
+ }
304
+ // Nothing else may reach stdout here — it is the file.
305
+ process.stdout.write(buffer);
306
+ return;
307
+ }
308
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
309
+ }
310
+ else {
311
+ response = await apiRequest(endpoint.method, apiPath, {
312
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
313
+ data,
314
+ contextName: flags.context,
315
+ acceptAny: true,
316
+ });
317
+ }
284
318
  // Check content-type for non-JSON responses (XML, text, etc.)
285
319
  const contentType = (response.headers['content-type'] || '').toLowerCase();
286
- if (contentType.includes('xml') || contentType.includes('text/')) {
320
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
287
321
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
288
322
  this.log(String(response.data));
289
323
  return;
@@ -310,11 +344,6 @@ export default class Tools extends Command {
310
344
  output = obj[arrayKeys[0]];
311
345
  }
312
346
  }
313
- // Filter fields if --fields flag is provided
314
- if (flags.fields) {
315
- const fieldList = flags.fields.split(',').map(f => f.trim());
316
- output = this.filterFields(output, fieldList);
317
- }
318
347
  // Sanitize output if requested
319
348
  if (flags.sanitize) {
320
349
  output = this.sanitizeOutput(output);
@@ -569,6 +598,8 @@ export default class Tools extends Command {
569
598
  baseParams.sortBy = normalizeSortBy(flags['sort-by']);
570
599
  if (flags.include)
571
600
  baseParams.include = flags.include;
601
+ if (flags.fields)
602
+ baseParams.fields = flags.fields;
572
603
  if (flags.query) {
573
604
  try {
574
605
  Object.assign(baseParams, JSON.parse(flags.query));
@@ -623,12 +654,7 @@ export default class Tools extends Command {
623
654
  }
624
655
  }
625
656
  process.stderr.write(' done\n');
626
- // Filter fields if specified
627
657
  let output = allItems;
628
- if (flags.fields) {
629
- const fieldList = flags.fields.split(',').map(f => f.trim());
630
- output = this.filterFields(allItems, fieldList);
631
- }
632
658
  // Sanitize output if requested
633
659
  if (flags.sanitize) {
634
660
  output = this.sanitizeOutput(output);
@@ -957,56 +983,6 @@ export default class Tools extends Command {
957
983
  return `\$${op}`;
958
984
  return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
959
985
  }
960
- filterFields(data, fields) {
961
- const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
962
- const pick = (obj, keys) => {
963
- // Group keys by their first segment so sibling sub-paths
964
- // (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
965
- // and don't clobber each other on merge.
966
- const directKeys = new Set();
967
- const nestedKeys = new Map();
968
- for (const key of keys) {
969
- if (key.split('.').some(isUnsafeKey))
970
- continue;
971
- if (key.includes('.')) {
972
- const [first, ...rest] = key.split('.');
973
- if (isUnsafeKey(first))
974
- continue;
975
- const list = nestedKeys.get(first);
976
- if (list) {
977
- list.push(rest.join('.'));
978
- }
979
- else {
980
- nestedKeys.set(first, [rest.join('.')]);
981
- }
982
- }
983
- else {
984
- directKeys.add(key);
985
- }
986
- }
987
- const result = {};
988
- for (const key of directKeys) {
989
- if (key in obj)
990
- result[key] = obj[key];
991
- }
992
- for (const [first, restKeys] of nestedKeys) {
993
- const val = obj[first];
994
- if (!val || typeof val !== 'object')
995
- continue;
996
- if (Array.isArray(val)) {
997
- result[first] = val.map(item => pick(item, restKeys));
998
- }
999
- else {
1000
- result[first] = pick(val, restKeys);
1001
- }
1002
- }
1003
- return result;
1004
- };
1005
- if (Array.isArray(data)) {
1006
- return data.map(item => pick(item, fields));
1007
- }
1008
- return pick(data, fields);
1009
- }
1010
986
  sanitizeOutput(data) {
1011
987
  const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
1012
988
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -0,0 +1,42 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Users 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
+ 'credential-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
+ provider: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ 'role-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
+ page: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
+ limit: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
22
+ include: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
23
+ search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
24
+ 'sort-by': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
25
+ filter: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
26
+ query: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
27
+ fields: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
28
+ sanitize: import("@oclif/core/interfaces").BooleanFlag<boolean>;
29
+ 'show-http-request': import("@oclif/core/interfaces").BooleanFlag<boolean>;
30
+ };
31
+ run(): Promise<void>;
32
+ private runImport;
33
+ private runExport;
34
+ private runCamt053Import;
35
+ private runNestedImport;
36
+ private parseFilterValue;
37
+ private sanitizeOutput;
38
+ private getColumns;
39
+ private getValue;
40
+ private printTable;
41
+ private printCsv;
42
+ }