@aifabrix/builder 2.22.2 → 2.31.1

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 (63) hide show
  1. package/jest.config.coverage.js +37 -0
  2. package/lib/api/pipeline.api.js +10 -9
  3. package/lib/app-deploy.js +36 -14
  4. package/lib/app-list.js +191 -71
  5. package/lib/app-prompts.js +77 -26
  6. package/lib/app-readme.js +123 -5
  7. package/lib/app-rotate-secret.js +210 -80
  8. package/lib/app-run-helpers.js +200 -172
  9. package/lib/app-run.js +137 -68
  10. package/lib/audit-logger.js +8 -7
  11. package/lib/build.js +161 -250
  12. package/lib/cli.js +73 -65
  13. package/lib/commands/login.js +45 -31
  14. package/lib/commands/logout.js +181 -0
  15. package/lib/commands/secure.js +59 -24
  16. package/lib/config.js +79 -45
  17. package/lib/datasource-deploy.js +89 -29
  18. package/lib/deployer.js +164 -129
  19. package/lib/diff.js +63 -21
  20. package/lib/environment-deploy.js +36 -19
  21. package/lib/external-system-deploy.js +134 -66
  22. package/lib/external-system-download.js +244 -171
  23. package/lib/external-system-test.js +199 -164
  24. package/lib/generator-external.js +145 -72
  25. package/lib/generator-helpers.js +49 -17
  26. package/lib/generator-split.js +105 -58
  27. package/lib/infra.js +101 -131
  28. package/lib/schema/application-schema.json +895 -896
  29. package/lib/schema/env-config.yaml +11 -4
  30. package/lib/template-validator.js +13 -4
  31. package/lib/utils/api.js +8 -8
  32. package/lib/utils/app-register-auth.js +36 -18
  33. package/lib/utils/app-run-containers.js +140 -0
  34. package/lib/utils/auth-headers.js +6 -6
  35. package/lib/utils/build-copy.js +60 -2
  36. package/lib/utils/build-helpers.js +94 -0
  37. package/lib/utils/cli-utils.js +177 -76
  38. package/lib/utils/compose-generator.js +12 -2
  39. package/lib/utils/config-tokens.js +151 -9
  40. package/lib/utils/deployment-errors.js +137 -69
  41. package/lib/utils/deployment-validation-helpers.js +103 -0
  42. package/lib/utils/docker-build.js +57 -0
  43. package/lib/utils/dockerfile-utils.js +13 -3
  44. package/lib/utils/env-copy.js +163 -94
  45. package/lib/utils/env-map.js +226 -86
  46. package/lib/utils/error-formatters/network-errors.js +0 -1
  47. package/lib/utils/external-system-display.js +14 -19
  48. package/lib/utils/external-system-env-helpers.js +107 -0
  49. package/lib/utils/external-system-test-helpers.js +144 -0
  50. package/lib/utils/health-check.js +10 -8
  51. package/lib/utils/infra-status.js +123 -0
  52. package/lib/utils/paths.js +228 -49
  53. package/lib/utils/schema-loader.js +125 -57
  54. package/lib/utils/token-manager.js +3 -3
  55. package/lib/utils/yaml-preserve.js +55 -16
  56. package/lib/validate.js +87 -89
  57. package/package.json +7 -5
  58. package/scripts/ci-fix.sh +19 -0
  59. package/scripts/ci-simulate.sh +19 -0
  60. package/scripts/install-local.js +210 -0
  61. package/templates/applications/miso-controller/test.yaml +1 -0
  62. package/templates/python/Dockerfile.hbs +8 -45
  63. package/templates/typescript/Dockerfile.hbs +8 -42
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Jest Configuration for Coverage Reports
3
+ * Extends base config with coverage settings
4
+ */
5
+
6
+ const baseConfig = require('./jest.config');
7
+
8
+ module.exports = {
9
+ ...baseConfig,
10
+ collectCoverageFrom: [
11
+ 'lib/**/*.js',
12
+ 'bin/**/*.js',
13
+ '!**/node_modules/**',
14
+ '!**/tests/**',
15
+ '!lib/infra.js',
16
+ '!bin/aifabrix.js'
17
+ ],
18
+ coverageDirectory: 'coverage',
19
+ coverageReporters: [
20
+ 'text',
21
+ 'lcov',
22
+ 'html'
23
+ ],
24
+ coverageProvider: 'v8',
25
+ coverageThreshold: {
26
+ global: {
27
+ branches: 80,
28
+ functions: 80,
29
+ lines: 80,
30
+ statements: 80
31
+ }
32
+ },
33
+ // Memory management for coverage runs
34
+ maxWorkers: 2,
35
+ workerIdleMemoryLimit: '500MB'
36
+ };
37
+
@@ -105,18 +105,19 @@ async function publishDatasourceViaPipeline(dataplaneUrl, systemKey, authConfig,
105
105
  * POST /api/v1/pipeline/{systemKey}/{datasourceKey}/test
106
106
  * @async
107
107
  * @function testDatasourceViaPipeline
108
- * @param {string} dataplaneUrl - Dataplane base URL
109
- * @param {string} systemKey - System key
110
- * @param {string} datasourceKey - Datasource key
111
- * @param {Object} authConfig - Authentication configuration
112
- * @param {Object} testData - Test data
113
- * @param {Object} testData.payloadTemplate - Test payload template
114
- * @param {Object} [options] - Request options
115
- * @param {number} [options.timeout] - Request timeout in milliseconds
108
+ * @param {Object} params - Function parameters
109
+ * @param {string} params.dataplaneUrl - Dataplane base URL
110
+ * @param {string} params.systemKey - System key
111
+ * @param {string} params.datasourceKey - Datasource key
112
+ * @param {Object} params.authConfig - Authentication configuration
113
+ * @param {Object} params.testData - Test data
114
+ * @param {Object} params.testData.payloadTemplate - Test payload template
115
+ * @param {Object} [params.options] - Request options
116
+ * @param {number} [params.options.timeout] - Request timeout in milliseconds
116
117
  * @returns {Promise<Object>} Test response
117
118
  * @throws {Error} If test fails
118
119
  */
119
- async function testDatasourceViaPipeline(dataplaneUrl, systemKey, datasourceKey, authConfig, testData, options = {}) {
120
+ async function testDatasourceViaPipeline({ dataplaneUrl, systemKey, datasourceKey, authConfig, testData, options = {} }) {
120
121
  const client = new ApiClient(dataplaneUrl, authConfig);
121
122
  const requestOptions = {
122
123
  body: testData
package/lib/app-deploy.js CHANGED
@@ -208,23 +208,13 @@ function validateDeploymentConfig(deploymentConfig) {
208
208
  }
209
209
 
210
210
  /**
211
- * Loads deployment configuration from variables.yaml and gets/refreshes token
211
+ * Configure deployment environment settings
212
212
  * @async
213
- * @param {string} appName - Application name
214
213
  * @param {Object} options - CLI options
215
- * @returns {Promise<Object>} Deployment configuration with token
216
- * @throws {Error} If configuration is invalid
214
+ * @param {Object} deploymentConfig - Deployment configuration to update
215
+ * @returns {Promise<void>}
217
216
  */
218
- async function loadDeploymentConfig(appName, options) {
219
- // Detect app type and get correct path (integration or builder)
220
- const { appPath } = await detectAppType(appName);
221
- await validateAppDirectory(appPath, appName);
222
-
223
- const variablesPath = path.join(appPath, 'variables.yaml');
224
- const variables = await loadVariablesFile(variablesPath);
225
-
226
- const deploymentConfig = extractDeploymentConfig(options, variables);
227
-
217
+ async function configureDeploymentEnvironment(options, deploymentConfig) {
228
218
  // Update root-level environment if provided
229
219
  if (options.environment) {
230
220
  await config.setCurrentEnvironment(options.environment);
@@ -233,7 +223,17 @@ async function loadDeploymentConfig(appName, options) {
233
223
  // Get current environment from root-level config
234
224
  const currentEnvironment = await config.getCurrentEnvironment();
235
225
  deploymentConfig.envKey = deploymentConfig.envKey || currentEnvironment;
226
+ }
236
227
 
228
+ /**
229
+ * Refresh deployment token and configure authentication
230
+ * @async
231
+ * @param {string} appName - Application name
232
+ * @param {Object} deploymentConfig - Deployment configuration to update
233
+ * @returns {Promise<void>}
234
+ * @throws {Error} If authentication fails
235
+ */
236
+ async function refreshDeploymentToken(appName, deploymentConfig) {
237
237
  // Get controller URL
238
238
  if (!deploymentConfig.controllerUrl) {
239
239
  throw new Error('Controller URL is required. Set it in variables.yaml or use --controller flag');
@@ -257,6 +257,28 @@ async function loadDeploymentConfig(appName, options) {
257
257
  } catch (error) {
258
258
  throw new Error(`Failed to get authentication: ${error.message}`);
259
259
  }
260
+ }
261
+
262
+ /**
263
+ * Loads deployment configuration from variables.yaml and gets/refreshes token
264
+ * @async
265
+ * @param {string} appName - Application name
266
+ * @param {Object} options - CLI options
267
+ * @returns {Promise<Object>} Deployment configuration with token
268
+ * @throws {Error} If configuration is invalid
269
+ */
270
+ async function loadDeploymentConfig(appName, options) {
271
+ // Detect app type and get correct path (integration or builder)
272
+ const { appPath } = await detectAppType(appName);
273
+ await validateAppDirectory(appPath, appName);
274
+
275
+ const variablesPath = path.join(appPath, 'variables.yaml');
276
+ const variables = await loadVariablesFile(variablesPath);
277
+
278
+ const deploymentConfig = extractDeploymentConfig(options, variables);
279
+
280
+ await configureDeploymentEnvironment(options, deploymentConfig);
281
+ await refreshDeploymentToken(appName, deploymentConfig);
260
282
 
261
283
  validateDeploymentConfig(deploymentConfig);
262
284
 
package/lib/app-list.js CHANGED
@@ -16,6 +16,54 @@ const { formatApiError } = require('./utils/api-error-handler');
16
16
  const { formatAuthenticationError } = require('./utils/error-formatters/http-status-errors');
17
17
  const logger = require('./utils/logger');
18
18
 
19
+ /**
20
+ * Extract wrapped array format: { success: true, data: { success: true, data: [...] } }
21
+ * @param {Object} apiResponse - API response data
22
+ * @returns {Array|null} Applications array or null if not this format
23
+ */
24
+ function extractWrappedArray(apiResponse) {
25
+ if (apiResponse && apiResponse.data && Array.isArray(apiResponse.data)) {
26
+ return apiResponse.data;
27
+ }
28
+ return null;
29
+ }
30
+
31
+ /**
32
+ * Extract direct array format: { success: true, data: [...] }
33
+ * @param {Object} apiResponse - API response data
34
+ * @returns {Array|null} Applications array or null if not this format
35
+ */
36
+ function extractDirectArray(apiResponse) {
37
+ if (Array.isArray(apiResponse)) {
38
+ return apiResponse;
39
+ }
40
+ return null;
41
+ }
42
+
43
+ /**
44
+ * Extract paginated items format: { success: true, data: { items: [...] } }
45
+ * @param {Object} apiResponse - API response data
46
+ * @returns {Array|null} Applications array or null if not this format
47
+ */
48
+ function extractPaginatedItems(apiResponse) {
49
+ if (apiResponse && Array.isArray(apiResponse.items)) {
50
+ return apiResponse.items;
51
+ }
52
+ return null;
53
+ }
54
+
55
+ /**
56
+ * Extract wrapped paginated items format: { success: true, data: { success: true, data: { items: [...] } } }
57
+ * @param {Object} apiResponse - API response data
58
+ * @returns {Array|null} Applications array or null if not this format
59
+ */
60
+ function extractWrappedPaginatedItems(apiResponse) {
61
+ if (apiResponse && apiResponse.data && apiResponse.data.items && Array.isArray(apiResponse.data.items)) {
62
+ return apiResponse.data.items;
63
+ }
64
+ return null;
65
+ }
66
+
19
67
  /**
20
68
  * Extract applications array from API response
21
69
  * Handles multiple response formats:
@@ -29,21 +77,14 @@ const logger = require('./utils/logger');
29
77
  */
30
78
  function extractApplications(response) {
31
79
  const apiResponse = response.data;
32
- let applications;
33
80
 
34
- // Check if apiResponse.data is an array (wrapped format)
35
- if (apiResponse && apiResponse.data && Array.isArray(apiResponse.data)) {
36
- applications = apiResponse.data;
37
- } else if (Array.isArray(apiResponse)) {
38
- // Check if apiResponse is directly an array
39
- applications = apiResponse;
40
- } else if (apiResponse && Array.isArray(apiResponse.items)) {
41
- // Check if apiResponse.items is an array (paginated format)
42
- applications = apiResponse.items;
43
- } else if (apiResponse && apiResponse.data && apiResponse.data.items && Array.isArray(apiResponse.data.items)) {
44
- // Check if apiResponse.data.items is an array (wrapped paginated format)
45
- applications = apiResponse.data.items;
46
- } else {
81
+ // Try each format in sequence
82
+ const applications = extractWrappedArray(apiResponse) ||
83
+ extractDirectArray(apiResponse) ||
84
+ extractPaginatedItems(apiResponse) ||
85
+ extractWrappedPaginatedItems(apiResponse);
86
+
87
+ if (!applications) {
47
88
  logger.error(chalk.red('❌ Invalid response: expected data array or items array'));
48
89
  logger.error(chalk.gray('\nAPI response type:'), typeof apiResponse);
49
90
  logger.error(chalk.gray('API response:'), JSON.stringify(apiResponse, null, 2));
@@ -55,6 +96,36 @@ function extractApplications(response) {
55
96
  return applications;
56
97
  }
57
98
 
99
+ /**
100
+ * Find device token from config by trying each stored URL
101
+ * @async
102
+ * @param {Object} deviceConfig - Device configuration object
103
+ * @returns {Promise<Object|null>} Token result with token and controllerUrl, or null if not found
104
+ */
105
+ async function findDeviceTokenFromConfig(deviceConfig) {
106
+ const deviceUrls = Object.keys(deviceConfig);
107
+ if (deviceUrls.length === 0) {
108
+ return null;
109
+ }
110
+
111
+ for (const storedUrl of deviceUrls) {
112
+ try {
113
+ const normalizedStoredUrl = normalizeControllerUrl(storedUrl);
114
+ const deviceToken = await getOrRefreshDeviceToken(normalizedStoredUrl);
115
+ if (deviceToken && deviceToken.token) {
116
+ return {
117
+ token: deviceToken.token,
118
+ controllerUrl: deviceToken.controller || normalizedStoredUrl
119
+ };
120
+ }
121
+ } catch (error) {
122
+ // Continue to next URL
123
+ }
124
+ }
125
+
126
+ return null;
127
+ }
128
+
58
129
  /**
59
130
  * Display applications list
60
131
  * @param {Array} applications - Array of application objects
@@ -79,58 +150,57 @@ function displayApplications(applications, environment) {
79
150
  }
80
151
 
81
152
  /**
82
- * List applications in an environment
153
+ * Try to get device token from controller URL
83
154
  * @async
84
- * @param {Object} options - Command options
85
- * @param {string} options.environment - Environment ID or key
86
- * @param {string} [options.controller] - Controller URL (optional, uses configured controller if not provided)
87
- * @throws {Error} If listing fails
155
+ * @param {string} controllerUrl - Controller URL
156
+ * @returns {Promise<Object|null>} Object with token and controllerUrl, or null
88
157
  */
89
- async function listApplications(options) {
90
- const config = await getConfig();
91
-
92
- // Get controller URL with priority: options.controller > device tokens
93
- const controllerUrl = options.controller || null;
94
- let token = null;
95
- let actualControllerUrl = null;
96
-
97
- // If controller URL provided, try to get device token
98
- if (controllerUrl) {
99
- try {
100
- const normalizedUrl = normalizeControllerUrl(controllerUrl);
101
- const deviceToken = await getOrRefreshDeviceToken(normalizedUrl);
102
- if (deviceToken && deviceToken.token) {
103
- token = deviceToken.token;
104
- actualControllerUrl = deviceToken.controller || normalizedUrl;
105
- }
106
- } catch (error) {
107
- // Show which controller URL failed
108
- logger.error(chalk.red(`❌ Failed to authenticate with controller: ${controllerUrl}`));
109
- logger.error(chalk.gray(`Error: ${error.message}`));
110
- process.exit(1);
158
+ async function tryGetTokenFromController(controllerUrl) {
159
+ try {
160
+ const normalizedUrl = normalizeControllerUrl(controllerUrl);
161
+ const deviceToken = await getOrRefreshDeviceToken(normalizedUrl);
162
+ if (deviceToken && deviceToken.token) {
163
+ return {
164
+ token: deviceToken.token,
165
+ actualControllerUrl: deviceToken.controller || normalizedUrl
166
+ };
111
167
  }
168
+ } catch (error) {
169
+ logger.error(chalk.red(`❌ Failed to authenticate with controller: ${controllerUrl}`));
170
+ logger.error(chalk.gray(`Error: ${error.message}`));
171
+ process.exit(1);
112
172
  }
173
+ return null;
174
+ }
113
175
 
114
- // If no token yet, try to find any device token in config
115
- if (!token && config.device) {
116
- const deviceUrls = Object.keys(config.device);
117
- if (deviceUrls.length > 0) {
118
- for (const storedUrl of deviceUrls) {
119
- try {
120
- const normalizedStoredUrl = normalizeControllerUrl(storedUrl);
121
- const deviceToken = await getOrRefreshDeviceToken(normalizedStoredUrl);
122
- if (deviceToken && deviceToken.token) {
123
- token = deviceToken.token;
124
- actualControllerUrl = deviceToken.controller || normalizedStoredUrl;
125
- break;
126
- }
127
- } catch (error) {
128
- // Continue to next URL
129
- }
130
- }
131
- }
176
+ /**
177
+ * Try to find device token from config
178
+ * @async
179
+ * @param {Object} deviceConfig - Device configuration
180
+ * @returns {Promise<Object|null>} Object with token and controllerUrl, or null
181
+ */
182
+ async function tryGetTokenFromConfig(deviceConfig) {
183
+ if (!deviceConfig) {
184
+ return null;
132
185
  }
186
+ const tokenResult = await findDeviceTokenFromConfig(deviceConfig);
187
+ if (tokenResult) {
188
+ return {
189
+ token: tokenResult.token,
190
+ actualControllerUrl: tokenResult.controllerUrl
191
+ };
192
+ }
193
+ return null;
194
+ }
133
195
 
196
+ /**
197
+ * Validate and return authentication token or exit
198
+ * @param {string|null} token - Authentication token
199
+ * @param {string|null} actualControllerUrl - Controller URL
200
+ * @param {string} controllerUrl - Original controller URL (for error message)
201
+ * @returns {Object} Object with token and actualControllerUrl
202
+ */
203
+ function validateAuthToken(token, actualControllerUrl, controllerUrl) {
134
204
  if (!token || !actualControllerUrl) {
135
205
  const formattedError = formatAuthenticationError({
136
206
  controllerUrl: controllerUrl || undefined,
@@ -139,23 +209,73 @@ async function listApplications(options) {
139
209
  logger.error(formattedError);
140
210
  process.exit(1);
141
211
  }
212
+ return { token, actualControllerUrl };
213
+ }
214
+
215
+ /**
216
+ * Get authentication token for listing applications
217
+ * @async
218
+ * @param {string} [controllerUrl] - Optional controller URL
219
+ * @param {Object} config - Configuration object
220
+ * @returns {Promise<Object>} Object with token and actualControllerUrl
221
+ * @throws {Error} If authentication fails
222
+ */
223
+ async function getListAuthToken(controllerUrl, config) {
224
+ // Try to get token from controller URL first
225
+ let authResult = null;
226
+ if (controllerUrl) {
227
+ authResult = await tryGetTokenFromController(controllerUrl);
228
+ }
229
+
230
+ // If no token yet, try to find device token from config
231
+ if (!authResult && config.device) {
232
+ authResult = await tryGetTokenFromConfig(config.device);
233
+ }
234
+
235
+ // Validate and return token or exit
236
+ return validateAuthToken(authResult?.token || null, authResult?.actualControllerUrl || null, controllerUrl);
237
+ }
238
+
239
+ /**
240
+ * Handle API response and extract applications
241
+ * @param {Object} response - API response
242
+ * @param {string} actualControllerUrl - Controller URL
243
+ * @returns {Array} Extracted applications
244
+ * @throws {Error} If response is invalid
245
+ */
246
+ function handleListResponse(response, actualControllerUrl) {
247
+ if (!response.success || !response.data) {
248
+ const formattedError = response.formattedError || formatApiError(response, actualControllerUrl);
249
+ logger.error(formattedError);
250
+ logger.error(chalk.gray(`\nController URL: ${actualControllerUrl}`));
251
+ logger.error(chalk.gray('\nFull response for debugging:'));
252
+ logger.error(chalk.gray(JSON.stringify(response, null, 2)));
253
+ process.exit(1);
254
+ }
255
+
256
+ return extractApplications(response);
257
+ }
258
+
259
+ /**
260
+ * List applications in an environment
261
+ * @async
262
+ * @param {Object} options - Command options
263
+ * @param {string} options.environment - Environment ID or key
264
+ * @param {string} [options.controller] - Controller URL (optional, uses configured controller if not provided)
265
+ * @throws {Error} If listing fails
266
+ */
267
+ async function listApplications(options) {
268
+ const config = await getConfig();
269
+
270
+ // Get authentication token
271
+ const controllerUrl = options.controller || null;
272
+ const { token, actualControllerUrl } = await getListAuthToken(controllerUrl, config);
142
273
 
143
274
  // Use centralized API client
144
275
  const authConfig = { type: 'bearer', token: token };
145
276
  try {
146
277
  const response = await listEnvironmentApplications(actualControllerUrl, options.environment, authConfig);
147
-
148
- if (!response.success || !response.data) {
149
- const formattedError = response.formattedError || formatApiError(response, actualControllerUrl);
150
- logger.error(formattedError);
151
- logger.error(chalk.gray(`\nController URL: ${actualControllerUrl}`));
152
- // Log full response for debugging
153
- logger.error(chalk.gray('\nFull response for debugging:'));
154
- logger.error(chalk.gray(JSON.stringify(response, null, 2)));
155
- process.exit(1);
156
- }
157
-
158
- const applications = extractApplications(response);
278
+ const applications = handleListResponse(response, actualControllerUrl);
159
279
  displayApplications(applications, options.environment);
160
280
  } catch (error) {
161
281
  logger.error(chalk.red(`❌ Failed to list applications from controller: ${actualControllerUrl}`));
@@ -300,6 +300,75 @@ function resolveOptionalBoolean(optionValue, answerValue, defaultValue) {
300
300
  return answerValue !== undefined ? answerValue : defaultValue;
301
301
  }
302
302
 
303
+ /**
304
+ * Resolve a single config field from options or answers
305
+ * @function resolveConfigField
306
+ * @param {Object} options - Provided options
307
+ * @param {Object} answers - Prompt answers
308
+ * @param {string} fieldName - Field name to resolve
309
+ * @param {*} defaultValue - Default value
310
+ * @returns {*} Resolved value
311
+ */
312
+ function resolveConfigField(options, answers, fieldName, defaultValue) {
313
+ return resolveField(options[fieldName], answers[fieldName], defaultValue);
314
+ }
315
+
316
+ /**
317
+ * Resolve a single external system field if present
318
+ * @function resolveExternalSystemField
319
+ * @param {Object} options - Provided options
320
+ * @param {Object} answers - Prompt answers
321
+ * @param {string} fieldName - Field name to resolve
322
+ * @param {*} defaultValue - Default value
323
+ * @returns {*|null} Resolved value or null if not present
324
+ */
325
+ function resolveExternalSystemField(options, answers, fieldName, defaultValue) {
326
+ if (answers[fieldName] || options[fieldName]) {
327
+ return resolveField(options[fieldName], answers[fieldName], defaultValue);
328
+ }
329
+ return null;
330
+ }
331
+
332
+ /**
333
+ * Resolve external system fields and add to config
334
+ * @function resolveExternalSystemFields
335
+ * @param {Object} options - Provided options
336
+ * @param {Object} answers - Prompt answers
337
+ * @param {Object} config - Configuration object to update
338
+ * @returns {void}
339
+ */
340
+ function resolveExternalSystemFields(options, answers, config) {
341
+ const systemKey = resolveExternalSystemField(options, answers, 'systemKey', undefined);
342
+ if (systemKey !== null) {
343
+ config.systemKey = systemKey;
344
+ }
345
+
346
+ const systemDisplayName = resolveExternalSystemField(options, answers, 'systemDisplayName', undefined);
347
+ if (systemDisplayName !== null) {
348
+ config.systemDisplayName = systemDisplayName;
349
+ }
350
+
351
+ const systemDescription = resolveExternalSystemField(options, answers, 'systemDescription', undefined);
352
+ if (systemDescription !== null) {
353
+ config.systemDescription = systemDescription;
354
+ }
355
+
356
+ const systemType = resolveExternalSystemField(options, answers, 'systemType', 'openapi');
357
+ if (systemType !== null) {
358
+ config.systemType = systemType;
359
+ }
360
+
361
+ const authType = resolveExternalSystemField(options, answers, 'authType', 'apikey');
362
+ if (authType !== null) {
363
+ config.authType = authType;
364
+ }
365
+
366
+ const datasourceCount = resolveExternalSystemField(options, answers, 'datasourceCount', 1);
367
+ if (datasourceCount !== null) {
368
+ config.datasourceCount = parseInt(datasourceCount, 10);
369
+ }
370
+ }
371
+
303
372
  /**
304
373
  * Resolves conflicts between options and answers
305
374
  * @function resolveConflicts
@@ -309,36 +378,18 @@ function resolveOptionalBoolean(optionValue, answerValue, defaultValue) {
309
378
  */
310
379
  function resolveConflicts(options, answers) {
311
380
  const config = {
312
- port: parseInt(resolveField(options.port, answers.port, 3000), 10),
313
- language: resolveField(options.language, answers.language, 'typescript'),
314
- database: resolveField(options.database, answers.database, false),
315
- redis: resolveField(options.redis, answers.redis, false),
316
- storage: resolveField(options.storage, answers.storage, false),
317
- authentication: resolveField(options.authentication, answers.authentication, false),
381
+ port: parseInt(resolveConfigField(options, answers, 'port', 3000), 10),
382
+ language: resolveConfigField(options, answers, 'language', 'typescript'),
383
+ database: resolveConfigField(options, answers, 'database', false),
384
+ redis: resolveConfigField(options, answers, 'redis', false),
385
+ storage: resolveConfigField(options, answers, 'storage', false),
386
+ authentication: resolveConfigField(options, answers, 'authentication', false),
318
387
  github: resolveOptionalBoolean(options.github, answers.github, false),
319
388
  controller: resolveOptionalBoolean(options.controller, answers.controller, false),
320
- controllerUrl: resolveField(options.controllerUrl, answers.controllerUrl, undefined)
389
+ controllerUrl: resolveConfigField(options, answers, 'controllerUrl', undefined)
321
390
  };
322
391
 
323
- // Add external system fields if present
324
- if (answers.systemKey || options.systemKey) {
325
- config.systemKey = resolveField(options.systemKey, answers.systemKey, undefined);
326
- }
327
- if (answers.systemDisplayName || options.systemDisplayName) {
328
- config.systemDisplayName = resolveField(options.systemDisplayName, answers.systemDisplayName, undefined);
329
- }
330
- if (answers.systemDescription || options.systemDescription) {
331
- config.systemDescription = resolveField(options.systemDescription, answers.systemDescription, undefined);
332
- }
333
- if (answers.systemType || options.systemType) {
334
- config.systemType = resolveField(options.systemType, answers.systemType, 'openapi');
335
- }
336
- if (answers.authType || options.authType) {
337
- config.authType = resolveField(options.authType, answers.authType, 'apikey');
338
- }
339
- if (answers.datasourceCount || options.datasourceCount) {
340
- config.datasourceCount = parseInt(resolveField(options.datasourceCount, answers.datasourceCount, 1), 10);
341
- }
392
+ resolveExternalSystemFields(options, answers, config);
342
393
 
343
394
  return config;
344
395
  }