@aifabrix/builder 2.33.5 → 2.36.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.
package/lib/cli.js CHANGED
@@ -155,10 +155,10 @@ function setupInfraCommands(program) {
155
155
  });
156
156
 
157
157
  program.command('up-miso')
158
- .description('Install keycloak and miso-controller from images (no build). Infra must be up. Uses auto-generated secrets for testing.')
159
- .option('-r, --registry <url>', 'Override registry for both apps (e.g. myacr.azurecr.io)')
158
+ .description('Install keycloak, miso-controller, and dataplane from images (no build). Infra must be up. Uses auto-generated secrets for testing.')
159
+ .option('-r, --registry <url>', 'Override registry for all apps (e.g. myacr.azurecr.io)')
160
160
  .option('--registry-mode <mode>', 'Override registry mode (acr|external)')
161
- .option('-i, --image <key>=<value>', 'Override image (e.g. keycloak=myreg/keycloak:v1, miso-controller=myreg/m:v1); can be repeated', (v, prev) => (prev || []).concat([v]))
161
+ .option('-i, --image <key>=<value>', 'Override image (e.g. keycloak=myreg/k:v1, miso-controller=myreg/m:v1, dataplane=myreg/d:v1); can be repeated', (v, prev) => (prev || []).concat([v]))
162
162
  .action(async(options) => {
163
163
  try {
164
164
  await handleUpMiso(options);
@@ -409,14 +409,29 @@ function setupAppCommands(program) {
409
409
  }
410
410
  });
411
411
 
412
- program.command('wizard')
413
- .description('Interactive wizard for creating external systems (or headless with --config)')
414
- .option('-a, --app <app>', 'Application name (if not provided, will prompt)')
415
- .option('--config <file>', 'Path to wizard.yaml config file for headless mode')
416
- .action(async(options) => {
412
+ program.command('wizard [appName]')
413
+ .description('Create or extend external systems (OpenAPI, MCP, or known platforms like HubSpot) via guided steps or a config file')
414
+ .option('-a, --app <app>', 'Application name (synonym for positional appName)')
415
+ .option('--config <file>', 'Run headless using a wizard.yaml file (appName, mode, source, credential, preferences)')
416
+ .option('--silent', 'Run with saved integration/<app>/wizard.yaml only; no prompts (requires app name and existing wizard.yaml)')
417
+ .addHelpText('after', `
418
+ Examples:
419
+ $ aifabrix wizard Run interactively (mode first, then prompts)
420
+ $ aifabrix wizard my-integration Load wizard.yaml if present → show summary → "Run with saved config?" or start from step 1
421
+ $ aifabrix wizard my-integration --silent Run headless with integration/my-integration/wizard.yaml (no prompts)
422
+ $ aifabrix wizard -a my-integration Same as above (app name set)
423
+ $ aifabrix wizard --config wizard.yaml Run headless from a wizard config file
424
+
425
+ Config path: When appName is provided, integration/<appName>/wizard.yaml is used for load/save and error.log.
426
+ To change settings after a run, edit that file and run "aifabrix wizard <app>" again.
427
+ Headless config must include: appName, mode (create-system|add-datasource), source (type + filePath/url/platform).
428
+ See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`)
429
+ .action(async(positionalAppName, options) => {
417
430
  try {
431
+ const appName = positionalAppName || options.app;
432
+ const configPath = appName ? path.join(process.cwd(), 'integration', appName, 'wizard.yaml') : null;
418
433
  const { handleWizard } = require('./commands/wizard');
419
- await handleWizard(options);
434
+ await handleWizard({ ...options, app: appName, config: options.config, configPath });
420
435
  } catch (error) {
421
436
  handleCommandError(error, 'wizard');
422
437
  process.exit(1);
@@ -685,6 +700,20 @@ function setupUtilityCommands(program) {
685
700
  }
686
701
  });
687
702
 
703
+ program.command('show <appKey>')
704
+ .description('Show application info from local builder/ or integration/ (offline) or from controller (--online)')
705
+ .option('--online', 'Fetch application data from the controller')
706
+ .option('--json', 'Output as JSON')
707
+ .action(async(appKey, options) => {
708
+ try {
709
+ const { showApp } = require('./app/show');
710
+ await showApp(appKey, { online: options.online, json: options.json });
711
+ } catch (error) {
712
+ logger.error(chalk.red(`Error: ${error.message}`));
713
+ process.exit(1);
714
+ }
715
+ });
716
+
688
717
  program.command('validate <appOrFile>')
689
718
  .description('Validate application or external integration file')
690
719
  .option('--type <type>', 'Application type (external) - if set, only checks integration folder')
@@ -15,6 +15,9 @@ const { getConfig } = config;
15
15
  const { getOrRefreshDeviceToken } = require('../utils/token-manager');
16
16
  const { getAuthUser } = require('../api/auth.api');
17
17
  const { resolveControllerUrl } = require('../utils/controller-url');
18
+ const { findDataplaneServiceAppKey } = require('./wizard-dataplane');
19
+ const { getDataplaneUrl } = require('../datasource/deploy');
20
+ const { checkDataplaneHealth } = require('../utils/dataplane-health');
18
21
 
19
22
  /**
20
23
  * Format expiration date for display
@@ -212,13 +215,53 @@ function displayTokenInfo(tokenInfo) {
212
215
  displayUserInfo(tokenInfo.user);
213
216
  }
214
217
 
218
+ /**
219
+ * Resolve dataplane URL from controller without progress logs (for auth status display)
220
+ * @async
221
+ * @param {string} controllerUrl - Controller URL
222
+ * @param {string} environment - Environment key
223
+ * @param {Object} authConfig - Authentication configuration
224
+ * @returns {Promise<string|null>} Dataplane URL or null if not discoverable
225
+ */
226
+ async function resolveDataplaneUrlSilent(controllerUrl, environment, authConfig) {
227
+ try {
228
+ const dataplaneAppKey = await findDataplaneServiceAppKey(controllerUrl, environment, authConfig);
229
+ if (dataplaneAppKey) {
230
+ return await getDataplaneUrl(controllerUrl, dataplaneAppKey, environment, authConfig);
231
+ }
232
+ return await getDataplaneUrl(controllerUrl, 'dataplane', environment, authConfig);
233
+ } catch {
234
+ return null;
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Display dataplane section when authenticated
240
+ * @param {string|null} dataplaneUrl - Dataplane URL or null
241
+ * @param {boolean} dataplaneConnected - Whether dataplane health check passed
242
+ */
243
+ function displayDataplaneSection(dataplaneUrl, dataplaneConnected) {
244
+ if (dataplaneUrl) {
245
+ logger.log(`Dataplane: ${chalk.cyan(dataplaneUrl)}`);
246
+ const statusIcon = dataplaneConnected ? chalk.green('✓') : chalk.red('✗');
247
+ const statusText = dataplaneConnected ? 'Connected' : 'Not reachable';
248
+ logger.log(`Status: ${statusIcon} ${statusText}`);
249
+ } else {
250
+ logger.log(`Dataplane: ${chalk.gray('—')}`);
251
+ logger.log(`Status: ${chalk.gray('Not discovered')}`);
252
+ }
253
+ }
254
+
215
255
  /**
216
256
  * Display authentication status
217
257
  * @param {string} controllerUrl - Controller URL
218
258
  * @param {string} environment - Environment key
219
259
  * @param {Object|null} tokenInfo - Token information
260
+ * @param {Object} [dataplaneInfo] - Optional dataplane URL and health
261
+ * @param {string|null} [dataplaneInfo.url] - Dataplane URL
262
+ * @param {boolean} [dataplaneInfo.connected] - Whether dataplane is reachable
220
263
  */
221
- function displayStatus(controllerUrl, environment, tokenInfo) {
264
+ function displayStatus(controllerUrl, environment, tokenInfo, dataplaneInfo) {
222
265
  logger.log(chalk.bold('\n🔐 Authentication Status\n'));
223
266
  logger.log(`Controller: ${chalk.cyan(controllerUrl)}`);
224
267
  logger.log(`Environment: ${chalk.cyan(environment || 'Not specified')}\n`);
@@ -231,6 +274,11 @@ function displayStatus(controllerUrl, environment, tokenInfo) {
231
274
  }
232
275
 
233
276
  displayTokenInfo(tokenInfo);
277
+
278
+ if (tokenInfo.authenticated && dataplaneInfo) {
279
+ displayDataplaneSection(dataplaneInfo.url, dataplaneInfo.connected);
280
+ }
281
+
234
282
  logger.log('');
235
283
  }
236
284
 
@@ -255,7 +303,15 @@ async function handleAuthStatus(_options) {
255
303
  tokenInfo = await checkClientToken(controllerUrl, environment);
256
304
  }
257
305
 
258
- displayStatus(controllerUrl, environment, tokenInfo);
306
+ let dataplaneInfo = null;
307
+ if (tokenInfo && tokenInfo.authenticated && tokenInfo.token) {
308
+ const authConfig = { type: 'bearer', token: tokenInfo.token };
309
+ const dataplaneUrl = await resolveDataplaneUrlSilent(controllerUrl, environment, authConfig);
310
+ const connected = dataplaneUrl ? await checkDataplaneHealth(dataplaneUrl, 5000) : false;
311
+ dataplaneInfo = { url: dataplaneUrl, connected };
312
+ }
313
+
314
+ displayStatus(controllerUrl, environment, tokenInfo, dataplaneInfo);
259
315
  }
260
316
 
261
317
  module.exports = { handleAuthStatus };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * AI Fabrix Builder - Up Miso Command
3
3
  *
4
- * Installs miso-controller and keycloak from images (no build).
4
+ * Installs keycloak, miso-controller, and dataplane from images (no build).
5
5
  * Assumes infra is up; sets dev secrets and resolves (no force; existing .env values preserved).
6
6
  *
7
7
  * @fileoverview up-miso command implementation
@@ -25,11 +25,13 @@ const { ensureAppFromTemplate } = require('./up-common');
25
25
  const KEYCLOAK_BASE_PORT = 8082;
26
26
  /** Miso controller base port (dev-config app base) */
27
27
  const MISO_BASE_PORT = 3000;
28
+ /** Dataplane base port (from templates/applications/dataplane/variables.yaml) */
29
+ const _DATAPLANE_BASE_PORT = 3001;
28
30
 
29
31
  /**
30
- * Parse --image options array into map { keycloak?: string, 'miso-controller'?: string }
31
- * @param {string[]|string} imageOpts - Option value(s) e.g. ['keycloak=reg/k:v1', 'miso-controller=reg/m:v1']
32
- * @returns {{ keycloak?: string, 'miso-controller'?: string }}
32
+ * Parse --image options array into map { keycloak?: string, 'miso-controller'?: string, dataplane?: string }
33
+ * @param {string[]|string} imageOpts - Option value(s) e.g. ['keycloak=reg/k:v1', 'miso-controller=reg/m:v1', 'dataplane=reg/d:v1']
34
+ * @returns {{ keycloak?: string, 'miso-controller'?: string, dataplane?: string }}
33
35
  */
34
36
  function parseImageOptions(imageOpts) {
35
37
  const map = {};
@@ -48,7 +50,7 @@ function parseImageOptions(imageOpts) {
48
50
 
49
51
  /**
50
52
  * Build full image ref from registry and app variables (registry/name:tag)
51
- * @param {string} appName - keycloak or miso-controller
53
+ * @param {string} appName - keycloak, miso-controller, or dataplane
52
54
  * @param {string} registry - Registry URL
53
55
  * @returns {string} Full image reference
54
56
  */
@@ -65,7 +67,7 @@ function buildImageRefFromRegistry(appName, registry) {
65
67
  }
66
68
 
67
69
  /**
68
- * Set URL secrets and resolve keycloak + miso-controller (no force; existing .env preserved)
70
+ * Set URL secrets and resolve keycloak + miso-controller + dataplane (no force; existing .env preserved)
69
71
  * @async
70
72
  * @param {number} devIdNum - Developer ID number
71
73
  */
@@ -77,11 +79,12 @@ async function setMisoSecretsAndResolve(devIdNum) {
77
79
  logger.log(chalk.green('✓ Set keycloak and miso-controller URL secrets'));
78
80
  await secrets.generateEnvFile('keycloak', undefined, 'docker', false, true);
79
81
  await secrets.generateEnvFile('miso-controller', undefined, 'docker', false, true);
80
- logger.log(chalk.green(' Resolved keycloak and miso-controller'));
82
+ await secrets.generateEnvFile('dataplane', undefined, 'docker', false, true);
83
+ logger.log(chalk.green('✓ Resolved keycloak, miso-controller, and dataplane'));
81
84
  }
82
85
 
83
86
  /**
84
- * Build run options and run keycloak then miso-controller
87
+ * Build run options and run keycloak, miso-controller, then dataplane
85
88
  * @async
86
89
  * @param {Object} options - Commander options (image, registry, registryMode)
87
90
  */
@@ -89,23 +92,27 @@ async function runMisoApps(options) {
89
92
  const imageMap = parseImageOptions(options.image);
90
93
  const keycloakImage = imageMap.keycloak || (options.registry ? buildImageRefFromRegistry('keycloak', options.registry) : undefined);
91
94
  const misoImage = imageMap['miso-controller'] || (options.registry ? buildImageRefFromRegistry('miso-controller', options.registry) : undefined);
92
- const keycloakRunOpts = { image: keycloakImage, registry: options.registry, registryMode: options.registryMode, skipEnvOutputPath: true };
93
- const misoRunOpts = { image: misoImage, registry: options.registry, registryMode: options.registryMode, skipEnvOutputPath: true };
95
+ const dataplaneImage = imageMap.dataplane || (options.registry ? buildImageRefFromRegistry('dataplane', options.registry) : undefined);
96
+ const keycloakRunOpts = { image: keycloakImage, registry: options.registry, registryMode: options.registryMode, skipEnvOutputPath: true, skipInfraCheck: true };
97
+ const misoRunOpts = { image: misoImage, registry: options.registry, registryMode: options.registryMode, skipEnvOutputPath: true, skipInfraCheck: true };
98
+ const dataplaneRunOpts = { image: dataplaneImage, registry: options.registry, registryMode: options.registryMode, skipEnvOutputPath: true, skipInfraCheck: true };
94
99
  logger.log(chalk.blue('Starting keycloak...'));
95
100
  await app.runApp('keycloak', keycloakRunOpts);
96
101
  logger.log(chalk.blue('Starting miso-controller...'));
97
102
  await app.runApp('miso-controller', misoRunOpts);
103
+ logger.log(chalk.blue('Starting dataplane...'));
104
+ await app.runApp('dataplane', dataplaneRunOpts);
98
105
  }
99
106
 
100
107
  /**
101
- * Handle up-miso command: ensure infra, ensure app dirs, set secrets, resolve (preserve existing .env), run keycloak then miso-controller.
108
+ * Handle up-miso command: ensure infra, ensure app dirs, set secrets, resolve (preserve existing .env), run keycloak, miso-controller, then dataplane.
102
109
  *
103
110
  * @async
104
111
  * @function handleUpMiso
105
112
  * @param {Object} options - Commander options
106
- * @param {string} [options.registry] - Override registry for both apps
113
+ * @param {string} [options.registry] - Override registry for all apps
107
114
  * @param {string} [options.registryMode] - Override registry mode (acr|external)
108
- * @param {string[]|string} [options.image] - Override images e.g. keycloak=reg/k:v1 or miso-controller=reg/m:v1
115
+ * @param {string[]|string} [options.image] - Override images e.g. keycloak=reg/k:v1, miso-controller=reg/m:v1, dataplane=reg/d:v1
109
116
  * @returns {Promise<void>}
110
117
  * @throws {Error} If infra not up or any step fails
111
118
  */
@@ -114,8 +121,9 @@ async function handleUpMiso(options = {}) {
114
121
  if (builderDir) {
115
122
  process.env.AIFABRIX_BUILDER_DIR = builderDir;
116
123
  }
117
- logger.log(chalk.blue('Starting up-miso (keycloak + miso-controller from images)...\n'));
118
- const health = await infra.checkInfraHealth();
124
+ logger.log(chalk.blue('Starting up-miso (keycloak + miso-controller + dataplane from images)...\n'));
125
+ // Strict: only this developer's infra (same as status), so up-miso and status agree
126
+ const health = await infra.checkInfraHealth(undefined, { strict: true });
119
127
  const allHealthy = Object.values(health).every(status => status === 'healthy');
120
128
  if (!allHealthy) {
121
129
  throw new Error('Infrastructure is not up. Run \'aifabrix up\' first.');
@@ -123,11 +131,12 @@ async function handleUpMiso(options = {}) {
123
131
  logger.log(chalk.green('✓ Infrastructure is up'));
124
132
  await ensureAppFromTemplate('keycloak');
125
133
  await ensureAppFromTemplate('miso-controller');
134
+ await ensureAppFromTemplate('dataplane');
126
135
  const developerId = await config.getDeveloperId();
127
136
  const devIdNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
128
137
  await setMisoSecretsAndResolve(devIdNum);
129
138
  await runMisoApps(options);
130
- logger.log(chalk.green('\n✓ up-miso complete. Keycloak and miso-controller are running.'));
139
+ logger.log(chalk.green('\n✓ up-miso complete. Keycloak, miso-controller, and dataplane are running.'));
131
140
  logger.log(chalk.gray(' Run onboarding and register Keycloak from the miso-controller repo if needed.'));
132
141
  }
133
142
 
@@ -0,0 +1,278 @@
1
+ /**
2
+ * @fileoverview Wizard core helpers - OpenAPI/MCP parsing, credential loop, config build/error formatting
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ const chalk = require('chalk');
8
+ const ora = require('ora');
9
+ const logger = require('../utils/logger');
10
+ const { parseOpenApi, testMcpConnection, credentialSelection } = require('../api/wizard.api');
11
+
12
+ /**
13
+ * Parse OpenAPI file or URL
14
+ * @async
15
+ * @function parseOpenApiSource
16
+ * @param {string} dataplaneUrl - Dataplane URL
17
+ * @param {Object} authConfig - Authentication configuration
18
+ * @param {string} sourceType - Source type (openapi-file or openapi-url)
19
+ * @param {string} sourceData - Source data (file path or URL)
20
+ * @returns {Promise<Object|null>} OpenAPI spec or null
21
+ */
22
+ async function parseOpenApiSource(dataplaneUrl, authConfig, sourceType, sourceData) {
23
+ const isUrl = sourceType === 'openapi-url';
24
+ const spinner = ora(`Parsing OpenAPI ${isUrl ? 'URL' : 'file'}...`).start();
25
+ try {
26
+ const parseResponse = await parseOpenApi(dataplaneUrl, authConfig, sourceData, isUrl);
27
+ spinner.stop();
28
+ if (!parseResponse.success) {
29
+ throw new Error(`OpenAPI parsing failed: ${parseResponse.error || parseResponse.formattedError}`);
30
+ }
31
+ logger.log(chalk.green(`\u2713 OpenAPI ${isUrl ? 'URL' : 'file'} parsed successfully`));
32
+ return parseResponse.data?.spec;
33
+ } catch (error) {
34
+ spinner.stop();
35
+ throw error;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Test MCP server connection
41
+ * @async
42
+ * @function testMcpServerConnection
43
+ * @param {string} dataplaneUrl - Dataplane URL
44
+ * @param {Object} authConfig - Authentication configuration
45
+ * @param {string} sourceData - MCP server details JSON string
46
+ * @returns {Promise<null>} Always returns null
47
+ */
48
+ async function testMcpServerConnection(dataplaneUrl, authConfig, sourceData) {
49
+ const mcpDetails = JSON.parse(sourceData);
50
+ const spinner = ora('Testing MCP server connection...').start();
51
+ try {
52
+ const testResponse = await testMcpConnection(dataplaneUrl, authConfig, mcpDetails.serverUrl, mcpDetails.token);
53
+ spinner.stop();
54
+ if (!testResponse.success || !testResponse.data?.connected) {
55
+ throw new Error(`MCP connection failed: ${testResponse.data?.error || 'Unable to connect'}`);
56
+ }
57
+ logger.log(chalk.green('\u2713 MCP server connection successful'));
58
+ } catch (error) {
59
+ spinner.stop();
60
+ throw error;
61
+ }
62
+ return null;
63
+ }
64
+
65
+ /**
66
+ * Normalize credential config to selection data
67
+ * @param {Object} [configCredential] - Credential config from wizard.yaml or prompt
68
+ * @returns {Object} Selection data for API
69
+ */
70
+ function normalizeCredentialSelectionInput(configCredential) {
71
+ if (!configCredential) return { action: 'skip' };
72
+ return {
73
+ action: configCredential.action,
74
+ credentialConfig: configCredential.config,
75
+ credentialIdOrKey: configCredential.credentialIdOrKey
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Run a single credential selection API call
81
+ * @async
82
+ * @param {string} dataplaneUrl - Dataplane URL
83
+ * @param {Object} authConfig - Authentication configuration
84
+ * @param {Object} selectionData - Selection data
85
+ * @returns {Promise<{response: Object|null, error: string|null}>}
86
+ */
87
+ async function runCredentialAttempt(dataplaneUrl, authConfig, selectionData) {
88
+ const spinner = ora('Processing credential selection...').start();
89
+ try {
90
+ const response = await credentialSelection(dataplaneUrl, authConfig, selectionData);
91
+ spinner.stop();
92
+ return { response, error: null };
93
+ } catch (err) {
94
+ spinner.stop();
95
+ return { response: null, error: err.message || String(err) };
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Handle credential retry prompt or fail
101
+ * @async
102
+ * @param {string} errorMsg - Error message
103
+ * @param {boolean} allowRetry - Whether to allow retry (interactive)
104
+ * @param {Object} selectionData - Current selection data
105
+ * @returns {Promise<{done: boolean, value: null}|{done: boolean, selectionData: Object}>}
106
+ */
107
+ async function handleCredentialRetryOrFail(errorMsg, allowRetry, selectionData) {
108
+ const { promptForCredentialIdOrKeyRetry } = require('../generator/wizard-prompts');
109
+ if (selectionData.action === 'select' && allowRetry) {
110
+ const retryResult = await promptForCredentialIdOrKeyRetry(errorMsg);
111
+ if (retryResult.skip) {
112
+ logger.log(chalk.gray(' Skipping credential selection'));
113
+ return { done: true, value: null };
114
+ }
115
+ return { done: false, selectionData: { action: 'select', credentialIdOrKey: retryResult.credentialIdOrKey } };
116
+ }
117
+ logger.log(chalk.yellow(`Warning: Credential selection failed: ${errorMsg}`));
118
+ return { done: true, value: null };
119
+ }
120
+
121
+ /**
122
+ * Run credential selection loop until success or skip
123
+ * @async
124
+ * @param {string} dataplaneUrl - Dataplane URL
125
+ * @param {Object} authConfig - Authentication configuration
126
+ * @param {Object} selectionData - Initial selection data
127
+ * @param {boolean} allowRetry - Whether to re-prompt on failure
128
+ * @returns {Promise<string|null>} Credential ID/key or null
129
+ */
130
+ async function runCredentialSelectionLoop(dataplaneUrl, authConfig, selectionData, allowRetry) {
131
+ for (;;) {
132
+ const { response, error: attemptError } = await runCredentialAttempt(dataplaneUrl, authConfig, selectionData);
133
+ if (attemptError) {
134
+ const ret = await handleCredentialRetryOrFail(attemptError, allowRetry, selectionData);
135
+ if (ret.done) return ret.value;
136
+ selectionData = ret.selectionData;
137
+ continue;
138
+ }
139
+ if (response.success) {
140
+ const actionText = selectionData.action === 'create' ? 'created' : 'selected';
141
+ logger.log(chalk.green(`\u2713 Credential ${actionText}`));
142
+ return response.data?.credentialIdOrKey || null;
143
+ }
144
+ const errorMsg = response.error || response.formattedError || response.message || 'Unknown error';
145
+ const ret = await handleCredentialRetryOrFail(errorMsg, allowRetry, selectionData);
146
+ if (ret.done) return ret.value;
147
+ selectionData = ret.selectionData;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Build configuration preferences from configPrefs object
153
+ * @param {Object} [configPrefs] - Preferences from wizard.yaml
154
+ * @returns {Object} Configuration preferences object
155
+ */
156
+ function buildConfigPreferences(configPrefs) {
157
+ return {
158
+ intent: configPrefs?.intent || 'general integration',
159
+ fieldOnboardingLevel: configPrefs?.fieldOnboardingLevel || 'full',
160
+ enableOpenAPIGeneration: configPrefs?.enableOpenAPIGeneration !== false,
161
+ userPreferences: {
162
+ enableMCP: configPrefs?.enableMCP || false,
163
+ enableABAC: configPrefs?.enableABAC || false,
164
+ enableRBAC: configPrefs?.enableRBAC || false
165
+ }
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Build configuration payload for API call
171
+ * @param {Object} params - Parameters object
172
+ * @param {Object} params.openapiSpec - OpenAPI specification
173
+ * @param {Object} params.detectedType - Detected type info
174
+ * @param {string} params.mode - Selected mode
175
+ * @param {Object} params.prefs - Configuration preferences
176
+ * @param {string} [params.credentialIdOrKey] - Credential ID or key
177
+ * @param {string} [params.systemIdOrKey] - System ID or key
178
+ * @returns {Object} Configuration payload
179
+ */
180
+ function buildConfigPayload({ openapiSpec, detectedType, mode, prefs, credentialIdOrKey, systemIdOrKey }) {
181
+ const detectedTypeValue = detectedType?.recommendedType || detectedType?.apiType || detectedType?.selectedType || 'record-based';
182
+ const payload = {
183
+ openapiSpec,
184
+ detectedType: detectedTypeValue,
185
+ intent: prefs.intent,
186
+ mode,
187
+ fieldOnboardingLevel: prefs.fieldOnboardingLevel,
188
+ enableOpenAPIGeneration: prefs.enableOpenAPIGeneration,
189
+ userPreferences: prefs.userPreferences
190
+ };
191
+ if (credentialIdOrKey) payload.credentialIdOrKey = credentialIdOrKey;
192
+ if (systemIdOrKey) payload.systemIdOrKey = systemIdOrKey;
193
+ return payload;
194
+ }
195
+
196
+ /**
197
+ * Extract configuration from API response
198
+ * @param {Object} generateResponse - API response
199
+ * @returns {Object} Extracted configuration
200
+ */
201
+ function extractConfigurationFromResponse(generateResponse) {
202
+ const systemConfig = generateResponse.data?.systemConfig;
203
+ const datasourceConfigs = generateResponse.data?.datasourceConfigs ||
204
+ (generateResponse.data?.datasourceConfig ? [generateResponse.data.datasourceConfig] : []);
205
+ if (!systemConfig) throw new Error('System configuration not found');
206
+ return { systemConfig, datasourceConfigs, systemKey: generateResponse.data?.systemKey };
207
+ }
208
+
209
+ /**
210
+ * Format API errorData as plain text (no chalk) for logging and error.message
211
+ * @param {Object} [errorData] - API error response errorData
212
+ * @returns {string} Plain-text validation details
213
+ */
214
+ function formatValidationDetailsPlain(errorData) {
215
+ if (!errorData || typeof errorData !== 'object') {
216
+ return '';
217
+ }
218
+ const lines = [];
219
+ const main = errorData.detail || errorData.title || errorData.errorDescription || errorData.message || errorData.error;
220
+ if (main) {
221
+ lines.push(String(main));
222
+ }
223
+ if (Array.isArray(errorData.errors) && errorData.errors.length > 0) {
224
+ lines.push('Validation errors:');
225
+ errorData.errors.forEach(err => {
226
+ const field = err.field || err.path || (err.loc && Array.isArray(err.loc) ? err.loc.join('.') : 'validation');
227
+ const message = err.msg || err.message || 'Invalid value';
228
+ const value = err.value !== undefined ? ` (value: ${JSON.stringify(err.value)})` : '';
229
+ lines.push(` • ${field}: ${message}${value}`);
230
+ });
231
+ }
232
+ if (errorData.configuration && errorData.configuration.errors) {
233
+ const configErrs = errorData.configuration.errors;
234
+ lines.push('Configuration errors:');
235
+ if (Array.isArray(configErrs)) {
236
+ configErrs.forEach(err => {
237
+ const field = err.field || err.path || 'configuration';
238
+ const message = err.message || 'Invalid value';
239
+ lines.push(` • ${field}: ${message}`);
240
+ });
241
+ } else if (typeof configErrs === 'object') {
242
+ Object.keys(configErrs).forEach(key => {
243
+ lines.push(` • configuration.${key}: ${configErrs[key]}`);
244
+ });
245
+ }
246
+ }
247
+ return lines.join('\n');
248
+ }
249
+
250
+ /**
251
+ * Create and throw config generation error with optional formatted message
252
+ * @param {Object} generateResponse - API response (error)
253
+ * @throws {Error}
254
+ */
255
+ function throwConfigGenerationError(generateResponse) {
256
+ const summary = generateResponse.error || generateResponse.formattedError || 'Unknown error';
257
+ const detailsPlain = formatValidationDetailsPlain(generateResponse.errorData);
258
+ const message = detailsPlain
259
+ ? `Configuration generation failed: ${summary}\n${detailsPlain}`
260
+ : `Configuration generation failed: ${summary}`;
261
+ const err = new Error(message);
262
+ if (generateResponse.formattedError) {
263
+ err.formatted = generateResponse.formattedError;
264
+ }
265
+ throw err;
266
+ }
267
+
268
+ module.exports = {
269
+ parseOpenApiSource,
270
+ testMcpServerConnection,
271
+ normalizeCredentialSelectionInput,
272
+ runCredentialSelectionLoop,
273
+ buildConfigPreferences,
274
+ buildConfigPayload,
275
+ extractConfigurationFromResponse,
276
+ formatValidationDetailsPlain,
277
+ throwConfigGenerationError
278
+ };