@aifabrix/builder 2.31.1 → 2.32.2
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 +9 -9
- package/integration/hubspot/README.md +2 -2
- package/integration/hubspot/hubspot-deploy-company.json +17 -14
- package/integration/hubspot/hubspot-deploy-contact.json +19 -16
- package/integration/hubspot/hubspot-deploy-deal.json +21 -18
- package/lib/api/types/datasources.types.js +31 -5
- package/lib/api/types/wizard.types.js +142 -0
- package/lib/api/wizard.api.js +177 -0
- package/lib/{app-config.js → app/config.js} +4 -4
- package/lib/{app-deploy.js → app/deploy.js} +8 -8
- package/lib/app/display.js +90 -0
- package/lib/{app-dockerfile.js → app/dockerfile.js} +4 -4
- package/lib/{app-down.js → app/down.js} +4 -4
- package/lib/app/helpers.js +218 -0
- package/lib/app/index.js +298 -0
- package/lib/{app-list.js → app/list.js} +6 -6
- package/lib/{app-push.js → app/push.js} +4 -4
- package/lib/{app-readme.js → app/readme.js} +34 -13
- package/lib/{app-register.js → app/register.js} +9 -9
- package/lib/{app-rotate-secret.js → app/rotate-secret.js} +10 -10
- package/lib/{app-run-helpers.js → app/run-helpers.js} +10 -10
- package/lib/{app-run.js → app/run.js} +6 -6
- package/lib/{build.js → build/index.js} +59 -32
- package/lib/build/package.json +7 -0
- package/lib/cli.js +245 -179
- package/lib/commands/app.js +3 -3
- package/lib/commands/datasource.js +4 -4
- package/lib/commands/login-credentials.js +209 -0
- package/lib/commands/login-device.js +254 -0
- package/lib/commands/login.js +67 -378
- package/lib/commands/logout.js +1 -1
- package/lib/commands/secrets-set.js +1 -1
- package/lib/commands/secure.js +2 -2
- package/lib/commands/wizard.js +498 -0
- package/lib/{audit-logger.js → core/audit-logger.js} +1 -1
- package/lib/{config.js → core/config.js} +28 -26
- package/lib/{diff.js → core/diff.js} +157 -72
- package/lib/{secrets.js → core/secrets.js} +86 -49
- package/lib/{templates.js → core/templates-env.js} +14 -222
- package/lib/core/templates.js +279 -0
- package/lib/{datasource-deploy.js → datasource/deploy.js} +6 -6
- package/lib/{datasource-diff.js → datasource/diff.js} +2 -2
- package/lib/datasource/list.js +223 -0
- package/lib/{datasource-validate.js → datasource/validate.js} +2 -2
- package/lib/{deployer.js → deployment/deployer.js} +48 -18
- package/lib/{environment-deploy.js → deployment/environment.js} +163 -84
- package/lib/{push.js → deployment/push.js} +1 -1
- package/lib/external-system/deploy-helpers.js +145 -0
- package/lib/{external-system-deploy.js → external-system/deploy.js} +156 -111
- package/lib/external-system/download-helpers.js +114 -0
- package/lib/{external-system-download.js → external-system/download.js} +92 -135
- package/lib/{external-system-generator.js → external-system/generator.js} +15 -11
- package/lib/external-system/test-auth.js +40 -0
- package/lib/external-system/test-execution.js +84 -0
- package/lib/external-system/test-helpers.js +109 -0
- package/lib/{external-system-test.js → external-system/test.js} +174 -192
- package/lib/{generator-builders.js → generator/builders.js} +87 -10
- package/lib/{generator-external.js → generator/external.js} +115 -52
- package/lib/{github-generator.js → generator/github.js} +116 -15
- package/lib/{generator-helpers.js → generator/helpers.js} +92 -42
- package/lib/{generator.js → generator/index.js} +49 -22
- package/lib/{generator-split.js → generator/split.js} +108 -55
- package/lib/generator/wizard-prompts.js +357 -0
- package/lib/generator/wizard.js +490 -0
- package/lib/{infra.js → infrastructure/index.js} +49 -22
- package/lib/schema/external-datasource.schema.json +158 -136
- package/lib/schema/external-system.schema.json +43 -1
- package/lib/utils/api.js +9 -5
- package/lib/utils/app-register-api.js +60 -32
- package/lib/utils/app-register-auth.js +172 -47
- package/lib/utils/app-register-config.js +130 -59
- package/lib/utils/app-run-containers.js +29 -8
- package/lib/utils/build-helpers.js +1 -1
- package/lib/utils/cli-utils.js +78 -30
- package/lib/utils/compose-generator.js +145 -65
- package/lib/utils/config-paths.js +2 -0
- package/lib/utils/deployment-errors.js +1 -1
- package/lib/utils/device-code.js +99 -41
- package/lib/utils/env-config-loader.js +1 -1
- package/lib/utils/env-copy.js +21 -18
- package/lib/utils/env-endpoints.js +115 -67
- package/lib/utils/env-map.js +13 -14
- package/lib/utils/env-ports.js +45 -25
- package/lib/utils/env-template.js +84 -42
- package/lib/utils/error-formatter.js +26 -9
- package/lib/utils/error-formatters/error-parser.js +90 -4
- package/lib/utils/error-formatters/http-status-errors.js +54 -17
- package/lib/utils/error-formatters/network-errors.js +103 -26
- package/lib/utils/external-system-display.js +184 -90
- package/lib/utils/external-system-validators.js +164 -42
- package/lib/utils/file-upload.js +109 -0
- package/lib/utils/health-check.js +199 -83
- package/lib/utils/infra-containers.js +1 -1
- package/lib/utils/infra-status.js +66 -15
- package/lib/utils/local-secrets.js +45 -25
- package/lib/utils/paths.js +45 -33
- package/lib/utils/schema-loader.js +42 -25
- package/lib/utils/schema-resolver.js +123 -74
- package/lib/utils/secrets-encryption.js +62 -25
- package/lib/utils/secrets-helpers.js +126 -63
- package/lib/utils/secrets-path.js +1 -1
- package/lib/utils/secrets-url.js +1 -1
- package/lib/utils/token-manager-refresh.js +181 -0
- package/lib/utils/token-manager.js +76 -123
- package/lib/utils/variable-transformer.js +154 -77
- package/lib/utils/yaml-preserve.js +41 -47
- package/lib/{template-validator.js → validation/template.js} +54 -23
- package/lib/{validate.js → validation/validate.js} +205 -125
- package/lib/{validator.js → validation/validator.js} +58 -39
- package/package.json +31 -2
- package/templates/external-system/deploy.ps1.hbs +34 -0
- package/templates/external-system/deploy.sh.hbs +34 -0
- package/templates/external-system/external-datasource.json.hbs +31 -12
- package/lib/app.js +0 -467
- package/lib/datasource-list.js +0 -141
- /package/lib/{app-prompts.js → app/prompts.js} +0 -0
- /package/lib/{env-reader.js → core/env-reader.js} +0 -0
- /package/lib/{key-generator.js → core/key-generator.js} +0 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Wizard command handler - interactive external system creation
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
* @version 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const ora = require('ora');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs').promises;
|
|
11
|
+
const logger = require('../utils/logger');
|
|
12
|
+
const config = require('../core/config');
|
|
13
|
+
const { getDeploymentAuth } = require('../utils/token-manager');
|
|
14
|
+
const { getDataplaneUrl } = require('../datasource/deploy');
|
|
15
|
+
const {
|
|
16
|
+
selectMode,
|
|
17
|
+
selectSource,
|
|
18
|
+
parseOpenApi,
|
|
19
|
+
detectType,
|
|
20
|
+
generateConfig,
|
|
21
|
+
validateWizardConfig,
|
|
22
|
+
getDeploymentDocs
|
|
23
|
+
} = require('../api/wizard.api');
|
|
24
|
+
const {
|
|
25
|
+
promptForMode,
|
|
26
|
+
promptForSourceType,
|
|
27
|
+
promptForOpenApiFile,
|
|
28
|
+
promptForOpenApiUrl,
|
|
29
|
+
promptForMcpServer,
|
|
30
|
+
promptForKnownPlatform,
|
|
31
|
+
promptForUserIntent,
|
|
32
|
+
promptForUserPreferences,
|
|
33
|
+
promptForConfigReview,
|
|
34
|
+
promptForAppName
|
|
35
|
+
} = require('../generator/wizard-prompts');
|
|
36
|
+
const { generateWizardFiles } = require('../generator/wizard');
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate app name and check if directory exists
|
|
40
|
+
* @async
|
|
41
|
+
* @function validateAndCheckAppDirectory
|
|
42
|
+
* @param {string} appName - Application name
|
|
43
|
+
* @returns {Promise<boolean>} True if should continue, false if cancelled
|
|
44
|
+
* @throws {Error} If validation fails
|
|
45
|
+
*/
|
|
46
|
+
async function validateAndCheckAppDirectory(appName) {
|
|
47
|
+
// Validate app name
|
|
48
|
+
if (!/^[a-z0-9-_]+$/.test(appName)) {
|
|
49
|
+
throw new Error('Application name must contain only lowercase letters, numbers, hyphens, and underscores');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check if app directory already exists
|
|
53
|
+
const appPath = path.join(process.cwd(), 'integration', appName);
|
|
54
|
+
try {
|
|
55
|
+
await fs.access(appPath);
|
|
56
|
+
const { overwrite } = await require('inquirer').prompt([
|
|
57
|
+
{
|
|
58
|
+
type: 'confirm',
|
|
59
|
+
name: 'overwrite',
|
|
60
|
+
message: `Directory ${appPath} already exists. Overwrite?`,
|
|
61
|
+
default: false
|
|
62
|
+
}
|
|
63
|
+
]);
|
|
64
|
+
if (!overwrite) {
|
|
65
|
+
logger.log(chalk.yellow('Wizard cancelled.'));
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
// Directory doesn't exist, continue
|
|
70
|
+
if (error.code !== 'ENOENT') {
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Handle mode selection step
|
|
79
|
+
* @async
|
|
80
|
+
* @function handleModeSelection
|
|
81
|
+
* @param {string} dataplaneUrl - Dataplane URL
|
|
82
|
+
* @param {Object} authConfig - Authentication configuration
|
|
83
|
+
* @returns {Promise<string>} Selected mode
|
|
84
|
+
* @throws {Error} If mode selection fails
|
|
85
|
+
*/
|
|
86
|
+
async function handleModeSelection(dataplaneUrl, authConfig) {
|
|
87
|
+
logger.log(chalk.blue('\n📋 Step 1: Mode Selection'));
|
|
88
|
+
const mode = await promptForMode();
|
|
89
|
+
const modeResponse = await selectMode(dataplaneUrl, authConfig, mode);
|
|
90
|
+
if (!modeResponse.success) {
|
|
91
|
+
throw new Error(`Mode selection failed: ${modeResponse.error || modeResponse.formattedError}`);
|
|
92
|
+
}
|
|
93
|
+
return mode;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Handle source selection step
|
|
98
|
+
* @async
|
|
99
|
+
* @function handleSourceSelection
|
|
100
|
+
* @param {string} dataplaneUrl - Dataplane URL
|
|
101
|
+
* @param {Object} authConfig - Authentication configuration
|
|
102
|
+
* @returns {Promise<Object>} Object with sourceType and sourceData
|
|
103
|
+
* @throws {Error} If source selection fails
|
|
104
|
+
*/
|
|
105
|
+
async function handleSourceSelection(dataplaneUrl, authConfig) {
|
|
106
|
+
logger.log(chalk.blue('\n📋 Step 2: Source Selection'));
|
|
107
|
+
const sourceType = await promptForSourceType();
|
|
108
|
+
let sourceData = null;
|
|
109
|
+
|
|
110
|
+
if (sourceType === 'openapi-file') {
|
|
111
|
+
const filePath = await promptForOpenApiFile();
|
|
112
|
+
sourceData = filePath;
|
|
113
|
+
} else if (sourceType === 'openapi-url') {
|
|
114
|
+
const url = await promptForOpenApiUrl();
|
|
115
|
+
sourceData = url;
|
|
116
|
+
} else if (sourceType === 'mcp-server') {
|
|
117
|
+
const mcpDetails = await promptForMcpServer();
|
|
118
|
+
sourceData = JSON.stringify(mcpDetails);
|
|
119
|
+
} else if (sourceType === 'known-platform') {
|
|
120
|
+
const platform = await promptForKnownPlatform();
|
|
121
|
+
sourceData = platform;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const sourceResponse = await selectSource(dataplaneUrl, authConfig, sourceType, sourceData);
|
|
125
|
+
if (!sourceResponse.success) {
|
|
126
|
+
throw new Error(`Source selection failed: ${sourceResponse.error || sourceResponse.formattedError}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { sourceType, sourceData };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Parse OpenAPI file
|
|
134
|
+
* @async
|
|
135
|
+
* @function parseOpenApiFile
|
|
136
|
+
* @param {string} dataplaneUrl - Dataplane URL
|
|
137
|
+
* @param {Object} authConfig - Authentication configuration
|
|
138
|
+
* @param {string} sourceData - Source data (file path)
|
|
139
|
+
* @returns {Promise<Object>} OpenAPI spec
|
|
140
|
+
* @throws {Error} If parsing fails
|
|
141
|
+
*/
|
|
142
|
+
async function parseOpenApiFile(dataplaneUrl, authConfig, sourceData) {
|
|
143
|
+
logger.log(chalk.blue('\n📋 Step 3: Parsing OpenAPI File'));
|
|
144
|
+
const spinner = ora('Parsing OpenAPI file...').start();
|
|
145
|
+
try {
|
|
146
|
+
const parseResponse = await parseOpenApi(dataplaneUrl, authConfig, sourceData);
|
|
147
|
+
spinner.stop();
|
|
148
|
+
if (!parseResponse.success) {
|
|
149
|
+
throw new Error(`OpenAPI parsing failed: ${parseResponse.error || parseResponse.formattedError}`);
|
|
150
|
+
}
|
|
151
|
+
logger.log(chalk.green('✓ OpenAPI file parsed successfully'));
|
|
152
|
+
return parseResponse.data?.spec;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
spinner.stop();
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Handle OpenAPI parsing step
|
|
161
|
+
* @async
|
|
162
|
+
* @function handleOpenApiParsing
|
|
163
|
+
* @param {string} dataplaneUrl - Dataplane URL
|
|
164
|
+
* @param {Object} authConfig - Authentication configuration
|
|
165
|
+
* @param {string} sourceType - Source type
|
|
166
|
+
* @param {string} sourceData - Source data
|
|
167
|
+
* @returns {Promise<Object|null>} OpenAPI spec or null
|
|
168
|
+
* @throws {Error} If parsing fails
|
|
169
|
+
*/
|
|
170
|
+
async function handleOpenApiParsing(dataplaneUrl, authConfig, sourceType, sourceData) {
|
|
171
|
+
if (sourceType === 'openapi-file') {
|
|
172
|
+
return await parseOpenApiFile(dataplaneUrl, authConfig, sourceData);
|
|
173
|
+
}
|
|
174
|
+
if (sourceType === 'openapi-url') {
|
|
175
|
+
logger.log(chalk.blue('\n📋 Step 3: Parsing OpenAPI URL'));
|
|
176
|
+
logger.log(chalk.green('✓ OpenAPI URL processed'));
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Handle type detection step
|
|
184
|
+
* @async
|
|
185
|
+
* @function handleTypeDetection
|
|
186
|
+
* @param {string} dataplaneUrl - Dataplane URL
|
|
187
|
+
* @param {Object} authConfig - Authentication configuration
|
|
188
|
+
* @param {Object} openApiSpec - OpenAPI specification
|
|
189
|
+
* @returns {Promise<Object|null>} Detected type or null
|
|
190
|
+
*/
|
|
191
|
+
async function handleTypeDetection(dataplaneUrl, authConfig, openApiSpec) {
|
|
192
|
+
if (!openApiSpec) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
logger.log(chalk.blue('\n📋 Step 4: Detecting API Type'));
|
|
197
|
+
const spinner = ora('Detecting API type...').start();
|
|
198
|
+
try {
|
|
199
|
+
const detectResponse = await detectType(dataplaneUrl, authConfig, openApiSpec);
|
|
200
|
+
spinner.stop();
|
|
201
|
+
if (detectResponse.success && detectResponse.data) {
|
|
202
|
+
const detectedType = detectResponse.data;
|
|
203
|
+
logger.log(chalk.green(`✓ API type detected: ${detectedType.apiType || 'unknown'}`));
|
|
204
|
+
if (detectedType.category) {
|
|
205
|
+
logger.log(chalk.gray(` Category: ${detectedType.category}`));
|
|
206
|
+
}
|
|
207
|
+
return detectedType;
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
spinner.stop();
|
|
211
|
+
logger.log(chalk.yellow(`⚠ Type detection failed: ${error.message}`));
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Handle configuration generation step
|
|
218
|
+
* @async
|
|
219
|
+
* @function handleConfigurationGeneration
|
|
220
|
+
* @param {string} dataplaneUrl - Dataplane URL
|
|
221
|
+
* @param {Object} authConfig - Authentication configuration
|
|
222
|
+
* @param {string} mode - Selected mode
|
|
223
|
+
* @param {string} sourceType - Source type
|
|
224
|
+
* @param {Object} openApiSpec - OpenAPI specification
|
|
225
|
+
* @returns {Promise<Object>} Generated configuration with systemConfig, datasourceConfigs, and systemKey
|
|
226
|
+
* @throws {Error} If generation fails
|
|
227
|
+
*/
|
|
228
|
+
async function handleConfigurationGeneration(dataplaneUrl, authConfig, mode, sourceType, openApiSpec) {
|
|
229
|
+
logger.log(chalk.blue('\n📋 Step 5: User Preferences'));
|
|
230
|
+
const userIntent = await promptForUserIntent();
|
|
231
|
+
const preferences = await promptForUserPreferences();
|
|
232
|
+
|
|
233
|
+
logger.log(chalk.blue('\n📋 Step 6: Generating Configuration'));
|
|
234
|
+
const spinner = ora('Generating configuration via AI (this may take 10-30 seconds)...').start();
|
|
235
|
+
try {
|
|
236
|
+
const generateResponse = await generateConfig(dataplaneUrl, authConfig, {
|
|
237
|
+
mode,
|
|
238
|
+
sourceType,
|
|
239
|
+
openApiSpec,
|
|
240
|
+
userIntent,
|
|
241
|
+
preferences
|
|
242
|
+
});
|
|
243
|
+
spinner.stop();
|
|
244
|
+
if (!generateResponse.success) {
|
|
245
|
+
throw new Error(`Configuration generation failed: ${generateResponse.error || generateResponse.formattedError}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const systemConfig = generateResponse.data?.systemConfig;
|
|
249
|
+
const datasourceConfigs = generateResponse.data?.datasourceConfigs || [];
|
|
250
|
+
const systemKey = generateResponse.data?.systemKey;
|
|
251
|
+
|
|
252
|
+
if (!systemConfig) {
|
|
253
|
+
throw new Error('System configuration not found in generation response');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
logger.log(chalk.green('✓ Configuration generated successfully'));
|
|
257
|
+
return { systemConfig, datasourceConfigs, systemKey };
|
|
258
|
+
} catch (error) {
|
|
259
|
+
spinner.stop();
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Validate wizard configuration
|
|
266
|
+
* @async
|
|
267
|
+
* @function validateWizardConfiguration
|
|
268
|
+
* @param {string} dataplaneUrl - Dataplane URL
|
|
269
|
+
* @param {Object} authConfig - Authentication configuration
|
|
270
|
+
* @param {Object} systemConfig - System configuration
|
|
271
|
+
* @param {Object[]} datasourceConfigs - Datasource configurations
|
|
272
|
+
* @throws {Error} If validation fails
|
|
273
|
+
*/
|
|
274
|
+
async function validateWizardConfiguration(dataplaneUrl, authConfig, systemConfig, datasourceConfigs) {
|
|
275
|
+
const validateSpinner = ora('Validating configuration...').start();
|
|
276
|
+
try {
|
|
277
|
+
const validateResponse = await validateWizardConfig(dataplaneUrl, authConfig, systemConfig, datasourceConfigs);
|
|
278
|
+
validateSpinner.stop();
|
|
279
|
+
if (!validateResponse.success || !validateResponse.data?.valid) {
|
|
280
|
+
const errors = validateResponse.data?.errors || [];
|
|
281
|
+
const errorMsg = errors.length > 0
|
|
282
|
+
? errors.map(e => e.message || e).join(', ')
|
|
283
|
+
: validateResponse.error || validateResponse.formattedError || 'Validation failed';
|
|
284
|
+
throw new Error(`Configuration validation failed: ${errorMsg}`);
|
|
285
|
+
}
|
|
286
|
+
logger.log(chalk.green('✓ Configuration validated successfully'));
|
|
287
|
+
if (validateResponse.data?.warnings && validateResponse.data.warnings.length > 0) {
|
|
288
|
+
logger.log(chalk.yellow('\n⚠ Warnings:'));
|
|
289
|
+
validateResponse.data.warnings.forEach(warning => {
|
|
290
|
+
logger.log(chalk.yellow(` - ${warning.message || warning}`));
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
} catch (error) {
|
|
294
|
+
validateSpinner.stop();
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Handle configuration review and validation step
|
|
301
|
+
* @async
|
|
302
|
+
* @function handleConfigurationReview
|
|
303
|
+
* @param {string} dataplaneUrl - Dataplane URL
|
|
304
|
+
* @param {Object} authConfig - Authentication configuration
|
|
305
|
+
* @param {Object} systemConfig - System configuration
|
|
306
|
+
* @param {Object[]} datasourceConfigs - Datasource configurations
|
|
307
|
+
* @returns {Promise<Object>} Final configurations with systemConfig and datasourceConfigs
|
|
308
|
+
* @throws {Error} If validation fails
|
|
309
|
+
*/
|
|
310
|
+
async function handleConfigurationReview(dataplaneUrl, authConfig, systemConfig, datasourceConfigs) {
|
|
311
|
+
logger.log(chalk.blue('\n📋 Step 7: Review & Validate'));
|
|
312
|
+
const reviewResult = await promptForConfigReview(systemConfig, datasourceConfigs);
|
|
313
|
+
|
|
314
|
+
if (reviewResult.action === 'cancel') {
|
|
315
|
+
logger.log(chalk.yellow('Wizard cancelled.'));
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Use edited configs if user edited them
|
|
320
|
+
const finalSystemConfig = reviewResult.systemConfig || systemConfig;
|
|
321
|
+
const finalDatasourceConfigs = reviewResult.datasourceConfigs || datasourceConfigs;
|
|
322
|
+
|
|
323
|
+
// Validate configuration
|
|
324
|
+
await validateWizardConfiguration(dataplaneUrl, authConfig, finalSystemConfig, finalDatasourceConfigs);
|
|
325
|
+
|
|
326
|
+
return { systemConfig: finalSystemConfig, datasourceConfigs: finalDatasourceConfigs };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Handle file saving step
|
|
331
|
+
* @async
|
|
332
|
+
* @function handleFileSaving
|
|
333
|
+
* @param {string} appName - Application name
|
|
334
|
+
* @param {Object} systemConfig - System configuration
|
|
335
|
+
* @param {Object[]} datasourceConfigs - Datasource configurations
|
|
336
|
+
* @param {string} systemKey - System key
|
|
337
|
+
* @param {string} dataplaneUrl - Dataplane URL
|
|
338
|
+
* @param {Object} authConfig - Authentication configuration
|
|
339
|
+
* @returns {Promise<Object>} Generated files information
|
|
340
|
+
* @throws {Error} If file saving fails
|
|
341
|
+
*/
|
|
342
|
+
async function handleFileSaving(appName, systemConfig, datasourceConfigs, systemKey, dataplaneUrl, authConfig) {
|
|
343
|
+
logger.log(chalk.blue('\n📋 Step 8: Saving Files'));
|
|
344
|
+
const saveSpinner = ora('Saving files...').start();
|
|
345
|
+
try {
|
|
346
|
+
let aiGeneratedReadme = null;
|
|
347
|
+
if (systemKey && dataplaneUrl && authConfig) {
|
|
348
|
+
try {
|
|
349
|
+
const docsResponse = await getDeploymentDocs(dataplaneUrl, authConfig, systemKey);
|
|
350
|
+
if (docsResponse.success && docsResponse.data?.content) {
|
|
351
|
+
aiGeneratedReadme = docsResponse.data.content;
|
|
352
|
+
logger.log(chalk.gray(' ✓ Fetched AI-generated README.md from dataplane'));
|
|
353
|
+
}
|
|
354
|
+
} catch (error) {
|
|
355
|
+
logger.log(chalk.gray(` ⚠ Could not fetch AI-generated README: ${error.message}`));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const generatedFiles = await generateWizardFiles(appName, systemConfig, datasourceConfigs, systemKey, { aiGeneratedReadme });
|
|
359
|
+
saveSpinner.stop();
|
|
360
|
+
logger.log(chalk.green('\n✓ Wizard completed successfully!'));
|
|
361
|
+
logger.log(chalk.green(`\nFiles created in: ${generatedFiles.appPath}`));
|
|
362
|
+
logger.log(chalk.blue('\nNext steps:'));
|
|
363
|
+
[` 1. Review the generated files in integration/${appName}/`, ' 2. Update env.template with your authentication details', ` 3. Deploy using: ./deploy.sh or .\\deploy.ps1 (or aifabrix deploy ${appName})`].forEach(step => logger.log(chalk.gray(step)));
|
|
364
|
+
return generatedFiles;
|
|
365
|
+
} catch (error) {
|
|
366
|
+
saveSpinner.stop();
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Execute wizard flow steps
|
|
373
|
+
* @async
|
|
374
|
+
* @function executeWizardFlow
|
|
375
|
+
* @param {string} appName - Application name
|
|
376
|
+
* @param {string} dataplaneUrl - Dataplane URL
|
|
377
|
+
* @param {Object} authConfig - Authentication configuration
|
|
378
|
+
* @returns {Promise<void>} Resolves when wizard flow completes
|
|
379
|
+
* @throws {Error} If wizard flow fails
|
|
380
|
+
*/
|
|
381
|
+
async function executeWizardFlow(appName, dataplaneUrl, authConfig) {
|
|
382
|
+
// Step 1: Mode Selection
|
|
383
|
+
const mode = await handleModeSelection(dataplaneUrl, authConfig);
|
|
384
|
+
|
|
385
|
+
// Step 2: Source Selection
|
|
386
|
+
const { sourceType, sourceData } = await handleSourceSelection(dataplaneUrl, authConfig);
|
|
387
|
+
|
|
388
|
+
// Step 3: Parse OpenAPI (if applicable)
|
|
389
|
+
const openApiSpec = await handleOpenApiParsing(dataplaneUrl, authConfig, sourceType, sourceData);
|
|
390
|
+
|
|
391
|
+
// Step 4: Detect Type (optional, if OpenAPI spec available)
|
|
392
|
+
await handleTypeDetection(dataplaneUrl, authConfig, openApiSpec);
|
|
393
|
+
|
|
394
|
+
// Step 5-6: Generate Configuration
|
|
395
|
+
const { systemConfig, datasourceConfigs, systemKey } = await handleConfigurationGeneration(
|
|
396
|
+
dataplaneUrl,
|
|
397
|
+
authConfig,
|
|
398
|
+
mode,
|
|
399
|
+
sourceType,
|
|
400
|
+
openApiSpec
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
// Step 7: Review & Validate
|
|
404
|
+
const finalConfigs = await handleConfigurationReview(dataplaneUrl, authConfig, systemConfig, datasourceConfigs);
|
|
405
|
+
if (!finalConfigs) {
|
|
406
|
+
return; // User cancelled
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Step 8: Save Files
|
|
410
|
+
await handleFileSaving(
|
|
411
|
+
appName,
|
|
412
|
+
finalConfigs.systemConfig,
|
|
413
|
+
finalConfigs.datasourceConfigs,
|
|
414
|
+
systemKey || appName,
|
|
415
|
+
dataplaneUrl,
|
|
416
|
+
authConfig
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Handle wizard command
|
|
422
|
+
* @async
|
|
423
|
+
* @function handleWizard
|
|
424
|
+
* @param {Object} options - Command options
|
|
425
|
+
* @param {string} [options.app] - Application name
|
|
426
|
+
* @param {string} [options.controller] - Controller URL
|
|
427
|
+
* @param {string} [options.environment] - Environment key
|
|
428
|
+
* @param {string} [options.dataplane] - Dataplane URL (overrides controller lookup)
|
|
429
|
+
* @returns {Promise<void>} Resolves when wizard completes
|
|
430
|
+
* @throws {Error} If wizard fails
|
|
431
|
+
*/
|
|
432
|
+
async function handleWizard(options = {}) {
|
|
433
|
+
try {
|
|
434
|
+
logger.log(chalk.blue('\n🧙 AI Fabrix External System Wizard\n'));
|
|
435
|
+
|
|
436
|
+
// Get or prompt for app name
|
|
437
|
+
let appName = options.app;
|
|
438
|
+
if (!appName) {
|
|
439
|
+
appName = await promptForAppName();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Validate app name and check directory
|
|
443
|
+
const shouldContinue = await validateAndCheckAppDirectory(appName);
|
|
444
|
+
if (!shouldContinue) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Get dataplane URL and authentication
|
|
449
|
+
const { dataplaneUrl, authConfig } = await setupDataplaneAndAuth(options, appName);
|
|
450
|
+
|
|
451
|
+
// Execute wizard flow
|
|
452
|
+
await executeWizardFlow(appName, dataplaneUrl, authConfig);
|
|
453
|
+
} catch (error) {
|
|
454
|
+
logger.error(chalk.red(`\n❌ Wizard failed: ${error.message}`));
|
|
455
|
+
throw error;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Setup dataplane URL and authentication
|
|
461
|
+
* @async
|
|
462
|
+
* @function setupDataplaneAndAuth
|
|
463
|
+
* @param {Object} options - Command options
|
|
464
|
+
* @param {string} appName - Application name
|
|
465
|
+
* @returns {Promise<Object>} Object with dataplaneUrl and authConfig
|
|
466
|
+
* @throws {Error} If setup fails
|
|
467
|
+
*/
|
|
468
|
+
async function setupDataplaneAndAuth(options, appName) {
|
|
469
|
+
const configData = await config.getConfig();
|
|
470
|
+
const environment = options.environment || 'dev';
|
|
471
|
+
const controllerUrl = options.controller || configData.deployment?.controllerUrl || 'http://localhost:3000';
|
|
472
|
+
|
|
473
|
+
// Get dataplane URL (either from option or from controller)
|
|
474
|
+
let dataplaneUrl = options.dataplane;
|
|
475
|
+
if (!dataplaneUrl) {
|
|
476
|
+
// Get authentication first
|
|
477
|
+
const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
|
|
478
|
+
if (!authConfig.token && !authConfig.clientId) {
|
|
479
|
+
throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Get dataplane URL from controller
|
|
483
|
+
logger.log(chalk.blue('🌐 Getting dataplane URL from controller...'));
|
|
484
|
+
dataplaneUrl = await getDataplaneUrl(controllerUrl, appName, environment, authConfig);
|
|
485
|
+
logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
|
|
486
|
+
|
|
487
|
+
return { dataplaneUrl, authConfig };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// If dataplane URL provided directly, still need auth
|
|
491
|
+
const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
|
|
492
|
+
if (!authConfig.token && !authConfig.clientId) {
|
|
493
|
+
throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return { dataplaneUrl, authConfig };
|
|
497
|
+
}
|
|
498
|
+
module.exports = { handleWizard };
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
const fs = require('fs').promises;
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const os = require('os');
|
|
17
|
-
const paths = require('
|
|
17
|
+
const paths = require('../utils/paths');
|
|
18
18
|
|
|
19
19
|
// Audit log file path (in user's home directory for compliance)
|
|
20
20
|
let auditLogPath = null;
|
|
@@ -12,7 +12,7 @@ const fs = require('fs').promises;
|
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const yaml = require('js-yaml');
|
|
14
14
|
const os = require('os');
|
|
15
|
-
const { encryptToken, decryptToken, isTokenEncrypted } = require('
|
|
15
|
+
const { encryptToken, decryptToken, isTokenEncrypted } = require('../utils/token-encryption');
|
|
16
16
|
// Avoid importing paths here to prevent circular dependency.
|
|
17
17
|
// Config location is always under OS home at ~/.aifabrix/config.yaml
|
|
18
18
|
|
|
@@ -62,19 +62,19 @@ function validateAndNormalizeDeveloperId(developerId) {
|
|
|
62
62
|
|
|
63
63
|
if (typeof developerId === 'number') {
|
|
64
64
|
if (developerId < 0 || !Number.isFinite(developerId)) {
|
|
65
|
-
throw new Error(
|
|
65
|
+
throw new Error('Developer ID must be a non-negative digit string or number');
|
|
66
66
|
}
|
|
67
67
|
return String(developerId);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
if (typeof developerId === 'string') {
|
|
71
71
|
if (!DEV_ID_DIGITS_REGEX.test(developerId)) {
|
|
72
|
-
throw new Error(
|
|
72
|
+
throw new Error('Developer ID must be a non-negative digit string or number');
|
|
73
73
|
}
|
|
74
74
|
return developerId;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
throw new Error(
|
|
77
|
+
throw new Error('Developer ID must be a non-negative digit string or number');
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/**
|
|
@@ -204,24 +204,15 @@ async function getDeveloperId() {
|
|
|
204
204
|
* @param {number|string} developerId - Developer ID to set (digit-only string preserved, or number). "0" = default infra, > "0" = developer-specific
|
|
205
205
|
* @returns {Promise<void>}
|
|
206
206
|
*/
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
devIdString = developerId;
|
|
217
|
-
} else {
|
|
218
|
-
throw new Error(errorMsg);
|
|
219
|
-
}
|
|
220
|
-
cachedDeveloperId = null;
|
|
221
|
-
const config = await getConfig();
|
|
222
|
-
config['developer-id'] = devIdString;
|
|
223
|
-
cachedDeveloperId = devIdString;
|
|
224
|
-
await saveConfig(config);
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Verifies developer ID was saved correctly
|
|
210
|
+
* @async
|
|
211
|
+
* @function verifyDeveloperIdSaved
|
|
212
|
+
* @param {string} devIdString - Developer ID string
|
|
213
|
+
* @throws {Error} If verification fails
|
|
214
|
+
*/
|
|
215
|
+
async function verifyDeveloperIdSaved(devIdString) {
|
|
225
216
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
226
217
|
const savedContent = await fs.readFile(RUNTIME_CONFIG_FILE, 'utf8');
|
|
227
218
|
const savedConfig = yaml.load(savedContent);
|
|
@@ -229,6 +220,17 @@ async function setDeveloperId(developerId) {
|
|
|
229
220
|
if (savedDevIdString !== devIdString) {
|
|
230
221
|
throw new Error(`Failed to save developer ID: expected ${devIdString}, got ${savedDevIdString}. File content: ${savedContent.substring(0, 200)}`);
|
|
231
222
|
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function setDeveloperId(developerId) {
|
|
226
|
+
const devIdString = validateAndNormalizeDeveloperId(developerId);
|
|
227
|
+
|
|
228
|
+
cachedDeveloperId = null;
|
|
229
|
+
const config = await getConfig();
|
|
230
|
+
config['developer-id'] = devIdString;
|
|
231
|
+
cachedDeveloperId = devIdString;
|
|
232
|
+
await saveConfig(config);
|
|
233
|
+
await verifyDeveloperIdSaved(devIdString);
|
|
232
234
|
cachedDeveloperId = null;
|
|
233
235
|
}
|
|
234
236
|
|
|
@@ -320,7 +322,7 @@ async function setSecretsEncryptionKey(key) {
|
|
|
320
322
|
}
|
|
321
323
|
|
|
322
324
|
// Validate key format using encryption utilities
|
|
323
|
-
const { validateEncryptionKey } = require('
|
|
325
|
+
const { validateEncryptionKey } = require('../utils/secrets-encryption');
|
|
324
326
|
validateEncryptionKey(key);
|
|
325
327
|
|
|
326
328
|
const config = await getConfig();
|
|
@@ -377,19 +379,19 @@ Object.defineProperty(exportsObj, 'developerId', {
|
|
|
377
379
|
});
|
|
378
380
|
|
|
379
381
|
// Token management functions - created after dependencies are defined
|
|
380
|
-
const { createTokenManagementFunctions } = require('
|
|
382
|
+
const { createTokenManagementFunctions } = require('../utils/config-tokens');
|
|
381
383
|
const tokenFunctions = createTokenManagementFunctions({
|
|
382
384
|
getConfigFn: getConfig,
|
|
383
385
|
saveConfigFn: saveConfig,
|
|
384
386
|
getSecretsEncryptionKeyFn: getSecretsEncryptionKey,
|
|
385
387
|
encryptTokenValueFn: encryptTokenValue,
|
|
386
388
|
decryptTokenValueFn: decryptTokenValue,
|
|
387
|
-
isTokenEncryptedFn: require('
|
|
389
|
+
isTokenEncryptedFn: require('../utils/token-encryption').isTokenEncrypted
|
|
388
390
|
});
|
|
389
391
|
Object.assign(exportsObj, tokenFunctions);
|
|
390
392
|
|
|
391
393
|
// Path configuration functions - created after getConfig/saveConfig are defined
|
|
392
|
-
const { createPathConfigFunctions } = require('
|
|
394
|
+
const { createPathConfigFunctions } = require('../utils/config-paths');
|
|
393
395
|
const pathConfigFunctions = createPathConfigFunctions(getConfig, saveConfig);
|
|
394
396
|
Object.assign(exportsObj, pathConfigFunctions);
|
|
395
397
|
|