@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.
- package/README.md +5 -0
- package/integration/test-hubspot/wizard.yaml +8 -0
- package/lib/api/wizard.api.js +24 -1
- package/lib/app/show-display.js +184 -0
- package/lib/app/show.js +642 -0
- package/lib/cli.js +28 -7
- package/lib/commands/wizard-core-helpers.js +278 -0
- package/lib/commands/wizard-core.js +26 -145
- package/lib/commands/wizard-headless.js +2 -2
- package/lib/commands/wizard-helpers.js +143 -0
- package/lib/commands/wizard.js +275 -68
- package/lib/generator/index.js +32 -0
- package/lib/generator/wizard-prompts.js +111 -44
- package/lib/utils/cli-utils.js +40 -1
- package/lib/validation/wizard-config-validator.js +35 -0
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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
|
-
* @
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|