@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/README.md +5 -0
- package/integration/test-hubspot/wizard.yaml +8 -0
- package/lib/api/wizard.api.js +24 -1
- package/lib/app/run-helpers.js +7 -2
- package/lib/app/run.js +2 -2
- package/lib/app/show-display.js +184 -0
- package/lib/app/show.js +642 -0
- package/lib/cli.js +38 -9
- package/lib/commands/auth-status.js +58 -2
- package/lib/commands/up-miso.js +25 -16
- package/lib/commands/wizard-core-helpers.js +278 -0
- package/lib/commands/wizard-core.js +74 -161
- package/lib/commands/wizard-headless.js +2 -2
- package/lib/commands/wizard-helpers.js +143 -0
- package/lib/commands/wizard.js +282 -69
- package/lib/datasource/list.js +6 -3
- package/lib/generator/index.js +32 -0
- package/lib/generator/wizard-prompts.js +111 -44
- package/lib/infrastructure/services.js +6 -3
- package/lib/utils/app-register-auth.js +9 -2
- package/lib/utils/cli-utils.js +40 -1
- package/lib/utils/error-formatters/http-status-errors.js +8 -0
- package/lib/utils/error-formatters/permission-errors.js +44 -1
- package/lib/utils/infra-containers.js +19 -16
- package/lib/utils/infra-status.js +12 -3
- package/lib/validation/wizard-config-validator.js +35 -0
- package/package.json +2 -2
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
|
|
159
|
-
.option('-r, --registry <url>', 'Override registry for
|
|
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/
|
|
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('
|
|
414
|
-
.option('-a, --app <app>', 'Application name (
|
|
415
|
-
.option('--config <file>', '
|
|
416
|
-
.
|
|
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
|
-
|
|
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 };
|
package/lib/commands/up-miso.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AI Fabrix Builder - Up Miso Command
|
|
3
3
|
*
|
|
4
|
-
* Installs miso-controller and
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
93
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
+
};
|