@aifabrix/builder 2.22.2 → 2.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) 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 +101 -57
  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 +4 -4
  58. package/scripts/ci-fix.sh +19 -0
  59. package/scripts/ci-simulate.sh +19 -0
  60. package/templates/applications/miso-controller/test.yaml +1 -0
  61. package/templates/python/Dockerfile.hbs +8 -45
  62. package/templates/typescript/Dockerfile.hbs +8 -42
@@ -20,6 +20,7 @@ const { getDataplaneUrl } = require('./datasource-deploy');
20
20
  const { getConfig } = require('./config');
21
21
  const { detectAppType } = require('./utils/paths');
22
22
  const logger = require('./utils/logger');
23
+ const { generateEnvTemplate } = require('./utils/external-system-env-helpers');
23
24
 
24
25
  /**
25
26
  * Validates system type from downloaded application
@@ -95,55 +96,11 @@ function handlePartialDownload(systemKey, systemData, datasourceErrors) {
95
96
  }
96
97
 
97
98
  /**
98
- * Extracts environment variables from authentication configuration
99
- * @param {Object} application - External system configuration
100
- * @returns {string} Environment variables template content
99
+ * Extract OAuth2 environment variables
100
+ * @param {Object} oauth2 - OAuth2 configuration
101
+ * @param {string} systemKey - System key
102
+ * @param {Array<string>} lines - Lines array to append to
101
103
  */
102
- function generateEnvTemplate(application) {
103
- const lines = ['# Environment variables for external system'];
104
- lines.push(`# System: ${application.key || 'unknown'}`);
105
- lines.push('');
106
-
107
- if (!application.authentication) {
108
- return lines.join('\n');
109
- }
110
-
111
- const auth = application.authentication;
112
-
113
- // OAuth2 configuration
114
- if (auth.type === 'oauth2' && auth.oauth2) {
115
- if (auth.oauth2.clientId && auth.oauth2.clientId.includes('{{')) {
116
- const key = auth.oauth2.clientId.replace(/[{}]/g, '').trim();
117
- lines.push(`${key}=kv://secrets/${application.key}/client-id`);
118
- }
119
- if (auth.oauth2.clientSecret && auth.oauth2.clientSecret.includes('{{')) {
120
- const key = auth.oauth2.clientSecret.replace(/[{}]/g, '').trim();
121
- lines.push(`${key}=kv://secrets/${application.key}/client-secret`);
122
- }
123
- }
124
-
125
- // API Key configuration
126
- if (auth.type === 'apikey' && auth.apikey) {
127
- if (auth.apikey.key && auth.apikey.key.includes('{{')) {
128
- const key = auth.apikey.key.replace(/[{}]/g, '').trim();
129
- lines.push(`${key}=kv://secrets/${application.key}/api-key`);
130
- }
131
- }
132
-
133
- // Basic Auth configuration
134
- if (auth.type === 'basic' && auth.basic) {
135
- if (auth.basic.username && auth.basic.username.includes('{{')) {
136
- const key = auth.basic.username.replace(/[{}]/g, '').trim();
137
- lines.push(`${key}=kv://secrets/${application.key}/username`);
138
- }
139
- if (auth.basic.password && auth.basic.password.includes('{{')) {
140
- const key = auth.basic.password.replace(/[{}]/g, '').trim();
141
- lines.push(`${key}=kv://secrets/${application.key}/password`);
142
- }
143
- }
144
-
145
- return lines.join('\n');
146
- }
147
104
 
148
105
  /**
149
106
  * Generates variables.yaml with externalIntegration block
@@ -244,6 +201,235 @@ function generateReadme(systemKey, application, dataSources) {
244
201
  return lines.join('\n');
245
202
  }
246
203
 
204
+ /**
205
+ * Setup authentication and get dataplane URL
206
+ * @async
207
+ * @param {string} systemKey - System key
208
+ * @param {Object} options - Download options
209
+ * @param {Object} config - Configuration object
210
+ * @returns {Promise<Object>} Object with authConfig and dataplaneUrl
211
+ * @throws {Error} If authentication fails
212
+ */
213
+ async function setupAuthenticationAndDataplane(systemKey, options, config) {
214
+ const environment = options.environment || 'dev';
215
+ const controllerUrl = options.controller || config.deployment?.controllerUrl || 'http://localhost:3000';
216
+ const authConfig = await getDeploymentAuth(controllerUrl, environment, systemKey);
217
+
218
+ if (!authConfig.token && !authConfig.clientId) {
219
+ throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
220
+ }
221
+
222
+ logger.log(chalk.blue('🌐 Getting dataplane URL from controller...'));
223
+ const dataplaneUrl = await getDataplaneUrl(controllerUrl, systemKey, environment, authConfig);
224
+ logger.log(chalk.green(`āœ“ Dataplane URL: ${dataplaneUrl}`));
225
+
226
+ return { authConfig, dataplaneUrl };
227
+ }
228
+
229
+ /**
230
+ * Download system configuration from dataplane
231
+ * @async
232
+ * @param {string} dataplaneUrl - Dataplane URL
233
+ * @param {string} systemKey - System key
234
+ * @param {Object} authConfig - Authentication configuration
235
+ * @returns {Promise<Object>} Object with application and dataSources
236
+ * @throws {Error} If download fails
237
+ */
238
+ async function downloadSystemConfiguration(dataplaneUrl, systemKey, authConfig) {
239
+ logger.log(chalk.blue(`šŸ“” Downloading system configuration: ${systemKey}`));
240
+ const response = await getExternalSystemConfig(dataplaneUrl, systemKey, authConfig);
241
+
242
+ if (!response.success || !response.data) {
243
+ throw new Error(`Failed to download system configuration: ${response.error || response.formattedError || 'Unknown error'}`);
244
+ }
245
+
246
+ const downloadData = response.data.data || response.data;
247
+ const application = downloadData.application;
248
+ const dataSources = downloadData.dataSources || [];
249
+
250
+ if (!application) {
251
+ throw new Error('Application configuration not found in download response');
252
+ }
253
+
254
+ return { application, dataSources };
255
+ }
256
+
257
+ /**
258
+ * Generate files in temporary directory
259
+ * @async
260
+ * @param {string} tempDir - Temporary directory path
261
+ * @param {string} systemKey - System key
262
+ * @param {Object} application - Application configuration
263
+ * @param {Array} dataSources - Array of datasource configurations
264
+ * @returns {Promise<Object>} Object with file paths
265
+ * @throws {Error} If file generation fails
266
+ */
267
+ async function generateFilesInTempDir(tempDir, systemKey, application, dataSources) {
268
+ const systemFileName = `${systemKey}-deploy.json`;
269
+ const systemFilePath = path.join(tempDir, systemFileName);
270
+ await fs.writeFile(systemFilePath, JSON.stringify(application, null, 2), 'utf8');
271
+
272
+ // Generate datasource files
273
+ const datasourceErrors = [];
274
+ const datasourceFiles = [];
275
+ for (const datasource of dataSources) {
276
+ try {
277
+ const entityKey = datasource.entityKey || datasource.key.split('-').pop();
278
+ const datasourceFileName = `${systemKey}-deploy-${entityKey}.json`;
279
+ const datasourceFilePath = path.join(tempDir, datasourceFileName);
280
+ await fs.writeFile(datasourceFilePath, JSON.stringify(datasource, null, 2), 'utf8');
281
+ datasourceFiles.push(datasourceFilePath);
282
+ } catch (error) {
283
+ datasourceErrors.push(new Error(`Failed to write datasource ${datasource.key}: ${error.message}`));
284
+ }
285
+ }
286
+
287
+ // Handle partial downloads
288
+ if (datasourceErrors.length > 0) {
289
+ handlePartialDownload(systemKey, application, datasourceErrors);
290
+ }
291
+
292
+ // Generate variables.yaml
293
+ const variables = generateVariablesYaml(systemKey, application, dataSources);
294
+ const variablesPath = path.join(tempDir, 'variables.yaml');
295
+ await fs.writeFile(variablesPath, yaml.dump(variables, { indent: 2, lineWidth: 120, noRefs: true }), 'utf8');
296
+
297
+ // Generate env.template
298
+ const envTemplate = generateEnvTemplate(application);
299
+ const envTemplatePath = path.join(tempDir, 'env.template');
300
+ await fs.writeFile(envTemplatePath, envTemplate, 'utf8');
301
+
302
+ // Generate README.md
303
+ const readme = generateReadme(systemKey, application, dataSources);
304
+ const readmePath = path.join(tempDir, 'README.md');
305
+ await fs.writeFile(readmePath, readme, 'utf8');
306
+
307
+ return {
308
+ systemFilePath,
309
+ variablesPath,
310
+ envTemplatePath,
311
+ readmePath,
312
+ datasourceFiles
313
+ };
314
+ }
315
+
316
+ /**
317
+ * Move files from temporary directory to final location
318
+ * @async
319
+ * @param {string} tempDir - Temporary directory path
320
+ * @param {string} finalPath - Final destination path
321
+ * @param {string} systemKey - System key
322
+ * @param {Object} filePaths - Object with file paths
323
+ * @throws {Error} If file move fails
324
+ */
325
+ async function moveFilesToFinalLocation(tempDir, finalPath, systemKey, filePaths) {
326
+ logger.log(chalk.blue(`šŸ“ Creating directory: ${finalPath}`));
327
+ await fs.mkdir(finalPath, { recursive: true });
328
+
329
+ const systemFileName = `${systemKey}-deploy.json`;
330
+ const filesToMove = [
331
+ { from: filePaths.systemFilePath, to: path.join(finalPath, systemFileName) },
332
+ { from: filePaths.variablesPath, to: path.join(finalPath, 'variables.yaml') },
333
+ { from: filePaths.envTemplatePath, to: path.join(finalPath, 'env.template') },
334
+ { from: filePaths.readmePath, to: path.join(finalPath, 'README.md') }
335
+ ];
336
+
337
+ for (const dsFile of filePaths.datasourceFiles) {
338
+ const fileName = path.basename(dsFile);
339
+ filesToMove.push({ from: dsFile, to: path.join(finalPath, fileName) });
340
+ }
341
+
342
+ for (const file of filesToMove) {
343
+ await fs.copyFile(file.from, file.to);
344
+ logger.log(chalk.green(`āœ“ Created: ${path.relative(process.cwd(), file.to)}`));
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Validate system key format
350
+ * @param {string} systemKey - System key to validate
351
+ * @throws {Error} If system key format is invalid
352
+ */
353
+ function validateSystemKeyFormat(systemKey) {
354
+ if (!systemKey || typeof systemKey !== 'string') {
355
+ throw new Error('System key is required and must be a string');
356
+ }
357
+ if (!/^[a-z0-9-_]+$/.test(systemKey)) {
358
+ throw new Error('System key must contain only lowercase letters, numbers, hyphens, and underscores');
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Handle dry run mode
364
+ * @param {string} systemKey - System key
365
+ * @param {string} dataplaneUrl - Dataplane URL
366
+ */
367
+ function handleDryRun(systemKey, dataplaneUrl) {
368
+ logger.log(chalk.yellow('šŸ” Dry run mode - would download from:'));
369
+ logger.log(chalk.gray(` ${dataplaneUrl}/api/v1/external/systems/${systemKey}/config`));
370
+ logger.log(chalk.yellow('\nWould create:'));
371
+ logger.log(chalk.gray(` integration/${systemKey}/`));
372
+ logger.log(chalk.gray(` integration/${systemKey}/variables.yaml`));
373
+ logger.log(chalk.gray(` integration/${systemKey}/${systemKey}-deploy.json`));
374
+ logger.log(chalk.gray(` integration/${systemKey}/env.template`));
375
+ logger.log(chalk.gray(` integration/${systemKey}/README.md`));
376
+ }
377
+
378
+ /**
379
+ * Validate and log downloaded data
380
+ * @param {Object} application - Application configuration
381
+ * @param {Array} dataSources - Array of datasource configurations
382
+ * @returns {string} System type
383
+ */
384
+ function validateAndLogDownloadedData(application, dataSources) {
385
+ logger.log(chalk.blue('šŸ” Validating downloaded data...'));
386
+ validateDownloadedData(application, dataSources);
387
+ const systemType = validateSystemType(application);
388
+ logger.log(chalk.green(`āœ“ System type: ${systemType}`));
389
+ logger.log(chalk.green(`āœ“ Found ${dataSources.length} datasource(s)`));
390
+ return systemType;
391
+ }
392
+
393
+ /**
394
+ * Process downloaded system (generate files, move, cleanup)
395
+ * @async
396
+ * @param {string} systemKey - System key
397
+ * @param {Object} application - Application configuration
398
+ * @param {Array} dataSources - Array of datasource configurations
399
+ * @param {string} tempDir - Temporary directory path
400
+ * @returns {Promise<string>} Final destination path
401
+ * @throws {Error} If processing fails
402
+ */
403
+ async function processDownloadedSystem(systemKey, application, dataSources, tempDir) {
404
+ // Generate files in temporary folder first
405
+ const filePaths = await generateFilesInTempDir(tempDir, systemKey, application, dataSources);
406
+
407
+ // Determine final destination (integration folder)
408
+ const { appPath } = await detectAppType(systemKey);
409
+ const finalPath = appPath || path.join(process.cwd(), 'integration', systemKey);
410
+
411
+ // Move files from temp to final location
412
+ await moveFilesToFinalLocation(tempDir, finalPath, systemKey, filePaths);
413
+
414
+ // Clean up temporary folder
415
+ await fs.rm(tempDir, { recursive: true, force: true });
416
+
417
+ return finalPath;
418
+ }
419
+
420
+ /**
421
+ * Display download success message
422
+ * @param {string} systemKey - System key
423
+ * @param {string} finalPath - Final destination path
424
+ * @param {number} datasourceCount - Number of datasources
425
+ */
426
+ function displayDownloadSuccess(systemKey, finalPath, datasourceCount) {
427
+ logger.log(chalk.green('\nāœ… External system downloaded successfully!'));
428
+ logger.log(chalk.blue(`Location: ${finalPath}`));
429
+ logger.log(chalk.blue(`System: ${systemKey}`));
430
+ logger.log(chalk.blue(`Datasources: ${datasourceCount}`));
431
+ }
432
+
247
433
  /**
248
434
  * Downloads external system from dataplane to local development structure
249
435
  * @async
@@ -257,147 +443,34 @@ function generateReadme(systemKey, application, dataSources) {
257
443
  * @throws {Error} If download fails
258
444
  */
259
445
  async function downloadExternalSystem(systemKey, options = {}) {
260
- if (!systemKey || typeof systemKey !== 'string') {
261
- throw new Error('System key is required and must be a string');
262
- }
263
-
264
- // Validate system key format (alphanumeric, hyphens, underscores)
265
- if (!/^[a-z0-9-_]+$/.test(systemKey)) {
266
- throw new Error('System key must contain only lowercase letters, numbers, hyphens, and underscores');
267
- }
446
+ validateSystemKeyFormat(systemKey);
268
447
 
269
448
  try {
270
449
  logger.log(chalk.blue(`\nšŸ“„ Downloading external system: ${systemKey}`));
271
450
 
272
- // Get authentication
451
+ // Get authentication and dataplane URL
273
452
  const config = await getConfig();
274
- const environment = options.environment || 'dev';
275
- const controllerUrl = options.controller || config.deployment?.controllerUrl || 'http://localhost:3000';
276
- const authConfig = await getDeploymentAuth(controllerUrl, environment, systemKey);
277
-
278
- if (!authConfig.token && !authConfig.clientId) {
279
- throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
280
- }
281
-
282
- // Get dataplane URL from controller
283
- logger.log(chalk.blue('🌐 Getting dataplane URL from controller...'));
284
- const dataplaneUrl = await getDataplaneUrl(controllerUrl, systemKey, environment, authConfig);
285
- logger.log(chalk.green(`āœ“ Dataplane URL: ${dataplaneUrl}`));
286
-
287
- // Download system configuration using centralized API client
288
- logger.log(chalk.blue(`šŸ“” Downloading system configuration: ${systemKey}`));
453
+ const { authConfig, dataplaneUrl } = await setupAuthenticationAndDataplane(systemKey, options, config);
289
454
 
455
+ // Handle dry run
290
456
  if (options.dryRun) {
291
- logger.log(chalk.yellow('šŸ” Dry run mode - would download from:'));
292
- logger.log(chalk.gray(` ${dataplaneUrl}/api/v1/external/systems/${systemKey}/config`));
293
- logger.log(chalk.yellow('\nWould create:'));
294
- logger.log(chalk.gray(` integration/${systemKey}/`));
295
- logger.log(chalk.gray(` integration/${systemKey}/variables.yaml`));
296
- logger.log(chalk.gray(` integration/${systemKey}/${systemKey}-deploy.json`));
297
- logger.log(chalk.gray(` integration/${systemKey}/env.template`));
298
- logger.log(chalk.gray(` integration/${systemKey}/README.md`));
457
+ handleDryRun(systemKey, dataplaneUrl);
299
458
  return;
300
459
  }
301
460
 
302
- const response = await getExternalSystemConfig(dataplaneUrl, systemKey, authConfig);
303
-
304
- if (!response.success || !response.data) {
305
- throw new Error(`Failed to download system configuration: ${response.error || response.formattedError || 'Unknown error'}`);
306
- }
307
-
308
- const downloadData = response.data.data || response.data;
309
- const application = downloadData.application;
310
- const dataSources = downloadData.dataSources || [];
311
-
312
- if (!application) {
313
- throw new Error('Application configuration not found in download response');
314
- }
461
+ // Download system configuration
462
+ const { application, dataSources } = await downloadSystemConfiguration(dataplaneUrl, systemKey, authConfig);
315
463
 
316
464
  // Validate downloaded data
317
- logger.log(chalk.blue('šŸ” Validating downloaded data...'));
318
- validateDownloadedData(application, dataSources);
319
- const systemType = validateSystemType(application);
320
- logger.log(chalk.green(`āœ“ System type: ${systemType}`));
321
- logger.log(chalk.green(`āœ“ Found ${dataSources.length} datasource(s)`));
465
+ validateAndLogDownloadedData(application, dataSources);
322
466
 
323
467
  // Create temporary folder for validation
324
468
  const tempDir = path.join(os.tmpdir(), `aifabrix-download-${systemKey}-${Date.now()}`);
325
469
  await fs.mkdir(tempDir, { recursive: true });
326
470
 
327
471
  try {
328
- // Generate files in temporary folder first
329
- const systemFileName = `${systemKey}-deploy.json`;
330
- const systemFilePath = path.join(tempDir, systemFileName);
331
- await fs.writeFile(systemFilePath, JSON.stringify(application, null, 2), 'utf8');
332
-
333
- // Generate datasource files
334
- const datasourceErrors = [];
335
- const datasourceFiles = [];
336
- for (const datasource of dataSources) {
337
- try {
338
- const entityKey = datasource.entityKey || datasource.key.split('-').pop();
339
- const datasourceFileName = `${systemKey}-deploy-${entityKey}.json`;
340
- const datasourceFilePath = path.join(tempDir, datasourceFileName);
341
- await fs.writeFile(datasourceFilePath, JSON.stringify(datasource, null, 2), 'utf8');
342
- datasourceFiles.push(datasourceFilePath);
343
- } catch (error) {
344
- datasourceErrors.push(new Error(`Failed to write datasource ${datasource.key}: ${error.message}`));
345
- }
346
- }
347
-
348
- // Handle partial downloads
349
- if (datasourceErrors.length > 0) {
350
- handlePartialDownload(systemKey, application, datasourceErrors);
351
- }
352
-
353
- // Generate variables.yaml
354
- const variables = generateVariablesYaml(systemKey, application, dataSources);
355
- const variablesPath = path.join(tempDir, 'variables.yaml');
356
- await fs.writeFile(variablesPath, yaml.dump(variables, { indent: 2, lineWidth: 120, noRefs: true }), 'utf8');
357
-
358
- // Generate env.template
359
- const envTemplate = generateEnvTemplate(application);
360
- const envTemplatePath = path.join(tempDir, 'env.template');
361
- await fs.writeFile(envTemplatePath, envTemplate, 'utf8');
362
-
363
- // Generate README.md
364
- const readme = generateReadme(systemKey, application, dataSources);
365
- const readmePath = path.join(tempDir, 'README.md');
366
- await fs.writeFile(readmePath, readme, 'utf8');
367
-
368
- // Determine final destination (integration folder)
369
- const { appPath } = await detectAppType(systemKey);
370
- const finalPath = appPath || path.join(process.cwd(), 'integration', systemKey);
371
-
372
- // Create final directory
373
- await fs.mkdir(finalPath, { recursive: true });
374
-
375
- // Move files from temp to final location
376
- logger.log(chalk.blue(`šŸ“ Creating directory: ${finalPath}`));
377
- const filesToMove = [
378
- { from: systemFilePath, to: path.join(finalPath, systemFileName) },
379
- { from: variablesPath, to: path.join(finalPath, 'variables.yaml') },
380
- { from: envTemplatePath, to: path.join(finalPath, 'env.template') },
381
- { from: readmePath, to: path.join(finalPath, 'README.md') }
382
- ];
383
-
384
- for (const dsFile of datasourceFiles) {
385
- const fileName = path.basename(dsFile);
386
- filesToMove.push({ from: dsFile, to: path.join(finalPath, fileName) });
387
- }
388
-
389
- for (const file of filesToMove) {
390
- await fs.copyFile(file.from, file.to);
391
- logger.log(chalk.green(`āœ“ Created: ${path.relative(process.cwd(), file.to)}`));
392
- }
393
-
394
- // Clean up temporary folder
395
- await fs.rm(tempDir, { recursive: true, force: true });
396
-
397
- logger.log(chalk.green('\nāœ… External system downloaded successfully!'));
398
- logger.log(chalk.blue(`Location: ${finalPath}`));
399
- logger.log(chalk.blue(`System: ${systemKey}`));
400
- logger.log(chalk.blue(`Datasources: ${dataSources.length}`));
472
+ const finalPath = await processDownloadedSystem(systemKey, application, dataSources, tempDir);
473
+ displayDownloadSuccess(systemKey, finalPath, dataSources.length);
401
474
  } catch (error) {
402
475
  // Clean up temporary folder on error
403
476
  try {