@aifabrix/builder 2.44.3 → 2.44.5

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 (72) hide show
  1. package/.npmrc.token +1 -1
  2. package/integration/roundtrip-test-local/README.md +1 -2
  3. package/integration/roundtrip-test-local2/README.md +1 -2
  4. package/jest.projects.js +31 -15
  5. package/lib/api/certificates.api.js +21 -3
  6. package/lib/api/types/wizard.types.js +2 -1
  7. package/lib/certification/post-unified-cert-sync.js +13 -2
  8. package/lib/certification/sync-after-external-command.js +6 -3
  9. package/lib/certification/sync-system-certification.js +60 -14
  10. package/lib/cli/setup-app.help.js +1 -1
  11. package/lib/cli/setup-app.test-commands.js +75 -39
  12. package/lib/cli/setup-infra.js +6 -2
  13. package/lib/cli/setup-utility.js +20 -1
  14. package/lib/commands/datasource-unified-test-cli.js +81 -46
  15. package/lib/commands/datasource-unified-test-cli.options.js +4 -2
  16. package/lib/commands/datasource.js +3 -31
  17. package/lib/commands/repair-datasource-keys.js +1 -1
  18. package/lib/commands/repair-datasource-openapi.js +57 -0
  19. package/lib/commands/repair-datasource.js +5 -0
  20. package/lib/commands/repair-internal.js +2 -4
  21. package/lib/commands/repair-rbac.js +25 -2
  22. package/lib/commands/repair.js +2 -19
  23. package/lib/commands/test-e2e-external.js +9 -9
  24. package/lib/commands/up-common.js +25 -0
  25. package/lib/commands/upload.js +18 -4
  26. package/lib/commands/wizard-core.js +53 -11
  27. package/lib/commands/wizard-dataplane.js +14 -6
  28. package/lib/commands/wizard-entity-selection.js +71 -14
  29. package/lib/commands/wizard-headless.js +5 -2
  30. package/lib/commands/wizard-helpers.js +13 -1
  31. package/lib/commands/wizard.js +208 -60
  32. package/lib/datasource/datasource-validate-display.js +162 -0
  33. package/lib/datasource/datasource-validate-summary.js +194 -0
  34. package/lib/datasource/test-e2e.js +65 -37
  35. package/lib/datasource/unified-validation-run-body.js +1 -2
  36. package/lib/datasource/validate.js +14 -6
  37. package/lib/external-system/test.js +12 -8
  38. package/lib/generator/external-controller-manifest.js +12 -2
  39. package/lib/generator/wizard-prompts.js +7 -1
  40. package/lib/generator/wizard.js +34 -0
  41. package/lib/schema/cip-capacity-display.fallback.json +7 -0
  42. package/lib/schema/datasource-test-run.schema.json +79 -1
  43. package/lib/schema/external-datasource.schema.json +94 -2
  44. package/lib/schema/flag-map-validation-run.json +1 -2
  45. package/lib/schema/type/document-storage.json +83 -3
  46. package/lib/schema/wizard-config.schema.json +1 -1
  47. package/lib/utils/configuration-env-resolver.js +38 -0
  48. package/lib/utils/dataplane-resolver.js +3 -2
  49. package/lib/utils/datasource-test-run-capacity-operations.js +149 -0
  50. package/lib/utils/datasource-test-run-debug-display.js +143 -1
  51. package/lib/utils/datasource-test-run-display.js +46 -33
  52. package/lib/utils/datasource-test-run-tty-log.js +6 -2
  53. package/lib/utils/datasource-test-run-tty-meta-lines.js +123 -0
  54. package/lib/utils/error-formatter.js +32 -2
  55. package/lib/utils/external-readme.js +47 -3
  56. package/lib/utils/external-system-readiness-core.js +39 -0
  57. package/lib/utils/external-system-readiness-deploy-display.js +2 -3
  58. package/lib/utils/external-system-readiness-display-internals.js +3 -2
  59. package/lib/utils/external-system-system-test-tty.js +33 -9
  60. package/lib/utils/external-system-validators.js +62 -5
  61. package/lib/utils/load-cip-capacity-display-config.js +130 -0
  62. package/lib/utils/paths.js +10 -3
  63. package/lib/utils/schema-resolver.js +98 -2
  64. package/lib/utils/urls-local-registry.js +52 -10
  65. package/lib/utils/validation-run-poll.js +15 -4
  66. package/lib/utils/validation-run-request.js +4 -6
  67. package/lib/validation/dimension-display-helpers.js +60 -0
  68. package/lib/validation/validate-display-log-helpers.js +39 -0
  69. package/lib/validation/validate-display.js +89 -83
  70. package/package.json +1 -1
  71. package/templates/applications/miso-controller/env.template +6 -6
  72. package/templates/external-system/README.md.hbs +58 -32
@@ -275,7 +275,8 @@ async function handleTypeDetection(dataplaneUrl, authConfig, openapiSpec) {
275
275
  * @param {string} [options.systemIdOrKey] - System ID or key (optional)
276
276
  * @param {string} [options.sourceType] - Source type (use 'known-platform' to call platforms config endpoint)
277
277
  * @param {string} [options.platformKey] - Platform key for known-platform (e.g. 'hubspot')
278
- * @param {string} [options.appName] - App name for writing debug.log when debug=true
278
+ * @param {string} [options.appName] - Integration app key; for create-system, used as systemIdOrKey
279
+ * when not set so kv paths match the folder (avoids spec-title keys like "companies")
279
280
  * @returns {Promise<Object>} Generated configuration
280
281
  */
281
282
  async function callGenerateApi(dataplaneUrl, authConfig, options, prefs) {
@@ -291,13 +292,19 @@ async function callGenerateApi(dataplaneUrl, authConfig, options, prefs) {
291
292
  });
292
293
  return await getPlatformConfig(dataplaneUrl, authConfig, options.platformKey, platformPayload);
293
294
  }
295
+ // Headless/interactive create-system uses appName as integration folder key; OpenAPI title alone
296
+ // can yield a wrong system key (e.g. title "Companies" → "companies"). Prefer appName when set.
297
+ let systemIdOrKeyForPayload = options.systemIdOrKey;
298
+ if (options.mode === 'create-system' && options.appName) {
299
+ systemIdOrKeyForPayload = systemIdOrKeyForPayload || options.appName;
300
+ }
294
301
  const configPayload = buildConfigPayload({
295
302
  openapiSpec: options.openapiSpec,
296
303
  detectedType: options.detectedType,
297
304
  mode: options.mode,
298
305
  prefs,
299
306
  credentialIdOrKey: options.credentialIdOrKey,
300
- systemIdOrKey: options.systemIdOrKey,
307
+ systemIdOrKey: systemIdOrKeyForPayload,
301
308
  entityName: options.entityName,
302
309
  systemDisplayName: options.systemDisplayName
303
310
  });
@@ -410,6 +417,39 @@ async function tryUpdateReadmeFromDeploymentDocs(appPath, appName, dataplaneUrl,
410
417
  }
411
418
  }
412
419
 
420
+ /**
421
+ * Writes rbac.yaml / rbac.json from datasource resourceType + capabilities (same as `af repair --rbac`).
422
+ * @param {Object} generatedFiles - Result of generateWizardFiles (appPath, systemFilePath, datasourceFilePaths)
423
+ * @param {string} format - Project format: yaml | json
424
+ */
425
+ function logWizardFileSaveFooter(appName, generatedFiles) {
426
+ logger.log(chalk.green('\n\u2713 Wizard completed successfully!'));
427
+ logger.log(chalk.green(`\nFiles created in: ${generatedFiles.appPath}`));
428
+ logger.log(chalk.blue('\nNext steps:'));
429
+ logger.log(chalk.gray(` 1. Review the generated files in integration/${appName}/`));
430
+ logger.log(chalk.gray(' 2. Update env.template with your authentication details'));
431
+ logger.log(chalk.gray(` 3. Deploy using: node deploy.js or aifabrix deploy ${appName}`));
432
+ }
433
+
434
+ function mergeRbacAfterWizardFilesWritten(generatedFiles, format) {
435
+ const { mergeRbacFromDatasources, extractRbacFromSystem } = require('./repair-rbac');
436
+ const { loadConfigFile } = require('../utils/config-format');
437
+ const systemParsedForRbac = loadConfigFile(generatedFiles.systemFilePath);
438
+ const datasourceFileNames = (generatedFiles.datasourceFilePaths || []).map((p) => path.basename(p));
439
+ const rbacChanges = [];
440
+ const rbacUpdated = mergeRbacFromDatasources(
441
+ generatedFiles.appPath,
442
+ systemParsedForRbac,
443
+ datasourceFileNames,
444
+ extractRbacFromSystem,
445
+ { format: format === 'json' ? 'json' : 'yaml', dryRun: false, changes: rbacChanges }
446
+ );
447
+ if (rbacUpdated && rbacChanges.length) {
448
+ rbacChanges.forEach((c) => logger.log(chalk.gray(` RBAC: ${c}`)));
449
+ logger.log(chalk.green(' RBAC file updated from datasource capabilities (enableRBAC).'));
450
+ }
451
+ }
452
+
413
453
  /**
414
454
  * Handle file saving step
415
455
  * @async
@@ -418,17 +458,24 @@ async function tryUpdateReadmeFromDeploymentDocs(appPath, appName, dataplaneUrl,
418
458
  * @param {Object} systemConfig - System configuration
419
459
  * @param {Object[]} datasourceConfigs - Datasource configurations
420
460
  * @param {string} systemKey - System key
421
- * @param {string} dataplaneUrl - Dataplane URL
422
- * @param {Object} authConfig - Authentication configuration
461
+ * @param {{ dataplaneUrl: string, authConfig: Object, enableRBAC?: boolean }} ctx - Dataplane auth + optional RBAC generation
423
462
  * @returns {Promise<Object>} Generated files information
424
463
  */
425
- async function handleFileSaving(appName, systemConfig, datasourceConfigs, systemKey, dataplaneUrl, authConfig) {
464
+ async function handleFileSaving(appName, systemConfig, datasourceConfigs, systemKey, ctx) {
465
+ const { dataplaneUrl, authConfig, enableRBAC = false } = ctx || {};
426
466
  logger.log(chalk.blue('\n\uD83D\uDCCB Step 7: Save Files'));
427
467
  const spinner = ora('Saving files...').start();
428
468
  try {
429
469
  const config = require('../core/config');
430
470
  const format = (await config.getFormat()) || 'yaml';
431
471
  const generatedFiles = await generateWizardFiles(appName, systemConfig, datasourceConfigs, systemKey, { aiGeneratedReadme: null, format });
472
+ if (enableRBAC && generatedFiles.appPath && generatedFiles.systemFilePath) {
473
+ try {
474
+ mergeRbacAfterWizardFilesWritten(generatedFiles, format);
475
+ } catch (e) {
476
+ logger.log(chalk.yellow(` Could not generate RBAC file: ${e.message}`));
477
+ }
478
+ }
432
479
  if (systemKey && dataplaneUrl && authConfig && generatedFiles.appPath) {
433
480
  try {
434
481
  await tryUpdateReadmeFromDeploymentDocs(generatedFiles.appPath, appName, dataplaneUrl, authConfig, systemKey);
@@ -437,12 +484,7 @@ async function handleFileSaving(appName, systemConfig, datasourceConfigs, system
437
484
  }
438
485
  }
439
486
  spinner.stop();
440
- logger.log(chalk.green('\n\u2713 Wizard completed successfully!'));
441
- logger.log(chalk.green(`\nFiles created in: ${generatedFiles.appPath}`));
442
- logger.log(chalk.blue('\nNext steps:'));
443
- logger.log(chalk.gray(` 1. Review the generated files in integration/${appName}/`));
444
- logger.log(chalk.gray(' 2. Update env.template with your authentication details'));
445
- logger.log(chalk.gray(` 3. Deploy using: node deploy.js or aifabrix deploy ${appName}`));
487
+ logWizardFileSaveFooter(appName, generatedFiles);
446
488
  return generatedFiles;
447
489
  } catch (error) {
448
490
  spinner.stop();
@@ -73,10 +73,12 @@ function createDataplaneNotFoundError() {
73
73
  * @returns {Promise<string>} Dataplane URL
74
74
  * @throws {Error} If dataplane URL cannot be retrieved
75
75
  */
76
- async function tryFallbackDataplaneUrl(controllerUrl, environment, authConfig) {
76
+ async function tryFallbackDataplaneUrl(controllerUrl, environment, authConfig, silent) {
77
77
  try {
78
78
  const fallbackUrl = await getDataplaneUrl(controllerUrl, 'dataplane', environment, authConfig);
79
- logger.log(formatSuccessLine(`Dataplane URL: ${fallbackUrl}`));
79
+ if (!silent) {
80
+ logger.log(formatSuccessLine(`Dataplane URL: ${fallbackUrl}`));
81
+ }
80
82
  return fallbackUrl;
81
83
  } catch (fallbackError) {
82
84
  if (isNotFoundError(fallbackError)) {
@@ -93,19 +95,25 @@ async function tryFallbackDataplaneUrl(controllerUrl, environment, authConfig) {
93
95
  * @param {string} controllerUrl - Controller URL
94
96
  * @param {string} environment - Environment key
95
97
  * @param {Object} authConfig - Authentication configuration
98
+ * @param {{ silent?: boolean }} [opts] - When silent, skip progress/success lines (e.g. cert sync right after a run).
96
99
  * @returns {Promise<string>} Dataplane URL
97
100
  * @throws {Error} If dataplane URL cannot be discovered
98
101
  */
99
- async function discoverDataplaneUrl(controllerUrl, environment, authConfig) {
100
- logger.log(infoLine('🌐 Getting dataplane URL from controller...'));
102
+ async function discoverDataplaneUrl(controllerUrl, environment, authConfig, opts = {}) {
103
+ const silent = opts.silent === true;
104
+ if (!silent) {
105
+ logger.log(infoLine('🌐 Getting dataplane URL from controller...'));
106
+ }
101
107
  try {
102
108
  const dataplaneAppKey = await findDataplaneServiceAppKey(controllerUrl, environment, authConfig);
103
109
  if (dataplaneAppKey) {
104
110
  const dataplaneUrl = await getDataplaneUrl(controllerUrl, dataplaneAppKey, environment, authConfig);
105
- logger.log(formatSuccessLine(`Dataplane URL: ${dataplaneUrl}`));
111
+ if (!silent) {
112
+ logger.log(formatSuccessLine(`Dataplane URL: ${dataplaneUrl}`));
113
+ }
106
114
  return dataplaneUrl;
107
115
  }
108
- return await tryFallbackDataplaneUrl(controllerUrl, environment, authConfig);
116
+ return await tryFallbackDataplaneUrl(controllerUrl, environment, authConfig, silent);
109
117
  } catch (error) {
110
118
  if (error.message.includes('Could not discover dataplane URL')) {
111
119
  throw error;
@@ -10,30 +10,87 @@ const { discoverEntities } = require('../api/wizard.api');
10
10
  const { validateEntityNameForOpenApi } = require('../validation/wizard-datasource-validation');
11
11
  const { promptForEntitySelection } = require('../generator/wizard-prompts');
12
12
 
13
+ /**
14
+ * If wizard.yaml entity name matches discover-entities list, use it; else warn.
15
+ * @param {string} trimmed - Trimmed entity name from prefill
16
+ * @param {Array<{name: string}>} entities - Discovered entities
17
+ * @returns {string|null} Resolved name or null to prompt
18
+ */
19
+ function resolvePrefillEntityName(trimmed, entities) {
20
+ const prefillCheck = validateEntityNameForOpenApi(trimmed, entities);
21
+ if (prefillCheck.valid) {
22
+ logger.log(chalk.gray(
23
+ `Using entity from wizard.yaml (${trimmed}). Skipping entity prompts.`
24
+ ));
25
+ logger.log(chalk.green(`\u2713 Selected entity: ${trimmed}`));
26
+ return trimmed;
27
+ }
28
+ logger.log(chalk.yellow(
29
+ `Warning: wizard.yaml source.entityName '${trimmed}' is not in the discover-entities list; choose manually.`
30
+ ));
31
+ return null;
32
+ }
33
+
34
+ /**
35
+ * Prompt for entity and validate against list.
36
+ * @param {Array<{name: string}>} entities - Discovered entities
37
+ * @returns {Promise<string>} Valid entity name
38
+ */
39
+ async function promptForValidatedEntity(entities) {
40
+ const entityName = await promptForEntitySelection(entities);
41
+ const validation = validateEntityNameForOpenApi(entityName, entities);
42
+ if (!validation.valid) {
43
+ throw new Error(`Invalid entity '${entityName}'. Available: ${entities.map(e => e.name).join(', ')}`);
44
+ }
45
+ logger.log(chalk.green(`\u2713 Selected entity: ${entityName}`));
46
+ return entityName;
47
+ }
48
+
49
+ /**
50
+ * Discover entities and select one (single-entity shortcut, prefill, or prompt).
51
+ * @param {string} dataplaneUrl - Dataplane URL
52
+ * @param {Object} authConfig - Authentication configuration
53
+ * @param {Object} openapiSpec - OpenAPI specification
54
+ * @param {string} [prefillEntityName] - From wizard.yaml `source.entityName` when valid
55
+ * @returns {Promise<string|null>} Selected entity name or null (skip)
56
+ */
57
+ async function discoverAndSelectEntity(dataplaneUrl, authConfig, openapiSpec, prefillEntityName) {
58
+ const response = await discoverEntities(dataplaneUrl, authConfig, openapiSpec);
59
+ const entities = response?.data?.entities;
60
+ if (!Array.isArray(entities) || entities.length === 0) return null;
61
+
62
+ logger.log(chalk.blue('\n\uD83D\uDCCB Step 4.5: Select Entity'));
63
+
64
+ if (entities.length === 1) {
65
+ const only = entities[0].name;
66
+ logger.log(chalk.green(`\u2713 Only one entity discovered; using: ${only}`));
67
+ return only;
68
+ }
69
+
70
+ const trimmed =
71
+ typeof prefillEntityName === 'string' ? prefillEntityName.trim() : '';
72
+ if (trimmed) {
73
+ const resolved = resolvePrefillEntityName(trimmed, entities);
74
+ if (resolved) return resolved;
75
+ }
76
+
77
+ return promptForValidatedEntity(entities);
78
+ }
79
+
13
80
  /**
14
81
  * Handle entity selection step (OpenAPI multi-entity).
15
- * Calls discover-entities; if entities found, prompts user to select one.
82
+ * Calls discover-entities; prompts unless prefill or a single entity applies.
16
83
  * @async
17
84
  * @param {string} dataplaneUrl - Dataplane URL
18
85
  * @param {Object} authConfig - Authentication configuration
19
86
  * @param {Object} openapiSpec - OpenAPI specification
87
+ * @param {string} [prefillEntityName] - From wizard.yaml `source.entityName` when valid
20
88
  * @returns {Promise<string|null>} Selected entity name or null (skip)
21
89
  */
22
- async function handleEntitySelection(dataplaneUrl, authConfig, openapiSpec) {
90
+ async function handleEntitySelection(dataplaneUrl, authConfig, openapiSpec, prefillEntityName) {
23
91
  if (!openapiSpec || typeof openapiSpec !== 'object') return null;
24
92
  try {
25
- const response = await discoverEntities(dataplaneUrl, authConfig, openapiSpec);
26
- const entities = response?.data?.entities;
27
- if (!Array.isArray(entities) || entities.length === 0) return null;
28
-
29
- logger.log(chalk.blue('\n\uD83D\uDCCB Step 4.5: Select Entity'));
30
- const entityName = await promptForEntitySelection(entities);
31
- const validation = validateEntityNameForOpenApi(entityName, entities);
32
- if (!validation.valid) {
33
- throw new Error(`Invalid entity '${entityName}'. Available: ${entities.map(e => e.name).join(', ')}`);
34
- }
35
- logger.log(chalk.green(`\u2713 Selected entity: ${entityName}`));
36
- return entityName;
93
+ return await discoverAndSelectEntity(dataplaneUrl, authConfig, openapiSpec, prefillEntityName);
37
94
  } catch (error) {
38
95
  logger.log(chalk.yellow(`Warning: Entity discovery failed, using default: ${error.message}`));
39
96
  return null;
@@ -111,8 +111,11 @@ async function executeWizardFromConfig(wizardConfig, dataplaneUrl, authConfig, o
111
111
  systemConfig,
112
112
  datasourceConfigs,
113
113
  systemKey || appName,
114
- dataplaneUrl,
115
- authConfig
114
+ {
115
+ dataplaneUrl,
116
+ authConfig,
117
+ enableRBAC: Boolean(preferences?.enableRBAC)
118
+ }
116
119
  );
117
120
  }
118
121
 
@@ -47,6 +47,12 @@ function buildSourceForSave(source) {
47
47
  out.token = source.token ? '(set)' : undefined;
48
48
  }
49
49
  if (source.type === 'known-platform' && source.platform) out.platform = source.platform;
50
+ if (
51
+ (source.type === 'openapi-file' || source.type === 'openapi-url') &&
52
+ source.entityName
53
+ ) {
54
+ out.entityName = source.entityName;
55
+ }
50
56
  return out;
51
57
  }
52
58
 
@@ -75,7 +81,13 @@ function buildWizardStateForSave(opts) {
75
81
  function formatSourceLine(source) {
76
82
  if (!source) return null;
77
83
  const s = source;
78
- return s.type + (s.filePath ? ` (${s.filePath})` : s.url ? ` (${s.url})` : s.platform ? ` (${s.platform})` : '');
84
+ let line =
85
+ s.type +
86
+ (s.filePath ? ` (${s.filePath})` : s.url ? ` (${s.url})` : s.platform ? ` (${s.platform})` : '');
87
+ if (s.entityName && (s.type === 'openapi-file' || s.type === 'openapi-url')) {
88
+ line += ` [entity: ${s.entityName}]`;
89
+ }
90
+ return line;
79
91
  }
80
92
 
81
93
  /**
@@ -26,6 +26,7 @@ const {
26
26
  validateAndCheckAppDirectory,
27
27
  formatDataplaneRejectedTokenMessage,
28
28
  extractSessionId,
29
+ handleSourceSelection,
29
30
  handleOpenApiParsing,
30
31
  handleCredentialSelection,
31
32
  handleTypeDetection,
@@ -50,6 +51,149 @@ const {
50
51
  } = require('./wizard-helpers');
51
52
  const { humanizeAppKey } = require('../generator/wizard-prompts-secondary');
52
53
 
54
+ /**
55
+ * Map resolved source type/data onto wizard state.source.
56
+ * @param {Object} state - Mutable wizard state
57
+ * @param {string} sourceType - Source type key
58
+ * @param {unknown} sourceData - Raw source payload from prompts
59
+ */
60
+ function applySourceSelectionToState(state, sourceType, sourceData) {
61
+ state.source = { type: sourceType };
62
+ if (sourceType === 'openapi-file') state.source.filePath = sourceData;
63
+ else if (sourceType === 'openapi-url') state.source.url = sourceData;
64
+ else if (sourceType === 'mcp-server') state.source.serverUrl = JSON.parse(sourceData).serverUrl;
65
+ else if (sourceType === 'known-platform') state.source.platform = sourceData;
66
+ }
67
+
68
+ /**
69
+ * Interactive or prefill source selection (Step 2).
70
+ * @returns {Promise<{ sourceType: string, sourceData: unknown }>}
71
+ */
72
+ async function resolveWizardSourcePhase(dataplaneUrl, sessionId, authConfig, platforms, prefill) {
73
+ if (prefill?.source?.type) {
74
+ logger.log(chalk.gray(
75
+ `Using source from wizard.yaml (${prefill.source.type}). Skipping source prompts.`
76
+ ));
77
+ return handleSourceSelection(dataplaneUrl, sessionId, authConfig, prefill.source);
78
+ }
79
+ return handleInteractiveSourceSelection(dataplaneUrl, sessionId, authConfig, platforms);
80
+ }
81
+
82
+ /**
83
+ * Credential selection with optional prefill (Step 3).
84
+ * @returns {Promise<string>} credentialIdOrKey for generation step
85
+ */
86
+ async function resolveWizardCredentialPhase(dataplaneUrl, authConfig, prefill, state) {
87
+ if (prefill?.credential) {
88
+ state.credential = prefill.credential;
89
+ return handleCredentialSelection(dataplaneUrl, authConfig, prefill.credential);
90
+ }
91
+ const credentialAction = await promptForCredentialAction();
92
+ const configCredential = await resolveCredentialConfig(dataplaneUrl, authConfig, credentialAction);
93
+ state.credential = configCredential;
94
+ return handleCredentialSelection(dataplaneUrl, authConfig, configCredential);
95
+ }
96
+
97
+ /**
98
+ * Collect user intent and UI preferences (Step 5), with optional wizard.yaml prefill.
99
+ * @returns {Promise<{ userIntent: string, preferences: Object, hasPrefillIntent: boolean }>}
100
+ */
101
+ async function collectIntentAndPreferences(preferencesPrefill) {
102
+ const prefillPrefs = preferencesPrefill;
103
+ const hasPrefillIntent =
104
+ prefillPrefs &&
105
+ typeof prefillPrefs.intent === 'string' &&
106
+ prefillPrefs.intent.trim().length > 0;
107
+
108
+ if (hasPrefillIntent) {
109
+ logger.log(chalk.gray(
110
+ 'Using preferences from wizard.yaml (intent and toggles). Skipping preference prompts.'
111
+ ));
112
+ const level = prefillPrefs.fieldOnboardingLevel;
113
+ const validLevel = level === 'standard' || level === 'minimal' ? level : 'full';
114
+ return {
115
+ userIntent: prefillPrefs.intent.trim(),
116
+ preferences: {
117
+ fieldOnboardingLevel: validLevel,
118
+ mcp: Boolean(prefillPrefs.enableMCP),
119
+ abac: Boolean(prefillPrefs.enableABAC),
120
+ rbac: Boolean(prefillPrefs.enableRBAC)
121
+ },
122
+ hasPrefillIntent: true
123
+ };
124
+ }
125
+
126
+ const userIntent = await promptForUserIntent();
127
+ const preferences = await promptForUserPreferences();
128
+ return { userIntent, preferences, hasPrefillIntent: false };
129
+ }
130
+
131
+ /**
132
+ * Run Step 5 (generate), Step 6–7 (review), and save files; updates state.preferences.
133
+ * @param {Object} payload
134
+ * @param {string} payload.appKey - Application key
135
+ * @param {string} payload.dataplaneUrl - Dataplane URL
136
+ * @param {Object} payload.authConfig - Auth configuration
137
+ * @param {string} payload.sessionId - Wizard session ID
138
+ * @param {Object} payload.state - Mutable wizard state
139
+ * @param {Object} payload.flowOpts - Flow options (mode, systemIdOrKey, debug, prefill, etc.)
140
+ * @param {Object} payload.genInput - Generation inputs (openapiSpec, detectedType, credential…)
141
+ * @returns {Promise<Object|null>} Updated state or null if review cancelled
142
+ */
143
+ async function completeWizardGenerateReviewSave(payload) {
144
+ const {
145
+ appKey,
146
+ dataplaneUrl,
147
+ authConfig,
148
+ sessionId,
149
+ state,
150
+ flowOpts,
151
+ genInput
152
+ } = payload;
153
+ const { mode, systemIdOrKey, debug, prefill } = flowOpts;
154
+ const genResult = await handleInteractiveConfigGeneration({
155
+ dataplaneUrl,
156
+ authConfig,
157
+ mode,
158
+ openapiSpec: genInput.openapiSpec,
159
+ detectedType: genInput.detectedType,
160
+ credentialIdOrKey: genInput.credentialIdOrKey,
161
+ systemIdOrKey,
162
+ sourceType: genInput.sourceType,
163
+ sourceData: genInput.sourceData,
164
+ entityName: genInput.entityName,
165
+ appName: appKey,
166
+ debug,
167
+ systemDisplayName: flowOpts.systemDisplayName,
168
+ preferencesPrefill: prefill?.preferences
169
+ });
170
+ const { systemConfig, datasourceConfigs, systemKey, preferences: savedPrefs } = genResult;
171
+ state.preferences = savedPrefs || {};
172
+
173
+ const finalConfigs = await handleConfigurationReview(
174
+ dataplaneUrl,
175
+ authConfig,
176
+ sessionId,
177
+ systemConfig,
178
+ datasourceConfigs,
179
+ { appKey, debug }
180
+ );
181
+ if (!finalConfigs) return null;
182
+
183
+ await handleFileSaving(
184
+ appKey,
185
+ finalConfigs.systemConfig,
186
+ finalConfigs.datasourceConfigs,
187
+ systemKey || appKey,
188
+ {
189
+ dataplaneUrl,
190
+ authConfig,
191
+ enableRBAC: Boolean(savedPrefs?.enableRBAC)
192
+ }
193
+ );
194
+ return state;
195
+ }
196
+
53
197
  /**
54
198
  * Create wizard session with given mode and optional systemIdOrKey (no prompts)
55
199
  * @async
@@ -125,12 +269,15 @@ async function handleInteractiveSourceSelection(dataplaneUrl, sessionId, authCon
125
269
  * @param {Object} options.detectedType - Detected type info
126
270
  * @param {string} [options.credentialIdOrKey] - Credential ID or key (optional)
127
271
  * @param {string} [options.systemIdOrKey] - System ID or key (optional)
272
+ * @param {Object} [options.preferencesPrefill] - From wizard.yaml `preferences` (skip Step 5 prompts when intent is set)
128
273
  * @returns {Promise<Object>} Generated configuration and preferences { systemConfig, datasourceConfigs, systemKey, preferences }
129
274
  */
130
275
  async function handleInteractiveConfigGeneration(options) {
131
276
  logger.log(chalk.blue('\n\uD83D\uDCCB Step 5: User Preferences'));
132
- const userIntent = await promptForUserIntent();
133
- const preferences = await promptForUserPreferences();
277
+
278
+ const { userIntent, preferences, hasPrefillIntent } = await collectIntentAndPreferences(
279
+ options.preferencesPrefill
280
+ );
134
281
 
135
282
  const configPrefs = {
136
283
  intent: userIntent,
@@ -138,6 +285,9 @@ async function handleInteractiveConfigGeneration(options) {
138
285
  enableMCP: preferences.mcp,
139
286
  enableABAC: preferences.abac,
140
287
  enableRBAC: preferences.rbac,
288
+ enableOpenAPIGeneration: hasPrefillIntent
289
+ ? options.preferencesPrefill?.enableOpenAPIGeneration !== false
290
+ : true,
141
291
  debug: options.debug === true
142
292
  };
143
293
 
@@ -222,62 +372,43 @@ async function handleConfigurationReview(dataplaneUrl, authConfig, sessionId, sy
222
372
  * @returns {Promise<Object>} Collected state (source, credential, preferences) for wizard.yaml save
223
373
  */
224
374
  async function doWizardSteps(appKey, dataplaneUrl, authConfig, sessionId, flowOpts, state) {
225
- const { mode, systemIdOrKey, platforms, debug } = flowOpts;
226
- const { sourceType, sourceData } = await handleInteractiveSourceSelection(
227
- dataplaneUrl, sessionId, authConfig, platforms
375
+ const { platforms, prefill } = flowOpts;
376
+
377
+ const { sourceType, sourceData } = await resolveWizardSourcePhase(
378
+ dataplaneUrl,
379
+ sessionId,
380
+ authConfig,
381
+ platforms,
382
+ prefill
228
383
  );
229
- state.source = { type: sourceType };
230
- if (sourceType === 'openapi-file') state.source.filePath = sourceData;
231
- else if (sourceType === 'openapi-url') state.source.url = sourceData;
232
- else if (sourceType === 'mcp-server') state.source.serverUrl = JSON.parse(sourceData).serverUrl;
233
- else if (sourceType === 'known-platform') state.source.platform = sourceData;
384
+ applySourceSelectionToState(state, sourceType, sourceData);
234
385
 
235
386
  const openapiSpec = await handleOpenApiParsing(dataplaneUrl, authConfig, sourceType, sourceData);
236
- const credentialAction = await promptForCredentialAction();
237
- const configCredential = await resolveCredentialConfig(dataplaneUrl, authConfig, credentialAction);
238
- state.credential = configCredential;
239
- const credentialIdOrKey = await handleCredentialSelection(dataplaneUrl, authConfig, configCredential);
387
+ const credentialIdOrKey = await resolveWizardCredentialPhase(dataplaneUrl, authConfig, prefill, state);
240
388
 
241
389
  const detectedType = await handleTypeDetection(dataplaneUrl, authConfig, openapiSpec);
390
+ const prefillEntityName = prefill?.source?.entityName;
242
391
  const entityName = openapiSpec && sourceType !== 'known-platform'
243
- ? await handleEntitySelection(dataplaneUrl, authConfig, openapiSpec) : null;
244
- const genResult = await handleInteractiveConfigGeneration({
245
- dataplaneUrl,
246
- authConfig,
247
- mode,
248
- openapiSpec,
249
- detectedType,
250
- credentialIdOrKey,
251
- systemIdOrKey,
252
- sourceType,
253
- sourceData,
254
- entityName: entityName || undefined,
255
- appName: appKey,
256
- debug,
257
- systemDisplayName: flowOpts.systemDisplayName
258
- });
259
- const { systemConfig, datasourceConfigs, systemKey, preferences: savedPrefs } = genResult;
260
- state.preferences = savedPrefs || {};
261
-
262
- const finalConfigs = await handleConfigurationReview(
392
+ ? await handleEntitySelection(dataplaneUrl, authConfig, openapiSpec, prefillEntityName) : null;
393
+ if (entityName && state.source) {
394
+ state.source.entityName = entityName;
395
+ }
396
+ return completeWizardGenerateReviewSave({
397
+ appKey,
263
398
  dataplaneUrl,
264
399
  authConfig,
265
400
  sessionId,
266
- systemConfig,
267
- datasourceConfigs,
268
- { appKey, debug }
269
- );
270
- if (!finalConfigs) return null;
271
-
272
- await handleFileSaving(
273
- appKey,
274
- finalConfigs.systemConfig,
275
- finalConfigs.datasourceConfigs,
276
- systemKey || appKey,
277
- dataplaneUrl,
278
- authConfig
279
- );
280
- return state;
401
+ state,
402
+ flowOpts,
403
+ genInput: {
404
+ openapiSpec,
405
+ detectedType,
406
+ credentialIdOrKey,
407
+ sourceType,
408
+ sourceData,
409
+ entityName: entityName || undefined
410
+ }
411
+ });
281
412
  }
282
413
 
283
414
  async function runWizardStepsAfterSession(appKey, dataplaneUrl, authConfig, sessionId, flowOpts) {
@@ -317,7 +448,7 @@ async function runWizardStepsAfterSession(appKey, dataplaneUrl, authConfig, sess
317
448
  * @returns {Promise<void>} Resolves when wizard flow completes
318
449
  */
319
450
  async function executeWizardFlow(appKey, dataplaneUrl, authConfig, flowOpts = {}) {
320
- const { mode, systemIdOrKey, configPath, debug, systemDisplayName } = flowOpts;
451
+ const { mode, systemIdOrKey, configPath, debug, systemDisplayName, prefill } = flowOpts;
321
452
 
322
453
  if (debug) {
323
454
  logger.log(chalk.gray(`[DEBUG] Wizard debug mode enabled for app: ${appKey}`));
@@ -333,7 +464,8 @@ async function executeWizardFlow(appKey, dataplaneUrl, authConfig, flowOpts = {}
333
464
  platforms,
334
465
  configPath,
335
466
  debug: flowOpts.debug,
336
- systemDisplayName
467
+ systemDisplayName,
468
+ prefill
337
469
  });
338
470
  if (!state) return;
339
471
 
@@ -348,6 +480,11 @@ async function executeWizardFlow(appKey, dataplaneUrl, authConfig, flowOpts = {}
348
480
  * @param {string} [appName] - App name (for log message)
349
481
  * @returns {Promise<Object|null>} Loaded config or null
350
482
  */
483
+ /**
484
+ * Load wizard.yaml when present. Returns full valid config, or invalid-but-parsed config for interactive prefill.
485
+ *
486
+ * @returns {Promise<{ valid: true, config: Object } | { valid: false, config: Object, errors: string[] } | null>}
487
+ */
351
488
  async function loadWizardConfigIfExists(configPath, appName) {
352
489
  if (!configPath) return null;
353
490
  const displayPath = appName ? `integration/${appName}/wizard.yaml` : configPath;
@@ -360,10 +497,16 @@ async function loadWizardConfigIfExists(configPath, appName) {
360
497
  const result = await validateWizardConfig(configPath, { validateFilePaths: false });
361
498
  if (result.valid && result.config) {
362
499
  logger.log(chalk.green(`Loaded saved state from ${displayPath}. Resuming with saved choices.`));
363
- return result.config;
500
+ return { valid: true, config: result.config };
364
501
  }
365
502
  if (result.errors?.length) {
366
- logger.log(chalk.yellow(`Loaded ${displayPath} but it has errors; prompting for missing fields.`));
503
+ logger.log(chalk.yellow(
504
+ `Loaded ${displayPath} but it does not fully validate; prefilling from file where possible.`
505
+ ));
506
+ result.errors.forEach(err => logger.log(chalk.gray(` • ${err}`)));
507
+ }
508
+ if (result.config) {
509
+ return { valid: false, config: result.config, errors: result.errors || [] };
367
510
  }
368
511
  } catch (e) {
369
512
  logger.log(chalk.gray(`Could not load wizard config from ${displayPath}: ${e.message}`));
@@ -470,15 +613,16 @@ async function handleWizardWithSavedConfig(options, loadedConfig, displayPath) {
470
613
  }
471
614
 
472
615
  async function handleWizardInteractive(options) {
616
+ const prefill = options.wizardPrefill;
473
617
  const allowAddDatasource = !options.app;
474
618
  const mode = allowAddDatasource ? await promptForMode(undefined, true) : 'create-system';
475
619
  const resolved = mode === 'create-system'
476
- ? await resolveCreateNewPath(options, null)
477
- : await resolveAddDatasourcePath(options, null);
620
+ ? await resolveCreateNewPath(options, prefill || null)
621
+ : await resolveAddDatasourcePath(options, prefill || null);
478
622
  if (!resolved) return;
479
623
  const { appKey, configPath, dataplaneUrl, authConfig } = resolved;
480
624
  const systemIdOrKey = mode === 'add-datasource' ? resolved.systemIdOrKey : undefined;
481
- const systemDisplayName = options.systemDisplayName || options.displayName ||
625
+ const systemDisplayName = options.systemDisplayName || options.displayName || prefill?.systemDisplayName ||
482
626
  (mode === 'create-system' ? humanizeAppKey(appKey) : undefined);
483
627
  try {
484
628
  await executeWizardFlow(appKey, dataplaneUrl, authConfig, {
@@ -486,7 +630,8 @@ async function handleWizardInteractive(options) {
486
630
  systemIdOrKey,
487
631
  configPath,
488
632
  debug: options.debug,
489
- systemDisplayName
633
+ systemDisplayName,
634
+ prefill: prefill || undefined
490
635
  });
491
636
  logger.log(chalk.gray(`To change settings, edit integration/${appKey}/wizard.yaml and run: aifabrix wizard ${appKey}`));
492
637
  } catch (error) {
@@ -504,9 +649,12 @@ async function handleWizard(options = {}) {
504
649
  return await handleWizardSilent(options);
505
650
  }
506
651
  logger.log(chalk.blue('\n\uD83E\uDDD9 AI Fabrix External System Wizard\n'));
507
- const loadedConfig = await loadWizardConfigIfExists(options.configPath, options.app);
508
- if (loadedConfig) {
509
- return await handleWizardWithSavedConfig(options, loadedConfig, displayPath);
652
+ const loadResult = await loadWizardConfigIfExists(options.configPath, options.app);
653
+ if (loadResult?.valid && loadResult.config) {
654
+ return await handleWizardWithSavedConfig(options, loadResult.config, displayPath);
655
+ }
656
+ if (loadResult?.config && loadResult.valid === false) {
657
+ return await handleWizardInteractive({ ...options, wizardPrefill: loadResult.config });
510
658
  }
511
659
  return await handleWizardInteractive(options);
512
660
  }