@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
@@ -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 = [...["events", "list", "create", "update", "delete"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'events': { method: 'GET', path: '/v1/event-processor-mappings/events', summary: 'List all valid event types', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'list': { method: 'GET', path: '/v1/event-processor-mappings', summary: 'List all event type processor mappings for the tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
- 'create': { method: 'POST', path: '/v1/event-processor-mappings', summary: 'Create event type processor mappings', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['eventTypes', 'includes'] },
14
- 'update': { method: 'PATCH', path: '/v1/event-processor-mappings/{id}', summary: 'Update an event type processor mapping', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['includes'] },
15
- 'delete': { method: 'DELETE', path: '/v1/event-processor-mappings/{id}', summary: 'Delete an event type processor mapping', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'events': { method: 'GET', path: '/v1/event-processor-mappings/events', summary: 'List all valid event types', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'list': { method: 'GET', path: '/v1/event-processor-mappings', summary: 'List all event type processor mappings for the tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
13
+ 'create': { method: 'POST', path: '/v1/event-processor-mappings', summary: 'Create event type processor mappings', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['eventTypes', 'includes'] },
14
+ 'update': { method: 'PATCH', path: '/v1/event-processor-mappings/{id}', summary: 'Update an event type processor mapping', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['includes'] },
15
+ 'delete': { method: 'DELETE', path: '/v1/event-processor-mappings/{id}', summary: 'Delete an event type processor mapping', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
16
  };
17
17
  export default class EventProcessorMappings extends Command {
18
18
  static description = 'Manage event processor mappings';
@@ -42,7 +42,7 @@ export default class EventProcessorMappings extends Command {
42
42
  'sort-by': Flags.string({ description: 'Sort field(s), comma-separated. Direction (:ASC/:DESC) is optional; date-like fields default to DESC, others to ASC.' }),
43
43
  filter: Flags.string({ description: 'Filter (e.g. --filter status=ACTIVE). Operators: in:A,B, like:%, gt:, lt:, null:', multiple: true }),
44
44
  query: Flags.string({ description: 'Additional query params as JSON' }),
45
- fields: Flags.string({ description: 'Comma-separated list of fields to show (e.g. --fields=id,name,status)' }),
45
+ fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
46
46
  sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
47
47
  'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
48
48
  };
@@ -114,6 +114,8 @@ export default class EventProcessorMappings extends Command {
114
114
  queryParams.search = flags.search;
115
115
  if (flags['sort-by'])
116
116
  queryParams.sortBy = normalizeSortBy(flags['sort-by']);
117
+ if (flags.fields)
118
+ queryParams.fields = flags.fields;
117
119
  if (flags.query) {
118
120
  try {
119
121
  Object.assign(queryParams, JSON.parse(flags.query));
@@ -261,10 +263,11 @@ export default class EventProcessorMappings extends Command {
261
263
  process.stderr.write('\n');
262
264
  }
263
265
  try {
264
- // Binary response: download to file
266
+ // Binary response: to -f / --file, or to stdout when it is redirected.
265
267
  if (endpoint.isBinaryResponse) {
266
- if (!flags.file) {
267
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
268
+ const toStdout = !flags.file;
269
+ if (toStdout && process.stdout.isTTY) {
270
+ 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.`);
268
271
  }
269
272
  const binResponse = await apiRequest(endpoint.method, apiPath, {
270
273
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -272,24 +275,55 @@ export default class EventProcessorMappings extends Command {
272
275
  contextName: flags.context,
273
276
  binary: true,
274
277
  });
275
- const fsModule = await import('node:fs');
276
278
  const binData = binResponse.data;
277
279
  const buffer = Buffer.isBuffer(binData)
278
280
  ? binData
279
281
  : Buffer.from(binData);
282
+ if (toStdout) {
283
+ // Nothing else may reach stdout here — it is the file.
284
+ process.stdout.write(buffer);
285
+ return;
286
+ }
287
+ const fsModule = await import('node:fs');
280
288
  fsModule.writeFileSync(flags.file, buffer);
281
289
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
282
290
  return;
283
291
  }
284
- const response = await apiRequest(endpoint.method, apiPath, {
285
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
286
- data,
287
- contextName: flags.context,
288
- acceptAny: true,
289
- });
292
+ let response;
293
+ if (endpoint.hasBinaryVariant) {
294
+ // This route answers JSON or a file depending on the request body, so the
295
+ // bytes are read first and the response's own content-type decides. Asking
296
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
297
+ const raw = await apiRequest(endpoint.method, apiPath, {
298
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
299
+ data,
300
+ contextName: flags.context,
301
+ binary: true,
302
+ });
303
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
304
+ const rawData = raw.data;
305
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
306
+ if (!isTextualMediaType(rawType)) {
307
+ if (process.stdout.isTTY) {
308
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
309
+ }
310
+ // Nothing else may reach stdout here — it is the file.
311
+ process.stdout.write(buffer);
312
+ return;
313
+ }
314
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
315
+ }
316
+ else {
317
+ response = await apiRequest(endpoint.method, apiPath, {
318
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
319
+ data,
320
+ contextName: flags.context,
321
+ acceptAny: true,
322
+ });
323
+ }
290
324
  // Check content-type for non-JSON responses (XML, text, etc.)
291
325
  const contentType = (response.headers['content-type'] || '').toLowerCase();
292
- if (contentType.includes('xml') || contentType.includes('text/')) {
326
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
293
327
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
294
328
  this.log(String(response.data));
295
329
  return;
@@ -316,11 +350,6 @@ export default class EventProcessorMappings extends Command {
316
350
  output = obj[arrayKeys[0]];
317
351
  }
318
352
  }
319
- // Filter fields if --fields flag is provided
320
- if (flags.fields) {
321
- const fieldList = flags.fields.split(',').map(f => f.trim());
322
- output = this.filterFields(output, fieldList);
323
- }
324
353
  // Sanitize output if requested
325
354
  if (flags.sanitize) {
326
355
  output = this.sanitizeOutput(output);
@@ -575,6 +604,8 @@ export default class EventProcessorMappings extends Command {
575
604
  baseParams.sortBy = normalizeSortBy(flags['sort-by']);
576
605
  if (flags.include)
577
606
  baseParams.include = flags.include;
607
+ if (flags.fields)
608
+ baseParams.fields = flags.fields;
578
609
  if (flags.query) {
579
610
  try {
580
611
  Object.assign(baseParams, JSON.parse(flags.query));
@@ -629,12 +660,7 @@ export default class EventProcessorMappings extends Command {
629
660
  }
630
661
  }
631
662
  process.stderr.write(' done\n');
632
- // Filter fields if specified
633
663
  let output = allItems;
634
- if (flags.fields) {
635
- const fieldList = flags.fields.split(',').map(f => f.trim());
636
- output = this.filterFields(allItems, fieldList);
637
- }
638
664
  // Sanitize output if requested
639
665
  if (flags.sanitize) {
640
666
  output = this.sanitizeOutput(output);
@@ -963,56 +989,6 @@ export default class EventProcessorMappings extends Command {
963
989
  return `\$${op}`;
964
990
  return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
965
991
  }
966
- filterFields(data, fields) {
967
- const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
968
- const pick = (obj, keys) => {
969
- // Group keys by their first segment so sibling sub-paths
970
- // (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
971
- // and don't clobber each other on merge.
972
- const directKeys = new Set();
973
- const nestedKeys = new Map();
974
- for (const key of keys) {
975
- if (key.split('.').some(isUnsafeKey))
976
- continue;
977
- if (key.includes('.')) {
978
- const [first, ...rest] = key.split('.');
979
- if (isUnsafeKey(first))
980
- continue;
981
- const list = nestedKeys.get(first);
982
- if (list) {
983
- list.push(rest.join('.'));
984
- }
985
- else {
986
- nestedKeys.set(first, [rest.join('.')]);
987
- }
988
- }
989
- else {
990
- directKeys.add(key);
991
- }
992
- }
993
- const result = {};
994
- for (const key of directKeys) {
995
- if (key in obj)
996
- result[key] = obj[key];
997
- }
998
- for (const [first, restKeys] of nestedKeys) {
999
- const val = obj[first];
1000
- if (!val || typeof val !== 'object')
1001
- continue;
1002
- if (Array.isArray(val)) {
1003
- result[first] = val.map(item => pick(item, restKeys));
1004
- }
1005
- else {
1006
- result[first] = pick(val, restKeys);
1007
- }
1008
- }
1009
- return result;
1010
- };
1011
- if (Array.isArray(data)) {
1012
- return data.map(item => pick(item, fields));
1013
- }
1014
- return pick(data, fields);
1015
- }
1016
992
  sanitizeOutput(data) {
1017
993
  const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
1018
994
  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 Files 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 = 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';
@@ -50,7 +50,7 @@ export default class Files extends Command {
50
50
  'sort-by': Flags.string({ description: 'Sort field(s), comma-separated. Direction (:ASC/:DESC) is optional; date-like fields default to DESC, others to ASC.' }),
51
51
  filter: Flags.string({ description: 'Filter (e.g. --filter status=ACTIVE). Operators: in:A,B, like:%, gt:, lt:, null:', multiple: true }),
52
52
  query: Flags.string({ description: 'Additional query params as JSON' }),
53
- fields: Flags.string({ description: 'Comma-separated list of fields to show (e.g. --fields=id,name,status)' }),
53
+ fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
54
54
  sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
55
55
  'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
56
56
  };
@@ -122,6 +122,8 @@ export default class Files extends Command {
122
122
  queryParams.search = flags.search;
123
123
  if (flags['sort-by'])
124
124
  queryParams.sortBy = normalizeSortBy(flags['sort-by']);
125
+ if (flags.fields)
126
+ queryParams.fields = flags.fields;
125
127
  if (flags.query) {
126
128
  try {
127
129
  Object.assign(queryParams, JSON.parse(flags.query));
@@ -269,10 +271,11 @@ export default class Files extends Command {
269
271
  process.stderr.write('\n');
270
272
  }
271
273
  try {
272
- // Binary response: download to file
274
+ // Binary response: to -f / --file, or to stdout when it is redirected.
273
275
  if (endpoint.isBinaryResponse) {
274
- if (!flags.file) {
275
- 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.`);
276
279
  }
277
280
  const binResponse = await apiRequest(endpoint.method, apiPath, {
278
281
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -280,24 +283,55 @@ export default class Files extends Command {
280
283
  contextName: flags.context,
281
284
  binary: true,
282
285
  });
283
- const fsModule = await import('node:fs');
284
286
  const binData = binResponse.data;
285
287
  const buffer = Buffer.isBuffer(binData)
286
288
  ? binData
287
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');
288
296
  fsModule.writeFileSync(flags.file, buffer);
289
297
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
290
298
  return;
291
299
  }
292
- const response = await apiRequest(endpoint.method, apiPath, {
293
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
294
- data,
295
- contextName: flags.context,
296
- acceptAny: true,
297
- });
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
+ }
298
332
  // Check content-type for non-JSON responses (XML, text, etc.)
299
333
  const contentType = (response.headers['content-type'] || '').toLowerCase();
300
- if (contentType.includes('xml') || contentType.includes('text/')) {
334
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
301
335
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
302
336
  this.log(String(response.data));
303
337
  return;
@@ -324,11 +358,6 @@ export default class Files extends Command {
324
358
  output = obj[arrayKeys[0]];
325
359
  }
326
360
  }
327
- // Filter fields if --fields flag is provided
328
- if (flags.fields) {
329
- const fieldList = flags.fields.split(',').map(f => f.trim());
330
- output = this.filterFields(output, fieldList);
331
- }
332
361
  // Sanitize output if requested
333
362
  if (flags.sanitize) {
334
363
  output = this.sanitizeOutput(output);
@@ -583,6 +612,8 @@ export default class Files extends Command {
583
612
  baseParams.sortBy = normalizeSortBy(flags['sort-by']);
584
613
  if (flags.include)
585
614
  baseParams.include = flags.include;
615
+ if (flags.fields)
616
+ baseParams.fields = flags.fields;
586
617
  if (flags.query) {
587
618
  try {
588
619
  Object.assign(baseParams, JSON.parse(flags.query));
@@ -637,12 +668,7 @@ export default class Files extends Command {
637
668
  }
638
669
  }
639
670
  process.stderr.write(' done\n');
640
- // Filter fields if specified
641
671
  let output = allItems;
642
- if (flags.fields) {
643
- const fieldList = flags.fields.split(',').map(f => f.trim());
644
- output = this.filterFields(allItems, fieldList);
645
- }
646
672
  // Sanitize output if requested
647
673
  if (flags.sanitize) {
648
674
  output = this.sanitizeOutput(output);
@@ -971,56 +997,6 @@ export default class Files extends Command {
971
997
  return `\$${op}`;
972
998
  return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
973
999
  }
974
- filterFields(data, fields) {
975
- const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
976
- const pick = (obj, keys) => {
977
- // Group keys by their first segment so sibling sub-paths
978
- // (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
979
- // and don't clobber each other on merge.
980
- const directKeys = new Set();
981
- const nestedKeys = new Map();
982
- for (const key of keys) {
983
- if (key.split('.').some(isUnsafeKey))
984
- continue;
985
- if (key.includes('.')) {
986
- const [first, ...rest] = key.split('.');
987
- if (isUnsafeKey(first))
988
- continue;
989
- const list = nestedKeys.get(first);
990
- if (list) {
991
- list.push(rest.join('.'));
992
- }
993
- else {
994
- nestedKeys.set(first, [rest.join('.')]);
995
- }
996
- }
997
- else {
998
- directKeys.add(key);
999
- }
1000
- }
1001
- const result = {};
1002
- for (const key of directKeys) {
1003
- if (key in obj)
1004
- result[key] = obj[key];
1005
- }
1006
- for (const [first, restKeys] of nestedKeys) {
1007
- const val = obj[first];
1008
- if (!val || typeof val !== 'object')
1009
- continue;
1010
- if (Array.isArray(val)) {
1011
- result[first] = val.map(item => pick(item, restKeys));
1012
- }
1013
- else {
1014
- result[first] = pick(val, restKeys);
1015
- }
1016
- }
1017
- return result;
1018
- };
1019
- if (Array.isArray(data)) {
1020
- return data.map(item => pick(item, fields));
1021
- }
1022
- return pick(data, fields);
1023
- }
1024
1000
  sanitizeOutput(data) {
1025
1001
  const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
1026
1002
  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 Health 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;