@aifabrix/builder 2.41.0 → 2.42.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 (138) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +1 -1
  3. package/integration/hubspot/README.md +8 -4
  4. package/integration/hubspot/application.json +54 -0
  5. package/integration/hubspot/create-hubspot.js +9 -136
  6. package/integration/hubspot/env.template +3 -4
  7. package/integration/hubspot/hubspot-datasource-company.json +343 -5
  8. package/integration/hubspot/hubspot-datasource-contact.json +413 -5
  9. package/integration/hubspot/hubspot-datasource-deal.json +341 -4
  10. package/integration/hubspot/hubspot-datasource-users.json +116 -0
  11. package/integration/hubspot/hubspot-deploy.json +1250 -108
  12. package/integration/hubspot/hubspot-system.json +15 -32
  13. package/integration/hubspot/test-dataplane-down-tests.js +17 -16
  14. package/integration/hubspot/test-dataplane-down.js +2 -2
  15. package/jest.config.manual.js +2 -1
  16. package/lib/api/external-test.api.js +111 -0
  17. package/lib/api/index.js +42 -19
  18. package/lib/api/pipeline.api.js +66 -120
  19. package/lib/api/types/pipeline.types.js +37 -0
  20. package/lib/api/wizard-platform.api.js +61 -0
  21. package/lib/api/wizard.api.js +34 -1
  22. package/lib/app/config.js +23 -11
  23. package/lib/app/index.js +3 -1
  24. package/lib/app/prompts.js +44 -29
  25. package/lib/app/readme.js +8 -3
  26. package/lib/app/run-env-compose.js +64 -1
  27. package/lib/app/run-helpers.js +1 -1
  28. package/lib/app/show-display.js +1 -1
  29. package/lib/cli/setup-app.js +42 -11
  30. package/lib/cli/setup-credential-deployment.js +31 -6
  31. package/lib/cli/setup-dev.js +27 -0
  32. package/lib/cli/setup-environment.js +12 -4
  33. package/lib/cli/setup-external-system.js +19 -4
  34. package/lib/cli/setup-infra.js +54 -14
  35. package/lib/cli/setup-utility.js +117 -21
  36. package/lib/commands/credential-env.js +162 -0
  37. package/lib/commands/credential-list.js +17 -22
  38. package/lib/commands/credential-push.js +96 -0
  39. package/lib/commands/datasource.js +77 -6
  40. package/lib/commands/dev-init.js +39 -1
  41. package/lib/commands/repair-auth-config.js +99 -0
  42. package/lib/commands/repair-datasource-keys.js +208 -0
  43. package/lib/commands/repair-datasource.js +235 -0
  44. package/lib/commands/repair-env-template.js +348 -0
  45. package/lib/commands/repair-internal.js +85 -0
  46. package/lib/commands/repair-rbac.js +158 -0
  47. package/lib/commands/repair.js +507 -0
  48. package/lib/commands/test-e2e-external.js +165 -0
  49. package/lib/commands/upload.js +71 -40
  50. package/lib/commands/wizard-core-helpers.js +226 -4
  51. package/lib/commands/wizard-core.js +67 -29
  52. package/lib/commands/wizard-dataplane.js +1 -1
  53. package/lib/commands/wizard-entity-selection.js +43 -0
  54. package/lib/commands/wizard-headless.js +44 -5
  55. package/lib/commands/wizard-helpers.js +7 -3
  56. package/lib/commands/wizard.js +86 -64
  57. package/lib/core/config.js +7 -1
  58. package/lib/core/secrets.js +33 -12
  59. package/lib/datasource/deploy.js +12 -3
  60. package/lib/datasource/test-e2e.js +219 -0
  61. package/lib/datasource/test-integration.js +154 -0
  62. package/lib/deployment/deployer.js +7 -5
  63. package/lib/external-system/download.js +182 -204
  64. package/lib/external-system/generator.js +204 -56
  65. package/lib/external-system/test-execution.js +2 -1
  66. package/lib/external-system/test-system-level.js +73 -0
  67. package/lib/external-system/test.js +51 -18
  68. package/lib/generator/external-controller-manifest.js +29 -2
  69. package/lib/generator/external-schema-utils.js +1 -1
  70. package/lib/generator/external.js +10 -3
  71. package/lib/generator/index.js +4 -1
  72. package/lib/generator/split-readme.js +1 -0
  73. package/lib/generator/split-variables.js +7 -1
  74. package/lib/generator/split.js +194 -54
  75. package/lib/generator/wizard-prompts-secondary.js +294 -0
  76. package/lib/generator/wizard-prompts.js +105 -106
  77. package/lib/generator/wizard-readme.js +88 -0
  78. package/lib/generator/wizard.js +147 -158
  79. package/lib/infrastructure/compose.js +11 -1
  80. package/lib/infrastructure/index.js +11 -3
  81. package/lib/infrastructure/services.js +22 -11
  82. package/lib/schema/application-schema.json +8 -5
  83. package/lib/schema/external-datasource.schema.json +49 -26
  84. package/lib/schema/external-system.schema.json +82 -6
  85. package/lib/schema/wizard-config.schema.json +16 -0
  86. package/lib/utils/api.js +38 -10
  87. package/lib/utils/auth-headers.js +8 -7
  88. package/lib/utils/compose-generator.js +1 -1
  89. package/lib/utils/compose-handlebars-helpers.js +11 -0
  90. package/lib/utils/config-format-preference.js +51 -0
  91. package/lib/utils/config-format.js +36 -0
  92. package/lib/utils/configuration-env-resolver.js +179 -0
  93. package/lib/utils/credential-display.js +83 -0
  94. package/lib/utils/credential-secrets-env.js +115 -25
  95. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  96. package/lib/utils/deployment-validation-helpers.js +4 -4
  97. package/lib/utils/dev-ca-install.js +139 -0
  98. package/lib/utils/env-copy.js +23 -3
  99. package/lib/utils/error-formatters/http-status-errors.js +0 -1
  100. package/lib/utils/error-formatters/permission-errors.js +0 -1
  101. package/lib/utils/error-formatters/validation-errors.js +0 -1
  102. package/lib/utils/external-readme.js +56 -29
  103. package/lib/utils/external-system-display.js +59 -1
  104. package/lib/utils/external-system-test-helpers.js +21 -8
  105. package/lib/utils/external-system-validators.js +3 -0
  106. package/lib/utils/file-upload.js +20 -50
  107. package/lib/utils/help-builder.js +1 -0
  108. package/lib/utils/infra-status.js +50 -44
  109. package/lib/utils/local-secrets.js +5 -5
  110. package/lib/utils/paths.js +85 -4
  111. package/lib/utils/secrets-canonical.js +93 -0
  112. package/lib/utils/secrets-generator.js +20 -0
  113. package/lib/utils/secrets-helpers.js +75 -89
  114. package/lib/utils/test-log-writer.js +56 -0
  115. package/lib/utils/token-manager.js +24 -32
  116. package/lib/validation/env-template-auth.js +157 -0
  117. package/lib/validation/env-template-kv.js +41 -0
  118. package/lib/validation/external-manifest-validator.js +25 -0
  119. package/lib/validation/external-system-auth-rules.js +86 -0
  120. package/lib/validation/validate-batch.js +149 -0
  121. package/lib/validation/validate-datasource-keys-api.js +33 -0
  122. package/lib/validation/validate-display.js +94 -16
  123. package/lib/validation/validate.js +25 -12
  124. package/lib/validation/validator.js +7 -9
  125. package/lib/validation/wizard-datasource-validation.js +50 -0
  126. package/package.json +7 -2
  127. package/templates/applications/dataplane/application.yaml +1 -1
  128. package/templates/applications/dataplane/env.template +5 -5
  129. package/templates/applications/dataplane/rbac.yaml +2 -2
  130. package/templates/applications/miso-controller/env.template +1 -1
  131. package/templates/external-system/README.md.hbs +65 -25
  132. package/templates/external-system/deploy.js.hbs +4 -2
  133. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  134. package/templates/external-system/external-system.json.hbs +1 -18
  135. package/templates/infra/compose.yaml.hbs +6 -0
  136. package/templates/python/docker-compose.hbs +4 -4
  137. package/templates/typescript/docker-compose.hbs +4 -4
  138. package/integration/hubspot/application.yaml +0 -37
@@ -1,28 +1,37 @@
1
1
  /**
2
2
  * External System Download Module
3
3
  *
4
- * Downloads external systems from dataplane to local development structure.
5
- * Supports downloading system configuration and datasources for local development.
4
+ * Downloads full running manifest from dataplane and splits it into component files
5
+ * using split-json. The dataplane GET /api/v1/external/systems/{systemKey}/config
6
+ * returns { application, dataSources, version } in pipeline format. Supports legacy
7
+ * responses where datasources are inline in application.configuration.dataSources.
8
+ *
9
+ * Process: validate key → auth + dataplane (Bearer required) → GET full manifest →
10
+ * validate → build deploy JSON (augments configuration with auth KV_* for env.template) →
11
+ * write deploy JSON → splitDeployJson (merge env.template if exists; README overwrite per
12
+ * prompt or --force) → ensure placeholder secrets from env.template → convert to JSON if
13
+ * --format json.
6
14
  *
7
15
  * @fileoverview External system download functionality for AI Fabrix Builder
8
16
  * @author AI Fabrix Team
9
17
  * @version 2.0.0
18
+ * @see docs/commands/external-integration.md#aifabrix-download-system-key
10
19
  */
11
20
 
12
21
  const fs = require('fs').promises;
22
+ const fsSync = require('fs');
13
23
  const path = require('path');
14
- const os = require('os');
24
+ const readline = require('readline');
15
25
  const yaml = require('js-yaml');
16
26
  const chalk = require('chalk');
17
27
  const { getExternalSystemConfig } = require('../api/external-systems.api');
18
- const { getDeploymentAuth } = require('../utils/token-manager');
28
+ const { getDeploymentAuth, requireBearerForDataplanePipeline } = require('../utils/token-manager');
19
29
  const { getConfig } = require('../core/config');
20
- const { detectAppType } = require('../utils/paths');
30
+ const { getIntegrationPath } = require('../utils/paths');
31
+ const { retemplateConfigurationForDownload } = require('../utils/configuration-env-resolver');
21
32
  const logger = require('../utils/logger');
22
- const { writeConfigFile } = require('../utils/config-format');
23
- const { generateEnvTemplate } = require('../utils/external-system-env-helpers');
24
- const { generateVariablesYaml, generateReadme } = require('./download-helpers');
25
33
  const { resolveControllerUrl } = require('../utils/controller-url');
34
+ const generator = require('../generator');
26
35
 
27
36
  /**
28
37
  * Validates system type from downloaded application
@@ -97,25 +106,6 @@ function validateDownloadedData(application, dataSources) {
97
106
  }
98
107
  }
99
108
 
100
- /**
101
- * Handles partial download errors gracefully
102
- * @param {string} systemKey - System key
103
- * @param {Object} systemData - System data that was successfully downloaded
104
- * @param {Array<Error>} datasourceErrors - Array of errors from datasource downloads
105
- * @throws {Error} Aggregated error message
106
- */
107
- function handlePartialDownload(systemKey, systemData, datasourceErrors) {
108
- if (datasourceErrors.length === 0) {
109
- return;
110
- }
111
-
112
- const errorMessages = datasourceErrors.map(err => err.message).join('\n - ');
113
- throw new Error(
114
- `Partial download completed for system '${systemKey}', but some datasources failed:\n - ${errorMessages}\n\n` +
115
- 'System configuration was downloaded successfully. You may need to download datasources separately.'
116
- );
117
- }
118
-
119
109
  /**
120
110
  * Setup authentication and get dataplane URL
121
111
  * @async
@@ -131,7 +121,8 @@ async function setupAuthenticationAndDataplane(systemKey, _options, _config) {
131
121
  const controllerUrl = await resolveControllerUrl();
132
122
  const authConfig = await getDeploymentAuth(controllerUrl, environment, systemKey);
133
123
 
134
- if (!authConfig.token && !authConfig.clientId) {
124
+ requireBearerForDataplanePipeline(authConfig);
125
+ if (!authConfig.token) {
135
126
  throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
136
127
  }
137
128
 
@@ -144,16 +135,19 @@ async function setupAuthenticationAndDataplane(systemKey, _options, _config) {
144
135
  }
145
136
 
146
137
  /**
147
- * Download system configuration from dataplane
138
+ * Download full running manifest from dataplane
139
+ * GET /api/v1/external/systems/{systemKey}/config returns
140
+ * { application, dataSources, version } in pipeline format.
141
+ *
148
142
  * @async
149
143
  * @param {string} dataplaneUrl - Dataplane URL
150
144
  * @param {string} systemKey - System key
151
145
  * @param {Object} authConfig - Authentication configuration
152
- * @returns {Promise<Object>} Object with application and dataSources
146
+ * @returns {Promise<{application: Object, dataSources: Array, version?: string}>} Full manifest
153
147
  * @throws {Error} If download fails
154
148
  */
155
- async function downloadSystemConfiguration(dataplaneUrl, systemKey, authConfig) {
156
- logger.log(chalk.blue(`šŸ“” Downloading system configuration: ${systemKey}`));
149
+ async function downloadFullManifest(dataplaneUrl, systemKey, authConfig) {
150
+ logger.log(chalk.blue(`šŸ“” Downloading full manifest: ${systemKey}`));
157
151
  const response = await getExternalSystemConfig(dataplaneUrl, systemKey, authConfig);
158
152
 
159
153
  if (!response.success || !response.data) {
@@ -163,14 +157,14 @@ async function downloadSystemConfiguration(dataplaneUrl, systemKey, authConfig)
163
157
  const downloadData = response.data.data || response.data;
164
158
  let application = downloadData.application;
165
159
  let dataSources = downloadData.dataSources || [];
160
+ const version = downloadData.version;
166
161
 
167
- // Handle case where datasources are inline in application.configuration.dataSources
162
+ // Legacy: datasources inline in application.configuration.dataSources
168
163
  if (!application && downloadData.configuration) {
169
164
  application = downloadData;
170
165
  }
171
166
  if (application && application.configuration && Array.isArray(application.configuration.dataSources)) {
172
167
  dataSources = [...dataSources, ...application.configuration.dataSources];
173
- // Remove inline datasources from application to avoid duplication
174
168
  const { dataSources: _inlineDataSources, ...configWithoutDataSources } = application.configuration;
175
169
  application = { ...application, configuration: configWithoutDataSources };
176
170
  }
@@ -179,149 +173,78 @@ async function downloadSystemConfiguration(dataplaneUrl, systemKey, authConfig)
179
173
  throw new Error('Application configuration not found in download response');
180
174
  }
181
175
 
182
- return { application, dataSources };
176
+ return { application, dataSources, version };
183
177
  }
184
178
 
185
179
  /**
186
- * Generate files in temporary directory
187
- * @async
188
- * @param {string} tempDir - Temporary directory path
189
- * @param {string} systemKey - System key
190
- * @param {Object} application - Application configuration
191
- * @param {Array} dataSources - Array of datasource configurations
192
- * @returns {Promise<Object>} Object with file paths
193
- * @throws {Error} If file generation fails
194
- */
195
- /**
196
- * Generates system file
197
- * @async
198
- * @function generateSystemFile
199
- * @param {string} tempDir - Temporary directory
200
- * @param {string} systemKey - System key
201
- * @param {Object} application - Application object
202
- * @returns {Promise<string>} System file path
180
+ * Derives env variable name from system key and security key (e.g. KV_HUBSPOT_CLIENTID).
181
+ * @param {string} systemKey - System key (e.g. 'hubspot')
182
+ * @param {string} securityKey - Security key (e.g. 'clientId')
183
+ * @returns {string} Variable name
203
184
  */
204
- async function generateSystemFile(tempDir, systemKey, application) {
205
- const systemFileName = `${systemKey}-system.yaml`;
206
- const systemFilePath = path.join(tempDir, systemFileName);
207
- writeConfigFile(systemFilePath, application);
208
- return systemFilePath;
185
+ function deriveAuthVarName(systemKey, securityKey) {
186
+ const safeKey = (systemKey || '').toUpperCase().replace(/-/g, '_');
187
+ const safeSec = (securityKey || '').replace(/([A-Z])/g, '_$1').toUpperCase().replace(/^_/, '');
188
+ return `KV_${safeKey}_${safeSec}`;
209
189
  }
210
190
 
211
191
  /**
212
- * Generates datasource files
213
- * @async
214
- * @function generateDatasourceFiles
215
- * @param {string} tempDir - Temporary directory
216
- * @param {string} systemKey - System key
217
- * @param {Array} dataSources - Array of datasource objects
218
- * @returns {Promise<Object>} Object with datasourceFiles array and datasourceErrors array
192
+ * Collects kv:// paths already present in configuration array.
193
+ * @param {Array} config - Configuration array
194
+ * @returns {Set<string>} Set of kv paths
219
195
  */
220
- async function generateDatasourceFiles(tempDir, systemKey, dataSources) {
221
- const datasourceErrors = [];
222
- const datasourceFiles = [];
223
- for (const datasource of dataSources) {
224
- try {
225
- const datasourceKey = datasource.key || '';
226
- // Extract datasource key (remove system key prefix if present)
227
- let datasourceKeyOnly;
228
- if (datasourceKey.startsWith(`${systemKey}-`)) {
229
- datasourceKeyOnly = datasourceKey.substring(systemKey.length + 1);
230
- } else {
231
- const entityType = datasource.entityType || datasource.entityKey || datasourceKey.split('-').pop();
232
- datasourceKeyOnly = entityType;
233
- }
234
- const datasourceFileName = `${systemKey}-datasource-${datasourceKeyOnly}.yaml`;
235
- const datasourceFilePath = path.join(tempDir, datasourceFileName);
236
- writeConfigFile(datasourceFilePath, datasource);
237
- datasourceFiles.push(datasourceFilePath);
238
- } catch (error) {
239
- datasourceErrors.push(new Error(`Failed to write datasource ${datasource.key}: ${error.message}`));
240
- }
196
+ function collectExistingKvPaths(config) {
197
+ const paths = new Set();
198
+ for (const item of config) {
199
+ if (!item || !item.value) continue;
200
+ const val = String(item.value).trim();
201
+ if (val.startsWith('kv://')) paths.add(val);
202
+ else if (item.location === 'keyvault') paths.add(`kv://${val}`);
241
203
  }
242
- return { datasourceFiles, datasourceErrors };
204
+ return paths;
243
205
  }
244
206
 
245
207
  /**
246
- * Generates configuration files (application.yaml, env.template, README.md)
247
- * @async
248
- * @function generateConfigFiles
249
- * @param {string} tempDir - Temporary directory
250
- * @param {string} systemKey - System key
251
- * @param {Object} application - Application object
252
- * @param {Array} dataSources - Array of datasource objects
253
- * @returns {Promise<Object>} Object with file paths
208
+ * Augments system.configuration with entries for authentication.security kv paths
209
+ * so that extractEnvTemplate produces env.template that passes validateAuthKvCoverage.
210
+ * @param {Object} system - System config (mutated)
254
211
  */
255
- async function generateConfigFiles(tempDir, systemKey, application, dataSources) {
256
- // Generate application.yaml
257
- const variables = generateVariablesYaml(systemKey, application, dataSources);
258
- const variablesPath = path.join(tempDir, 'application.yaml');
259
- await fs.writeFile(variablesPath, yaml.dump(variables, { indent: 2, lineWidth: 120, noRefs: true }), 'utf8');
260
-
261
- // Generate env.template
262
- const envTemplate = generateEnvTemplate(application);
263
- const envTemplatePath = path.join(tempDir, 'env.template');
264
- await fs.writeFile(envTemplatePath, envTemplate, 'utf8');
265
-
266
- // Generate README.md
267
- const readme = generateReadme(systemKey, application, dataSources);
268
- const readmePath = path.join(tempDir, 'README.md');
269
- await fs.writeFile(readmePath, readme, 'utf8');
270
-
271
- return { variablesPath, envTemplatePath, readmePath };
272
- }
273
-
274
- async function generateFilesInTempDir(tempDir, systemKey, application, dataSources) {
275
- const systemFilePath = await generateSystemFile(tempDir, systemKey, application);
276
-
277
- const { datasourceFiles, datasourceErrors } = await generateDatasourceFiles(tempDir, systemKey, dataSources);
278
-
279
- // Handle partial downloads
280
- if (datasourceErrors.length > 0) {
281
- handlePartialDownload(systemKey, application, datasourceErrors);
212
+ function augmentConfigurationWithAuthSecrets(system) {
213
+ if (!system || typeof system !== 'object') return;
214
+ const security = system.authentication?.security;
215
+ if (!security || typeof security !== 'object') return;
216
+ const config = Array.isArray(system.configuration) ? [...system.configuration] : [];
217
+ const existingPaths = collectExistingKvPaths(config);
218
+ const systemKey = system.key || '';
219
+ for (const [key, val] of Object.entries(security)) {
220
+ if (typeof val !== 'string' || !/^kv:\/\/.+/.test(val)) continue;
221
+ if (existingPaths.has(val)) continue;
222
+ config.push({
223
+ name: deriveAuthVarName(systemKey, key),
224
+ value: val.replace(/^kv:\/\//, ''),
225
+ location: 'keyvault',
226
+ required: true
227
+ });
282
228
  }
283
-
284
- const { variablesPath, envTemplatePath, readmePath } = await generateConfigFiles(tempDir, systemKey, application, dataSources);
285
-
286
- return {
287
- systemFilePath,
288
- variablesPath,
289
- envTemplatePath,
290
- readmePath,
291
- datasourceFiles
292
- };
229
+ system.configuration = config;
293
230
  }
294
231
 
295
232
  /**
296
- * Move files from temporary directory to final location
297
- * @async
298
- * @param {string} tempDir - Temporary directory path
299
- * @param {string} finalPath - Final destination path
300
- * @param {string} systemKey - System key
301
- * @param {Object} filePaths - Object with file paths
302
- * @throws {Error} If file move fails
233
+ * Build deploy JSON from full running manifest (split-json compatible).
234
+ * @param {Object} application - System config from dataplane
235
+ * @param {Array} dataSources - Inline datasource configs
236
+ * @param {string} [version] - Optional version
237
+ * @returns {Object} Deploy JSON object for splitDeployJson
303
238
  */
304
- async function moveFilesToFinalLocation(tempDir, finalPath, systemKey, filePaths) {
305
- logger.log(chalk.blue(`šŸ“ Creating directory: ${finalPath}`));
306
- await fs.mkdir(finalPath, { recursive: true });
307
-
308
- const systemFileName = `${systemKey}-system.json`;
309
- const filesToMove = [
310
- { from: filePaths.systemFilePath, to: path.join(finalPath, systemFileName) },
311
- { from: filePaths.variablesPath, to: path.join(finalPath, 'application.yaml') },
312
- { from: filePaths.envTemplatePath, to: path.join(finalPath, 'env.template') },
313
- { from: filePaths.readmePath, to: path.join(finalPath, 'README.md') }
314
- ];
315
-
316
- for (const dsFile of filePaths.datasourceFiles) {
317
- const fileName = path.basename(dsFile);
318
- filesToMove.push({ from: dsFile, to: path.join(finalPath, fileName) });
319
- }
320
-
321
- for (const file of filesToMove) {
322
- await fs.copyFile(file.from, file.to);
323
- logger.log(chalk.green(`āœ“ Created: ${path.relative(process.cwd(), file.to)}`));
324
- }
239
+ function buildDeployJsonFromManifest(application, dataSources, version) {
240
+ const dataSourcesKeys = Array.isArray(dataSources)
241
+ ? dataSources.map(ds => (ds && ds.key) ? ds.key : ds)
242
+ : [];
243
+ const system = { ...application, dataSources: application.dataSources || dataSourcesKeys };
244
+ augmentConfigurationWithAuthSecrets(system);
245
+ const deploy = { system, dataSources };
246
+ if (version) deploy.version = version;
247
+ return deploy;
325
248
  }
326
249
 
327
250
  /**
@@ -346,8 +269,8 @@ function validateSystemKeyFormat(systemKey) {
346
269
  function handleDryRun(systemKey, dataplaneUrl) {
347
270
  logger.log(chalk.yellow('šŸ” Dry run mode - would download from:'));
348
271
  logger.log(chalk.gray(` ${dataplaneUrl}/api/v1/external/systems/${systemKey}/config`));
349
- logger.log(chalk.yellow('\nWould create:'));
350
- logger.log(chalk.gray(` integration/${systemKey}/`));
272
+ logger.log(chalk.yellow('\nWould create (via split-json):'));
273
+ logger.log(chalk.gray(` integration/${systemKey}/${systemKey}-deploy.json`));
351
274
  logger.log(chalk.gray(` integration/${systemKey}/application.yaml`));
352
275
  logger.log(chalk.gray(` integration/${systemKey}/${systemKey}-system.yaml`));
353
276
  logger.log(chalk.gray(` integration/${systemKey}/env.template`));
@@ -370,25 +293,85 @@ function validateAndLogDownloadedData(application, dataSources) {
370
293
  }
371
294
 
372
295
  /**
373
- * Process downloaded system (generate files, move, cleanup)
296
+ * Prompts user: "Do you want to replace README.md? (yes/no)"
297
+ * @returns {Promise<boolean>} True if user answers yes
298
+ */
299
+ function promptReplaceReadme() {
300
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
301
+ return new Promise(resolve => {
302
+ rl.question(chalk.yellow('README.md already exists. Do you want to replace it? (yes/no) '), answer => {
303
+ rl.close();
304
+ const normalized = (answer || '').trim().toLowerCase();
305
+ resolve(normalized === 'yes' || normalized === 'y');
306
+ });
307
+ });
308
+ }
309
+
310
+ /**
311
+ * Resolves split options for download: merge env.template if it exists, prompt for README replace if it exists (unless force).
312
+ * @param {string} finalPath - Integration directory path
313
+ * @param {Object} [options] - Download options
314
+ * @param {boolean} [options.force] - If true, overwrite README.md without prompting
315
+ * @returns {Promise<{ mergeEnvTemplate: boolean, overwriteReadme: boolean }>}
316
+ */
317
+ async function resolveDownloadSplitOptions(finalPath, options = {}) {
318
+ const opts = { mergeEnvTemplate: false, overwriteReadme: true };
319
+ if (!fsSync.existsSync(finalPath)) return opts;
320
+ const envPath = path.join(finalPath, 'env.template');
321
+ const readmePath = path.join(finalPath, 'README.md');
322
+ if (fsSync.existsSync(envPath)) opts.mergeEnvTemplate = true;
323
+ if (fsSync.existsSync(readmePath)) {
324
+ if (options.force) {
325
+ opts.overwriteReadme = true;
326
+ } else {
327
+ opts.overwriteReadme = await promptReplaceReadme();
328
+ if (!opts.overwriteReadme) logger.log(chalk.gray(' Keeping existing README.md'));
329
+ }
330
+ }
331
+ return opts;
332
+ }
333
+
334
+ /**
335
+ * Re-templates system file configuration from env.template when present (mutates file on disk).
336
+ * @param {string} systemKey - System key
337
+ * @param {string} systemFilePath - Path to *-system.yaml
338
+ * @returns {Promise<void>}
339
+ */
340
+ async function applyRetemplateToSystemFile(systemKey, systemFilePath) {
341
+ if (!systemFilePath || !fsSync.existsSync(systemFilePath)) return;
342
+ const systemContent = await fs.readFile(systemFilePath, 'utf8');
343
+ const systemObj = yaml.load(systemContent);
344
+ if (!Array.isArray(systemObj?.configuration)) return;
345
+ const applied = await retemplateConfigurationForDownload(systemKey, systemObj.configuration);
346
+ if (applied) {
347
+ await fs.writeFile(systemFilePath, yaml.dump(systemObj, { indent: 2, lineWidth: -1 }), 'utf8');
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Write deploy JSON and split into component files
374
353
  * @async
375
354
  * @param {string} systemKey - System key
376
- * @param {Object} application - Application configuration
377
- * @param {Array} dataSources - Array of datasource configurations
378
- * @param {string} tempDir - Temporary directory path
355
+ * @param {Object} manifest - Full manifest { application, dataSources, version }
356
+ * @param {Object} [splitOptions] - Options for split (mergeEnvTemplate, overwriteReadme)
379
357
  * @returns {Promise<string>} Final destination path
380
358
  * @throws {Error} If processing fails
381
359
  */
382
- async function processDownloadedSystem(systemKey, application, dataSources, tempDir) {
383
- // Generate files in temporary folder first
384
- const filePaths = await generateFilesInTempDir(tempDir, systemKey, application, dataSources);
360
+ async function processDownloadedSystem(systemKey, manifest, splitOptions = {}) {
361
+ const { application, dataSources, version } = manifest;
362
+ const finalPath = getIntegrationPath(systemKey);
385
363
 
386
- // Determine final destination (integration folder)
387
- const { appPath } = await detectAppType(systemKey);
388
- const finalPath = appPath || path.join(process.cwd(), 'integration', systemKey);
364
+ logger.log(chalk.blue(`šŸ“ Creating directory: ${finalPath}`));
365
+ await fs.mkdir(finalPath, { recursive: true });
389
366
 
390
- // Move files from temp to final location
391
- await moveFilesToFinalLocation(tempDir, finalPath, systemKey, filePaths);
367
+ const deployJson = buildDeployJsonFromManifest(application, dataSources, version);
368
+ const deployJsonPath = path.join(finalPath, `${systemKey}-deploy.json`);
369
+ await fs.writeFile(deployJsonPath, JSON.stringify(deployJson, null, 2), 'utf8');
370
+ logger.log(chalk.green(`āœ“ Created: ${path.relative(process.cwd(), deployJsonPath)}`));
371
+
372
+ logger.log(chalk.blue('šŸ“‚ Splitting deploy JSON into component files...'));
373
+ const splitResult = await generator.splitDeployJson(deployJsonPath, finalPath, splitOptions);
374
+ await applyRetemplateToSystemFile(systemKey, splitResult.systemFile);
392
375
 
393
376
  try {
394
377
  const secretsEnsure = require('../core/secrets-ensure');
@@ -397,9 +380,6 @@ async function processDownloadedSystem(systemKey, application, dataSources, temp
397
380
  if (err.code !== 'ENOENT') logger.warn(`Could not ensure integration placeholder secrets: ${err.message}`);
398
381
  }
399
382
 
400
- // Clean up temporary folder
401
- await fs.rm(tempDir, { recursive: true, force: true });
402
-
403
383
  return finalPath;
404
384
  }
405
385
 
@@ -422,50 +402,52 @@ function displayDownloadSuccess(systemKey, finalPath, datasourceCount) {
422
402
  * @function downloadExternalSystem
423
403
  * @param {string} systemKey - System key or ID
424
404
  * @param {Object} options - Download options
405
+ * @param {string} [options.format] - Output format: 'yaml' (default) or 'json' (runs convert after split)
425
406
  * @param {string} [options.environment] - Environment (dev, tst, pro)
426
407
  * @param {string} [options.controller] - Controller URL
427
408
  * @param {boolean} [options.dryRun] - Show what would be downloaded without actually downloading
409
+ * @param {boolean} [options.force] - Overwrite existing README.md without prompting
428
410
  * @returns {Promise<void>} Resolves when download completes
429
411
  * @throws {Error} If download fails
430
412
  */
413
+ async function runConvertToJsonIfRequested(systemKey) {
414
+ const { runConvert } = require('../commands/convert');
415
+ try {
416
+ await runConvert(systemKey, { format: 'json', force: true });
417
+ logger.log(chalk.green('āœ“ Converted component files to JSON'));
418
+ } catch (convertErr) {
419
+ throw new Error(`Download succeeded but convert to JSON failed: ${convertErr.message}`);
420
+ }
421
+ }
422
+
431
423
  async function downloadExternalSystem(systemKey, options = {}) {
432
424
  validateSystemKeyFormat(systemKey);
433
425
 
426
+ const format = (options.format || 'yaml').toLowerCase();
427
+
434
428
  try {
435
429
  logger.log(chalk.blue(`\nšŸ“„ Downloading external system: ${systemKey}`));
436
430
 
437
- // Get authentication and dataplane URL
438
431
  const config = await getConfig();
439
432
  const { authConfig, dataplaneUrl } = await setupAuthenticationAndDataplane(systemKey, options, config);
440
433
 
441
- // Handle dry run
442
434
  if (options.dryRun) {
443
435
  handleDryRun(systemKey, dataplaneUrl);
444
436
  return;
445
437
  }
446
438
 
447
- // Download system configuration
448
- const { application, dataSources } = await downloadSystemConfiguration(dataplaneUrl, systemKey, authConfig);
449
-
450
- // Validate downloaded data
451
- validateAndLogDownloadedData(application, dataSources);
452
-
453
- // Create temporary folder for validation
454
- const tempDir = path.join(os.tmpdir(), `aifabrix-download-${systemKey}-${Date.now()}`);
455
- await fs.mkdir(tempDir, { recursive: true });
456
-
457
- try {
458
- const finalPath = await processDownloadedSystem(systemKey, application, dataSources, tempDir);
459
- displayDownloadSuccess(systemKey, finalPath, dataSources.length);
460
- } catch (error) {
461
- // Clean up temporary folder on error
462
- try {
463
- await fs.rm(tempDir, { recursive: true, force: true });
464
- } catch {
465
- // Ignore cleanup errors
466
- }
467
- throw error;
439
+ const manifest = await downloadFullManifest(dataplaneUrl, systemKey, authConfig);
440
+ validateAndLogDownloadedData(manifest.application, manifest.dataSources);
441
+
442
+ const finalPath = getIntegrationPath(systemKey);
443
+ const splitOptions = await resolveDownloadSplitOptions(finalPath, options);
444
+ await processDownloadedSystem(systemKey, manifest, splitOptions);
445
+
446
+ if (format === 'json') {
447
+ await runConvertToJsonIfRequested(systemKey);
468
448
  }
449
+
450
+ displayDownloadSuccess(systemKey, finalPath, manifest.dataSources.length);
469
451
  } catch (error) {
470
452
  throw new Error(`Failed to download external system: ${error.message}`);
471
453
  }
@@ -474,9 +456,5 @@ async function downloadExternalSystem(systemKey, options = {}) {
474
456
  module.exports = {
475
457
  downloadExternalSystem,
476
458
  validateSystemType,
477
- validateDownloadedData,
478
- generateVariablesYaml,
479
- generateReadme,
480
- generateEnvTemplate,
481
- handlePartialDownload
459
+ validateDownloadedData
482
460
  };