@aifabrix/builder 2.33.6 → 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.
@@ -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
+ };
@@ -3,7 +3,7 @@
3
3
  * @author AI Fabrix Team
4
4
  * @version 2.0.0
5
5
  */
6
- /* eslint-disable max-lines */
6
+
7
7
  const chalk = require('chalk');
8
8
  const ora = require('ora');
9
9
  const path = require('path');
@@ -15,15 +15,22 @@ const { normalizeWizardConfigs } = require('./wizard-config-normalizer');
15
15
  const {
16
16
  createWizardSession,
17
17
  updateWizardSession,
18
- parseOpenApi,
19
- credentialSelection,
20
18
  detectType,
21
19
  generateConfig,
22
20
  validateWizardConfig,
23
- getDeploymentDocs,
24
- testMcpConnection
21
+ getDeploymentDocs
25
22
  } = require('../api/wizard.api');
26
23
  const { generateWizardFiles } = require('../generator/wizard');
24
+ const {
25
+ parseOpenApiSource,
26
+ testMcpServerConnection,
27
+ normalizeCredentialSelectionInput,
28
+ runCredentialSelectionLoop,
29
+ buildConfigPreferences,
30
+ buildConfigPayload,
31
+ extractConfigurationFromResponse,
32
+ throwConfigGenerationError
33
+ } = require('./wizard-core-helpers');
27
34
 
28
35
  /**
29
36
  * Validate app name and check if directory exists
@@ -161,59 +168,6 @@ async function handleSourceSelection(dataplaneUrl, sessionId, authConfig, config
161
168
  return { sourceType, sourceData };
162
169
  }
163
170
 
164
- /**
165
- * Parse OpenAPI file or URL
166
- * @async
167
- * @function parseOpenApiSource
168
- * @param {string} dataplaneUrl - Dataplane URL
169
- * @param {Object} authConfig - Authentication configuration
170
- * @param {string} sourceType - Source type (openapi-file or openapi-url)
171
- * @param {string} sourceData - Source data (file path or URL)
172
- * @returns {Promise<Object|null>} OpenAPI spec or null
173
- */
174
- async function parseOpenApiSource(dataplaneUrl, authConfig, sourceType, sourceData) {
175
- const isUrl = sourceType === 'openapi-url';
176
- const spinner = ora(`Parsing OpenAPI ${isUrl ? 'URL' : 'file'}...`).start();
177
- try {
178
- const parseResponse = await parseOpenApi(dataplaneUrl, authConfig, sourceData, isUrl);
179
- spinner.stop();
180
- if (!parseResponse.success) {
181
- throw new Error(`OpenAPI parsing failed: ${parseResponse.error || parseResponse.formattedError}`);
182
- }
183
- logger.log(chalk.green(`\u2713 OpenAPI ${isUrl ? 'URL' : 'file'} parsed successfully`));
184
- return parseResponse.data?.spec;
185
- } catch (error) {
186
- spinner.stop();
187
- throw error;
188
- }
189
- }
190
-
191
- /**
192
- * Test MCP server connection
193
- * @async
194
- * @function testMcpServerConnection
195
- * @param {string} dataplaneUrl - Dataplane URL
196
- * @param {Object} authConfig - Authentication configuration
197
- * @param {string} sourceData - MCP server details JSON string
198
- * @returns {Promise<null>} Always returns null
199
- */
200
- async function testMcpServerConnection(dataplaneUrl, authConfig, sourceData) {
201
- const mcpDetails = JSON.parse(sourceData);
202
- const spinner = ora('Testing MCP server connection...').start();
203
- try {
204
- const testResponse = await testMcpConnection(dataplaneUrl, authConfig, mcpDetails.serverUrl, mcpDetails.token);
205
- spinner.stop();
206
- if (!testResponse.success || !testResponse.data?.connected) {
207
- throw new Error(`MCP connection failed: ${testResponse.data?.error || 'Unable to connect'}`);
208
- }
209
- logger.log(chalk.green('\u2713 MCP server connection successful'));
210
- } catch (error) {
211
- spinner.stop();
212
- throw error;
213
- }
214
- return null;
215
- }
216
-
217
171
  /**
218
172
  * Handle OpenAPI parsing step
219
173
  * @async
@@ -242,41 +196,28 @@ async function handleOpenApiParsing(dataplaneUrl, authConfig, sourceType, source
242
196
  }
243
197
 
244
198
  /**
245
- * Handle credential selection step
199
+ * Handle credential selection step.
200
+ * Validation is done by the dataplane (POST /api/v1/wizard/credential-selection).
201
+ * When action is 'select' and the API fails (e.g. credential not found), and allowRetry is true,
202
+ * we re-prompt for credential ID/key or allow the user to skip (empty = skip).
246
203
  * @async
247
204
  * @function handleCredentialSelection
248
205
  * @param {string} dataplaneUrl - Dataplane URL
249
206
  * @param {Object} authConfig - Authentication configuration
250
- * @param {Object} [configCredential] - Credential config from wizard.yaml
251
- * @returns {Promise<string|null>} Credential ID/key or null if skipped
207
+ * @param {Object} [configCredential] - Credential config from wizard.yaml or prompt
208
+ * @param {Object} [options] - Options
209
+ * @param {boolean} [options.allowRetry=true] - If true (interactive), re-prompt on failure for 'select'; if false (headless), do not re-prompt
210
+ * @returns {Promise<string|null>} Credential ID/key or null if skipped / failed
252
211
  */
253
- async function handleCredentialSelection(dataplaneUrl, authConfig, configCredential = null) {
212
+ async function handleCredentialSelection(dataplaneUrl, authConfig, configCredential = null, options = {}) {
213
+ const allowRetry = options.allowRetry !== false;
254
214
  logger.log(chalk.blue('\n\uD83D\uDCCB Step 3: Credential Selection (Optional)'));
255
- const selectionData = configCredential ? {
256
- action: configCredential.action,
257
- credentialConfig: configCredential.config,
258
- credentialIdOrKey: configCredential.credentialIdOrKey
259
- } : { action: 'skip' };
215
+ const selectionData = normalizeCredentialSelectionInput(configCredential);
260
216
  if (selectionData.action === 'skip') {
261
217
  logger.log(chalk.gray(' Skipping credential selection'));
262
218
  return null;
263
219
  }
264
- const spinner = ora('Processing credential selection...').start();
265
- try {
266
- const response = await credentialSelection(dataplaneUrl, authConfig, selectionData);
267
- spinner.stop();
268
- if (!response.success) {
269
- logger.log(chalk.yellow(`Warning: Credential selection failed: ${response.error}`));
270
- return null;
271
- }
272
- const actionText = selectionData.action === 'create' ? 'created' : 'selected';
273
- logger.log(chalk.green(`\u2713 Credential ${actionText}`));
274
- return response.data?.credentialIdOrKey || null;
275
- } catch (error) {
276
- spinner.stop();
277
- logger.log(chalk.yellow(`Warning: Credential selection failed: ${error.message}`));
278
- return null;
279
- }
220
+ return await runCredentialSelectionLoop(dataplaneUrl, authConfig, selectionData, allowRetry);
280
221
  }
281
222
 
282
223
  /**
@@ -308,67 +249,6 @@ async function handleTypeDetection(dataplaneUrl, authConfig, openapiSpec) {
308
249
  return null;
309
250
  }
310
251
 
311
- /**
312
- * Build configuration preferences from configPrefs object
313
- * @function buildConfigPreferences
314
- * @param {Object} [configPrefs] - Preferences from wizard.yaml
315
- * @returns {Object} Configuration preferences object
316
- */
317
- function buildConfigPreferences(configPrefs) {
318
- return {
319
- intent: configPrefs?.intent || 'general integration',
320
- fieldOnboardingLevel: configPrefs?.fieldOnboardingLevel || 'full',
321
- enableOpenAPIGeneration: configPrefs?.enableOpenAPIGeneration !== false,
322
- userPreferences: {
323
- enableMCP: configPrefs?.enableMCP || false,
324
- enableABAC: configPrefs?.enableABAC || false,
325
- enableRBAC: configPrefs?.enableRBAC || false
326
- }
327
- };
328
- }
329
-
330
- /**
331
- * Build configuration payload for API call
332
- * @function buildConfigPayload
333
- * @param {Object} params - Parameters object
334
- * @param {Object} params.openapiSpec - OpenAPI specification
335
- * @param {Object} params.detectedType - Detected type info
336
- * @param {string} params.mode - Selected mode
337
- * @param {Object} params.prefs - Configuration preferences
338
- * @param {string} [params.credentialIdOrKey] - Credential ID or key
339
- * @param {string} [params.systemIdOrKey] - System ID or key
340
- * @returns {Object} Configuration payload
341
- */
342
- function buildConfigPayload({ openapiSpec, detectedType, mode, prefs, credentialIdOrKey, systemIdOrKey }) {
343
- const detectedTypeValue = detectedType?.recommendedType || detectedType?.apiType || detectedType?.selectedType || 'record-based';
344
- const payload = {
345
- openapiSpec,
346
- detectedType: detectedTypeValue,
347
- intent: prefs.intent,
348
- mode,
349
- fieldOnboardingLevel: prefs.fieldOnboardingLevel,
350
- enableOpenAPIGeneration: prefs.enableOpenAPIGeneration,
351
- userPreferences: prefs.userPreferences
352
- };
353
- if (credentialIdOrKey) payload.credentialIdOrKey = credentialIdOrKey;
354
- if (systemIdOrKey) payload.systemIdOrKey = systemIdOrKey;
355
- return payload;
356
- }
357
-
358
- /**
359
- * Extract configuration from API response
360
- * @function extractConfigurationFromResponse
361
- * @param {Object} generateResponse - API response
362
- * @returns {Object} Extracted configuration
363
- */
364
- function extractConfigurationFromResponse(generateResponse) {
365
- const systemConfig = generateResponse.data?.systemConfig;
366
- const datasourceConfigs = generateResponse.data?.datasourceConfigs ||
367
- (generateResponse.data?.datasourceConfig ? [generateResponse.data.datasourceConfig] : []);
368
- if (!systemConfig) throw new Error('System configuration not found');
369
- return { systemConfig, datasourceConfigs, systemKey: generateResponse.data?.systemKey };
370
- }
371
-
372
252
  /**
373
253
  * Handle configuration generation step
374
254
  * @async
@@ -384,6 +264,7 @@ function extractConfigurationFromResponse(generateResponse) {
384
264
  * @param {string} [options.systemIdOrKey] - System ID or key (optional)
385
265
  * @returns {Promise<Object>} Generated configuration
386
266
  */
267
+
387
268
  async function handleConfigurationGeneration(dataplaneUrl, authConfig, options) {
388
269
  logger.log(chalk.blue('\n\uD83D\uDCCB Step 5: Generate Configuration'));
389
270
  const prefs = buildConfigPreferences(options.configPrefs);
@@ -400,7 +281,7 @@ async function handleConfigurationGeneration(dataplaneUrl, authConfig, options)
400
281
  const generateResponse = await generateConfig(dataplaneUrl, authConfig, configPayload);
401
282
  spinner.stop();
402
283
  if (!generateResponse.success) {
403
- throw new Error(`Configuration generation failed: ${generateResponse.error || generateResponse.formattedError}`);
284
+ throwConfigGenerationError(generateResponse);
404
285
  }
405
286
  const result = extractConfigurationFromResponse(generateResponse);
406
287
  const normalized = normalizeWizardConfigs(result.systemConfig, result.datasourceConfigs);
@@ -45,8 +45,8 @@ async function executeWizardFromConfig(wizardConfig, dataplaneUrl, authConfig) {
45
45
  // Parse OpenAPI
46
46
  const openapiSpec = await handleOpenApiParsing(dataplaneUrl, authConfig, sourceType, sourceData);
47
47
 
48
- // Step 3: Credential Selection
49
- const credentialIdOrKey = await handleCredentialSelection(dataplaneUrl, authConfig, credential);
48
+ // Step 3: Credential Selection (no retry prompt in headless)
49
+ const credentialIdOrKey = await handleCredentialSelection(dataplaneUrl, authConfig, credential, { allowRetry: false });
50
50
 
51
51
  // Step 4: Detect Type
52
52
  const detectedType = await handleTypeDetection(dataplaneUrl, authConfig, openapiSpec);
@@ -0,0 +1,143 @@
1
+ /**
2
+ * @fileoverview Wizard command helpers - pure and I/O helpers for wizard flow
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ const path = require('path');
8
+ const fs = require('fs').promises;
9
+ const chalk = require('chalk');
10
+ const logger = require('../utils/logger');
11
+
12
+ /**
13
+ * Build preferences object for wizard.yaml (schema shape: intent, fieldOnboardingLevel, enableOpenAPIGeneration, enableMCP, enableABAC, enableRBAC)
14
+ * @param {string} intent - User intent
15
+ * @param {Object} preferences - From promptForUserPreferences (mcp, abac, rbac)
16
+ * @returns {Object} Preferences for wizard-config schema
17
+ */
18
+ function buildPreferencesForSave(intent, preferences) {
19
+ return {
20
+ intent: intent || 'general integration',
21
+ fieldOnboardingLevel: 'full',
22
+ enableOpenAPIGeneration: true,
23
+ enableMCP: Boolean(preferences?.mcp),
24
+ enableABAC: Boolean(preferences?.abac),
25
+ enableRBAC: Boolean(preferences?.rbac)
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Build source object for wizard.yaml (no secrets)
31
+ * @param {Object} [source] - Source state
32
+ * @returns {Object|undefined}
33
+ */
34
+ function buildSourceForSave(source) {
35
+ if (!source) return undefined;
36
+ const out = { type: source.type };
37
+ if (source.type === 'openapi-file' && source.filePath) out.filePath = source.filePath;
38
+ if (source.type === 'openapi-url' && source.url) out.url = source.url;
39
+ if (source.type === 'mcp-server' && source.serverUrl) {
40
+ out.serverUrl = source.serverUrl;
41
+ out.token = source.token ? '(set)' : undefined;
42
+ }
43
+ if (source.type === 'known-platform' && source.platform) out.platform = source.platform;
44
+ return out;
45
+ }
46
+
47
+ /**
48
+ * Build partial wizard state for saving to wizard.yaml (no secrets)
49
+ * @param {Object} opts - Collected state
50
+ * @returns {Object} Serializable wizard config shape
51
+ */
52
+ function buildWizardStateForSave(opts) {
53
+ const state = {
54
+ appName: opts.appKey,
55
+ mode: opts.mode,
56
+ source: buildSourceForSave(opts.source)
57
+ };
58
+ if (opts.mode === 'add-datasource' && opts.systemIdOrKey) state.systemIdOrKey = opts.systemIdOrKey;
59
+ if (opts.credential) state.credential = opts.credential;
60
+ if (opts.preferences) state.preferences = opts.preferences;
61
+ return state;
62
+ }
63
+
64
+ /**
65
+ * Format source config as a short line for display
66
+ * @param {Object} [source] - Source config
67
+ * @returns {string|null}
68
+ */
69
+ function formatSourceLine(source) {
70
+ if (!source) return null;
71
+ const s = source;
72
+ return s.type + (s.filePath ? ` (${s.filePath})` : s.url ? ` (${s.url})` : s.platform ? ` (${s.platform})` : '');
73
+ }
74
+
75
+ /**
76
+ * Format preferences config as a short line for display
77
+ * @param {Object} [preferences] - Preferences config
78
+ * @returns {string|null}
79
+ */
80
+ function formatPreferencesLine(preferences) {
81
+ if (!preferences || (!preferences.intent && (preferences.enableMCP === undefined || preferences.enableMCP === null))) {
82
+ return null;
83
+ }
84
+ const p = preferences;
85
+ return [p.intent && `intent=${p.intent}`, p.enableMCP && 'MCP', p.enableABAC && 'ABAC', p.enableRBAC && 'RBAC'].filter(Boolean).join(', ') || '(defaults)';
86
+ }
87
+
88
+ /**
89
+ * Show a short summary of loaded wizard config (for resume flow)
90
+ * @param {Object} config - Loaded wizard config
91
+ * @param {string} displayPath - Path to show (e.g. integration/test/wizard.yaml)
92
+ */
93
+ function showWizardConfigSummary(config, displayPath) {
94
+ logger.log(chalk.blue('\nšŸ“‹ Saved config summary'));
95
+ logger.log(chalk.gray(` From: ${displayPath}`));
96
+ if (config.appName) logger.log(chalk.gray(` App: ${config.appName}`));
97
+ if (config.mode) logger.log(chalk.gray(` Mode: ${config.mode}`));
98
+ const srcLine = formatSourceLine(config.source);
99
+ if (srcLine) logger.log(chalk.gray(` Source: ${srcLine}`));
100
+ if (config.credential) logger.log(chalk.gray(` Credential: ${config.credential.action || 'skip'}`));
101
+ const prefs = formatPreferencesLine(config.preferences);
102
+ if (prefs) logger.log(chalk.gray(` Preferences: ${prefs}`));
103
+ logger.log('');
104
+ }
105
+
106
+ /**
107
+ * Ensure integration/<appKey> directory exists
108
+ * @param {string} appKey - Application key
109
+ * @returns {Promise<string>} Resolved config path (integration/<appKey>/wizard.yaml)
110
+ */
111
+ async function ensureIntegrationDir(appKey) {
112
+ const dir = path.join(process.cwd(), 'integration', appKey);
113
+ await fs.mkdir(dir, { recursive: true });
114
+ return path.join(dir, 'wizard.yaml');
115
+ }
116
+
117
+ /** External system types that support add-datasource (excludes webapp/application) */
118
+ const EXTERNAL_SYSTEM_TYPES = ['openapi', 'mcp', 'custom'];
119
+
120
+ /**
121
+ * Returns true if the system is an external system that supports add-datasource (not a webapp).
122
+ * @param {Object} sys - System object from getExternalSystem (may have type, systemType, or kind)
123
+ * @returns {boolean}
124
+ */
125
+ function isExternalSystemForAddDatasource(sys) {
126
+ const type = (sys?.type || sys?.systemType || sys?.kind || '').toLowerCase();
127
+ if (!type) return true;
128
+ if (EXTERNAL_SYSTEM_TYPES.includes(type)) return true;
129
+ if (['webapp', 'application', 'app'].includes(type)) return false;
130
+ return true;
131
+ }
132
+
133
+ module.exports = {
134
+ buildPreferencesForSave,
135
+ buildSourceForSave,
136
+ buildWizardStateForSave,
137
+ formatSourceLine,
138
+ formatPreferencesLine,
139
+ showWizardConfigSummary,
140
+ ensureIntegrationDir,
141
+ EXTERNAL_SYSTEM_TYPES,
142
+ isExternalSystemForAddDatasource
143
+ };