@aifabrix/builder 2.44.4 → 2.44.6

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 (214) hide show
  1. package/.cursor/rules/cli-layout.mdc +1 -1
  2. package/.cursor/rules/project-rules.mdc +1 -1
  3. package/.npmrc.token +1 -1
  4. package/README.md +15 -23
  5. package/integration/hubspot-test/README.md +2 -0
  6. package/integration/hubspot-test/test.js +5 -3
  7. package/jest.projects.js +68 -17
  8. package/lib/api/controller-health.api.js +49 -0
  9. package/lib/api/dimension-values.api.js +82 -0
  10. package/lib/api/dimensions.api.js +114 -0
  11. package/lib/api/external-systems.api.js +1 -0
  12. package/lib/api/integration-clients.api.js +168 -0
  13. package/lib/api/types/dimension-values.types.js +28 -0
  14. package/lib/api/types/dimensions.types.js +31 -0
  15. package/lib/api/types/integration-clients.types.js +45 -0
  16. package/lib/api/types/wizard.types.js +2 -1
  17. package/lib/api/validation-runner.js +46 -25
  18. package/lib/app/deploy-config.js +11 -1
  19. package/lib/app/deploy-status-display.js +3 -3
  20. package/lib/app/deploy.js +36 -14
  21. package/lib/app/display.js +15 -11
  22. package/lib/app/push.js +46 -23
  23. package/lib/app/register.js +1 -1
  24. package/lib/app/restart-display.js +95 -0
  25. package/lib/app/rotate-secret.js +1 -1
  26. package/lib/app/run-container-start.js +12 -6
  27. package/lib/app/run-env-compose.js +30 -1
  28. package/lib/app/run-helpers.js +44 -12
  29. package/lib/app/run-reload-sync.js +148 -0
  30. package/lib/app/run-resolve-image.js +51 -1
  31. package/lib/app/run.js +99 -73
  32. package/lib/build/index.js +75 -45
  33. package/lib/cli/doctor-check.js +117 -0
  34. package/lib/cli/index.js +8 -2
  35. package/lib/cli/infra-guided.js +445 -0
  36. package/lib/cli/setup-app.help.js +1 -1
  37. package/lib/cli/setup-app.js +20 -2
  38. package/lib/cli/setup-app.test-commands.js +9 -5
  39. package/lib/cli/setup-auth.js +26 -0
  40. package/lib/cli/setup-dev-path-commands.js +50 -3
  41. package/lib/cli/setup-infra.js +138 -61
  42. package/lib/cli/setup-integration-client.js +182 -0
  43. package/lib/cli/setup-parameters.js +21 -2
  44. package/lib/cli/setup-platform.js +102 -0
  45. package/lib/cli/setup-secrets.js +18 -6
  46. package/lib/cli/setup-utility.js +97 -33
  47. package/lib/commands/datasource-capability-dimension-cli.js +128 -0
  48. package/lib/commands/datasource-capability-output.js +29 -0
  49. package/lib/commands/datasource-capability-relate-cli.js +140 -0
  50. package/lib/commands/datasource-capability.js +411 -0
  51. package/lib/commands/datasource-unified-test-cli.options.js +1 -1
  52. package/lib/commands/datasource.js +53 -13
  53. package/lib/commands/dev-down.js +3 -3
  54. package/lib/commands/dev-infra-gate.js +32 -0
  55. package/lib/commands/dev-init.js +13 -7
  56. package/lib/commands/dimension-value.js +179 -0
  57. package/lib/commands/dimension.js +330 -0
  58. package/lib/commands/integration-client.js +430 -0
  59. package/lib/commands/login-device.js +65 -30
  60. package/lib/commands/login.js +21 -10
  61. package/lib/commands/parameters-validate.js +78 -13
  62. package/lib/commands/repair-datasource-auto-rbac.js +166 -0
  63. package/lib/commands/repair-datasource-keys.js +10 -5
  64. package/lib/commands/repair-datasource.js +19 -7
  65. package/lib/commands/repair-env-template.js +4 -1
  66. package/lib/commands/repair-openapi-sync.js +172 -0
  67. package/lib/commands/repair-persist.js +102 -0
  68. package/lib/commands/repair-rbac-extract.js +27 -0
  69. package/lib/commands/repair-rbac-migrate.js +186 -0
  70. package/lib/commands/repair-rbac.js +225 -19
  71. package/lib/commands/repair-system-alignment.js +246 -0
  72. package/lib/commands/repair-system-permissions.js +168 -0
  73. package/lib/commands/repair.js +120 -354
  74. package/lib/commands/secure.js +1 -1
  75. package/lib/commands/setup-modes.js +455 -0
  76. package/lib/commands/setup-prompts.js +388 -0
  77. package/lib/commands/setup.js +149 -0
  78. package/lib/commands/teardown.js +228 -0
  79. package/lib/commands/test-e2e-external.js +4 -3
  80. package/lib/commands/up-common.js +97 -12
  81. package/lib/commands/up-dataplane.js +33 -11
  82. package/lib/commands/up-miso.js +7 -11
  83. package/lib/commands/upload.js +109 -23
  84. package/lib/commands/wizard-core-helpers.js +14 -11
  85. package/lib/commands/wizard-core.js +58 -15
  86. package/lib/commands/wizard-dataplane.js +2 -2
  87. package/lib/commands/wizard-entity-selection.js +72 -14
  88. package/lib/commands/wizard-headless.js +7 -3
  89. package/lib/commands/wizard-helpers.js +13 -1
  90. package/lib/commands/wizard.js +210 -61
  91. package/lib/constants/infra-compose-service-names.js +40 -0
  92. package/lib/core/env-reader.js +16 -3
  93. package/lib/core/secrets-admin-env.js +101 -0
  94. package/lib/core/secrets-ensure-infra.js +34 -1
  95. package/lib/core/secrets-ensure.js +88 -66
  96. package/lib/core/secrets-env-content.js +432 -0
  97. package/lib/core/secrets-env-write.js +27 -1
  98. package/lib/core/secrets-load.js +248 -0
  99. package/lib/core/secrets-names.js +32 -0
  100. package/lib/core/secrets.js +17 -757
  101. package/lib/datasource/capability/basic-exposure.js +76 -0
  102. package/lib/datasource/capability/capability-diff-slice.js +41 -0
  103. package/lib/datasource/capability/capability-key.js +34 -0
  104. package/lib/datasource/capability/capability-resolve.js +172 -0
  105. package/lib/datasource/capability/capability-storage-keys.js +22 -0
  106. package/lib/datasource/capability/copy-operations.js +348 -0
  107. package/lib/datasource/capability/copy-test-payload.js +139 -0
  108. package/lib/datasource/capability/create-operations.js +235 -0
  109. package/lib/datasource/capability/dimension-operations.js +151 -0
  110. package/lib/datasource/capability/dimension-validate.js +219 -0
  111. package/lib/datasource/capability/json-pointer.js +31 -0
  112. package/lib/datasource/capability/reference-rewrite.js +51 -0
  113. package/lib/datasource/capability/relate-operations.js +325 -0
  114. package/lib/datasource/capability/relate-validate.js +219 -0
  115. package/lib/datasource/capability/remove-operations.js +275 -0
  116. package/lib/datasource/capability/run-capability-copy.js +152 -0
  117. package/lib/datasource/capability/run-capability-diff.js +135 -0
  118. package/lib/datasource/capability/run-capability-dimension.js +291 -0
  119. package/lib/datasource/capability/run-capability-edit.js +377 -0
  120. package/lib/datasource/capability/run-capability-relate.js +193 -0
  121. package/lib/datasource/capability/run-capability-remove.js +105 -0
  122. package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
  123. package/lib/datasource/capability/validate-capability-slice.js +35 -0
  124. package/lib/datasource/list.js +136 -23
  125. package/lib/datasource/log-viewer.js +2 -4
  126. package/lib/datasource/unified-validation-run.js +51 -16
  127. package/lib/datasource/validate.js +53 -1
  128. package/lib/deployment/deploy-poll-ui.js +60 -0
  129. package/lib/deployment/deployer-status.js +29 -3
  130. package/lib/deployment/deployer.js +48 -30
  131. package/lib/deployment/environment.js +7 -2
  132. package/lib/deployment/poll-interval.js +72 -0
  133. package/lib/deployment/push.js +11 -9
  134. package/lib/external-system/deploy.js +4 -1
  135. package/lib/external-system/download.js +61 -32
  136. package/lib/external-system/sync-deploy-manifest.js +33 -0
  137. package/lib/generator/wizard-prompts.js +7 -1
  138. package/lib/generator/wizard.js +34 -0
  139. package/lib/infrastructure/index.js +49 -19
  140. package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
  141. package/lib/parameters/infra-kv-discovery.js +29 -4
  142. package/lib/parameters/infra-parameter-catalog.js +6 -3
  143. package/lib/parameters/infra-parameter-validate.js +67 -19
  144. package/lib/resolvers/datasource-resolver.js +53 -0
  145. package/lib/resolvers/dimension-file.js +52 -0
  146. package/lib/resolvers/manifest-resolver.js +133 -0
  147. package/lib/schema/external-datasource.schema.json +183 -53
  148. package/lib/schema/external-system.schema.json +23 -10
  149. package/lib/schema/infra.parameter.yaml +26 -11
  150. package/lib/schema/wizard-config.schema.json +2 -2
  151. package/lib/utils/aifabrix-config-dir-walk.js +40 -0
  152. package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
  153. package/lib/utils/app-run-containers.js +2 -2
  154. package/lib/utils/bash-secret-env.js +59 -0
  155. package/lib/utils/cli-secrets-error-format.js +78 -0
  156. package/lib/utils/cli-test-layout-chalk.js +31 -9
  157. package/lib/utils/cli-utils.js +4 -36
  158. package/lib/utils/datasource-test-run-display.js +8 -0
  159. package/lib/utils/dev-hosts-helper.js +3 -2
  160. package/lib/utils/dev-init-ssh-merge.js +2 -1
  161. package/lib/utils/docker-build.js +17 -9
  162. package/lib/utils/docker-reload-mount.js +127 -0
  163. package/lib/utils/external-readme.js +117 -4
  164. package/lib/utils/external-system-local-test-tty.js +3 -2
  165. package/lib/utils/external-system-readiness-core.js +45 -12
  166. package/lib/utils/external-system-readiness-deploy-display.js +3 -3
  167. package/lib/utils/external-system-readiness-display-internals.js +33 -3
  168. package/lib/utils/external-system-readiness-display.js +10 -1
  169. package/lib/utils/file-upload.js +40 -3
  170. package/lib/utils/health-check-db-init.js +107 -0
  171. package/lib/utils/health-check-public-warn.js +69 -0
  172. package/lib/utils/health-check-url.js +19 -4
  173. package/lib/utils/health-check.js +135 -105
  174. package/lib/utils/help-builder.js +5 -1
  175. package/lib/utils/image-name.js +34 -7
  176. package/lib/utils/integration-file-backup.js +74 -0
  177. package/lib/utils/mutagen-install.js +30 -3
  178. package/lib/utils/paths.js +108 -25
  179. package/lib/utils/postgres-wipe.js +212 -0
  180. package/lib/utils/register-aifabrix-shell-env.js +15 -0
  181. package/lib/utils/remote-dev-auth.js +21 -5
  182. package/lib/utils/remote-docker-env.js +9 -1
  183. package/lib/utils/remote-secrets-loader.js +42 -3
  184. package/lib/utils/resolve-docker-image-ref.js +9 -3
  185. package/lib/utils/secrets-ancestor-paths.js +47 -0
  186. package/lib/utils/secrets-helpers.js +17 -10
  187. package/lib/utils/secrets-kv-refs.js +42 -0
  188. package/lib/utils/secrets-kv-scope.js +19 -2
  189. package/lib/utils/secrets-materialize-local.js +134 -0
  190. package/lib/utils/secrets-path.js +24 -10
  191. package/lib/utils/secrets-utils.js +2 -2
  192. package/lib/utils/system-builder-root.js +34 -0
  193. package/lib/utils/url-declarative-resolve-build.js +6 -1
  194. package/lib/utils/url-declarative-runtime-base-path.js +32 -0
  195. package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
  196. package/lib/utils/urls-local-registry.js +73 -20
  197. package/lib/utils/validation-poll-ui.js +81 -0
  198. package/lib/utils/validation-run-poll.js +29 -5
  199. package/lib/utils/with-muted-logger.js +53 -0
  200. package/package.json +1 -1
  201. package/templates/applications/dataplane/application.yaml +1 -1
  202. package/templates/applications/dataplane/rbac.yaml +10 -10
  203. package/templates/applications/keycloak/env.template +8 -6
  204. package/templates/applications/miso-controller/application.yaml +7 -0
  205. package/templates/applications/miso-controller/env.template +7 -7
  206. package/templates/applications/miso-controller/rbac.yaml +9 -9
  207. package/templates/external-system/README.md.hbs +89 -102
  208. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  209. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  210. package/.nyc_output/processinfo/index.json +0 -1
  211. package/lib/api/service-users.api.js +0 -150
  212. package/lib/api/types/service-users.types.js +0 -65
  213. package/lib/cli/setup-service-user.js +0 -187
  214. package/lib/commands/service-user.js +0 -429
@@ -0,0 +1,430 @@
1
+ const {
2
+ formatBlockingError,
3
+ formatSuccessLine,
4
+ formatSuccessParagraph,
5
+ sectionTitle
6
+ } = require('../utils/cli-layout-chalk');
7
+ /**
8
+ * Integration client commands — OAuth/API clients on the Controller.
9
+ *
10
+ * @fileoverview integration-client command implementation
11
+ * @author AI Fabrix Team
12
+ * @version 2.0.0
13
+ */
14
+
15
+ const chalk = require('chalk');
16
+ const logger = require('../utils/logger');
17
+ const { resolveControllerUrl } = require('../utils/controller-url');
18
+ const { getOrRefreshDeviceToken } = require('../utils/token-manager');
19
+ const { normalizeControllerUrl } = require('../core/config');
20
+ const {
21
+ createIntegrationClient,
22
+ listIntegrationClients,
23
+ regenerateIntegrationClientSecret,
24
+ deleteIntegrationClient,
25
+ updateIntegrationClientGroups,
26
+ updateIntegrationClientRedirectUris
27
+ } = require('../api/integration-clients.api');
28
+
29
+ const ONE_TIME_WARNING =
30
+ 'Save this secret now; it will not be shown again.';
31
+
32
+ /** Controller-valid key: lowercase letter/digit start, then alphanumeric + hyphens */
33
+ const INTEGRATION_CLIENT_KEY_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
34
+
35
+ /**
36
+ * @param {string|undefined} key - Raw key from CLI
37
+ * @returns {string} Trimmed key
38
+ */
39
+ function requireValidIntegrationClientKey(key) {
40
+ const trimmed = key?.trim();
41
+ if (!trimmed) {
42
+ logger.error(formatBlockingError('Key is required. Use --key <key>.'));
43
+ process.exit(1);
44
+ }
45
+ if (!INTEGRATION_CLIENT_KEY_PATTERN.test(trimmed)) {
46
+ logger.error(
47
+ formatBlockingError(
48
+ 'Key must start with a letter or digit and contain only lowercase letters, digits, and hyphens (e.g. my-ci-client).'
49
+ )
50
+ );
51
+ process.exit(1);
52
+ }
53
+ return trimmed;
54
+ }
55
+
56
+ /**
57
+ * @param {string} controllerUrl - Controller base URL
58
+ * @returns {Promise<{token: string, controllerUrl: string}|null>}
59
+ */
60
+ async function getIntegrationClientAuth(controllerUrl) {
61
+ const normalizedUrl = normalizeControllerUrl(controllerUrl);
62
+ const deviceToken = await getOrRefreshDeviceToken(normalizedUrl);
63
+ if (deviceToken && deviceToken.token) {
64
+ return {
65
+ token: deviceToken.token,
66
+ controllerUrl: deviceToken.controller || normalizedUrl
67
+ };
68
+ }
69
+ return null;
70
+ }
71
+
72
+ /**
73
+ * @param {Object} response - API response (success: true, data: body)
74
+ * @returns {{ clientId: string, clientSecret: string }}
75
+ */
76
+ function extractCreateResponse(response) {
77
+ const payload = response?.data?.data ?? response?.data ?? response;
78
+ const ic = payload?.integrationClient;
79
+ const clientId =
80
+ ic?.keycloakClientId ??
81
+ ic?.key ??
82
+ payload?.clientId ??
83
+ '';
84
+ const clientSecret = payload?.clientSecret ?? '';
85
+ return { clientId, clientSecret };
86
+ }
87
+
88
+ const ID_WIDTH = 38;
89
+ const KEY_WIDTH = 22;
90
+ const DISPLAY_WIDTH = 28;
91
+ const CLIENT_ID_WIDTH = 26;
92
+ const STATUS_WIDTH = 12;
93
+ const TABLE_SEPARATOR_LENGTH =
94
+ ID_WIDTH + KEY_WIDTH + DISPLAY_WIDTH + CLIENT_ID_WIDTH + STATUS_WIDTH;
95
+
96
+ /**
97
+ * @param {Object} response - API response with success: false
98
+ */
99
+ function handleCreateError(response) {
100
+ const status = response.status;
101
+ const msg = response.formattedError || response.error || 'Request failed';
102
+ if (status === 400) {
103
+ logger.error(formatBlockingError(`Validation error: ${msg}`));
104
+ } else if (status === 401) {
105
+ logger.error(formatBlockingError('Unauthorized. Run "aifabrix login" and try again.'));
106
+ } else if (status === 403) {
107
+ logger.error(formatBlockingError('Missing permission: integration-client:create'));
108
+ logger.error(chalk.gray('Your account needs the integration-client:create permission on the controller.'));
109
+ } else {
110
+ logger.error(formatBlockingError(`Failed to create integration client: ${msg}`));
111
+ }
112
+ process.exit(1);
113
+ }
114
+
115
+ /**
116
+ * @param {Object} response - API response with success: false
117
+ * @param {'read'|'update'|'delete'} permissionScope - Permission hint
118
+ */
119
+ function handleIntegrationClientApiError(response, permissionScope) {
120
+ const status = response.status;
121
+ const msg = response.formattedError || response.error || 'Request failed';
122
+ if (status === 400) {
123
+ logger.error(formatBlockingError(`Validation error: ${msg}`));
124
+ } else if (status === 401) {
125
+ logger.error(formatBlockingError('Unauthorized. Run "aifabrix login" and try again.'));
126
+ } else if (status === 403) {
127
+ logger.error(formatBlockingError(`Missing permission: integration-client:${permissionScope}`));
128
+ logger.error(
129
+ chalk.gray(
130
+ `Your account needs the integration-client:${permissionScope} permission on the controller.`
131
+ )
132
+ );
133
+ } else if (status === 404) {
134
+ logger.error(formatBlockingError('Integration client not found.'));
135
+ const detail = response.error || '';
136
+ if (detail) {
137
+ logger.error(chalk.gray(detail));
138
+ }
139
+ } else {
140
+ logger.error(formatBlockingError(`Request failed: ${msg}`));
141
+ }
142
+ process.exit(1);
143
+ }
144
+
145
+ /**
146
+ * @async
147
+ * @param {Object} options - CLI options (controller optional)
148
+ * @returns {Promise<{ controllerUrl: string, authConfig: Object }>}
149
+ */
150
+ async function resolveControllerAndAuth(options) {
151
+ const controllerUrl = options.controller || (await resolveControllerUrl());
152
+ if (!controllerUrl) {
153
+ logger.error(formatBlockingError('Controller URL is required. Run "aifabrix login" first.'));
154
+ process.exit(1);
155
+ }
156
+ const authResult = await getIntegrationClientAuth(controllerUrl);
157
+ if (!authResult || !authResult.token) {
158
+ logger.error(formatBlockingError(`No authentication token for controller: ${controllerUrl}`));
159
+ logger.error(chalk.gray('Run: aifabrix login'));
160
+ process.exit(1);
161
+ }
162
+ return {
163
+ controllerUrl: authResult.controllerUrl,
164
+ authConfig: { type: 'bearer', token: authResult.token }
165
+ };
166
+ }
167
+
168
+ /**
169
+ * @param {Array<Record<string, unknown>>} items - Integration clients
170
+ */
171
+ function displayIntegrationClientList(items) {
172
+ logger.log(`\n${sectionTitle('Integration clients:')}\n`);
173
+ if (!items || items.length === 0) {
174
+ logger.log(chalk.gray(' No integration clients found.\n'));
175
+ return;
176
+ }
177
+ const idCol = 'Id'.padEnd(ID_WIDTH);
178
+ const keyCol = 'Key'.padEnd(KEY_WIDTH);
179
+ const displayCol = 'Display'.padEnd(DISPLAY_WIDTH);
180
+ const clientIdCol = 'ClientId'.padEnd(CLIENT_ID_WIDTH);
181
+ const statusCol = 'Status'.padEnd(STATUS_WIDTH);
182
+ logger.log(chalk.gray(`${idCol}${keyCol}${displayCol}${clientIdCol}${statusCol}`));
183
+ logger.log(chalk.gray('-'.repeat(TABLE_SEPARATOR_LENGTH)));
184
+ items.forEach((row) => {
185
+ const id = (row.id ?? '').toString().padEnd(ID_WIDTH);
186
+ const key = (row.key ?? '—').toString().padEnd(KEY_WIDTH);
187
+ const displayName = (row.displayName ?? '—').toString().padEnd(DISPLAY_WIDTH);
188
+ const kcId = (row.keycloakClientId ?? '—').toString().padEnd(CLIENT_ID_WIDTH);
189
+ const status = (row.status ?? '—').toString().padEnd(STATUS_WIDTH);
190
+ logger.log(`${id}${key}${displayName}${kcId}${status}`);
191
+ });
192
+ logger.log('');
193
+ }
194
+
195
+ /**
196
+ * @param {string} clientId - OAuth client id (Keycloak)
197
+ * @param {string} clientSecret - One-time client secret
198
+ */
199
+ function displayCreateSuccess(clientId, clientSecret) {
200
+ logger.log(formatSuccessParagraph('Integration client created.'));
201
+ logger.log(`${chalk.gray(' clientId:')} ${chalk.cyan(clientId)}`);
202
+ logger.log(`${chalk.gray(' clientSecret:')} ${chalk.cyan(clientSecret)}`);
203
+ logger.log(chalk.yellow(`\n⚠ ${ONE_TIME_WARNING}\n`));
204
+ }
205
+
206
+ /**
207
+ * @param {string} [val] - Comma-separated value
208
+ * @returns {string[]}
209
+ */
210
+ function parseList(val) {
211
+ if (val === undefined || val === null || String(val).trim() === '') {
212
+ return [];
213
+ }
214
+ return String(val)
215
+ .split(',')
216
+ .map(s => s.trim())
217
+ .filter(Boolean);
218
+ }
219
+
220
+ /**
221
+ * @param {Object} options - CLI options
222
+ * @returns {{ key: string, displayName: string, redirectUris: string[], groupNames: string[], description?: string, keycloakClientId?: string }}
223
+ */
224
+ function validateIntegrationClientCreateOptions(options) {
225
+ const key = requireValidIntegrationClientKey(options.key);
226
+ const displayName = options.displayName?.trim();
227
+ const redirectUris = parseList(options.redirectUris);
228
+ const groupNames = parseList(options.groupNames);
229
+ if (!displayName) {
230
+ logger.error(formatBlockingError('Display name is required. Use --display-name <name>.'));
231
+ process.exit(1);
232
+ }
233
+ if (redirectUris.length === 0) {
234
+ logger.error(formatBlockingError('At least one redirect URI is required. Use --redirect-uris <uri1,uri2,...>.'));
235
+ process.exit(1);
236
+ }
237
+ const out = {
238
+ key,
239
+ displayName,
240
+ redirectUris,
241
+ groupNames,
242
+ description: options.description?.trim() || undefined
243
+ };
244
+ const kc = options.keycloakClientId?.trim();
245
+ if (kc) {
246
+ out.keycloakClientId = kc;
247
+ }
248
+ return out;
249
+ }
250
+
251
+ /**
252
+ * @async
253
+ * @param {Object} options - CLI options
254
+ * @returns {Promise<Object>}
255
+ */
256
+ async function resolveOptionsAndAuth(options) {
257
+ const validated = validateIntegrationClientCreateOptions(options);
258
+ const controllerUrl = options.controller || (await resolveControllerUrl());
259
+ if (!controllerUrl) {
260
+ logger.error(formatBlockingError('Controller URL is required. Run "aifabrix login" first.'));
261
+ process.exit(1);
262
+ }
263
+ const authResult = await getIntegrationClientAuth(controllerUrl);
264
+ if (!authResult || !authResult.token) {
265
+ logger.error(formatBlockingError(`No authentication token for controller: ${controllerUrl}`));
266
+ logger.error(chalk.gray('Run: aifabrix login'));
267
+ process.exit(1);
268
+ }
269
+ return {
270
+ ...validated,
271
+ controllerUrl: authResult.controllerUrl,
272
+ authConfig: { type: 'bearer', token: authResult.token }
273
+ };
274
+ }
275
+
276
+ /**
277
+ * @async
278
+ * @param {Object} [options]
279
+ * @returns {Promise<void>}
280
+ */
281
+ async function runIntegrationClientCreate(options = {}) {
282
+ const ctx = await resolveOptionsAndAuth(options);
283
+ const body = {
284
+ key: ctx.key,
285
+ displayName: ctx.displayName,
286
+ redirectUris: ctx.redirectUris,
287
+ groupNames: ctx.groupNames,
288
+ description: ctx.description,
289
+ keycloakClientId: ctx.keycloakClientId
290
+ };
291
+ const response = await createIntegrationClient(ctx.controllerUrl, ctx.authConfig, body);
292
+ if (!response.success) {
293
+ handleCreateError(response);
294
+ return;
295
+ }
296
+ const { clientId, clientSecret } = extractCreateResponse(response);
297
+ displayCreateSuccess(clientId, clientSecret);
298
+ }
299
+
300
+ /**
301
+ * @param {string} [id]
302
+ * @returns {string}
303
+ */
304
+ function requireIntegrationClientId(id) {
305
+ const trimmed = (id && typeof id === 'string' ? id.trim() : '') || '';
306
+ if (!trimmed) {
307
+ logger.error(formatBlockingError('Integration client ID is required. Use --id <uuid>.'));
308
+ process.exit(1);
309
+ }
310
+ return trimmed;
311
+ }
312
+
313
+ /**
314
+ * @async
315
+ * @param {Object} [options]
316
+ * @returns {Promise<void>}
317
+ */
318
+ async function runIntegrationClientList(options = {}) {
319
+ const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
320
+ const listOptions = {
321
+ page: options.page,
322
+ pageSize: options.pageSize,
323
+ sort: options.sort,
324
+ filter: options.filter,
325
+ search: options.search
326
+ };
327
+ const response = await listIntegrationClients(controllerUrl, authConfig, listOptions);
328
+ if (response && response.success === false) {
329
+ handleIntegrationClientApiError(response, 'read');
330
+ return;
331
+ }
332
+ const body = response?.data?.data ?? response?.data ?? response ?? {};
333
+ const items = Array.isArray(body) ? body : (body.data ?? []);
334
+ displayIntegrationClientList(items);
335
+ }
336
+
337
+ /**
338
+ * @async
339
+ * @param {Object} [options]
340
+ * @returns {Promise<void>}
341
+ */
342
+ async function runIntegrationClientRotateSecret(options = {}) {
343
+ const id = requireIntegrationClientId(options.id);
344
+ const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
345
+ const response = await regenerateIntegrationClientSecret(controllerUrl, authConfig, id);
346
+ if (response && response.success === false) {
347
+ handleIntegrationClientApiError(response, 'update');
348
+ return;
349
+ }
350
+ const payload = response?.data?.data ?? response?.data ?? response ?? {};
351
+ const clientSecret = payload?.clientSecret ?? '';
352
+ if (response && response.success === true) {
353
+ logger.log(formatSuccessParagraph('Secret rotated.'));
354
+ logger.log(`${chalk.gray(' clientSecret:')} ${chalk.cyan(clientSecret)}`);
355
+ logger.log(chalk.yellow(`\n⚠ ${ONE_TIME_WARNING}\n`));
356
+ }
357
+ }
358
+
359
+ /**
360
+ * @async
361
+ * @param {Object} [options]
362
+ * @returns {Promise<void>}
363
+ */
364
+ async function runIntegrationClientDelete(options = {}) {
365
+ const id = requireIntegrationClientId(options.id);
366
+ const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
367
+ const response = await deleteIntegrationClient(controllerUrl, authConfig, id);
368
+ if (response && response.success === false) {
369
+ handleIntegrationClientApiError(response, 'delete');
370
+ return;
371
+ }
372
+ if (response && response.success === true) {
373
+ logger.log(formatSuccessLine('Integration client deactivated.\n'));
374
+ }
375
+ }
376
+
377
+ /**
378
+ * @async
379
+ * @param {Object} [options]
380
+ * @returns {Promise<void>}
381
+ */
382
+ async function runIntegrationClientUpdateGroups(options = {}) {
383
+ const id = requireIntegrationClientId(options.id);
384
+ const groupNames = parseList(options.groupNames);
385
+ if (groupNames.length === 0) {
386
+ logger.error(formatBlockingError('At least one group name is required. Use --group-names <name1,name2,...>.'));
387
+ process.exit(1);
388
+ }
389
+ const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
390
+ const response = await updateIntegrationClientGroups(controllerUrl, authConfig, id, { groupNames });
391
+ if (response && response.success === false) {
392
+ handleIntegrationClientApiError(response, 'update');
393
+ return;
394
+ }
395
+ if (response && response.success === true) {
396
+ logger.log(formatSuccessLine('Integration client groups updated.\n'));
397
+ }
398
+ }
399
+
400
+ /**
401
+ * @async
402
+ * @param {Object} [options]
403
+ * @returns {Promise<void>}
404
+ */
405
+ async function runIntegrationClientUpdateRedirectUris(options = {}) {
406
+ const id = requireIntegrationClientId(options.id);
407
+ const redirectUris = parseList(options.redirectUris);
408
+ if (redirectUris.length === 0) {
409
+ logger.error(formatBlockingError('At least one redirect URI is required. Use --redirect-uris <uri1,uri2,...>.'));
410
+ process.exit(1);
411
+ }
412
+ const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
413
+ const response = await updateIntegrationClientRedirectUris(controllerUrl, authConfig, id, { redirectUris });
414
+ if (response && response.success === false) {
415
+ handleIntegrationClientApiError(response, 'update');
416
+ return;
417
+ }
418
+ if (response && response.success === true) {
419
+ logger.log(formatSuccessLine('Integration client redirect URIs updated.\n'));
420
+ }
421
+ }
422
+
423
+ module.exports = {
424
+ runIntegrationClientCreate,
425
+ runIntegrationClientList,
426
+ runIntegrationClientRotateSecret,
427
+ runIntegrationClientDelete,
428
+ runIntegrationClientUpdateGroups,
429
+ runIntegrationClientUpdateRedirectUris
430
+ };
@@ -16,6 +16,7 @@ const { setCurrentEnvironment, saveDeviceToken, setControllerUrl } = require('..
16
16
  const { initiateDeviceCodeFlow } = require('../api/auth.api');
17
17
  const { pollDeviceCodeToken, displayDeviceCodeInfo } = require('../utils/api');
18
18
  const logger = require('../utils/logger');
19
+ const { formatSuccessLine } = require('../utils/cli-layout-chalk');
19
20
 
20
21
  /**
21
22
  * Validate environment key format
@@ -79,18 +80,22 @@ async function saveDeviceLoginConfig(controllerUrl, token, refreshToken, expires
79
80
  * @param {string} envKey - Environment key
80
81
  * @returns {Promise<void>}
81
82
  */
82
- async function saveTokenAndDisplaySuccess(controllerUrl, token, refreshToken, expiresAt, envKey) {
83
+ async function saveTokenAndDisplaySuccess(controllerUrl, token, refreshToken, expiresAt, envKey, opts = {}) {
83
84
  await saveDeviceLoginConfig(controllerUrl, token, refreshToken, expiresAt);
84
85
  await setControllerUrl(controllerUrl);
85
86
  if (envKey) {
86
87
  await setCurrentEnvironment(envKey);
87
88
  }
88
- logger.log(formatSuccessParagraph('Successfully logged in!'));
89
- logger.log(chalk.gray(`Controller: ${controllerUrl}`));
90
- if (envKey) {
91
- logger.log(chalk.gray(`Environment: ${envKey}`));
89
+ if (!opts.compact) {
90
+ logger.log(formatSuccessParagraph('Successfully logged in!'));
91
+ logger.log(chalk.gray(`Controller: ${controllerUrl}`));
92
+ if (envKey) {
93
+ logger.log(chalk.gray(`Environment: ${envKey}`));
94
+ }
95
+ logger.log(chalk.gray('Token stored securely in ~/.aifabrix/config.yaml\n'));
96
+ } else {
97
+ logger.log(formatSuccessLine('Authentication successful'));
92
98
  }
93
- logger.log(chalk.gray('Token stored securely in ~/.aifabrix/config.yaml\n'));
94
99
  }
95
100
 
96
101
  /**
@@ -103,16 +108,21 @@ async function saveTokenAndDisplaySuccess(controllerUrl, token, refreshToken, ex
103
108
  * @param {string} envKey - Environment key
104
109
  * @returns {Promise<{token: string, environment: string}>} Token and environment
105
110
  */
106
- async function pollAndSaveDeviceCodeToken(controllerUrl, deviceCode, interval, expiresIn, envKey) {
107
- const spinner = ora({
108
- text: 'Waiting for approval',
109
- spinner: 'dots'
110
- }).start();
111
+ async function pollAndSaveDeviceCodeToken(controllerUrl, deviceCode, interval, expiresIn, envKey, opts = {}) {
112
+ const useSpinner = !opts.compact;
113
+ const spinner = useSpinner
114
+ ? ora({ text: 'Waiting for approval', spinner: 'dots' }).start()
115
+ : null;
116
+ if (!useSpinner) {
117
+ logger.log('Waiting for approval...');
118
+ }
111
119
 
112
120
  let pollCount = 0;
113
121
  const pollCallback = () => {
114
122
  pollCount++;
115
- spinner.text = `Waiting for approval (attempt ${pollCount})...`;
123
+ if (spinner) {
124
+ spinner.text = `Waiting for approval (attempt ${pollCount})...`;
125
+ }
116
126
  };
117
127
 
118
128
  try {
@@ -124,16 +134,20 @@ async function pollAndSaveDeviceCodeToken(controllerUrl, deviceCode, interval, e
124
134
  pollCallback
125
135
  );
126
136
 
127
- spinner.succeed('Authentication approved!');
137
+ if (spinner) {
138
+ spinner.succeed('Authentication approved!');
139
+ }
128
140
 
129
141
  const token = tokenResponse.access_token;
130
142
  const refreshToken = tokenResponse.refresh_token;
131
143
  const expiresAt = new Date(Date.now() + (tokenResponse.expires_in * 1000)).toISOString();
132
144
 
133
- await saveTokenAndDisplaySuccess(controllerUrl, token, refreshToken, expiresAt, envKey);
145
+ await saveTokenAndDisplaySuccess(controllerUrl, token, refreshToken, expiresAt, envKey, opts);
134
146
  return { token, environment: envKey };
135
147
  } catch (pollError) {
136
- spinner.fail('Authentication failed');
148
+ if (spinner) {
149
+ spinner.fail('Authentication failed');
150
+ }
137
151
  throw pollError;
138
152
  }
139
153
  }
@@ -210,6 +224,32 @@ function convertDeviceCodeResponse(apiResponse) {
210
224
  };
211
225
  }
212
226
 
227
+ function logDeviceFlowStart(online, requestScope, opts) {
228
+ if (opts.compact) return;
229
+ logger.log(chalk.blue('\nšŸ“± Initiating device code flow...\n'));
230
+ if (!online && requestScope.includes('offline_access')) {
231
+ logger.log(chalk.gray(`Requesting offline token (scope: ${requestScope})\n`));
232
+ }
233
+ }
234
+
235
+ function logDeviceFlowError(deviceError, opts) {
236
+ if (opts.exitOnFailure === false) {
237
+ throw deviceError;
238
+ }
239
+ if (deviceError.formattedError) {
240
+ logger.error(chalk.red('\nāœ– Device code flow failed:'));
241
+ logger.log(deviceError.formattedError);
242
+ } else {
243
+ logger.error(chalk.red(`\nāœ– Device code flow failed: ${deviceError.message}`));
244
+ }
245
+ process.exit(1);
246
+ }
247
+
248
+ function printCompactDeviceUrl(deviceCodeResponse) {
249
+ logger.log('\nOpen browser to authenticate:');
250
+ logger.log(chalk.yellow(`${deviceCodeResponse.verification_uri}?user_code=${deviceCodeResponse.user_code}\n`));
251
+ }
252
+
213
253
  /**
214
254
  * Handle device code flow login
215
255
  * @async
@@ -219,14 +259,11 @@ function convertDeviceCodeResponse(apiResponse) {
219
259
  * @param {string} [scope] - Custom scope string
220
260
  * @returns {Promise<{token: string, environment: string}>} Token and environment
221
261
  */
222
- async function handleDeviceCodeLogin(controllerUrl, environment, online, scope) {
262
+ async function handleDeviceCodeLogin(controllerUrl, environment, online, scope, opts = {}) {
223
263
  const envKey = await getEnvironmentKey(environment);
224
264
  const requestScope = buildScope(online, scope);
225
265
 
226
- logger.log(chalk.blue('\nšŸ“± Initiating device code flow...\n'));
227
- if (!online && requestScope.includes('offline_access')) {
228
- logger.log(chalk.gray(`Requesting offline token (scope: ${requestScope})\n`));
229
- }
266
+ logDeviceFlowStart(online, requestScope, opts);
230
267
 
231
268
  try {
232
269
  // Use centralized API client for device code flow initiation
@@ -239,25 +276,23 @@ async function handleDeviceCodeLogin(controllerUrl, environment, online, scope)
239
276
  const apiResponse = deviceCodeApiResponse.data;
240
277
  const deviceCodeResponse = convertDeviceCodeResponse(apiResponse);
241
278
 
242
- displayDeviceCodeInfo(deviceCodeResponse.user_code, deviceCodeResponse.verification_uri, logger, chalk);
279
+ if (!opts.compact) {
280
+ displayDeviceCodeInfo(deviceCodeResponse.user_code, deviceCodeResponse.verification_uri, logger, chalk);
281
+ } else {
282
+ printCompactDeviceUrl(deviceCodeResponse);
283
+ }
243
284
 
244
285
  return await pollAndSaveDeviceCodeToken(
245
286
  controllerUrl,
246
287
  deviceCodeResponse.device_code,
247
288
  deviceCodeResponse.interval,
248
289
  deviceCodeResponse.expires_in,
249
- envKey
290
+ envKey,
291
+ opts
250
292
  );
251
293
 
252
294
  } catch (deviceError) {
253
- // Display formatted error if available (includes detailed validation info)
254
- if (deviceError.formattedError) {
255
- logger.error(chalk.red('\nāœ– Device code flow failed:'));
256
- logger.log(deviceError.formattedError);
257
- } else {
258
- logger.error(chalk.red(`\nāœ– Device code flow failed: ${deviceError.message}`));
259
- }
260
- process.exit(1);
295
+ logDeviceFlowError(deviceError, opts);
261
296
  }
262
297
  }
263
298
 
@@ -87,7 +87,9 @@ async function normalizeControllerUrl(options) {
87
87
  controllerUrl = String(controllerUrl).replace(/\/+$/, '');
88
88
  // Save controller URL to config
89
89
  await setControllerUrl(controllerUrl);
90
- logger.log(chalk.gray(`Controller URL: ${controllerUrl}`));
90
+ if (!options.compact) {
91
+ logger.log(chalk.gray(`Controller URL: ${controllerUrl}`));
92
+ }
91
93
  return controllerUrl;
92
94
  }
93
95
 
@@ -102,7 +104,9 @@ async function handleEnvironmentConfig(options) {
102
104
  if (options.environment) {
103
105
  const environment = options.environment.trim();
104
106
  await setCurrentEnvironment(environment);
105
- logger.log(chalk.gray(`Environment: ${environment}`));
107
+ if (!options.compact) {
108
+ logger.log(chalk.gray(`Environment: ${environment}`));
109
+ }
106
110
  return environment;
107
111
  }
108
112
  // Get current environment from config
@@ -151,11 +155,16 @@ async function handleCredentialsLoginFlow(controllerUrl, environment, options) {
151
155
  * @returns {Promise<{token: string, environment: string}>} Login result
152
156
  */
153
157
  async function handleDeviceCodeLoginFlow(controllerUrl, environment, options) {
154
- return await handleDeviceCodeLogin(controllerUrl, environment, options.online, options.scope);
158
+ return await handleDeviceCodeLogin(controllerUrl, environment, options.online, options.scope, {
159
+ compact: Boolean(options.compact),
160
+ exitOnFailure: options.exitOnFailure !== false
161
+ });
155
162
  }
156
163
 
157
164
  async function handleLogin(options) {
158
- logger.log(chalk.blue('\nšŸ” Logging in to Miso Controller...\n'));
165
+ if (!options.compact) {
166
+ logger.log(chalk.blue('\nšŸ” Logging in to Miso Controller...\n'));
167
+ }
159
168
 
160
169
  const controllerUrl = await normalizeControllerUrl(options);
161
170
  const environment = await handleEnvironmentConfig(options);
@@ -170,13 +179,15 @@ async function handleLogin(options) {
170
179
  return; // Early return for device flow (already saved config)
171
180
  }
172
181
 
173
- logger.log(formatSuccessParagraph('Successfully logged in!'));
174
- logger.log(chalk.gray(`Controller: ${controllerUrl}`));
175
- logger.log(chalk.gray(`Environment: ${environment}`));
176
- if (options.app) {
177
- logger.log(chalk.gray(`App: ${options.app}`));
182
+ if (!options.compact) {
183
+ logger.log(formatSuccessParagraph('Successfully logged in!'));
184
+ logger.log(chalk.gray(`Controller: ${controllerUrl}`));
185
+ logger.log(chalk.gray(`Environment: ${environment}`));
186
+ if (options.app) {
187
+ logger.log(chalk.gray(`App: ${options.app}`));
188
+ }
189
+ logger.log(chalk.gray('Token stored securely in ~/.aifabrix/config.yaml\n'));
178
190
  }
179
- logger.log(chalk.gray('Token stored securely in ~/.aifabrix/config.yaml\n'));
180
191
  }
181
192
 
182
193
  module.exports = { handleLogin };