@aifabrix/builder 2.41.0 → 2.42.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 (142) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +2 -2
  3. package/integration/hubspot/README.md +11 -5
  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 +36 -2
  22. package/lib/app/config.js +23 -11
  23. package/lib/app/index.js +5 -3
  24. package/lib/app/prompts.js +46 -31
  25. package/lib/app/readme.js +11 -4
  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 +45 -14
  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/auth-config.js +22 -12
  37. package/lib/commands/credential-env.js +162 -0
  38. package/lib/commands/credential-list.js +17 -22
  39. package/lib/commands/credential-push.js +96 -0
  40. package/lib/commands/datasource.js +77 -6
  41. package/lib/commands/dev-init.js +39 -1
  42. package/lib/commands/repair-auth-config.js +99 -0
  43. package/lib/commands/repair-datasource-keys.js +208 -0
  44. package/lib/commands/repair-datasource.js +235 -0
  45. package/lib/commands/repair-env-template.js +348 -0
  46. package/lib/commands/repair-internal.js +85 -0
  47. package/lib/commands/repair-rbac.js +158 -0
  48. package/lib/commands/repair.js +518 -0
  49. package/lib/commands/secrets-set.js +6 -0
  50. package/lib/commands/test-e2e-external.js +165 -0
  51. package/lib/commands/up-dataplane.js +90 -6
  52. package/lib/commands/upload.js +71 -40
  53. package/lib/commands/wizard-core-helpers.js +230 -5
  54. package/lib/commands/wizard-core.js +68 -29
  55. package/lib/commands/wizard-dataplane.js +1 -1
  56. package/lib/commands/wizard-entity-selection.js +43 -0
  57. package/lib/commands/wizard-headless.js +49 -5
  58. package/lib/commands/wizard-helpers.js +7 -3
  59. package/lib/commands/wizard.js +93 -64
  60. package/lib/core/config.js +7 -1
  61. package/lib/core/secrets.js +33 -12
  62. package/lib/datasource/deploy.js +12 -3
  63. package/lib/datasource/test-e2e.js +219 -0
  64. package/lib/datasource/test-integration.js +154 -0
  65. package/lib/deployment/deployer.js +7 -5
  66. package/lib/external-system/download-helpers.js +3 -1
  67. package/lib/external-system/download.js +182 -204
  68. package/lib/external-system/generator.js +204 -56
  69. package/lib/external-system/test-execution.js +2 -1
  70. package/lib/external-system/test-system-level.js +73 -0
  71. package/lib/external-system/test.js +51 -18
  72. package/lib/generator/external-controller-manifest.js +29 -2
  73. package/lib/generator/external-schema-utils.js +4 -2
  74. package/lib/generator/external.js +10 -3
  75. package/lib/generator/index.js +4 -1
  76. package/lib/generator/split-readme.js +1 -0
  77. package/lib/generator/split-variables.js +7 -1
  78. package/lib/generator/split.js +194 -54
  79. package/lib/generator/wizard-prompts-secondary.js +326 -0
  80. package/lib/generator/wizard-prompts.js +105 -106
  81. package/lib/generator/wizard-readme.js +91 -0
  82. package/lib/generator/wizard.js +180 -179
  83. package/lib/infrastructure/compose.js +11 -1
  84. package/lib/infrastructure/index.js +11 -3
  85. package/lib/infrastructure/services.js +22 -11
  86. package/lib/schema/application-schema.json +8 -5
  87. package/lib/schema/external-datasource.schema.json +49 -26
  88. package/lib/schema/external-system.schema.json +82 -6
  89. package/lib/schema/wizard-config.schema.json +23 -1
  90. package/lib/utils/api.js +38 -10
  91. package/lib/utils/auth-headers.js +8 -7
  92. package/lib/utils/compose-generator.js +1 -1
  93. package/lib/utils/compose-handlebars-helpers.js +11 -0
  94. package/lib/utils/config-format-preference.js +51 -0
  95. package/lib/utils/config-format.js +36 -0
  96. package/lib/utils/configuration-env-resolver.js +179 -0
  97. package/lib/utils/credential-display.js +83 -0
  98. package/lib/utils/credential-secrets-env.js +115 -25
  99. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  100. package/lib/utils/deployment-validation-helpers.js +4 -4
  101. package/lib/utils/dev-ca-install.js +139 -0
  102. package/lib/utils/env-copy.js +23 -3
  103. package/lib/utils/error-formatters/http-status-errors.js +0 -1
  104. package/lib/utils/error-formatters/permission-errors.js +0 -1
  105. package/lib/utils/error-formatters/validation-errors.js +0 -1
  106. package/lib/utils/external-readme.js +89 -30
  107. package/lib/utils/external-system-display.js +59 -1
  108. package/lib/utils/external-system-test-helpers.js +21 -8
  109. package/lib/utils/external-system-validators.js +3 -0
  110. package/lib/utils/file-upload.js +20 -50
  111. package/lib/utils/help-builder.js +1 -0
  112. package/lib/utils/infra-status.js +50 -44
  113. package/lib/utils/local-secrets.js +5 -5
  114. package/lib/utils/paths.js +85 -4
  115. package/lib/utils/secrets-canonical.js +93 -0
  116. package/lib/utils/secrets-generator.js +20 -0
  117. package/lib/utils/secrets-helpers.js +75 -89
  118. package/lib/utils/test-log-writer.js +56 -0
  119. package/lib/utils/token-manager.js +24 -32
  120. package/lib/validation/env-template-auth.js +157 -0
  121. package/lib/validation/env-template-kv.js +41 -0
  122. package/lib/validation/external-manifest-validator.js +25 -0
  123. package/lib/validation/external-system-auth-rules.js +86 -0
  124. package/lib/validation/validate-batch.js +149 -0
  125. package/lib/validation/validate-datasource-keys-api.js +33 -0
  126. package/lib/validation/validate-display.js +94 -16
  127. package/lib/validation/validate.js +25 -12
  128. package/lib/validation/validator.js +7 -9
  129. package/lib/validation/wizard-datasource-validation.js +50 -0
  130. package/package.json +7 -2
  131. package/templates/applications/dataplane/application.yaml +1 -1
  132. package/templates/applications/dataplane/env.template +5 -5
  133. package/templates/applications/dataplane/rbac.yaml +2 -2
  134. package/templates/applications/miso-controller/env.template +1 -1
  135. package/templates/external-system/README.md.hbs +75 -22
  136. package/templates/external-system/deploy.js.hbs +4 -2
  137. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  138. package/templates/external-system/external-system.json.hbs +1 -18
  139. package/templates/infra/compose.yaml.hbs +6 -0
  140. package/templates/python/docker-compose.hbs +4 -4
  141. package/templates/typescript/docker-compose.hbs +4 -4
  142. package/integration/hubspot/application.yaml +0 -37
@@ -249,7 +249,10 @@ async function buildDeploymentManifestInMemory(appName, options = {}) {
249
249
  * @returns {Promise<string>} Path to written deploy JSON
250
250
  */
251
251
  async function writeExternalDeployJson(appName, appPath, options) {
252
- const manifest = await generateControllerManifest(appName, options);
252
+ const manifest = await generateControllerManifest(appName, {
253
+ ...options,
254
+ skipMissingDatasourceFiles: true
255
+ });
253
256
  let effectivePort = 3000;
254
257
  try {
255
258
  const variablesPath = resolveApplicationConfigPath(appPath);
@@ -25,6 +25,7 @@ function buildReadmeConfigForExternal(deployment) {
25
25
  systemType: system.type || 'openapi',
26
26
  systemDisplayName: system.displayName || appName,
27
27
  systemDescription: system.description || `External system integration for ${appName}`,
28
+ fileExt: '.yaml',
28
29
  datasourceCount: dataSources.length,
29
30
  datasources: dataSources
30
31
  }
@@ -78,10 +78,12 @@ function extractPortalInputConfiguration(configuration) {
78
78
 
79
79
  /**
80
80
  * Datasource filename for externalIntegration.dataSources.
81
+ * Avoids duplicate "datasource" in name (e.g. hubspot-users-datasource -> hubspot-datasource-users).
82
+ *
81
83
  * @param {string} systemKey - System key
82
84
  * @param {Object} datasource - Datasource from deployment.dataSources
83
85
  * @param {number} index - Index
84
- * @returns {string} Filename e.g. test-hubspot-datasource-companies-data.yaml
86
+ * @returns {string} Filename e.g. hubspot-datasource-users.yaml
85
87
  */
86
88
  function getExternalDatasourceFileName(systemKey, datasource, index) {
87
89
  const key = datasource.key || '';
@@ -90,6 +92,10 @@ function getExternalDatasourceFileName(systemKey, datasource, index) {
90
92
  else if (key.startsWith(`${systemKey}-`)) suffix = key.slice(systemKey.length + 1);
91
93
  else if (key) suffix = key;
92
94
  else suffix = datasource.entityType || datasource.entityKey || `entity${index + 1}`;
95
+ // Strip trailing -datasource so we get hubspot-datasource-users not hubspot-datasource-users-datasource
96
+ if (suffix.endsWith('-datasource')) {
97
+ suffix = suffix.slice(0, -'-datasource'.length);
98
+ }
93
99
  return `${systemKey}-datasource-${suffix}.yaml`;
94
100
  }
95
101
 
@@ -139,6 +139,125 @@ async function writeComponentFile(filePath, content) {
139
139
  await fs.writeFile(filePath, content, { mode: 0o644, encoding: 'utf8' });
140
140
  }
141
141
 
142
+ /**
143
+ * Builds key -> line map from configuration array (for env.template merge).
144
+ * @param {Array} configuration - Configuration array from deployment
145
+ * @returns {Map<string, string>} Key to full line map
146
+ */
147
+ function buildEnvTemplateExpectedByKey(configuration) {
148
+ const expectedByKey = new Map();
149
+ if (!Array.isArray(configuration)) return expectedByKey;
150
+ const lines = extractEnvTemplate(configuration).split('\n').filter(Boolean);
151
+ for (const line of lines) {
152
+ const eq = line.indexOf('=');
153
+ if (eq > 0) {
154
+ const key = line.substring(0, eq).trim();
155
+ expectedByKey.set(key, `${key}=${line.substring(eq + 1)}`);
156
+ }
157
+ }
158
+ return expectedByKey;
159
+ }
160
+
161
+ /**
162
+ * Pushes the appropriate line for a key that exists in expectedByKey (preserves MISO_CONTROLLER_URL).
163
+ * @param {string} line - Existing line
164
+ * @param {string} key - Parsed key
165
+ * @param {Map<string, string>} expectedByKey - Expected key -> line
166
+ * @param {string[]} updatedLines - Output lines
167
+ * @param {Set<string>} keysWritten - Keys already written
168
+ */
169
+ function pushMergedKeyValueLine(line, key, expectedByKey, updatedLines, keysWritten) {
170
+ const valueToPush = key === 'MISO_CONTROLLER_URL' ? line : expectedByKey.get(key);
171
+ updatedLines.push(valueToPush);
172
+ keysWritten.add(key);
173
+ }
174
+
175
+ /**
176
+ * Merges existing env.template with new lines from configuration; preserves comments and unknown keys.
177
+ * When MISO_CONTROLLER_URL already exists in the file, its value is preserved (only add when missing).
178
+ * @param {string} existingContent - Current env.template content
179
+ * @param {Map<string, string>} expectedByKey - New key -> line from download
180
+ * @returns {string} Merged content
181
+ */
182
+ function mergeEnvTemplateWithExisting(existingContent, expectedByKey) {
183
+ const lines = existingContent.split(/\r?\n/);
184
+ const updatedLines = [];
185
+ const keysWritten = new Set();
186
+ for (const line of lines) {
187
+ const trimmed = line.trim();
188
+ if (!trimmed || trimmed.startsWith('#')) {
189
+ updatedLines.push(line);
190
+ continue;
191
+ }
192
+ const eq = line.indexOf('=');
193
+ if (eq <= 0) {
194
+ updatedLines.push(line);
195
+ continue;
196
+ }
197
+ const key = line.substring(0, eq).trim();
198
+ if (expectedByKey.has(key)) {
199
+ pushMergedKeyValueLine(line, key, expectedByKey, updatedLines, keysWritten);
200
+ } else {
201
+ updatedLines.push(line);
202
+ }
203
+ }
204
+ for (const key of expectedByKey.keys()) {
205
+ if (!keysWritten.has(key)) updatedLines.push(expectedByKey.get(key));
206
+ }
207
+ return updatedLines.join('\n') + (updatedLines.length > 0 ? '\n' : '');
208
+ }
209
+
210
+ /**
211
+ * Writes env.template (merge or overwrite).
212
+ * @param {string} outputDir - Output directory
213
+ * @param {string} envTemplate - Default env.template content
214
+ * @param {Object} options - Options (mergeEnvTemplate, configuration)
215
+ * @returns {Promise<string>} Path to env.template
216
+ */
217
+ async function writeEnvTemplateToDir(outputDir, envTemplate, options) {
218
+ const envTemplatePath = path.join(outputDir, 'env.template');
219
+ const fsSync = require('fs');
220
+ if (options.mergeEnvTemplate && options.configuration && fsSync.existsSync(envTemplatePath)) {
221
+ const expectedByKey = buildEnvTemplateExpectedByKey(options.configuration);
222
+ const existingContent = await fs.readFile(envTemplatePath, 'utf8');
223
+ const merged = mergeEnvTemplateWithExisting(existingContent, expectedByKey);
224
+ await writeComponentFile(envTemplatePath, merged);
225
+ } else {
226
+ await writeComponentFile(envTemplatePath, envTemplate);
227
+ }
228
+ return envTemplatePath;
229
+ }
230
+
231
+ /**
232
+ * Writes application.yaml, rbac.yaml, and README.md.
233
+ * @param {string} outputDir - Output directory
234
+ * @param {Object} variables - Variables object
235
+ * @param {Object|null} rbac - RBAC object or null
236
+ * @param {string} readme - README content
237
+ * @param {Object} options - Options (overwriteReadme)
238
+ * @returns {Promise<Object>} Results with variables, rbac?, readme? paths
239
+ */
240
+ async function writeVariablesRbacReadme(outputDir, variables, rbac, readme, options) {
241
+ const out = {};
242
+ const variablesPath = path.join(outputDir, 'application.yaml');
243
+ await writeComponentFile(variablesPath, yaml.dump(variables, { indent: 2, lineWidth: -1 }));
244
+ out.variables = variablesPath;
245
+ if (rbac) {
246
+ const rbacPath = path.join(outputDir, 'rbac.yaml');
247
+ await writeComponentFile(rbacPath, yaml.dump(rbac, { indent: 2, lineWidth: -1 }));
248
+ out.rbac = rbacPath;
249
+ }
250
+ const readmePath = path.join(outputDir, 'README.md');
251
+ const fsSync = require('fs');
252
+ if (options.overwriteReadme === false && fsSync.existsSync(readmePath)) {
253
+ out.readmeSkipped = readmePath;
254
+ } else {
255
+ await writeComponentFile(readmePath, readme);
256
+ out.readme = readmePath;
257
+ }
258
+ return out;
259
+ }
260
+
142
261
  /**
143
262
  * Writes all component files
144
263
  * @async
@@ -148,36 +267,36 @@ async function writeComponentFile(filePath, content) {
148
267
  * @param {Object} variables - Variables object
149
268
  * @param {Object|null} rbac - RBAC object or null
150
269
  * @param {string} readme - README content
270
+ * @param {Object} [options] - Options
271
+ * @param {boolean} [options.mergeEnvTemplate] - If true and env.template exists, merge instead of overwrite
272
+ * @param {Array} [options.configuration] - Configuration array for merge (required when mergeEnvTemplate)
273
+ * @param {boolean} [options.overwriteReadme] - If false and README.md exists, skip writing README
151
274
  * @returns {Promise<Object>} Results object with file paths
152
275
  */
153
- async function writeComponentFiles(outputDir, envTemplate, variables, rbac, readme) {
276
+ async function writeComponentFiles(outputDir, envTemplate, variables, rbac, readme, options = {}) {
154
277
  const results = {};
278
+ results.envTemplate = await writeEnvTemplateToDir(outputDir, envTemplate, options);
279
+ Object.assign(results, await writeVariablesRbacReadme(outputDir, variables, rbac, readme, options));
280
+ return results;
281
+ }
155
282
 
156
- // Write env.template
157
- const envTemplatePath = path.join(outputDir, 'env.template');
158
- await writeComponentFile(envTemplatePath, envTemplate);
159
- results.envTemplate = envTemplatePath;
160
-
161
- // Write application.yaml
162
- const variablesPath = path.join(outputDir, 'application.yaml');
163
- const variablesYaml = yaml.dump(variables, { indent: 2, lineWidth: -1 });
164
- await writeComponentFile(variablesPath, variablesYaml);
165
- results.variables = variablesPath;
166
-
167
- // Write rbac.yml (only if roles/permissions exist)
168
- if (rbac) {
169
- const rbacPath = path.join(outputDir, 'rbac.yml');
170
- const rbacYaml = yaml.dump(rbac, { indent: 2, lineWidth: -1 });
171
- await writeComponentFile(rbacPath, rbacYaml);
172
- results.rbac = rbacPath;
283
+ /**
284
+ * Writes datasource YAML files for external system.
285
+ * @param {string} outputDir - Output directory
286
+ * @param {string} systemKey - System key
287
+ * @param {Array} dataSourcesList - DataSources array
288
+ * @returns {Promise<string[]>} Paths to written datasource files
289
+ */
290
+ async function writeDatasourceFiles(outputDir, systemKey, dataSourcesList) {
291
+ const paths = [];
292
+ for (let i = 0; i < dataSourcesList.length; i++) {
293
+ const ds = dataSourcesList[i];
294
+ const fileName = getExternalDatasourceFileName(systemKey, ds, i);
295
+ const dsPath = path.join(outputDir, fileName);
296
+ await writeComponentFile(dsPath, yaml.dump(ds, { indent: 2, lineWidth: -1 }));
297
+ paths.push(dsPath);
173
298
  }
174
-
175
- // Write README.md
176
- const readmePath = path.join(outputDir, 'README.md');
177
- await writeComponentFile(readmePath, readme);
178
- results.readme = readmePath;
179
-
180
- return results;
299
+ return paths;
181
300
  }
182
301
 
183
302
  /**
@@ -194,25 +313,11 @@ async function writeExternalSystemAndDatasourceFiles(outputDir, deployment) {
194
313
  const system = deployment.system;
195
314
  const systemKey = system.key || 'external-system';
196
315
  const dataSourcesList = deployment.dataSources || deployment.datasources || [];
197
- const results = {};
198
-
316
+ const { roles: _roles, permissions: _permissions, ...systemWithoutRbac } = system;
199
317
  const systemPath = path.join(outputDir, `${systemKey}-system.yaml`);
200
- const systemYaml = yaml.dump(system, { indent: 2, lineWidth: -1 });
201
- await writeComponentFile(systemPath, systemYaml);
202
- results.systemFile = systemPath;
203
-
204
- const datasourcePaths = [];
205
- for (let i = 0; i < dataSourcesList.length; i++) {
206
- const ds = dataSourcesList[i];
207
- const fileName = getExternalDatasourceFileName(systemKey, ds, i);
208
- const dsPath = path.join(outputDir, fileName);
209
- const dsYaml = yaml.dump(ds, { indent: 2, lineWidth: -1 });
210
- await writeComponentFile(dsPath, dsYaml);
211
- datasourcePaths.push(dsPath);
212
- }
213
- results.datasourceFiles = datasourcePaths;
214
-
215
- return results;
318
+ await writeComponentFile(systemPath, yaml.dump(systemWithoutRbac, { indent: 2, lineWidth: -1 }));
319
+ const datasourcePaths = await writeDatasourceFiles(outputDir, systemKey, dataSourcesList);
320
+ return { systemFile: systemPath, datasourceFiles: datasourcePaths };
216
321
  }
217
322
 
218
323
  /**
@@ -247,7 +352,49 @@ function normalizeDeploymentForSplit(deployment) {
247
352
  * @returns {Promise<Object>} Object with paths to generated files
248
353
  * @throws {Error} If JSON file not found or invalid
249
354
  */
250
- async function splitDeployJson(deployJsonPath, outputDir = null) {
355
+ /**
356
+ * Builds write options for split from splitOptions and config array.
357
+ * @param {Object} splitOptions - Split options
358
+ * @param {Array} configArray - Deployment configuration array
359
+ * @returns {Object} Write options for writeComponentFiles
360
+ */
361
+ function buildSplitWriteOptions(splitOptions, configArray) {
362
+ const writeOptions = {};
363
+ if (splitOptions.mergeEnvTemplate) {
364
+ writeOptions.mergeEnvTemplate = true;
365
+ writeOptions.configuration = configArray;
366
+ }
367
+ if (splitOptions.overwriteReadme === false) {
368
+ writeOptions.overwriteReadme = false;
369
+ }
370
+ return writeOptions;
371
+ }
372
+
373
+ /**
374
+ * Writes external system/datasource files if deployment has system and assigns to result.
375
+ * @param {string} finalOutputDir - Output directory
376
+ * @param {Object} deployment - Deployment object
377
+ * @param {Object} result - Result object to mutate
378
+ */
379
+ async function applyExternalSystemFilesToResult(finalOutputDir, deployment, result) {
380
+ if (!deployment.system || typeof deployment.system !== 'object') {
381
+ return;
382
+ }
383
+ const externalFiles = await writeExternalSystemAndDatasourceFiles(finalOutputDir, deployment);
384
+ if (externalFiles.systemFile) result.systemFile = externalFiles.systemFile;
385
+ if (externalFiles.datasourceFiles && externalFiles.datasourceFiles.length > 0) {
386
+ result.datasourceFiles = externalFiles.datasourceFiles;
387
+ }
388
+ }
389
+
390
+ /**
391
+ * @param {string} deployJsonPath - Path to deployment JSON file
392
+ * @param {string} [outputDir] - Directory to write component files
393
+ * @param {Object} [splitOptions] - Options for split behavior
394
+ * @param {boolean} [splitOptions.mergeEnvTemplate] - If true and env.template exists, merge download config into it
395
+ * @param {boolean} [splitOptions.overwriteReadme] - If false and README.md exists, do not overwrite
396
+ */
397
+ async function splitDeployJson(deployJsonPath, outputDir = null, splitOptions = {}) {
251
398
  validateDeployJsonPath(deployJsonPath);
252
399
  const finalOutputDir = await prepareOutputDirectory(deployJsonPath, outputDir);
253
400
  const deployment = await loadDeploymentJson(deployJsonPath);
@@ -259,16 +406,9 @@ async function splitDeployJson(deployJsonPath, outputDir = null) {
259
406
  const rbac = extractRbacYaml(deployment);
260
407
  const readme = generateReadmeFromDeployJson(deployment);
261
408
 
262
- const result = await writeComponentFiles(finalOutputDir, envTemplate, variables, rbac, readme);
263
-
264
- if (deployment.system && typeof deployment.system === 'object') {
265
- const externalFiles = await writeExternalSystemAndDatasourceFiles(finalOutputDir, deployment);
266
- if (externalFiles.systemFile) result.systemFile = externalFiles.systemFile;
267
- if (externalFiles.datasourceFiles && externalFiles.datasourceFiles.length > 0) {
268
- result.datasourceFiles = externalFiles.datasourceFiles;
269
- }
270
- }
271
-
409
+ const writeOptions = buildSplitWriteOptions(splitOptions, configArray);
410
+ const result = await writeComponentFiles(finalOutputDir, envTemplate, variables, rbac, readme, writeOptions);
411
+ await applyExternalSystemFilesToResult(finalOutputDir, deployment, result);
272
412
  return result;
273
413
  }
274
414
 
@@ -0,0 +1,326 @@
1
+ /**
2
+ * @fileoverview Secondary wizard prompts (credential retry, platform, config review)
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ const inquirer = require('inquirer');
8
+ const yaml = require('js-yaml');
9
+
10
+ let hasAutocompletePrompt = false;
11
+ try {
12
+ const AutocompletePrompt = require('inquirer-autocomplete-prompt');
13
+ inquirer.registerPrompt('autocomplete', AutocompletePrompt);
14
+ hasAutocompletePrompt = true;
15
+ } catch {
16
+ // Fallback: use 'list' if plugin not installed (no search, pageSize 10)
17
+ }
18
+
19
+ /**
20
+ * Re-prompt for credential ID/key when validation failed (e.g. not found on dataplane).
21
+ * Empty input means skip.
22
+ * @async
23
+ * @param {string} [previousError] - Error message from dataplane (e.g. "Credential not found")
24
+ * @returns {Promise<Object>} { credentialIdOrKey: string } or { skip: true } if user leaves empty
25
+ */
26
+ async function promptForCredentialIdOrKeyRetry(previousError) {
27
+ const msg = previousError
28
+ ? `Credential not found or invalid (${String(previousError).slice(0, 60)}). Enter ID/key or leave empty to skip:`
29
+ : 'Enter credential ID or key (or leave empty to skip):';
30
+ const { credentialIdOrKey } = await inquirer.prompt([
31
+ { type: 'input', name: 'credentialIdOrKey', message: msg, default: '' }
32
+ ]);
33
+ const trimmed = (credentialIdOrKey && credentialIdOrKey.trim()) || '';
34
+ return trimmed ? { credentialIdOrKey: trimmed } : { skip: true };
35
+ }
36
+
37
+ /**
38
+ * Prompt for known platform selection
39
+ * @async
40
+ * @param {Array<{key: string, displayName?: string}>} [platforms] - List of available platforms
41
+ * @returns {Promise<string>} Selected platform key
42
+ */
43
+ async function promptForKnownPlatform(platforms = []) {
44
+ const defaultPlatforms = [
45
+ { name: 'HubSpot', value: 'hubspot' },
46
+ { name: 'Salesforce', value: 'salesforce' },
47
+ { name: 'Zendesk', value: 'zendesk' },
48
+ { name: 'Slack', value: 'slack' },
49
+ { name: 'Microsoft 365', value: 'microsoft365' }
50
+ ];
51
+ const choices = platforms.length > 0
52
+ ? platforms.map(p => ({ name: p.displayName || p.key, value: p.key }))
53
+ : defaultPlatforms;
54
+ const { platform } = await inquirer.prompt([
55
+ { type: 'list', name: 'platform', message: 'Select a platform:', choices, pageSize: 10 }
56
+ ]);
57
+ return platform;
58
+ }
59
+
60
+ /**
61
+ * Format entity for display in list
62
+ * @param {Object} e - Entity { name, pathCount? }
63
+ * @returns {string} Display string
64
+ */
65
+ function _formatEntityChoice(e) {
66
+ if (!e || typeof e.name !== 'string') return 'unknown';
67
+ return e.pathCount !== undefined && e.pathCount !== null ? `${e.name} (${e.pathCount} paths)` : e.name;
68
+ }
69
+
70
+ /**
71
+ * Prompt to select an entity from discover-entities list (searchable when plugin available)
72
+ * @async
73
+ * @param {Array<{name: string, pathCount?: number, schemaMatch?: boolean}>} entities - From discover-entities
74
+ * @returns {Promise<string>} Selected entity name
75
+ */
76
+ async function promptForEntitySelection(entities = []) {
77
+ if (!Array.isArray(entities) || entities.length === 0) {
78
+ throw new Error('At least one entity is required');
79
+ }
80
+ const choices = entities.map(e => ({
81
+ name: _formatEntityChoice(e),
82
+ value: e.name
83
+ }));
84
+
85
+ const promptConfig = {
86
+ type: hasAutocompletePrompt ? 'autocomplete' : 'list',
87
+ name: 'entityName',
88
+ message: 'Select entity for datasource generation:',
89
+ choices,
90
+ pageSize: 10
91
+ };
92
+
93
+ if (hasAutocompletePrompt) {
94
+ promptConfig.source = (answers, input) => {
95
+ const q = (input || '').toLowerCase();
96
+ const filtered = entities.filter(e => (e.name || '').toLowerCase().includes(q));
97
+ return Promise.resolve(
98
+ filtered.map(e => ({ name: _formatEntityChoice(e), value: e.name }))
99
+ );
100
+ };
101
+ }
102
+
103
+ const { entityName } = await inquirer.prompt([promptConfig]);
104
+ return entityName;
105
+ }
106
+
107
+ /** @param {*} o - Value to stringify */
108
+ const _s = (o) => (o === null || o === undefined || o === '' ? '—' : String(o));
109
+
110
+ /**
111
+ * Humanize app key for display name (must stay in sync with prepareWizardContext in lib/generator/wizard.js).
112
+ * @param {string} appKey - Application key (e.g. hubspot-demo)
113
+ * @returns {string} Display name (e.g. Hubspot Demo)
114
+ */
115
+ function humanizeAppKey(appKey) {
116
+ if (!appKey || typeof appKey !== 'string') return appKey || '';
117
+ return appKey.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
118
+ }
119
+
120
+ /** @param {Object} sys - systemSummary */
121
+ function _formatSystem(sys) {
122
+ return [
123
+ '\nSystem',
124
+ ` Key: ${_s(sys.key)}`,
125
+ ` Display name: ${_s(sys.displayName)}`,
126
+ ` Type: ${_s(sys.type)}`,
127
+ ` Base URL: ${_s(sys.baseUrl)}`,
128
+ ` Auth: ${_s(sys.authenticationType)}`,
129
+ ` Endpoints: ${_s(sys.endpointCount)}`
130
+ ];
131
+ }
132
+
133
+ /** @param {Object} ds - datasourceSummary */
134
+ function _formatDatasource(ds) {
135
+ return [
136
+ '\nDatasource',
137
+ ` Key: ${_s(ds.key)}`,
138
+ ` Entity: ${_s(ds.entity)}`,
139
+ ` Resource type: ${_s(ds.resourceType)}`,
140
+ ` CIP steps: ${_s(ds.cipStepCount)}`,
141
+ ` Field mappings: ${_s(ds.fieldMappingCount)}`,
142
+ ` Exposed: ${_s(ds.exposedProfileCount)} profiles`
143
+ ];
144
+ }
145
+
146
+ /** @param {Object} fm - fieldMappingsSummary */
147
+ function _formatFieldMappings(fm) {
148
+ const mapped = Array.isArray(fm.mappedFields) ? fm.mappedFields : [];
149
+ const unmapped = Array.isArray(fm.unmappedFields) ? fm.unmappedFields : [];
150
+ const mappedStr = mapped.length > 0
151
+ ? `${mapped.length} (${mapped.slice(0, 5).join(', ')}${mapped.length > 5 ? ', ...' : ''})`
152
+ : _s(fm.mappingCount);
153
+ const unmappedStr = unmapped.length > 0
154
+ ? `${unmapped.length} (${unmapped.slice(0, 3).join(', ')}${unmapped.length > 3 ? ', ...' : ''})`
155
+ : '0';
156
+ return ['\nField Mappings', ` Mapped: ${mappedStr}`, ` Unmapped: ${unmappedStr}`];
157
+ }
158
+
159
+ /**
160
+ * Derive a preview summary from systemConfig and datasourceConfigs when the dataplane
161
+ * preview API does not return summaries. Ensures a compact summary is always shown.
162
+ * When appKey is provided, system key/displayName and datasource keys are overridden
163
+ * to match what prepareWizardContext will write (so the preview matches saved files).
164
+ * @param {Object} systemConfig - System configuration
165
+ * @param {Object[]} datasourceConfigs - Array of datasource configurations
166
+ * @param {string} [appKey] - Optional app key; when set, overrides system key/displayName and rewrites datasource keys
167
+ * @returns {Object} Preview object compatible with formatPreviewSummary
168
+ */
169
+ function _buildDatasourceSummary(ds) {
170
+ let fieldMappingCount = 0;
171
+ const attrs = ds.fieldMappings?.attributes ?? ds.attributes ?? {};
172
+ if (typeof attrs === 'object' && !Array.isArray(attrs)) {
173
+ fieldMappingCount = Object.keys(attrs).length;
174
+ }
175
+ const exposedCount = ds.exposed?.attributes?.length ?? 0;
176
+ return {
177
+ key: ds.key,
178
+ entity: ds.entityType ?? ds.entity ?? ds.resourceType ?? ds.key?.split('-').pop(),
179
+ resourceType: ds.resourceType,
180
+ cipStepCount: null,
181
+ fieldMappingCount: fieldMappingCount || null,
182
+ exposedProfileCount: exposedCount || null
183
+ };
184
+ }
185
+
186
+ function _buildSystemSummary(sys) {
187
+ const baseUrl = sys.openapi?.servers?.[0]?.url ?? sys.baseUrl ?? sys.openapi?.baseUrl ??
188
+ sys.authentication?.variables?.baseUrl ?? null;
189
+ const authType = sys.authentication?.type ?? sys.authentication?.method ?? sys.authenticationType ?? null;
190
+ const endpointCount = sys.openapi?.endpoints?.length ??
191
+ (sys.openapi?.operations ? Object.keys(sys.openapi.operations || {}).length : null);
192
+ return {
193
+ key: sys.key,
194
+ displayName: sys.displayName,
195
+ type: sys.type,
196
+ baseUrl,
197
+ authenticationType: authType,
198
+ endpointCount
199
+ };
200
+ }
201
+
202
+ function _buildFieldMappingsSummary(ds0) {
203
+ const attrs0 = ds0.fieldMappings?.attributes ?? ds0.attributes ?? {};
204
+ const mappedFields = (typeof attrs0 === 'object' && !Array.isArray(attrs0)) ? Object.keys(attrs0) : [];
205
+ return mappedFields.length > 0 ? { mappingCount: mappedFields.length, mappedFields, unmappedFields: [] } : null;
206
+ }
207
+
208
+ function derivePreviewFromConfig(systemConfig, datasourceConfigs, appKey) {
209
+ const sys = systemConfig || {};
210
+ const dsList = Array.isArray(datasourceConfigs) ? datasourceConfigs : (datasourceConfigs ? [datasourceConfigs] : []);
211
+
212
+ const result = {
213
+ systemSummary: _buildSystemSummary(sys),
214
+ fieldMappingsSummary: _buildFieldMappingsSummary(dsList[0] || {})
215
+ };
216
+ let datasourceSummaries = dsList.map(_buildDatasourceSummary);
217
+
218
+ if (appKey && typeof appKey === 'string') {
219
+ result.systemSummary.key = appKey;
220
+ result.systemSummary.displayName = humanizeAppKey(appKey);
221
+ const originalSystemKey = sys.key || appKey;
222
+ const originalPrefix = `${originalSystemKey}-`;
223
+ datasourceSummaries = datasourceSummaries.map((summary, i) => {
224
+ const ds = dsList[i] || {};
225
+ const dsKey = ds.key || '';
226
+ const newKey = dsKey && dsKey.startsWith(originalPrefix)
227
+ ? `${appKey}-${dsKey.substring(originalPrefix.length)}`
228
+ : `${appKey}-${ds.entityType || ds.entityKey || (dsKey && dsKey.split('-').pop()) || 'default'}`;
229
+ return { ...summary, key: newKey };
230
+ });
231
+ }
232
+
233
+ if (datasourceSummaries.length === 1) {
234
+ result.datasourceSummary = datasourceSummaries[0];
235
+ } else if (datasourceSummaries.length > 1) {
236
+ result.datasourceSummaries = datasourceSummaries;
237
+ }
238
+ return result;
239
+ }
240
+
241
+ /**
242
+ * Format a preview summary for display (WizardPreviewResponse from dataplane)
243
+ * @param {Object} preview - Preview data from GET /api/v1/wizard/preview/{sessionId}
244
+ * @returns {string} Formatted summary text
245
+ */
246
+ function formatPreviewSummary(preview) {
247
+ const hasVal = (v) => v !== null && v !== undefined;
248
+ const parts = ['\nšŸ“‹ Configuration Preview (what will be created)', '─'.repeat(60)];
249
+
250
+ if (preview.systemSummary) parts.push(..._formatSystem(preview.systemSummary));
251
+ if (preview.datasourceSummaries && preview.datasourceSummaries.length > 0) {
252
+ preview.datasourceSummaries.forEach((ds, i) => {
253
+ const lines = _formatDatasource(ds);
254
+ const label = preview.datasourceSummaries.length > 1 ? `Datasource ${i + 1}` : 'Datasource';
255
+ parts.push(...lines.map((line, j) => (j === 0 ? line.replace('Datasource', label) : line)));
256
+ });
257
+ } else if (preview.datasourceSummary) {
258
+ parts.push(..._formatDatasource(preview.datasourceSummary));
259
+ }
260
+ if (preview.cipPipelineSummary) {
261
+ const cip = preview.cipPipelineSummary;
262
+ parts.push('\nCIP Pipeline', ` Steps: ${_s(cip.stepCount)}`, ` Est. execution: ${_s(cip.estimatedExecutionTime) || '—'}`);
263
+ }
264
+ if (preview.fieldMappingsSummary) parts.push(..._formatFieldMappings(preview.fieldMappingsSummary));
265
+ if (hasVal(preview.estimatedRecords) || hasVal(preview.estimatedSyncTime)) {
266
+ parts.push('\nEstimates', ` Records: ${_s(preview.estimatedRecords)}`, ` Sync: ${_s(preview.estimatedSyncTime)}`);
267
+ }
268
+ parts.push('\n' + '─'.repeat(60));
269
+ return parts.join('\n');
270
+ }
271
+
272
+ /**
273
+ * Prompt for configuration review and editing
274
+ * When preview is provided, displays a compact summary; otherwise dumps full YAML.
275
+ * @async
276
+ * @param {Object} opts - Options
277
+ * @param {Object|null} [opts.preview] - Preview data from getPreview (null = fallback to YAML)
278
+ * @param {Object} opts.systemConfig - System configuration
279
+ * @param {Object[]} opts.datasourceConfigs - Array of datasource configurations
280
+ * @param {string} [opts.appKey] - App key; when set and using fallback preview, overrides system key/displayName and datasource keys
281
+ * @returns {Promise<Object>} Object with action ('accept'|'cancel') and optionally edited configs
282
+ */
283
+ async function promptForConfigReview({ preview, systemConfig, datasourceConfigs, appKey }) {
284
+ const hasSummary = preview && (preview.systemSummary || preview.datasourceSummary || (preview.datasourceSummaries && preview.datasourceSummaries.length > 0));
285
+ const summaryToShow = hasSummary ? preview : derivePreviewFromConfig(systemConfig, datasourceConfigs, appKey);
286
+ const canShowSummary = summaryToShow.systemSummary || summaryToShow.datasourceSummary ||
287
+ (summaryToShow.datasourceSummaries && summaryToShow.datasourceSummaries.length > 0);
288
+
289
+ if (canShowSummary) {
290
+ // eslint-disable-next-line no-console
291
+ console.log(formatPreviewSummary(summaryToShow));
292
+ } else {
293
+ // eslint-disable-next-line no-console
294
+ console.log('\nšŸ“‹ Generated Configuration:\nSystem Configuration:');
295
+ // eslint-disable-next-line no-console
296
+ console.log(yaml.dump(systemConfig, { lineWidth: -1 }));
297
+ // eslint-disable-next-line no-console
298
+ console.log('Datasource Configurations:');
299
+ (datasourceConfigs || []).forEach((ds, index) => {
300
+ // eslint-disable-next-line no-console
301
+ console.log(`\nDatasource ${index + 1}:\n${yaml.dump(ds, { lineWidth: -1 })}`);
302
+ });
303
+ }
304
+ const { action } = await inquirer.prompt([
305
+ {
306
+ type: 'list',
307
+ name: 'action',
308
+ message: 'What would you like to do?',
309
+ choices: [
310
+ { name: 'Accept and save', value: 'accept' },
311
+ { name: 'Cancel', value: 'cancel' }
312
+ ]
313
+ }
314
+ ]);
315
+ return action === 'cancel' ? { action: 'cancel' } : { action: 'accept' };
316
+ }
317
+
318
+ module.exports = {
319
+ promptForCredentialIdOrKeyRetry,
320
+ promptForKnownPlatform,
321
+ promptForEntitySelection,
322
+ promptForConfigReview,
323
+ derivePreviewFromConfig,
324
+ formatPreviewSummary,
325
+ humanizeAppKey
326
+ };