@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,6 +1,7 @@
1
1
  import { confirm, input, password, select } from '@inquirer/prompts';
2
2
  import { Command, Flags } from '@oclif/core';
3
- import { addContext, DEFAULT_CLIENT_IDS, getContext, } from '../../lib/config.js';
3
+ import { keycloakBaseUrlOf, parseKeycloakBaseUrl } from '../../lib/auth/index.js';
4
+ import { addContext, DEFAULT_KEYCLOAK_CLIENT_ID, getContext, } from '../../lib/config.js';
4
5
  export default class ConfigAdd extends Command {
5
6
  static description = 'Add a new context configuration';
6
7
  static examples = [
@@ -8,6 +9,7 @@ export default class ConfigAdd extends Command {
8
9
  '<%= config.bin %> config add --name my-client --environment live',
9
10
  '<%= config.bin %> config add --name dev-context --environment dev --client-id abc123',
10
11
  '<%= config.bin %> config add --name ci --environment live --auth-type api-key --api-key <key>',
12
+ '<%= config.bin %> config add --name test-kc --environment test --keycloak-url https://iam-test.aderis.com',
11
13
  ];
12
14
  static flags = {
13
15
  'api-key': Flags.string({
@@ -25,14 +27,17 @@ export default class ConfigAdd extends Command {
25
27
  }),
26
28
  environment: Flags.string({
27
29
  char: 'e',
28
- description: 'Environment (dev, staging, live)',
29
- options: ['dev', 'staging', 'live'],
30
+ description: 'Environment (dev, test, staging, live)',
31
+ options: ['dev', 'test', 'staging', 'live'],
30
32
  }),
31
33
  force: Flags.boolean({
32
34
  char: 'f',
33
35
  default: false,
34
36
  description: 'Overwrite existing context without prompting',
35
37
  }),
38
+ 'keycloak-url': Flags.string({
39
+ description: 'Keycloak origin to use instead of the default host for the environment (https, allowlisted). The realm stays that of the environment',
40
+ }),
36
41
  name: Flags.string({
37
42
  char: 'n',
38
43
  description: 'Context name',
@@ -74,6 +79,7 @@ export default class ConfigAdd extends Command {
74
79
  choices: [
75
80
  { name: 'live (production)', value: 'live' },
76
81
  { name: 'staging', value: 'staging' },
82
+ { name: 'test (QA)', value: 'test' },
77
83
  { name: 'dev (development)', value: 'dev' },
78
84
  ],
79
85
  message: 'Environment:',
@@ -87,6 +93,9 @@ export default class ConfigAdd extends Command {
87
93
  if (flags['client-id'] || flags['client-secret']) {
88
94
  this.error('--client-id and --client-secret only apply to OAuth contexts; omit them with --auth-type api-key.');
89
95
  }
96
+ if (flags['keycloak-url']) {
97
+ this.error('--keycloak-url only applies to OAuth contexts; omit it with --auth-type api-key.');
98
+ }
90
99
  const apiKey = flags['api-key'] ??
91
100
  (await password({
92
101
  mask: true,
@@ -108,25 +117,21 @@ export default class ConfigAdd extends Command {
108
117
  this.log(`\nNo login needed — the CLI is ready to use.`);
109
118
  return;
110
119
  }
111
- // Get client ID
120
+ const keycloakBaseUrl = this.resolveKeycloakBaseUrl(flags['keycloak-url']);
121
+ // Without a terminal the default client id is taken without confirmation.
112
122
  let clientId = flags['client-id'];
113
123
  if (!clientId) {
114
- const defaultClientId = DEFAULT_CLIENT_IDS[environment];
115
- if (defaultClientId) {
116
- const useDefault = await confirm({
124
+ const useDefault = !process.stdin.isTTY ||
125
+ (await confirm({
117
126
  default: true,
118
- message: `Use default client ID for ${environment}?`,
119
- });
120
- if (useDefault) {
121
- clientId = defaultClientId;
122
- }
123
- }
124
- if (!clientId) {
125
- clientId = await input({
127
+ message: `Use default client ID (${DEFAULT_KEYCLOAK_CLIENT_ID})?`,
128
+ }));
129
+ clientId = useDefault
130
+ ? DEFAULT_KEYCLOAK_CLIENT_ID
131
+ : await input({
126
132
  message: 'OAuth client ID:',
127
133
  validate: (value) => (value.trim() ? true : 'Client ID is required'),
128
134
  });
129
- }
130
135
  }
131
136
  // Get client secret (optional)
132
137
  let clientSecret = flags['client-secret'];
@@ -148,10 +153,12 @@ export default class ConfigAdd extends Command {
148
153
  client_id: clientId,
149
154
  client_secret: clientSecret,
150
155
  environment,
156
+ keycloak_base_url: keycloakBaseUrl,
151
157
  name,
152
158
  });
153
159
  this.log(`\nContext "${name}" created successfully.`);
154
160
  this.log(`Environment: ${environment}`);
161
+ this.log(`Keycloak URL: ${keycloakBaseUrlOf({ environment, keycloak_base_url: keycloakBaseUrl })}`);
155
162
  this.log(`Client ID: ${clientId}`);
156
163
  this.log(`\nTo authenticate, run: aderis auth login`);
157
164
  }
@@ -161,6 +168,17 @@ export default class ConfigAdd extends Command {
161
168
  * OAuth, and a non-TTY stdin means we must not block on a prompt — both keep
162
169
  * scripted `config add` calls working exactly as before API-key support.
163
170
  */
171
+ /** Validates `--keycloak-url` before the context is stored. */
172
+ resolveKeycloakBaseUrl(flag) {
173
+ if (flag === undefined)
174
+ return undefined;
175
+ try {
176
+ return parseKeycloakBaseUrl(flag);
177
+ }
178
+ catch (error) {
179
+ this.error(`Invalid --keycloak-url: ${error instanceof Error ? error.message : String(error)}`);
180
+ }
181
+ }
164
182
  async resolveAuthType(flags) {
165
183
  if (flags['auth-type']) {
166
184
  return flags['auth-type'] === 'api-key' ? 'api_key' : 'oauth2';
@@ -1,4 +1,5 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
+ import { keycloakBaseUrlOf } from '../../lib/auth/index.js';
2
3
  import { getAuthType, getCurrentContext, hasCredentials } from '../../lib/config.js';
3
4
  export default class ConfigCurrent extends Command {
4
5
  static description = 'Show the current context';
@@ -28,6 +29,7 @@ export default class ConfigCurrent extends Command {
28
29
  authenticated: hasCredentials(context),
29
30
  client_id: context.client_id,
30
31
  environment: context.environment,
32
+ keycloak_base_url: getAuthType(context) === 'oauth2' ? keycloakBaseUrlOf(context) : undefined,
31
33
  name: context.name,
32
34
  tenant_id: context.tenant_id,
33
35
  }, null, 2));
@@ -38,6 +40,7 @@ export default class ConfigCurrent extends Command {
38
40
  this.log(`Environment: ${context.environment}`);
39
41
  this.log(`Auth: ${authType === 'api_key' ? 'API key (x-api-key)' : 'oauth2'}`);
40
42
  if (authType === 'oauth2') {
43
+ this.log(`Keycloak URL: ${keycloakBaseUrlOf(context)}`);
41
44
  this.log(`Client ID: ${context.client_id ?? 'N/A'}`);
42
45
  }
43
46
  if (context.tenant_id) {
@@ -1,4 +1,5 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
+ import { keycloakBaseUrlOf } from '../../lib/auth/index.js';
2
3
  import { getAuthType, getCurrentContext, hasCredentials, listContexts } from '../../lib/config.js';
3
4
  export default class ConfigList extends Command {
4
5
  static description = 'List all configured contexts';
@@ -20,6 +21,7 @@ export default class ConfigList extends Command {
20
21
  authenticated: hasCredentials(c),
21
22
  client_id: c.client_id,
22
23
  environment: c.environment,
24
+ keycloak_base_url: getAuthType(c) === 'oauth2' ? keycloakBaseUrlOf(c) : undefined,
23
25
  name: c.name,
24
26
  tenant_id: c.tenant_id,
25
27
  })),
@@ -42,6 +44,7 @@ export default class ConfigList extends Command {
42
44
  this.log(` Environment: ${ctx.environment}`);
43
45
  this.log(` Auth: ${authType === 'api_key' ? 'API key (x-api-key)' : 'oauth2'}`);
44
46
  if (authType === 'oauth2') {
47
+ this.log(` Keycloak URL: ${keycloakBaseUrlOf(ctx)}`);
45
48
  this.log(` Client ID: ${ctx.client_id ?? 'N/A'}`);
46
49
  }
47
50
  this.log(` Authenticated: ${auth}`);
@@ -16,6 +16,7 @@ export default class CreditCardAccounts extends Command {
16
16
  set: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
17
  'mapping-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
18
  'note-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ 'card-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
20
  page: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
21
  limit: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
22
  include: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -33,7 +34,6 @@ export default class CreditCardAccounts extends Command {
33
34
  private runCamt053Import;
34
35
  private runNestedImport;
35
36
  private parseFilterValue;
36
- private filterFields;
37
37
  private sanitizeOutput;
38
38
  private getColumns;
39
39
  private getValue;
@@ -1,28 +1,33 @@
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
- const NESTED_IMPORTS = ["ledger-mappings", "notes"];
9
- const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "list-ledger-mappings", "create-ledger-mappings", "get-ledger-mappings", "update-ledger-mappings", "delete-ledger-mappings", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
8
+ const NESTED_IMPORTS = ["ledger-mappings", "notes", "cards"];
9
+ const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "list-ledger-mappings", "create-ledger-mappings", "get-ledger-mappings", "update-ledger-mappings", "delete-ledger-mappings", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-cards", "create-cards", "get-cards", "update-cards", "delete-cards"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/credit-card-accounts', summary: 'Get all credit card accounts with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/credit-card-accounts', summary: 'Create a new credit card account (with optional notes)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['last4', 'notes'] },
13
- 'get': { method: 'GET', path: '/v1/credit-card-accounts/{id}', summary: 'Get a credit card account by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'update': { method: 'PATCH', path: '/v1/credit-card-accounts/{id}', summary: 'Update a credit card account', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['last4', 'notes'] },
15
- 'delete': { method: 'DELETE', path: '/v1/credit-card-accounts/{id}', summary: 'Delete a credit card account', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
- 'list-ledger-mappings': { method: 'GET', path: '/v1/credit-card-accounts/{creditCardAccountId}/ledger-mappings', summary: 'Get all ledger mappings for a credit card account', pathParams: ['creditCardAccountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
17
- 'create-ledger-mappings': { method: 'POST', path: '/v1/credit-card-accounts/{creditCardAccountId}/ledger-mappings', summary: 'Create a ledger mapping for a credit card account', pathParams: ['creditCardAccountId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
18
- 'get-ledger-mappings': { method: 'GET', path: '/v1/credit-card-accounts/{creditCardAccountId}/ledger-mappings/{mappingId}', summary: 'Get a ledger mapping by ID', pathParams: ['creditCardAccountId', 'mappingId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
19
- 'update-ledger-mappings': { method: 'PATCH', path: '/v1/credit-card-accounts/{creditCardAccountId}/ledger-mappings/{mappingId}', summary: 'Update a ledger mapping', pathParams: ['creditCardAccountId', 'mappingId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
20
- 'delete-ledger-mappings': { method: 'DELETE', path: '/v1/credit-card-accounts/{creditCardAccountId}/ledger-mappings/{mappingId}', summary: 'Delete a ledger mapping', pathParams: ['creditCardAccountId', 'mappingId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
21
- 'list-notes': { method: 'GET', path: '/v1/credit-card-accounts/{creditCardAccountId}/notes', summary: 'Get all notes for a credit card account', pathParams: ['creditCardAccountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
22
- 'create-notes': { method: 'POST', path: '/v1/credit-card-accounts/{creditCardAccountId}/notes', summary: 'Create a note for a credit card account', pathParams: ['creditCardAccountId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
23
- 'get-notes': { method: 'GET', path: '/v1/credit-card-accounts/{creditCardAccountId}/notes/{noteId}', summary: 'Get a specific note for a credit card account', pathParams: ['creditCardAccountId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
24
- 'update-notes': { method: 'PATCH', path: '/v1/credit-card-accounts/{creditCardAccountId}/notes/{noteId}', summary: 'Update a note for a credit card account', pathParams: ['creditCardAccountId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
25
- 'delete-notes': { method: 'DELETE', path: '/v1/credit-card-accounts/{creditCardAccountId}/notes/{noteId}', summary: 'Delete a note from a credit card account', pathParams: ['creditCardAccountId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list': { method: 'GET', path: '/v1/credit-card-accounts', summary: 'Get all credit card accounts with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/credit-card-accounts', summary: 'Create a new credit card account (with optional notes)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['notes'] },
13
+ 'get': { method: 'GET', path: '/v1/credit-card-accounts/{id}', summary: 'Get a credit card account by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
+ 'update': { method: 'PATCH', path: '/v1/credit-card-accounts/{id}', summary: 'Update a credit card account', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['notes'] },
15
+ 'delete': { method: 'DELETE', path: '/v1/credit-card-accounts/{id}', summary: 'Delete a credit card account', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
+ 'list-ledger-mappings': { method: 'GET', path: '/v1/credit-card-accounts/{creditCardAccountId}/ledger-mappings', summary: 'Get all ledger mappings for a credit card account', pathParams: ['creditCardAccountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
+ 'create-ledger-mappings': { method: 'POST', path: '/v1/credit-card-accounts/{creditCardAccountId}/ledger-mappings', summary: 'Create a ledger mapping for a credit card account', pathParams: ['creditCardAccountId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
+ 'get-ledger-mappings': { method: 'GET', path: '/v1/credit-card-accounts/{creditCardAccountId}/ledger-mappings/{mappingId}', summary: 'Get a ledger mapping by ID', pathParams: ['creditCardAccountId', 'mappingId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
+ 'update-ledger-mappings': { method: 'PATCH', path: '/v1/credit-card-accounts/{creditCardAccountId}/ledger-mappings/{mappingId}', summary: 'Update a ledger mapping', pathParams: ['creditCardAccountId', 'mappingId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
+ 'delete-ledger-mappings': { method: 'DELETE', path: '/v1/credit-card-accounts/{creditCardAccountId}/ledger-mappings/{mappingId}', summary: 'Delete a ledger mapping', pathParams: ['creditCardAccountId', 'mappingId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
+ 'list-notes': { method: 'GET', path: '/v1/credit-card-accounts/{creditCardAccountId}/notes', summary: 'Get all notes for a credit card account', pathParams: ['creditCardAccountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
22
+ 'create-notes': { method: 'POST', path: '/v1/credit-card-accounts/{creditCardAccountId}/notes', summary: 'Create a note for a credit card account', pathParams: ['creditCardAccountId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
23
+ 'get-notes': { method: 'GET', path: '/v1/credit-card-accounts/{creditCardAccountId}/notes/{noteId}', summary: 'Get a specific note for a credit card account', pathParams: ['creditCardAccountId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
24
+ 'update-notes': { method: 'PATCH', path: '/v1/credit-card-accounts/{creditCardAccountId}/notes/{noteId}', summary: 'Update a note for a credit card account', pathParams: ['creditCardAccountId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
25
+ 'delete-notes': { method: 'DELETE', path: '/v1/credit-card-accounts/{creditCardAccountId}/notes/{noteId}', summary: 'Delete a note from a credit card account', pathParams: ['creditCardAccountId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
26
+ 'list-cards': { method: 'GET', path: '/v1/credit-card-accounts/{creditCardAccountId}/cards', summary: 'Get all cards on a credit card account', pathParams: ['creditCardAccountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
27
+ 'create-cards': { method: 'POST', path: '/v1/credit-card-accounts/{creditCardAccountId}/cards', summary: 'Create a card on a credit card account', pathParams: ['creditCardAccountId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
28
+ 'get-cards': { method: 'GET', path: '/v1/credit-card-accounts/{creditCardAccountId}/cards/{cardId}', summary: 'Get a card by ID', pathParams: ['creditCardAccountId', 'cardId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
29
+ 'update-cards': { method: 'PATCH', path: '/v1/credit-card-accounts/{creditCardAccountId}/cards/{cardId}', summary: 'Update a card', pathParams: ['creditCardAccountId', 'cardId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
30
+ 'delete-cards': { method: 'DELETE', path: '/v1/credit-card-accounts/{creditCardAccountId}/cards/{cardId}', summary: 'Delete a manual card', pathParams: ['creditCardAccountId', 'cardId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
26
31
  };
27
32
  export default class CreditCardAccounts extends Command {
28
33
  static description = 'Manage credit card accounts';
@@ -48,6 +53,7 @@ export default class CreditCardAccounts extends Command {
48
53
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
49
54
  'mapping-id': Flags.string({ description: 'Path param: mappingId' }),
50
55
  'note-id': Flags.string({ description: 'Path param: noteId' }),
56
+ 'card-id': Flags.string({ description: 'Path param: cardId' }),
51
57
  page: Flags.string({ description: 'Page number' }),
52
58
  limit: Flags.string({ description: 'Items per page' }),
53
59
  include: Flags.string({ description: 'Relations to include' }),
@@ -55,7 +61,7 @@ export default class CreditCardAccounts extends Command {
55
61
  'sort-by': Flags.string({ description: 'Sort field(s), comma-separated. Direction (:ASC/:DESC) is optional; date-like fields default to DESC, others to ASC.' }),
56
62
  filter: Flags.string({ description: 'Filter (e.g. --filter status=ACTIVE). Operators: in:A,B, like:%, gt:, lt:, null:', multiple: true }),
57
63
  query: Flags.string({ description: 'Additional query params as JSON' }),
58
- fields: Flags.string({ description: 'Comma-separated list of fields to show (e.g. --fields=id,name,status)' }),
64
+ fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
59
65
  sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
60
66
  'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
61
67
  };
@@ -127,6 +133,8 @@ export default class CreditCardAccounts extends Command {
127
133
  queryParams.search = flags.search;
128
134
  if (flags['sort-by'])
129
135
  queryParams.sortBy = normalizeSortBy(flags['sort-by']);
136
+ if (flags.fields)
137
+ queryParams.fields = flags.fields;
130
138
  if (flags.query) {
131
139
  try {
132
140
  Object.assign(queryParams, JSON.parse(flags.query));
@@ -274,10 +282,11 @@ export default class CreditCardAccounts extends Command {
274
282
  process.stderr.write('\n');
275
283
  }
276
284
  try {
277
- // Binary response: download to file
285
+ // Binary response: to -f / --file, or to stdout when it is redirected.
278
286
  if (endpoint.isBinaryResponse) {
279
- if (!flags.file) {
280
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
287
+ const toStdout = !flags.file;
288
+ if (toStdout && process.stdout.isTTY) {
289
+ 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.`);
281
290
  }
282
291
  const binResponse = await apiRequest(endpoint.method, apiPath, {
283
292
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -285,24 +294,55 @@ export default class CreditCardAccounts extends Command {
285
294
  contextName: flags.context,
286
295
  binary: true,
287
296
  });
288
- const fsModule = await import('node:fs');
289
297
  const binData = binResponse.data;
290
298
  const buffer = Buffer.isBuffer(binData)
291
299
  ? binData
292
300
  : Buffer.from(binData);
301
+ if (toStdout) {
302
+ // Nothing else may reach stdout here — it is the file.
303
+ process.stdout.write(buffer);
304
+ return;
305
+ }
306
+ const fsModule = await import('node:fs');
293
307
  fsModule.writeFileSync(flags.file, buffer);
294
308
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
295
309
  return;
296
310
  }
297
- const response = await apiRequest(endpoint.method, apiPath, {
298
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
299
- data,
300
- contextName: flags.context,
301
- acceptAny: true,
302
- });
311
+ let response;
312
+ if (endpoint.hasBinaryVariant) {
313
+ // This route answers JSON or a file depending on the request body, so the
314
+ // bytes are read first and the response's own content-type decides. Asking
315
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
316
+ const raw = await apiRequest(endpoint.method, apiPath, {
317
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
318
+ data,
319
+ contextName: flags.context,
320
+ binary: true,
321
+ });
322
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
323
+ const rawData = raw.data;
324
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
325
+ if (!isTextualMediaType(rawType)) {
326
+ if (process.stdout.isTTY) {
327
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
328
+ }
329
+ // Nothing else may reach stdout here — it is the file.
330
+ process.stdout.write(buffer);
331
+ return;
332
+ }
333
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
334
+ }
335
+ else {
336
+ response = await apiRequest(endpoint.method, apiPath, {
337
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
338
+ data,
339
+ contextName: flags.context,
340
+ acceptAny: true,
341
+ });
342
+ }
303
343
  // Check content-type for non-JSON responses (XML, text, etc.)
304
344
  const contentType = (response.headers['content-type'] || '').toLowerCase();
305
- if (contentType.includes('xml') || contentType.includes('text/')) {
345
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
306
346
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
307
347
  this.log(String(response.data));
308
348
  return;
@@ -329,11 +369,6 @@ export default class CreditCardAccounts extends Command {
329
369
  output = obj[arrayKeys[0]];
330
370
  }
331
371
  }
332
- // Filter fields if --fields flag is provided
333
- if (flags.fields) {
334
- const fieldList = flags.fields.split(',').map(f => f.trim());
335
- output = this.filterFields(output, fieldList);
336
- }
337
372
  // Sanitize output if requested
338
373
  if (flags.sanitize) {
339
374
  output = this.sanitizeOutput(output);
@@ -588,6 +623,8 @@ export default class CreditCardAccounts extends Command {
588
623
  baseParams.sortBy = normalizeSortBy(flags['sort-by']);
589
624
  if (flags.include)
590
625
  baseParams.include = flags.include;
626
+ if (flags.fields)
627
+ baseParams.fields = flags.fields;
591
628
  if (flags.query) {
592
629
  try {
593
630
  Object.assign(baseParams, JSON.parse(flags.query));
@@ -642,12 +679,7 @@ export default class CreditCardAccounts extends Command {
642
679
  }
643
680
  }
644
681
  process.stderr.write(' done\n');
645
- // Filter fields if specified
646
682
  let output = allItems;
647
- if (flags.fields) {
648
- const fieldList = flags.fields.split(',').map(f => f.trim());
649
- output = this.filterFields(allItems, fieldList);
650
- }
651
683
  // Sanitize output if requested
652
684
  if (flags.sanitize) {
653
685
  output = this.sanitizeOutput(output);
@@ -976,56 +1008,6 @@ export default class CreditCardAccounts extends Command {
976
1008
  return `\$${op}`;
977
1009
  return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
978
1010
  }
979
- filterFields(data, fields) {
980
- const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
981
- const pick = (obj, keys) => {
982
- // Group keys by their first segment so sibling sub-paths
983
- // (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
984
- // and don't clobber each other on merge.
985
- const directKeys = new Set();
986
- const nestedKeys = new Map();
987
- for (const key of keys) {
988
- if (key.split('.').some(isUnsafeKey))
989
- continue;
990
- if (key.includes('.')) {
991
- const [first, ...rest] = key.split('.');
992
- if (isUnsafeKey(first))
993
- continue;
994
- const list = nestedKeys.get(first);
995
- if (list) {
996
- list.push(rest.join('.'));
997
- }
998
- else {
999
- nestedKeys.set(first, [rest.join('.')]);
1000
- }
1001
- }
1002
- else {
1003
- directKeys.add(key);
1004
- }
1005
- }
1006
- const result = {};
1007
- for (const key of directKeys) {
1008
- if (key in obj)
1009
- result[key] = obj[key];
1010
- }
1011
- for (const [first, restKeys] of nestedKeys) {
1012
- const val = obj[first];
1013
- if (!val || typeof val !== 'object')
1014
- continue;
1015
- if (Array.isArray(val)) {
1016
- result[first] = val.map(item => pick(item, restKeys));
1017
- }
1018
- else {
1019
- result[first] = pick(val, restKeys);
1020
- }
1021
- }
1022
- return result;
1023
- };
1024
- if (Array.isArray(data)) {
1025
- return data.map(item => pick(item, fields));
1026
- }
1027
- return pick(data, fields);
1028
- }
1029
1011
  sanitizeOutput(data) {
1030
1012
  const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
1031
1013
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -34,7 +34,6 @@ export default class DeliveryNotes extends Command {
34
34
  private runCamt053Import;
35
35
  private runNestedImport;
36
36
  private parseFilterValue;
37
- private filterFields;
38
37
  private sanitizeOutput;
39
38
  private getColumns;
40
39
  private getValue;