@aderis-erp/cli 1.0.14 → 1.0.17

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 (167) 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 +22 -78
  4. package/dist/commands/account-transaction-allocations.d.ts +0 -1
  5. package/dist/commands/account-transaction-allocations.js +22 -78
  6. package/dist/commands/account-transactions.d.ts +0 -1
  7. package/dist/commands/account-transactions.js +28 -82
  8. package/dist/commands/accounts.d.ts +0 -1
  9. package/dist/commands/accounts.js +24 -80
  10. package/dist/commands/agent-platform.d.ts +0 -1
  11. package/dist/commands/agent-platform.js +24 -79
  12. package/dist/commands/analytics.d.ts +39 -0
  13. package/dist/commands/analytics.js +1113 -0
  14. package/dist/commands/{documents.d.ts → api-keys.d.ts} +1 -2
  15. package/dist/commands/api-keys.js +1117 -0
  16. package/dist/commands/audit-log.d.ts +0 -1
  17. package/dist/commands/audit-log.js +22 -78
  18. package/dist/commands/auth/login.d.ts +9 -0
  19. package/dist/commands/auth/login.js +83 -21
  20. package/dist/commands/auth/logout.d.ts +6 -0
  21. package/dist/commands/auth/logout.js +23 -6
  22. package/dist/commands/auth/status.js +46 -23
  23. package/dist/commands/auth/tenant.js +15 -9
  24. package/dist/commands/auth.d.ts +0 -1
  25. package/dist/commands/auth.js +37 -51
  26. package/dist/commands/bank-accounts.d.ts +0 -1
  27. package/dist/commands/bank-accounts.js +22 -78
  28. package/dist/commands/bank-integrations.d.ts +0 -1
  29. package/dist/commands/bank-integrations.js +22 -78
  30. package/dist/commands/business-partners.d.ts +1 -1
  31. package/dist/commands/business-partners.js +38 -84
  32. package/dist/commands/chat.d.ts +0 -1
  33. package/dist/commands/chat.js +22 -78
  34. package/dist/commands/config/add.d.ts +12 -0
  35. package/dist/commands/config/add.js +97 -17
  36. package/dist/commands/config/current.js +12 -4
  37. package/dist/commands/config/list.js +12 -4
  38. package/dist/commands/config/set-api-key.d.ts +10 -0
  39. package/dist/commands/config/set-api-key.js +49 -0
  40. package/dist/commands/config/use.js +3 -4
  41. package/dist/commands/credit-card-accounts.d.ts +0 -1
  42. package/dist/commands/credit-card-accounts.js +22 -78
  43. package/dist/commands/delivery-notes.d.ts +0 -1
  44. package/dist/commands/delivery-notes.js +29 -84
  45. package/dist/commands/dunning-configs.d.ts +0 -1
  46. package/dist/commands/dunning-configs.js +22 -78
  47. package/dist/commands/dunning-reminders.d.ts +0 -1
  48. package/dist/commands/dunning-reminders.js +22 -78
  49. package/dist/commands/dunning-runs.d.ts +0 -1
  50. package/dist/commands/dunning-runs.js +22 -78
  51. package/dist/commands/email-transactions.d.ts +0 -1
  52. package/dist/commands/email-transactions.js +22 -78
  53. package/dist/commands/employees.d.ts +0 -1
  54. package/dist/commands/employees.js +24 -80
  55. package/dist/commands/entity-configurations.d.ts +0 -1
  56. package/dist/commands/entity-configurations.js +22 -78
  57. package/dist/commands/event-processor-mappings.d.ts +0 -1
  58. package/dist/commands/event-processor-mappings.js +22 -78
  59. package/dist/commands/{files-internal.d.ts → files.d.ts} +1 -2
  60. package/dist/commands/{files-internal.js → files.js} +40 -90
  61. package/dist/commands/health.d.ts +0 -1
  62. package/dist/commands/health.js +22 -78
  63. package/dist/commands/inbox.d.ts +0 -1
  64. package/dist/commands/inbox.js +22 -78
  65. package/dist/commands/integrations.d.ts +0 -1
  66. package/dist/commands/integrations.js +22 -78
  67. package/dist/commands/item-categories.d.ts +0 -1
  68. package/dist/commands/item-categories.js +22 -78
  69. package/dist/commands/items.d.ts +0 -1
  70. package/dist/commands/items.js +24 -80
  71. package/dist/commands/journal-lines.d.ts +0 -1
  72. package/dist/commands/journal-lines.js +22 -78
  73. package/dist/commands/journals.d.ts +1 -1
  74. package/dist/commands/journals.js +29 -81
  75. package/dist/commands/ledgers.d.ts +0 -1
  76. package/dist/commands/ledgers.js +26 -79
  77. package/dist/commands/nango-integrations.d.ts +39 -0
  78. package/dist/commands/nango-integrations.js +1115 -0
  79. package/dist/commands/office-locations.d.ts +0 -1
  80. package/dist/commands/office-locations.js +22 -78
  81. package/dist/commands/payment-methods.d.ts +0 -1
  82. package/dist/commands/payment-methods.js +22 -78
  83. package/dist/commands/payment-runs.d.ts +0 -1
  84. package/dist/commands/payment-runs.js +24 -79
  85. package/dist/commands/payment-terms.d.ts +0 -1
  86. package/dist/commands/payment-terms.js +22 -78
  87. package/dist/commands/payments.d.ts +0 -1
  88. package/dist/commands/payments.js +22 -78
  89. package/dist/commands/price-lists.d.ts +0 -1
  90. package/dist/commands/price-lists.js +22 -78
  91. package/dist/commands/processor-mappings.d.ts +0 -1
  92. package/dist/commands/processor-mappings.js +22 -78
  93. package/dist/commands/processors.d.ts +0 -1
  94. package/dist/commands/processors.js +22 -78
  95. package/dist/commands/purchase-order-types.d.ts +0 -1
  96. package/dist/commands/purchase-order-types.js +22 -78
  97. package/dist/commands/purchase-orders.d.ts +1 -1
  98. package/dist/commands/purchase-orders.js +35 -84
  99. package/dist/commands/reports.d.ts +0 -1
  100. package/dist/commands/reports.js +23 -79
  101. package/dist/commands/salary-types.d.ts +0 -1
  102. package/dist/commands/salary-types.js +22 -78
  103. package/dist/commands/sales-invoices.d.ts +0 -1
  104. package/dist/commands/sales-invoices.js +38 -89
  105. package/dist/commands/sales-orders.d.ts +0 -1
  106. package/dist/commands/sales-orders.js +26 -82
  107. package/dist/commands/saved-views.d.ts +0 -1
  108. package/dist/commands/saved-views.js +22 -78
  109. package/dist/commands/scheduled-processor-mappings.d.ts +0 -1
  110. package/dist/commands/scheduled-processor-mappings.js +22 -78
  111. package/dist/commands/schedules.d.ts +0 -1
  112. package/dist/commands/schedules.js +22 -78
  113. package/dist/commands/switch.js +14 -7
  114. package/dist/commands/tag-configurations.d.ts +39 -0
  115. package/dist/commands/{documents.js → tag-configurations.js} +40 -96
  116. package/dist/commands/tags.d.ts +0 -1
  117. package/dist/commands/tags.js +22 -78
  118. package/dist/commands/tax-codes.d.ts +0 -1
  119. package/dist/commands/tax-codes.js +22 -78
  120. package/dist/commands/tax-matrix.d.ts +0 -1
  121. package/dist/commands/tax-matrix.js +22 -78
  122. package/dist/commands/tenants.d.ts +0 -1
  123. package/dist/commands/tenants.js +24 -79
  124. package/dist/commands/text-templates.d.ts +0 -1
  125. package/dist/commands/text-templates.js +22 -78
  126. package/dist/commands/tools.d.ts +0 -1
  127. package/dist/commands/tools.js +22 -78
  128. package/dist/commands/vat-declarations.d.ts +1 -1
  129. package/dist/commands/vat-declarations.js +28 -79
  130. package/dist/commands/vendor-invoices.d.ts +0 -1
  131. package/dist/commands/vendor-invoices.js +31 -86
  132. package/dist/commands/webhooks.d.ts +0 -1
  133. package/dist/commands/webhooks.js +22 -78
  134. package/dist/commands/workflows.d.ts +0 -1
  135. package/dist/commands/workflows.js +22 -78
  136. package/dist/lib/api-client.js +8 -6
  137. package/dist/lib/auth/constants.d.ts +11 -0
  138. package/dist/lib/auth/constants.js +11 -0
  139. package/dist/lib/auth/errors.d.ts +17 -0
  140. package/dist/lib/auth/errors.js +22 -0
  141. package/dist/lib/auth/flow.d.ts +27 -0
  142. package/dist/lib/auth/flow.js +163 -0
  143. package/dist/lib/auth/guards.d.ts +5 -0
  144. package/dist/lib/auth/guards.js +14 -0
  145. package/dist/lib/auth/http.d.ts +16 -0
  146. package/dist/lib/auth/http.js +49 -0
  147. package/dist/lib/auth/index.d.ts +49 -0
  148. package/dist/lib/auth/index.js +217 -0
  149. package/dist/lib/auth/keycloak-provider.d.ts +75 -0
  150. package/dist/lib/auth/keycloak-provider.js +215 -0
  151. package/dist/lib/auth/pages.d.ts +25 -0
  152. package/dist/lib/auth/pages.js +123 -0
  153. package/dist/lib/auth/pkce.d.ts +11 -0
  154. package/dist/lib/auth/pkce.js +17 -0
  155. package/dist/lib/auth/tokens.d.ts +54 -0
  156. package/dist/lib/auth/tokens.js +104 -0
  157. package/dist/lib/auth/types.d.ts +57 -0
  158. package/dist/lib/auth/types.js +1 -0
  159. package/dist/lib/auth/validated-url.d.ts +6 -0
  160. package/dist/lib/auth/validated-url.js +53 -0
  161. package/dist/lib/base-api-command.js +6 -4
  162. package/dist/lib/config.d.ts +44 -4
  163. package/dist/lib/config.js +55 -19
  164. package/oclif.manifest.json +1181 -330
  165. package/package.json +3 -2
  166. package/dist/lib/auth.d.ts +0 -35
  167. package/dist/lib/auth.js +0 -566
@@ -1,29 +1,38 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
3
- import { getCurrentContext, getContext, getBaseUrls } from '../lib/config.js';
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 = ["notes", "contacts", "contacts-notes"];
9
- const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "merge", "activate", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-contacts", "create-contacts", "get-contacts", "update-contacts", "delete-contacts", "list-contacts-notes", "create-contacts-notes", "get-contacts-notes", "update-contacts-notes", "delete-contacts-notes"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
8
+ const NESTED_IMPORTS = ["notes", "files", "contacts", "contacts-notes"];
9
+ const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "merge", "activate", "get-favicon", "update-favicon", "trigger-favicon-backfill", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download", "list-contacts", "create-contacts", "get-contacts", "update-contacts", "delete-contacts", "list-contacts-notes", "create-contacts-notes", "get-contacts-notes", "update-contacts-notes", "delete-contacts-notes"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
11
  'list': { method: 'GET', path: '/v1/business-partners', summary: 'Get all business partners with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/business-partners', summary: 'Create a new business partner', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['salesOrderRecipientCc', 'salesOrderRecipientBcc', 'salesInvoiceRecipientCc', 'salesInvoiceRecipientBcc', 'deliveryNoteRecipientCc', 'deliveryNoteRecipientBcc', 'contacts', 'tagIds', 'salesEarlyPaymentDiscounts', 'purchaseEarlyPaymentDiscounts', 'notes'] },
12
+ 'create': { method: 'POST', path: '/v1/business-partners', summary: 'Create a new business partner', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['salesOrderRecipientCc', 'salesOrderRecipientBcc', 'salesInvoiceRecipientCc', 'salesInvoiceRecipientBcc', 'deliveryNoteRecipientCc', 'deliveryNoteRecipientBcc', 'contacts', 'tagIds', 'tagCodes', 'salesEarlyPaymentDiscounts', 'purchaseEarlyPaymentDiscounts', 'notes'] },
13
13
  'get': { method: 'GET', path: '/v1/business-partners/{id}', summary: 'Get a business partner by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'update': { method: 'PATCH', path: '/v1/business-partners/{id}', summary: 'Update a business partner', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['salesOrderRecipientCc', 'salesOrderRecipientBcc', 'salesInvoiceRecipientCc', 'salesInvoiceRecipientBcc', 'deliveryNoteRecipientCc', 'deliveryNoteRecipientBcc', 'contacts', 'tagIds', 'salesEarlyPaymentDiscounts', 'purchaseEarlyPaymentDiscounts', 'notes'] },
14
+ 'update': { method: 'PATCH', path: '/v1/business-partners/{id}', summary: 'Update a business partner', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['salesOrderRecipientCc', 'salesOrderRecipientBcc', 'salesInvoiceRecipientCc', 'salesInvoiceRecipientBcc', 'deliveryNoteRecipientCc', 'deliveryNoteRecipientBcc', 'contacts', 'tagIds', 'tagCodes', 'salesEarlyPaymentDiscounts', 'purchaseEarlyPaymentDiscounts', 'notes'] },
15
15
  'delete': { method: 'DELETE', path: '/v1/business-partners/{id}', summary: 'Delete a business partner', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
16
  'merge': { method: 'POST', path: '/v1/business-partners/{id}/merge', summary: 'Merge a business partner into another', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
17
17
  'activate': { method: 'POST', path: '/v1/business-partners/{id}/activate', summary: 'Activate a business partner', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
18
+ 'get-favicon': { method: 'GET', path: '/v1/business-partners/{id}/favicon', summary: 'Download a business partner\'s stored favicon', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
19
+ 'update-favicon': { method: 'PUT', path: '/v1/business-partners/{id}/favicon', summary: 'Manually upload a business partner\'s favicon', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
20
+ 'trigger-favicon-backfill': { method: 'POST', path: '/v1/business-partners/favicon-backfill', summary: 'Fetch favicons for every business partner in this tenant that has none yet', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
18
21
  'list-notes': { method: 'GET', path: '/v1/business-partners/{businessPartnerId}/notes', summary: 'Get all notes for a business partner', pathParams: ['businessPartnerId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
19
22
  'create-notes': { method: 'POST', path: '/v1/business-partners/{businessPartnerId}/notes', summary: 'Create a note for a business partner', pathParams: ['businessPartnerId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
20
23
  'get-notes': { method: 'GET', path: '/v1/business-partners/{businessPartnerId}/notes/{noteId}', summary: 'Get a specific note for a business partner', pathParams: ['businessPartnerId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
21
24
  'update-notes': { method: 'PATCH', path: '/v1/business-partners/{businessPartnerId}/notes/{noteId}', summary: 'Update a note for a business partner', pathParams: ['businessPartnerId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
22
25
  'delete-notes': { method: 'DELETE', path: '/v1/business-partners/{businessPartnerId}/notes/{noteId}', summary: 'Delete a note from a business partner', pathParams: ['businessPartnerId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
26
+ 'list-files': { method: 'GET', path: '/v1/business-partners/{parentId}/files', summary: 'List file attachments for a business partner', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
27
+ 'create-files': { method: 'POST', path: '/v1/business-partners/{parentId}/files', summary: 'Upload a file attachment to a business partner', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, bodyArrayFields: [] },
28
+ 'get-files': { method: 'GET', path: '/v1/business-partners/{parentId}/files/{fileId}', summary: 'Get a file attachment for a business partner', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
29
+ 'update-files': { method: 'PATCH', path: '/v1/business-partners/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a business partner', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
30
+ 'delete-files': { method: 'DELETE', path: '/v1/business-partners/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a business partner', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
31
+ 'files-download': { method: 'GET', path: '/v1/business-partners/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a business partner', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
23
32
  'list-contacts': { method: 'GET', path: '/v1/business-partners/{businessPartnerId}/contacts', summary: 'Get all contacts for a business partner with pagination', pathParams: ['businessPartnerId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
24
- 'create-contacts': { method: 'POST', path: '/v1/business-partners/{businessPartnerId}/contacts', summary: 'Create a new contact for a business partner', pathParams: ['businessPartnerId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['notes'] },
33
+ 'create-contacts': { method: 'POST', path: '/v1/business-partners/{businessPartnerId}/contacts', summary: 'Create a new contact for a business partner', pathParams: ['businessPartnerId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['tagIds', 'tagCodes', 'notes'] },
25
34
  'get-contacts': { method: 'GET', path: '/v1/business-partners/{businessPartnerId}/contacts/{id}', summary: 'Get a contact by ID', pathParams: ['id', 'businessPartnerId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
26
- 'update-contacts': { method: 'PATCH', path: '/v1/business-partners/{businessPartnerId}/contacts/{id}', summary: 'Update a contact', pathParams: ['id', 'businessPartnerId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['notes'] },
35
+ 'update-contacts': { method: 'PATCH', path: '/v1/business-partners/{businessPartnerId}/contacts/{id}', summary: 'Update a contact', pathParams: ['id', 'businessPartnerId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['tagIds', 'tagCodes', 'notes'] },
27
36
  'delete-contacts': { method: 'DELETE', path: '/v1/business-partners/{businessPartnerId}/contacts/{id}', summary: 'Delete a contact', pathParams: ['id', 'businessPartnerId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
28
37
  'list-contacts-notes': { method: 'GET', path: '/v1/business-partners/contacts/{contactId}/notes', summary: 'Get all notes for a contact', pathParams: ['contactId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
29
38
  'create-contacts-notes': { method: 'POST', path: '/v1/business-partners/contacts/{contactId}/notes', summary: 'Create a note for a contact', pathParams: ['contactId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
@@ -46,7 +55,7 @@ export default class BusinessPartners extends Command {
46
55
  id: Args.string({ description: 'Resource ID (for get, update, delete operations)', required: false }),
47
56
  };
48
57
  static flags = {
49
- context: Flags.string({ char: 'c', description: 'Context to use' }),
58
+ context: Flags.string({ char: 'c', aliases: ['config'], env: 'ADERIS_CONTEXT', description: 'Context to use for this command, overriding the current context (alias: --config)' }),
50
59
  output: Flags.string({ char: 'o', description: 'Output format (default: table for import, json for other commands)', options: ['json', 'table', 'csv'] }),
51
60
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
52
61
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
@@ -54,6 +63,7 @@ export default class BusinessPartners extends Command {
54
63
  file: Flags.string({ char: 'f', description: 'Read request body from file' }),
55
64
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
56
65
  'note-id': Flags.string({ description: 'Path param: noteId' }),
66
+ 'file-id': Flags.string({ description: 'Path param: fileId' }),
57
67
  'business-partner-id': Flags.string({ description: 'Path param: businessPartnerId' }),
58
68
  page: Flags.string({ description: 'Page number' }),
59
69
  limit: Flags.string({ description: 'Items per page' }),
@@ -62,7 +72,7 @@ export default class BusinessPartners extends Command {
62
72
  'sort-by': Flags.string({ description: 'Sort field(s), comma-separated. Direction (:ASC/:DESC) is optional; date-like fields default to DESC, others to ASC.' }),
63
73
  filter: Flags.string({ description: 'Filter (e.g. --filter status=ACTIVE). Operators: in:A,B, like:%, gt:, lt:, null:', multiple: true }),
64
74
  query: Flags.string({ description: 'Additional query params as JSON' }),
65
- fields: Flags.string({ description: 'Comma-separated list of fields to show (e.g. --fields=id,name,status)' }),
75
+ fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
66
76
  sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
67
77
  'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
68
78
  };
@@ -120,9 +130,9 @@ export default class BusinessPartners extends Command {
120
130
  }
121
131
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
122
132
  if (!context)
123
- this.error('No context configured. Run: aderis auth login --new');
124
- if (!context.tokens)
125
- this.error('Not logged in. Run: aderis auth login');
133
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
134
+ if (!hasCredentials(context))
135
+ this.error(missingCredentialsMessage(context));
126
136
  const queryParams = {};
127
137
  if (flags.page)
128
138
  queryParams.page = flags.page;
@@ -134,6 +144,8 @@ export default class BusinessPartners extends Command {
134
144
  queryParams.search = flags.search;
135
145
  if (flags['sort-by'])
136
146
  queryParams.sortBy = normalizeSortBy(flags['sort-by']);
147
+ if (flags.fields)
148
+ queryParams.fields = flags.fields;
137
149
  if (flags.query) {
138
150
  try {
139
151
  Object.assign(queryParams, JSON.parse(flags.query));
@@ -336,11 +348,6 @@ export default class BusinessPartners extends Command {
336
348
  output = obj[arrayKeys[0]];
337
349
  }
338
350
  }
339
- // Filter fields if --fields flag is provided
340
- if (flags.fields) {
341
- const fieldList = flags.fields.split(',').map(f => f.trim());
342
- output = this.filterFields(output, fieldList);
343
- }
344
351
  // Sanitize output if requested
345
352
  if (flags.sanitize) {
346
353
  output = this.sanitizeOutput(output);
@@ -365,9 +372,9 @@ export default class BusinessPartners extends Command {
365
372
  async runImport(flags) {
366
373
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
367
374
  if (!context)
368
- this.error('No context configured. Run: aderis auth login --new');
369
- if (!context.tokens)
370
- this.error('Not logged in. Run: aderis auth login');
375
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
376
+ if (!hasCredentials(context))
377
+ this.error(missingCredentialsMessage(context));
371
378
  // Get data from file, --data, or --set flags
372
379
  let items = [];
373
380
  if (flags.file) {
@@ -577,9 +584,9 @@ export default class BusinessPartners extends Command {
577
584
  async runExport(flags) {
578
585
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
579
586
  if (!context)
580
- this.error('No context configured. Run: aderis auth login --new');
581
- if (!context.tokens)
582
- this.error('Not logged in. Run: aderis auth login');
587
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
588
+ if (!hasCredentials(context))
589
+ this.error(missingCredentialsMessage(context));
583
590
  const listEndpoint = ENDPOINTS['list'];
584
591
  if (!listEndpoint)
585
592
  this.error('Export requires a list endpoint');
@@ -595,6 +602,8 @@ export default class BusinessPartners extends Command {
595
602
  baseParams.sortBy = normalizeSortBy(flags['sort-by']);
596
603
  if (flags.include)
597
604
  baseParams.include = flags.include;
605
+ if (flags.fields)
606
+ baseParams.fields = flags.fields;
598
607
  if (flags.query) {
599
608
  try {
600
609
  Object.assign(baseParams, JSON.parse(flags.query));
@@ -649,12 +658,7 @@ export default class BusinessPartners extends Command {
649
658
  }
650
659
  }
651
660
  process.stderr.write(' done\n');
652
- // Filter fields if specified
653
661
  let output = allItems;
654
- if (flags.fields) {
655
- const fieldList = flags.fields.split(',').map(f => f.trim());
656
- output = this.filterFields(allItems, fieldList);
657
- }
658
662
  // Sanitize output if requested
659
663
  if (flags.sanitize) {
660
664
  output = this.sanitizeOutput(output);
@@ -671,9 +675,9 @@ export default class BusinessPartners extends Command {
671
675
  async runCamt053Import(flags) {
672
676
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
673
677
  if (!context)
674
- this.error('No context configured. Run: aderis auth login --new');
675
- if (!context.tokens)
676
- this.error('Not logged in. Run: aderis auth login');
678
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
679
+ if (!hasCredentials(context))
680
+ this.error(missingCredentialsMessage(context));
677
681
  if (!flags.file) {
678
682
  this.error('CAMT.053 import requires --file (-f) flag with path to XML file');
679
683
  }
@@ -761,9 +765,9 @@ export default class BusinessPartners extends Command {
761
765
  }
762
766
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
763
767
  if (!context)
764
- this.error('No context configured. Run: aderis auth login --new');
765
- if (!context.tokens)
766
- this.error('Not logged in. Run: aderis auth login');
768
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
769
+ if (!hasCredentials(context))
770
+ this.error(missingCredentialsMessage(context));
767
771
  // Get data from file, --data, or --set flags
768
772
  let items = [];
769
773
  if (flags.file) {
@@ -983,56 +987,6 @@ export default class BusinessPartners extends Command {
983
987
  return `\$${op}`;
984
988
  return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
985
989
  }
986
- filterFields(data, fields) {
987
- const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
988
- const pick = (obj, keys) => {
989
- // Group keys by their first segment so sibling sub-paths
990
- // (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
991
- // and don't clobber each other on merge.
992
- const directKeys = new Set();
993
- const nestedKeys = new Map();
994
- for (const key of keys) {
995
- if (key.split('.').some(isUnsafeKey))
996
- continue;
997
- if (key.includes('.')) {
998
- const [first, ...rest] = key.split('.');
999
- if (isUnsafeKey(first))
1000
- continue;
1001
- const list = nestedKeys.get(first);
1002
- if (list) {
1003
- list.push(rest.join('.'));
1004
- }
1005
- else {
1006
- nestedKeys.set(first, [rest.join('.')]);
1007
- }
1008
- }
1009
- else {
1010
- directKeys.add(key);
1011
- }
1012
- }
1013
- const result = {};
1014
- for (const key of directKeys) {
1015
- if (key in obj)
1016
- result[key] = obj[key];
1017
- }
1018
- for (const [first, restKeys] of nestedKeys) {
1019
- const val = obj[first];
1020
- if (!val || typeof val !== 'object')
1021
- continue;
1022
- if (Array.isArray(val)) {
1023
- result[first] = val.map(item => pick(item, restKeys));
1024
- }
1025
- else {
1026
- result[first] = pick(val, restKeys);
1027
- }
1028
- }
1029
- return result;
1030
- };
1031
- if (Array.isArray(data)) {
1032
- return data.map(item => pick(item, fields));
1033
- }
1034
- return pick(data, fields);
1035
- }
1036
990
  sanitizeOutput(data) {
1037
991
  const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
1038
992
  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 Chat 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,6 +1,6 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
3
- import { getCurrentContext, getContext, getBaseUrls } from '../lib/config.js';
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 = false;
@@ -28,7 +28,7 @@ export default class Chat extends Command {
28
28
  id: Args.string({ description: 'Resource ID (for get, update, delete operations)', required: false }),
29
29
  };
30
30
  static flags = {
31
- context: Flags.string({ char: 'c', description: 'Context to use' }),
31
+ context: Flags.string({ char: 'c', aliases: ['config'], env: 'ADERIS_CONTEXT', description: 'Context to use for this command, overriding the current context (alias: --config)' }),
32
32
  output: Flags.string({ char: 'o', description: 'Output format (default: table for import, json for other commands)', options: ['json', 'table', 'csv'] }),
33
33
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
34
34
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
@@ -42,7 +42,7 @@ export default class Chat 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
  };
@@ -100,9 +100,9 @@ export default class Chat extends Command {
100
100
  }
101
101
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
102
102
  if (!context)
103
- this.error('No context configured. Run: aderis auth login --new');
104
- if (!context.tokens)
105
- this.error('Not logged in. Run: aderis auth login');
103
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
104
+ if (!hasCredentials(context))
105
+ this.error(missingCredentialsMessage(context));
106
106
  const queryParams = {};
107
107
  if (flags.page)
108
108
  queryParams.page = flags.page;
@@ -114,6 +114,8 @@ export default class Chat 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));
@@ -316,11 +318,6 @@ export default class Chat extends Command {
316
318
  output = obj[arrayKeys[0]];
317
319
  }
318
320
  }
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
321
  // Sanitize output if requested
325
322
  if (flags.sanitize) {
326
323
  output = this.sanitizeOutput(output);
@@ -345,9 +342,9 @@ export default class Chat extends Command {
345
342
  async runImport(flags) {
346
343
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
347
344
  if (!context)
348
- this.error('No context configured. Run: aderis auth login --new');
349
- if (!context.tokens)
350
- this.error('Not logged in. Run: aderis auth login');
345
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
346
+ if (!hasCredentials(context))
347
+ this.error(missingCredentialsMessage(context));
351
348
  // Get data from file, --data, or --set flags
352
349
  let items = [];
353
350
  if (flags.file) {
@@ -557,9 +554,9 @@ export default class Chat extends Command {
557
554
  async runExport(flags) {
558
555
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
559
556
  if (!context)
560
- this.error('No context configured. Run: aderis auth login --new');
561
- if (!context.tokens)
562
- this.error('Not logged in. Run: aderis auth login');
557
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
558
+ if (!hasCredentials(context))
559
+ this.error(missingCredentialsMessage(context));
563
560
  const listEndpoint = ENDPOINTS['list'];
564
561
  if (!listEndpoint)
565
562
  this.error('Export requires a list endpoint');
@@ -575,6 +572,8 @@ export default class Chat extends Command {
575
572
  baseParams.sortBy = normalizeSortBy(flags['sort-by']);
576
573
  if (flags.include)
577
574
  baseParams.include = flags.include;
575
+ if (flags.fields)
576
+ baseParams.fields = flags.fields;
578
577
  if (flags.query) {
579
578
  try {
580
579
  Object.assign(baseParams, JSON.parse(flags.query));
@@ -629,12 +628,7 @@ export default class Chat extends Command {
629
628
  }
630
629
  }
631
630
  process.stderr.write(' done\n');
632
- // Filter fields if specified
633
631
  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
632
  // Sanitize output if requested
639
633
  if (flags.sanitize) {
640
634
  output = this.sanitizeOutput(output);
@@ -651,9 +645,9 @@ export default class Chat extends Command {
651
645
  async runCamt053Import(flags) {
652
646
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
653
647
  if (!context)
654
- this.error('No context configured. Run: aderis auth login --new');
655
- if (!context.tokens)
656
- this.error('Not logged in. Run: aderis auth login');
648
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
649
+ if (!hasCredentials(context))
650
+ this.error(missingCredentialsMessage(context));
657
651
  if (!flags.file) {
658
652
  this.error('CAMT.053 import requires --file (-f) flag with path to XML file');
659
653
  }
@@ -741,9 +735,9 @@ export default class Chat extends Command {
741
735
  }
742
736
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
743
737
  if (!context)
744
- this.error('No context configured. Run: aderis auth login --new');
745
- if (!context.tokens)
746
- this.error('Not logged in. Run: aderis auth login');
738
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
739
+ if (!hasCredentials(context))
740
+ this.error(missingCredentialsMessage(context));
747
741
  // Get data from file, --data, or --set flags
748
742
  let items = [];
749
743
  if (flags.file) {
@@ -963,56 +957,6 @@ export default class Chat extends Command {
963
957
  return `\$${op}`;
964
958
  return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
965
959
  }
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
960
  sanitizeOutput(data) {
1017
961
  const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
1018
962
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -3,11 +3,23 @@ export default class ConfigAdd extends Command {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
+ 'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ 'auth-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
8
  'client-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
9
  'client-secret': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
10
  environment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
11
  force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ 'keycloak-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
13
  name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
14
  };
12
15
  run(): Promise<void>;
16
+ /**
17
+ * Decide the auth type from flags, falling back to a prompt only when the
18
+ * invocation leaves it genuinely open. `--client-id`/`--client-secret` imply
19
+ * OAuth, and a non-TTY stdin means we must not block on a prompt — both keep
20
+ * scripted `config add` calls working exactly as before API-key support.
21
+ */
22
+ /** Validates `--keycloak-url` before the context is stored. */
23
+ private resolveKeycloakBaseUrl;
24
+ private resolveAuthType;
13
25
  }
@@ -1,14 +1,24 @@
1
- import { confirm, input, select } from '@inquirer/prompts';
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 = [
7
8
  '<%= config.bin %> config add',
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',
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',
10
13
  ];
11
14
  static flags = {
15
+ 'api-key': Flags.string({
16
+ description: 'API key to store (only with --auth-type api-key). Prompted for if omitted.',
17
+ }),
18
+ 'auth-type': Flags.string({
19
+ description: 'How to authenticate: oauth2 (browser login) or api-key (x-api-key header)',
20
+ options: ['oauth2', 'api-key'],
21
+ }),
12
22
  'client-id': Flags.string({
13
23
  description: 'OAuth client ID (uses default if not specified)',
14
24
  }),
@@ -17,14 +27,17 @@ export default class ConfigAdd extends Command {
17
27
  }),
18
28
  environment: Flags.string({
19
29
  char: 'e',
20
- description: 'Environment (dev, staging, live)',
21
- options: ['dev', 'staging', 'live'],
30
+ description: 'Environment (dev, test, staging, live)',
31
+ options: ['dev', 'test', 'staging', 'live'],
22
32
  }),
23
33
  force: Flags.boolean({
24
34
  char: 'f',
25
35
  default: false,
26
36
  description: 'Overwrite existing context without prompting',
27
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
+ }),
28
41
  name: Flags.string({
29
42
  char: 'n',
30
43
  description: 'Context name',
@@ -66,30 +79,59 @@ export default class ConfigAdd extends Command {
66
79
  choices: [
67
80
  { name: 'live (production)', value: 'live' },
68
81
  { name: 'staging', value: 'staging' },
82
+ { name: 'test (QA)', value: 'test' },
69
83
  { name: 'dev (development)', value: 'dev' },
70
84
  ],
71
85
  message: 'Environment:',
72
86
  }));
73
87
  }
74
- // Get client ID
88
+ // Get auth type. The flag uses the hyphenated `api-key` spelling; the stored
89
+ // value is the `api_key` AuthType. Only prompt when nothing in the invocation
90
+ // implies an answer, so existing non-interactive usage keeps working.
91
+ const authType = await this.resolveAuthType(flags);
92
+ if (authType === 'api_key') {
93
+ if (flags['client-id'] || flags['client-secret']) {
94
+ this.error('--client-id and --client-secret only apply to OAuth contexts; omit them with --auth-type api-key.');
95
+ }
96
+ if (flags['keycloak-url']) {
97
+ this.error('--keycloak-url only applies to OAuth contexts; omit it with --auth-type api-key.');
98
+ }
99
+ const apiKey = flags['api-key'] ??
100
+ (await password({
101
+ mask: true,
102
+ message: 'API key:',
103
+ validate: (value) => (value.trim() ? true : 'API key is required'),
104
+ }));
105
+ if (!apiKey.trim()) {
106
+ this.error('API key is required');
107
+ }
108
+ addContext({
109
+ api_key: apiKey.trim(),
110
+ auth_type: 'api_key',
111
+ environment,
112
+ name,
113
+ });
114
+ this.log(`\nContext "${name}" created successfully.`);
115
+ this.log(`Environment: ${environment}`);
116
+ this.log(`Auth: API key (sent as x-api-key)`);
117
+ this.log(`\nNo login needed — the CLI is ready to use.`);
118
+ return;
119
+ }
120
+ const keycloakBaseUrl = this.resolveKeycloakBaseUrl(flags['keycloak-url']);
121
+ // Without a terminal the default client id is taken without confirmation.
75
122
  let clientId = flags['client-id'];
76
123
  if (!clientId) {
77
- const defaultClientId = DEFAULT_CLIENT_IDS[environment];
78
- if (defaultClientId) {
79
- const useDefault = await confirm({
124
+ const useDefault = !process.stdin.isTTY ||
125
+ (await confirm({
80
126
  default: true,
81
- message: `Use default client ID for ${environment}?`,
82
- });
83
- if (useDefault) {
84
- clientId = defaultClientId;
85
- }
86
- }
87
- if (!clientId) {
88
- 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({
89
132
  message: 'OAuth client ID:',
90
133
  validate: (value) => (value.trim() ? true : 'Client ID is required'),
91
134
  });
92
- }
93
135
  }
94
136
  // Get client secret (optional)
95
137
  let clientSecret = flags['client-secret'];
@@ -107,14 +149,52 @@ export default class ConfigAdd extends Command {
107
149
  }
108
150
  // Create the context
109
151
  addContext({
152
+ auth_type: 'oauth2',
110
153
  client_id: clientId,
111
154
  client_secret: clientSecret,
112
155
  environment,
156
+ keycloak_base_url: keycloakBaseUrl,
113
157
  name,
114
158
  });
115
159
  this.log(`\nContext "${name}" created successfully.`);
116
160
  this.log(`Environment: ${environment}`);
161
+ this.log(`Keycloak URL: ${keycloakBaseUrlOf({ environment, keycloak_base_url: keycloakBaseUrl })}`);
117
162
  this.log(`Client ID: ${clientId}`);
118
163
  this.log(`\nTo authenticate, run: aderis auth login`);
119
164
  }
165
+ /**
166
+ * Decide the auth type from flags, falling back to a prompt only when the
167
+ * invocation leaves it genuinely open. `--client-id`/`--client-secret` imply
168
+ * OAuth, and a non-TTY stdin means we must not block on a prompt — both keep
169
+ * scripted `config add` calls working exactly as before API-key support.
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
+ }
182
+ async resolveAuthType(flags) {
183
+ if (flags['auth-type']) {
184
+ return flags['auth-type'] === 'api-key' ? 'api_key' : 'oauth2';
185
+ }
186
+ // A key without --auth-type unambiguously means API-key auth.
187
+ if (flags['api-key'])
188
+ return 'api_key';
189
+ // OAuth-only flags, or a non-interactive shell: keep the historical default.
190
+ if (flags['client-id'] || flags['client-secret'] || !process.stdin.isTTY)
191
+ return 'oauth2';
192
+ return select({
193
+ choices: [
194
+ { description: 'Browser login via OAuth2 (PKCE)', name: 'oauth2', value: 'oauth2' },
195
+ { description: 'Long-lived API key, sent as the x-api-key header', name: 'api-key', value: 'api_key' },
196
+ ],
197
+ message: 'Authentication method:',
198
+ });
199
+ }
120
200
  }