@aifabrix/builder 2.31.1 → 2.32.2

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 (118) hide show
  1. package/README.md +9 -9
  2. package/integration/hubspot/README.md +2 -2
  3. package/integration/hubspot/hubspot-deploy-company.json +17 -14
  4. package/integration/hubspot/hubspot-deploy-contact.json +19 -16
  5. package/integration/hubspot/hubspot-deploy-deal.json +21 -18
  6. package/lib/api/types/datasources.types.js +31 -5
  7. package/lib/api/types/wizard.types.js +142 -0
  8. package/lib/api/wizard.api.js +177 -0
  9. package/lib/{app-config.js → app/config.js} +4 -4
  10. package/lib/{app-deploy.js → app/deploy.js} +8 -8
  11. package/lib/app/display.js +90 -0
  12. package/lib/{app-dockerfile.js → app/dockerfile.js} +4 -4
  13. package/lib/{app-down.js → app/down.js} +4 -4
  14. package/lib/app/helpers.js +218 -0
  15. package/lib/app/index.js +298 -0
  16. package/lib/{app-list.js → app/list.js} +6 -6
  17. package/lib/{app-push.js → app/push.js} +4 -4
  18. package/lib/{app-readme.js → app/readme.js} +34 -13
  19. package/lib/{app-register.js → app/register.js} +9 -9
  20. package/lib/{app-rotate-secret.js → app/rotate-secret.js} +10 -10
  21. package/lib/{app-run-helpers.js → app/run-helpers.js} +10 -10
  22. package/lib/{app-run.js → app/run.js} +6 -6
  23. package/lib/{build.js → build/index.js} +59 -32
  24. package/lib/build/package.json +7 -0
  25. package/lib/cli.js +245 -179
  26. package/lib/commands/app.js +3 -3
  27. package/lib/commands/datasource.js +4 -4
  28. package/lib/commands/login-credentials.js +209 -0
  29. package/lib/commands/login-device.js +254 -0
  30. package/lib/commands/login.js +67 -378
  31. package/lib/commands/logout.js +1 -1
  32. package/lib/commands/secrets-set.js +1 -1
  33. package/lib/commands/secure.js +2 -2
  34. package/lib/commands/wizard.js +498 -0
  35. package/lib/{audit-logger.js → core/audit-logger.js} +1 -1
  36. package/lib/{config.js → core/config.js} +28 -26
  37. package/lib/{diff.js → core/diff.js} +157 -72
  38. package/lib/{secrets.js → core/secrets.js} +86 -49
  39. package/lib/{templates.js → core/templates-env.js} +14 -222
  40. package/lib/core/templates.js +279 -0
  41. package/lib/{datasource-deploy.js → datasource/deploy.js} +6 -6
  42. package/lib/{datasource-diff.js → datasource/diff.js} +2 -2
  43. package/lib/datasource/list.js +223 -0
  44. package/lib/{datasource-validate.js → datasource/validate.js} +2 -2
  45. package/lib/{deployer.js → deployment/deployer.js} +48 -18
  46. package/lib/{environment-deploy.js → deployment/environment.js} +163 -84
  47. package/lib/{push.js → deployment/push.js} +1 -1
  48. package/lib/external-system/deploy-helpers.js +145 -0
  49. package/lib/{external-system-deploy.js → external-system/deploy.js} +156 -111
  50. package/lib/external-system/download-helpers.js +114 -0
  51. package/lib/{external-system-download.js → external-system/download.js} +92 -135
  52. package/lib/{external-system-generator.js → external-system/generator.js} +15 -11
  53. package/lib/external-system/test-auth.js +40 -0
  54. package/lib/external-system/test-execution.js +84 -0
  55. package/lib/external-system/test-helpers.js +109 -0
  56. package/lib/{external-system-test.js → external-system/test.js} +174 -192
  57. package/lib/{generator-builders.js → generator/builders.js} +87 -10
  58. package/lib/{generator-external.js → generator/external.js} +115 -52
  59. package/lib/{github-generator.js → generator/github.js} +116 -15
  60. package/lib/{generator-helpers.js → generator/helpers.js} +92 -42
  61. package/lib/{generator.js → generator/index.js} +49 -22
  62. package/lib/{generator-split.js → generator/split.js} +108 -55
  63. package/lib/generator/wizard-prompts.js +357 -0
  64. package/lib/generator/wizard.js +490 -0
  65. package/lib/{infra.js → infrastructure/index.js} +49 -22
  66. package/lib/schema/external-datasource.schema.json +158 -136
  67. package/lib/schema/external-system.schema.json +43 -1
  68. package/lib/utils/api.js +9 -5
  69. package/lib/utils/app-register-api.js +60 -32
  70. package/lib/utils/app-register-auth.js +172 -47
  71. package/lib/utils/app-register-config.js +130 -59
  72. package/lib/utils/app-run-containers.js +29 -8
  73. package/lib/utils/build-helpers.js +1 -1
  74. package/lib/utils/cli-utils.js +78 -30
  75. package/lib/utils/compose-generator.js +145 -65
  76. package/lib/utils/config-paths.js +2 -0
  77. package/lib/utils/deployment-errors.js +1 -1
  78. package/lib/utils/device-code.js +99 -41
  79. package/lib/utils/env-config-loader.js +1 -1
  80. package/lib/utils/env-copy.js +21 -18
  81. package/lib/utils/env-endpoints.js +115 -67
  82. package/lib/utils/env-map.js +13 -14
  83. package/lib/utils/env-ports.js +45 -25
  84. package/lib/utils/env-template.js +84 -42
  85. package/lib/utils/error-formatter.js +26 -9
  86. package/lib/utils/error-formatters/error-parser.js +90 -4
  87. package/lib/utils/error-formatters/http-status-errors.js +54 -17
  88. package/lib/utils/error-formatters/network-errors.js +103 -26
  89. package/lib/utils/external-system-display.js +184 -90
  90. package/lib/utils/external-system-validators.js +164 -42
  91. package/lib/utils/file-upload.js +109 -0
  92. package/lib/utils/health-check.js +199 -83
  93. package/lib/utils/infra-containers.js +1 -1
  94. package/lib/utils/infra-status.js +66 -15
  95. package/lib/utils/local-secrets.js +45 -25
  96. package/lib/utils/paths.js +45 -33
  97. package/lib/utils/schema-loader.js +42 -25
  98. package/lib/utils/schema-resolver.js +123 -74
  99. package/lib/utils/secrets-encryption.js +62 -25
  100. package/lib/utils/secrets-helpers.js +126 -63
  101. package/lib/utils/secrets-path.js +1 -1
  102. package/lib/utils/secrets-url.js +1 -1
  103. package/lib/utils/token-manager-refresh.js +181 -0
  104. package/lib/utils/token-manager.js +76 -123
  105. package/lib/utils/variable-transformer.js +154 -77
  106. package/lib/utils/yaml-preserve.js +41 -47
  107. package/lib/{template-validator.js → validation/template.js} +54 -23
  108. package/lib/{validate.js → validation/validate.js} +205 -125
  109. package/lib/{validator.js → validation/validator.js} +58 -39
  110. package/package.json +31 -2
  111. package/templates/external-system/deploy.ps1.hbs +34 -0
  112. package/templates/external-system/deploy.sh.hbs +34 -0
  113. package/templates/external-system/external-datasource.json.hbs +31 -12
  114. package/lib/app.js +0 -467
  115. package/lib/datasource-list.js +0 -141
  116. /package/lib/{app-prompts.js → app/prompts.js} +0 -0
  117. /package/lib/{env-reader.js → core/env-reader.js} +0 -0
  118. /package/lib/{key-generator.js → core/key-generator.js} +0 -0
@@ -11,24 +11,10 @@
11
11
 
12
12
  const inquirer = require('inquirer');
13
13
  const chalk = require('chalk');
14
- const ora = require('ora');
15
- const { setCurrentEnvironment, saveDeviceToken, saveClientToken } = require('../config');
16
- const { getToken, initiateDeviceCodeFlow } = require('../api/auth.api');
17
- const { pollDeviceCodeToken, displayDeviceCodeInfo } = require('../utils/api');
18
- const { formatApiError } = require('../utils/api-error-handler');
19
- const { loadClientCredentials } = require('../utils/token-manager');
14
+ const { setCurrentEnvironment, saveClientToken } = require('../core/config');
20
15
  const logger = require('../utils/logger');
21
-
22
- /**
23
- * Validate environment key format
24
- * @param {string} envKey - Environment key to validate
25
- * @throws {Error} If environment key format is invalid
26
- */
27
- function validateEnvironmentKey(envKey) {
28
- if (!/^[a-z0-9-_]+$/i.test(envKey)) {
29
- throw new Error('Environment key must contain only letters, numbers, hyphens, and underscores');
30
- }
31
- }
16
+ const { handleCredentialsLogin } = require('./login-credentials');
17
+ const { handleDeviceCodeLogin } = require('./login-device');
32
18
 
33
19
  /**
34
20
  * Determine and validate authentication method
@@ -57,95 +43,6 @@ async function determineAuthMethod(method) {
57
43
  return authMethod.method;
58
44
  }
59
45
 
60
- /**
61
- * Prompt for credentials if not provided
62
- * @async
63
- * @param {string} [clientId] - Existing client ID
64
- * @param {string} [clientSecret] - Existing client secret
65
- * @returns {Promise<{clientId: string, clientSecret: string}>} Credentials
66
- */
67
- async function promptForCredentials(clientId, clientSecret) {
68
- if (clientId && clientSecret) {
69
- return { clientId: clientId.trim(), clientSecret: clientSecret.trim() };
70
- }
71
-
72
- const credentials = await inquirer.prompt([
73
- {
74
- type: 'input',
75
- name: 'clientId',
76
- message: 'Client ID:',
77
- default: clientId || '',
78
- validate: (input) => {
79
- const value = input.trim();
80
- if (!value || value.length === 0) {
81
- return 'Client ID is required';
82
- }
83
- return true;
84
- }
85
- },
86
- {
87
- type: 'password',
88
- name: 'clientSecret',
89
- message: 'Client Secret:',
90
- default: clientSecret || '',
91
- mask: '*',
92
- validate: (input) => {
93
- const value = input.trim();
94
- if (!value || value.length === 0) {
95
- return 'Client Secret is required';
96
- }
97
- return true;
98
- }
99
- }
100
- ]);
101
-
102
- return {
103
- clientId: credentials.clientId.trim(),
104
- clientSecret: credentials.clientSecret.trim()
105
- };
106
- }
107
-
108
- /**
109
- * Get and validate environment key
110
- * @async
111
- * @param {string} [environment] - Environment key from options
112
- * @returns {Promise<string>} Validated environment key
113
- */
114
- async function getEnvironmentKey(environment) {
115
- if (environment) {
116
- const envKey = environment.trim();
117
- validateEnvironmentKey(envKey);
118
- return envKey;
119
- }
120
-
121
- const envPrompt = await inquirer.prompt([{
122
- type: 'input',
123
- name: 'environment',
124
- message: 'Environment key (e.g., miso, dev, tst, pro):',
125
- validate: (input) => {
126
- if (!input || input.trim().length === 0) {
127
- return 'Environment key is required';
128
- }
129
- validateEnvironmentKey(input.trim());
130
- return true;
131
- }
132
- }]);
133
-
134
- return envPrompt.environment.trim();
135
- }
136
-
137
- /**
138
- * Save device token configuration (root level, controller-specific)
139
- * @async
140
- * @param {string} controllerUrl - Controller URL (used as key)
141
- * @param {string} token - Authentication token
142
- * @param {string} refreshToken - Refresh token for token renewal
143
- * @param {string} expiresAt - Token expiration time
144
- */
145
- async function saveDeviceLoginConfig(controllerUrl, token, refreshToken, expiresAt) {
146
- await saveDeviceToken(controllerUrl, token, refreshToken, expiresAt);
147
- }
148
-
149
46
  /**
150
47
  * Save client credentials token configuration
151
48
  * @async
@@ -160,314 +57,106 @@ async function saveCredentialsLoginConfig(controllerUrl, token, expiresAt, envir
160
57
  }
161
58
 
162
59
  /**
163
- * Handle credentials-based login
164
- * Uses OpenAPI /api/v1/auth/token endpoint with x-client-id and x-client-secret headers
165
- * Reads credentials from secrets.local.yaml using pattern <app-name>-client-idKeyVault
60
+ * Handle login command action
166
61
  * @async
167
- * @param {string} controllerUrl - Controller URL
168
- * @param {string} appName - Application name
169
- * @param {string} [clientId] - Client ID from options (optional, overrides secrets.local.yaml)
170
- * @param {string} [clientSecret] - Client Secret from options (optional, overrides secrets.local.yaml)
171
- * @returns {Promise<string>} Authentication token
62
+ * @function handleLogin
63
+ * @param {Object} options - Login options
64
+ * @param {string} [options.controller] - Controller URL (default: 'http://localhost:3000')
65
+ * @param {string} [options.method] - Authentication method ('device' or 'credentials')
66
+ * @param {string} [options.app] - Application name (for credentials method, reads from secrets.local.yaml)
67
+ * @param {string} [options.clientId] - Client ID (for credentials method, overrides secrets.local.yaml)
68
+ * @param {string} [options.clientSecret] - Client Secret (for credentials method, overrides secrets.local.yaml)
69
+ * @param {string} [options.environment] - Environment key (updates root-level environment in config.yaml)
70
+ * @returns {Promise<void>} Resolves when login completes
71
+ * @throws {Error} If login fails
172
72
  */
173
- async function handleCredentialsLogin(controllerUrl, appName, clientId, clientSecret) {
174
- let credentials;
175
-
176
- // Try to load from secrets.local.yaml if appName provided and credentials not provided
177
- if (appName && !clientId && !clientSecret) {
178
- credentials = await loadClientCredentials(appName);
179
- if (!credentials) {
180
- logger.log(chalk.yellow(`⚠️ Credentials not found in secrets.local.yaml for app '${appName}'`));
181
- logger.log(chalk.gray(` Looking for: '${appName}-client-idKeyVault' and '${appName}-client-secretKeyVault'`));
182
- logger.log(chalk.gray(' Prompting for credentials...\n'));
183
- }
184
- }
185
-
186
- // If still no credentials, prompt for them
187
- if (!credentials) {
188
- credentials = await promptForCredentials(clientId, clientSecret);
189
- }
190
-
191
- // Use centralized API client for token generation
192
- const response = await getToken(credentials.clientId, credentials.clientSecret, controllerUrl);
193
-
194
- if (!response.success) {
195
- const formattedError = response.formattedError || formatApiError(response);
196
- logger.error(formattedError);
197
-
198
- // Provide additional context for login failures
199
- if (response.status === 401) {
200
- logger.log(chalk.gray('\n💡 Tip: Verify your client credentials are correct.'));
201
- logger.log(chalk.gray(' Check secrets.local.yaml for:'));
202
- logger.log(chalk.gray(` - ${appName}-client-idKeyVault`));
203
- logger.log(chalk.gray(` - ${appName}-client-secretKeyVault`));
204
- }
205
-
206
- process.exit(1);
207
- }
208
-
209
- // OpenAPI spec response: { success: boolean, token: string, expiresIn: number, expiresAt: string, ... }
210
- // Handle both flat and nested response structures (some APIs wrap in data field)
211
- const apiResponse = response.data;
212
- const responseData = apiResponse.data || apiResponse;
213
-
214
- if (!responseData || !responseData.token) {
215
- logger.error(chalk.red('❌ Invalid response: missing token'));
216
- if (responseData) {
217
- logger.error(chalk.gray(`Response structure: ${JSON.stringify(responseData, null, 2)}`));
218
- }
219
- process.exit(1);
220
- }
221
-
222
- // Calculate expiration (use expiresAt if provided, otherwise calculate from expiresIn, default to 24 hours)
223
- let expiresAt;
224
- if (responseData.expiresAt) {
225
- expiresAt = responseData.expiresAt;
226
- } else if (responseData.expiresIn) {
227
- expiresAt = new Date(Date.now() + responseData.expiresIn * 1000).toISOString();
228
- } else {
229
- expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
230
- }
231
-
232
- return {
233
- token: responseData.token,
234
- expiresAt: expiresAt
235
- };
236
- }
237
-
238
73
  /**
239
- * Poll for device code token and save configuration
240
- * @async
241
- * @param {string} controllerUrl - Controller URL
242
- * @param {string} deviceCode - Device code
243
- * @param {number} interval - Polling interval
244
- * @param {number} expiresIn - Expiration time
245
- * @param {string} envKey - Environment key
246
- * @returns {Promise<{token: string, environment: string}>} Token and environment
74
+ * Normalizes and logs controller URL
75
+ * @function normalizeControllerUrl
76
+ * @param {Object} options - Login options
77
+ * @returns {string} Normalized controller URL
247
78
  */
248
- async function pollAndSaveDeviceCodeToken(controllerUrl, deviceCode, interval, expiresIn, envKey) {
249
- const spinner = ora({
250
- text: 'Waiting for approval',
251
- spinner: 'dots'
252
- }).start();
253
-
254
- let pollCount = 0;
255
- const pollCallback = () => {
256
- pollCount++;
257
- spinner.text = `Waiting for approval (attempt ${pollCount})...`;
258
- };
259
-
260
- try {
261
- const tokenResponse = await pollDeviceCodeToken(
262
- controllerUrl,
263
- deviceCode,
264
- interval,
265
- expiresIn,
266
- pollCallback
267
- );
268
-
269
- spinner.succeed('Authentication approved!');
270
-
271
- const token = tokenResponse.access_token;
272
- const refreshToken = tokenResponse.refresh_token;
273
- const expiresAt = new Date(Date.now() + (tokenResponse.expires_in * 1000)).toISOString();
274
-
275
- // Save device token at root level (controller-specific, not environment-specific)
276
- await saveDeviceLoginConfig(controllerUrl, token, refreshToken, expiresAt);
277
-
278
- // Still set current environment if provided (for other purposes)
279
- if (envKey) {
280
- await setCurrentEnvironment(envKey);
281
- }
282
-
283
- logger.log(chalk.green('\n✅ Successfully logged in!'));
284
- logger.log(chalk.gray(`Controller: ${controllerUrl}`));
285
- if (envKey) {
286
- logger.log(chalk.gray(`Environment: ${envKey}`));
287
- }
288
- logger.log(chalk.gray('Token stored securely in ~/.aifabrix/config.yaml\n'));
289
-
290
- return { token, environment: envKey };
291
-
292
- } catch (pollError) {
293
- spinner.fail('Authentication failed');
294
- throw pollError;
295
- }
79
+ function normalizeControllerUrl(options) {
80
+ const controllerUrl = (options.controller || options.url || 'http://localhost:3000').replace(/\/$/, '');
81
+ logger.log(chalk.gray(`Controller URL: ${controllerUrl}`));
82
+ return controllerUrl;
296
83
  }
297
84
 
298
85
  /**
299
- * Build scope string from options
300
- * @param {boolean} [offline] - Whether to request offline_access
301
- * @param {string} [customScope] - Custom scope string
302
- * @returns {string} Scope string
86
+ * Handles environment configuration
87
+ * @async
88
+ * @function handleEnvironmentConfig
89
+ * @param {Object} options - Login options
90
+ * @returns {Promise<string>} Environment key
303
91
  */
304
- function buildScope(offline, customScope) {
305
- const defaultScope = 'openid profile email';
306
-
307
- if (customScope) {
308
- // If custom scope provided, use it and optionally add offline_access
309
- if (offline && !customScope.includes('offline_access')) {
310
- return `${customScope} offline_access`;
311
- }
312
- return customScope;
313
- }
314
-
315
- // Default scope with optional offline_access
316
- if (offline) {
317
- return `${defaultScope} offline_access`;
92
+ async function handleEnvironmentConfig(options) {
93
+ if (options.environment) {
94
+ const environment = options.environment.trim();
95
+ await setCurrentEnvironment(environment);
96
+ logger.log(chalk.gray(`Environment: ${environment}`));
97
+ return environment;
318
98
  }
319
-
320
- return defaultScope;
99
+ // Get current environment from config
100
+ const { getCurrentEnvironment } = require('../core/config');
101
+ return await getCurrentEnvironment();
321
102
  }
322
103
 
323
104
  /**
324
- * Validate device code API response
325
- * @param {Object} deviceCodeApiResponse - API response
326
- * @throws {Error} If response is invalid
105
+ * Validates scope options for credentials method
106
+ * @function validateScopeOptions
107
+ * @param {string} method - Authentication method
108
+ * @param {Object} options - Login options
327
109
  */
328
- function validateDeviceCodeResponse(deviceCodeApiResponse) {
329
- if (!deviceCodeApiResponse) {
330
- throw new Error('Device code flow initiation returned no response');
331
- }
332
-
333
- if (!deviceCodeApiResponse.success) {
334
- const errorMessage = deviceCodeApiResponse.formattedError ||
335
- deviceCodeApiResponse.error ||
336
- 'Device code flow initiation failed';
337
- const error = new Error(errorMessage);
338
- if (deviceCodeApiResponse.formattedError) {
339
- error.formattedError = deviceCodeApiResponse.formattedError;
340
- }
341
- throw error;
342
- }
343
-
344
- if (!deviceCodeApiResponse.data) {
345
- throw new Error('Device code flow initiation returned no data');
110
+ function validateScopeOptions(method, options) {
111
+ if (method === 'credentials' && (options.offline || options.scope)) {
112
+ logger.log(chalk.yellow('⚠️ Warning: --offline and --scope options are only available for device flow'));
113
+ logger.log(chalk.gray(' These options will be ignored for credentials method\n'));
346
114
  }
347
115
  }
348
116
 
349
117
  /**
350
- * Convert API response to device code format
351
- * @param {Object} apiResponse - API response data
352
- * @returns {Object} Device code response in snake_case format
353
- */
354
- function convertDeviceCodeResponse(apiResponse) {
355
- const deviceCodeData = apiResponse.data || apiResponse;
356
- return {
357
- device_code: deviceCodeData.deviceCode || deviceCodeData.device_code,
358
- user_code: deviceCodeData.userCode || deviceCodeData.user_code,
359
- verification_uri: deviceCodeData.verificationUri || deviceCodeData.verification_uri,
360
- expires_in: deviceCodeData.expiresIn || deviceCodeData.expires_in || 600,
361
- interval: deviceCodeData.interval || 5
362
- };
363
- }
364
-
365
- /**
366
- * Handle device code flow login
118
+ * Handles credentials login flow
367
119
  * @async
120
+ * @function handleCredentialsLoginFlow
368
121
  * @param {string} controllerUrl - Controller URL
369
- * @param {string} [environment] - Environment key from options
370
- * @param {boolean} [offline] - Whether to request offline_access scope
371
- * @param {string} [scope] - Custom scope string
372
- * @returns {Promise<{token: string, environment: string}>} Token and environment
122
+ * @param {string} environment - Environment key
123
+ * @param {Object} options - Login options
124
+ * @returns {Promise<void>}
373
125
  */
374
- async function handleDeviceCodeLogin(controllerUrl, environment, offline, scope) {
375
- const envKey = await getEnvironmentKey(environment);
376
- const requestScope = buildScope(offline, scope);
377
-
378
- logger.log(chalk.blue('\n📱 Initiating device code flow...\n'));
379
- if (offline) {
380
- logger.log(chalk.gray(`Requesting offline token (scope: ${requestScope})\n`));
381
- }
382
-
383
- try {
384
- // Use centralized API client for device code flow initiation
385
- const deviceCodeApiResponse = await initiateDeviceCodeFlow(controllerUrl, envKey, requestScope);
386
-
387
- // Validate response structure
388
- validateDeviceCodeResponse(deviceCodeApiResponse);
389
-
390
- // Convert API response to device code format
391
- const apiResponse = deviceCodeApiResponse.data;
392
- const deviceCodeResponse = convertDeviceCodeResponse(apiResponse);
393
-
394
- displayDeviceCodeInfo(deviceCodeResponse.user_code, deviceCodeResponse.verification_uri, logger, chalk);
395
-
396
- return await pollAndSaveDeviceCodeToken(
397
- controllerUrl,
398
- deviceCodeResponse.device_code,
399
- deviceCodeResponse.interval,
400
- deviceCodeResponse.expires_in,
401
- envKey
402
- );
403
-
404
- } catch (deviceError) {
405
- // Display formatted error if available (includes detailed validation info)
406
- if (deviceError.formattedError) {
407
- logger.error(chalk.red('\n❌ Device code flow failed:'));
408
- logger.log(deviceError.formattedError);
409
- } else {
410
- logger.error(chalk.red(`\n❌ Device code flow failed: ${deviceError.message}`));
411
- }
126
+ async function handleCredentialsLoginFlow(controllerUrl, environment, options) {
127
+ if (!options.app) {
128
+ logger.error(chalk.red('❌ --app is required for credentials login method'));
412
129
  process.exit(1);
413
130
  }
131
+ const loginResult = await handleCredentialsLogin(controllerUrl, options.app, options.clientId, options.clientSecret);
132
+ await saveCredentialsLoginConfig(controllerUrl, loginResult.token, loginResult.expiresAt, environment, options.app);
414
133
  }
415
134
 
416
135
  /**
417
- * Handle login command action
136
+ * Handles device code login flow
418
137
  * @async
419
- * @function handleLogin
138
+ * @function handleDeviceCodeLoginFlow
139
+ * @param {string} controllerUrl - Controller URL
420
140
  * @param {Object} options - Login options
421
- * @param {string} [options.controller] - Controller URL (default: 'http://localhost:3000')
422
- * @param {string} [options.method] - Authentication method ('device' or 'credentials')
423
- * @param {string} [options.app] - Application name (for credentials method, reads from secrets.local.yaml)
424
- * @param {string} [options.clientId] - Client ID (for credentials method, overrides secrets.local.yaml)
425
- * @param {string} [options.clientSecret] - Client Secret (for credentials method, overrides secrets.local.yaml)
426
- * @param {string} [options.environment] - Environment key (updates root-level environment in config.yaml)
427
- * @returns {Promise<void>} Resolves when login completes
428
- * @throws {Error} If login fails
141
+ * @returns {Promise<{token: string, environment: string}>} Login result
429
142
  */
143
+ async function handleDeviceCodeLoginFlow(controllerUrl, options) {
144
+ return await handleDeviceCodeLogin(controllerUrl, options.environment, options.offline, options.scope);
145
+ }
146
+
430
147
  async function handleLogin(options) {
431
148
  logger.log(chalk.blue('\n🔐 Logging in to Miso Controller...\n'));
432
149
 
433
- const controllerUrl = (options.controller || options.url || 'http://localhost:3000').replace(/\/$/, '');
434
- logger.log(chalk.gray(`Controller URL: ${controllerUrl}`));
435
-
436
- // Update root-level environment if provided
437
- let environment = null;
438
- if (options.environment) {
439
- environment = options.environment.trim();
440
- await setCurrentEnvironment(environment);
441
- logger.log(chalk.gray(`Environment: ${environment}`));
442
- } else {
443
- // Get current environment from config
444
- const { getCurrentEnvironment } = require('../config');
445
- environment = await getCurrentEnvironment();
446
- }
447
-
150
+ const controllerUrl = normalizeControllerUrl(options);
151
+ const environment = await handleEnvironmentConfig(options);
448
152
  const method = await determineAuthMethod(options.method);
449
- let token;
450
- let expiresAt;
451
153
 
452
- // Validate scope options - only applicable to device flow
453
- if (method === 'credentials' && (options.offline || options.scope)) {
454
- logger.log(chalk.yellow('⚠️ Warning: --offline and --scope options are only available for device flow'));
455
- logger.log(chalk.gray(' These options will be ignored for credentials method\n'));
456
- }
154
+ validateScopeOptions(method, options);
457
155
 
458
156
  if (method === 'credentials') {
459
- if (!options.app) {
460
- logger.error(chalk.red('❌ --app is required for credentials login method'));
461
- process.exit(1);
462
- }
463
- const loginResult = await handleCredentialsLogin(controllerUrl, options.app, options.clientId, options.clientSecret);
464
- token = loginResult.token;
465
- expiresAt = loginResult.expiresAt;
466
- await saveCredentialsLoginConfig(controllerUrl, token, expiresAt, environment, options.app);
157
+ await handleCredentialsLoginFlow(controllerUrl, environment, options);
467
158
  } else if (method === 'device') {
468
- const result = await handleDeviceCodeLogin(controllerUrl, options.environment, options.offline, options.scope);
469
- token = result.token;
470
- environment = result.environment;
159
+ await handleDeviceCodeLoginFlow(controllerUrl, options);
471
160
  return; // Early return for device flow (already saved config)
472
161
  }
473
162
 
@@ -17,7 +17,7 @@ const {
17
17
  clearAllClientTokens,
18
18
  clearClientTokensForEnvironment,
19
19
  normalizeControllerUrl
20
- } = require('../config');
20
+ } = require('../core/config');
21
21
  const logger = require('../utils/logger');
22
22
  const os = require('os');
23
23
  const path = require('path');
@@ -12,7 +12,7 @@
12
12
  const path = require('path');
13
13
  const chalk = require('chalk');
14
14
  const logger = require('../utils/logger');
15
- const { getAifabrixSecretsPath } = require('../config');
15
+ const { getAifabrixSecretsPath } = require('../core/config');
16
16
  const { saveLocalSecret, saveSecret } = require('../utils/local-secrets');
17
17
  const pathsUtil = require('../utils/paths');
18
18
 
@@ -15,7 +15,7 @@ const yaml = require('js-yaml');
15
15
  const inquirer = require('inquirer');
16
16
  const chalk = require('chalk');
17
17
  const logger = require('../utils/logger');
18
- const { setSecretsEncryptionKey, getSecretsEncryptionKey } = require('../config');
18
+ const { setSecretsEncryptionKey, getSecretsEncryptionKey } = require('../core/config');
19
19
  const { validateEncryptionKey } = require('../utils/secrets-encryption');
20
20
  const { encryptYamlValues } = require('../utils/yaml-preserve');
21
21
  const pathsUtil = require('../utils/paths');
@@ -39,7 +39,7 @@ async function findSecretsFiles() {
39
39
 
40
40
  // Check config.yaml for aifabrix-secrets
41
41
  try {
42
- const { getAifabrixSecretsPath } = require('../config');
42
+ const { getAifabrixSecretsPath } = require('../core/config');
43
43
  const generalSecretsPath = await getAifabrixSecretsPath();
44
44
  if (generalSecretsPath) {
45
45
  const resolvedPath = path.isAbsolute(generalSecretsPath)