@aifabrix/builder 2.32.2 → 2.33.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.
Files changed (130) hide show
  1. package/.cursor/rules/project-rules.mdc +8 -0
  2. package/README.md +36 -8
  3. package/bin/aifabrix.js +6 -8
  4. package/integration/hubspot/README.md +8 -7
  5. package/integration/hubspot/companies.json +2048 -0
  6. package/integration/hubspot/create-hubspot.js +665 -0
  7. package/integration/hubspot/{hubspot-deploy-company.json → hubspot-datasource-company.json} +1 -1
  8. package/integration/hubspot/{hubspot-deploy-contact.json → hubspot-datasource-contact.json} +1 -1
  9. package/integration/hubspot/{hubspot-deploy-deal.json → hubspot-datasource-deal.json} +1 -1
  10. package/integration/hubspot/hubspot-deploy.json +832 -81
  11. package/integration/hubspot/hubspot-system.json +99 -0
  12. package/integration/hubspot/test-artifacts/wizard-hubspot-credential-real.yaml +20 -0
  13. package/integration/hubspot/test-artifacts/wizard-hubspot-env-vars.yaml +9 -0
  14. package/integration/hubspot/test-artifacts/wizard-invalid-add-datasource.yaml +5 -0
  15. package/integration/hubspot/test-artifacts/wizard-invalid-app-name.yaml +5 -0
  16. package/integration/hubspot/test-artifacts/wizard-invalid-credential-create.yaml +7 -0
  17. package/integration/hubspot/test-artifacts/wizard-invalid-credential-select.yaml +7 -0
  18. package/integration/hubspot/test-artifacts/wizard-invalid-known-platform.yaml +4 -0
  19. package/integration/hubspot/test-artifacts/wizard-invalid-missing-app.yaml +4 -0
  20. package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
  21. package/integration/hubspot/test-artifacts/wizard-invalid-mode.yaml +5 -0
  22. package/integration/hubspot/test-artifacts/wizard-invalid-openapi-file.yaml +5 -0
  23. package/integration/hubspot/test-artifacts/wizard-invalid-openapi-url.yaml +4 -0
  24. package/integration/hubspot/test-artifacts/wizard-invalid-source.yaml +4 -0
  25. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-array-test.yaml +5 -0
  26. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
  27. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
  28. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-test.yaml +5 -0
  29. package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-test.yaml +5 -0
  30. package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +5 -0
  31. package/integration/hubspot/test-dataplane-down-helpers.js +246 -0
  32. package/integration/hubspot/test-dataplane-down-tests.js +419 -0
  33. package/integration/hubspot/test-dataplane-down.js +157 -0
  34. package/integration/hubspot/test.js +1517 -0
  35. package/integration/hubspot/variables.yaml +4 -4
  36. package/integration/hubspot/wizard-hubspot-e2e.yaml +16 -0
  37. package/integration/hubspot/wizard-hubspot-platform.yaml +8 -0
  38. package/lib/api/applications.api.js +1 -0
  39. package/lib/api/index.js +10 -5
  40. package/lib/api/types/wizard.types.js +176 -38
  41. package/lib/api/wizard.api.js +207 -38
  42. package/lib/app/deploy.js +116 -54
  43. package/lib/app/display.js +6 -5
  44. package/lib/app/dockerfile.js +2 -1
  45. package/lib/app/list.js +78 -37
  46. package/lib/app/prompts.js +9 -5
  47. package/lib/app/readme.js +41 -112
  48. package/lib/app/register.js +44 -9
  49. package/lib/app/rotate-secret.js +50 -32
  50. package/lib/cli.js +243 -65
  51. package/lib/commands/app.js +4 -9
  52. package/lib/commands/auth-config.js +125 -0
  53. package/lib/commands/auth-status.js +261 -0
  54. package/lib/commands/datasource.js +3 -6
  55. package/lib/commands/login-credentials.js +4 -4
  56. package/lib/commands/login-device.js +43 -29
  57. package/lib/commands/login.js +22 -13
  58. package/lib/commands/wizard-config-normalizer.js +92 -0
  59. package/lib/commands/wizard-core.js +515 -0
  60. package/lib/commands/wizard-dataplane.js +122 -0
  61. package/lib/commands/wizard-headless.js +115 -0
  62. package/lib/commands/wizard.js +129 -357
  63. package/lib/core/config.js +46 -0
  64. package/lib/core/secrets.js +3 -22
  65. package/lib/core/templates-env.js +1 -1
  66. package/lib/datasource/deploy.js +34 -23
  67. package/lib/datasource/list.js +8 -6
  68. package/lib/deployment/deployer.js +25 -0
  69. package/lib/deployment/environment.js +10 -13
  70. package/lib/external-system/delete.js +151 -0
  71. package/lib/external-system/deploy.js +54 -378
  72. package/lib/external-system/download-helpers.js +45 -65
  73. package/lib/external-system/download.js +34 -13
  74. package/lib/external-system/generator.js +11 -7
  75. package/lib/external-system/test-auth.js +5 -3
  76. package/lib/generator/builders.js +3 -1
  77. package/lib/generator/external-controller-manifest.js +157 -0
  78. package/lib/generator/external-schema-utils.js +236 -0
  79. package/lib/generator/external.js +55 -3
  80. package/lib/generator/index.js +22 -10
  81. package/lib/generator/wizard-prompts.js +33 -10
  82. package/lib/generator/wizard.js +69 -86
  83. package/lib/infrastructure/compose.js +100 -0
  84. package/lib/infrastructure/helpers.js +139 -0
  85. package/lib/infrastructure/index.js +52 -311
  86. package/lib/infrastructure/services.js +168 -0
  87. package/lib/schema/application-schema.json +24 -5
  88. package/lib/schema/external-datasource.schema.json +303 -17
  89. package/lib/schema/external-system.schema.json +1 -1
  90. package/lib/schema/wizard-config.schema.json +234 -0
  91. package/lib/utils/api.js +37 -42
  92. package/lib/utils/app-existence.js +42 -0
  93. package/lib/utils/app-register-config.js +7 -2
  94. package/lib/utils/app-register-display.js +2 -1
  95. package/lib/utils/auth-config-validator.js +92 -0
  96. package/lib/utils/cli-utils.js +3 -1
  97. package/lib/utils/command-header.js +43 -0
  98. package/lib/utils/compose-generator.js +113 -70
  99. package/lib/utils/controller-url.js +115 -0
  100. package/lib/utils/dataplane-health.js +115 -0
  101. package/lib/utils/dataplane-resolver.js +29 -0
  102. package/lib/utils/dev-config.js +6 -2
  103. package/lib/utils/env-copy.js +2 -1
  104. package/lib/utils/env-map.js +2 -1
  105. package/lib/utils/env-ports.js +2 -1
  106. package/lib/utils/env-template.js +1 -1
  107. package/lib/utils/error-formatter.js +149 -28
  108. package/lib/utils/external-readme.js +125 -0
  109. package/lib/utils/help-builder.js +190 -0
  110. package/lib/utils/infra-status.js +13 -3
  111. package/lib/utils/paths.js +17 -2
  112. package/lib/utils/port-resolver.js +111 -0
  113. package/lib/utils/secrets-helpers.js +3 -15
  114. package/lib/utils/secrets-utils.js +2 -2
  115. package/lib/utils/token-manager.js +69 -4
  116. package/lib/utils/variable-transformer.js +7 -2
  117. package/lib/validation/external-manifest-validator.js +202 -0
  118. package/lib/validation/validate-display.js +406 -0
  119. package/lib/validation/validate.js +159 -123
  120. package/lib/validation/validator.js +38 -4
  121. package/lib/validation/wizard-config-validator.js +267 -0
  122. package/package.json +4 -2
  123. package/templates/applications/README.md.hbs +19 -17
  124. package/templates/applications/miso-controller/env.template +1 -1
  125. package/templates/applications/miso-controller/rbac.yaml +7 -7
  126. package/templates/external-system/README.md.hbs +99 -0
  127. package/templates/external-system/external-system.json.hbs +1 -1
  128. package/templates/infra/compose.yaml.hbs +35 -0
  129. package/templates/python/docker-compose.hbs +26 -0
  130. package/templates/typescript/docker-compose.hbs +26 -0
@@ -0,0 +1,515 @@
1
+ /**
2
+ * @fileoverview Wizard core functions - shared between interactive and headless modes
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+ /* eslint-disable max-lines */
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 { getDeploymentAuth, getDeviceOnlyAuth } = require('../utils/token-manager');
13
+ const { resolveControllerUrl } = require('../utils/controller-url');
14
+ const { normalizeWizardConfigs } = require('./wizard-config-normalizer');
15
+ const {
16
+ createWizardSession,
17
+ updateWizardSession,
18
+ parseOpenApi,
19
+ credentialSelection,
20
+ detectType,
21
+ generateConfig,
22
+ validateWizardConfig,
23
+ getDeploymentDocs,
24
+ testMcpConnection
25
+ } = require('../api/wizard.api');
26
+ const { generateWizardFiles } = require('../generator/wizard');
27
+
28
+ /**
29
+ * Validate app name and check if directory exists
30
+ * @async
31
+ * @function validateAndCheckAppDirectory
32
+ * @param {string} appName - Application name
33
+ * @param {boolean} [interactive=true] - Whether to prompt for confirmation
34
+ * @returns {Promise<boolean>} True if should continue, false if cancelled
35
+ */
36
+ async function validateAndCheckAppDirectory(appName, interactive = true) {
37
+ if (!/^[a-z0-9-_]+$/.test(appName)) {
38
+ throw new Error('Application name must contain only lowercase letters, numbers, hyphens, and underscores');
39
+ }
40
+ const appPath = path.join(process.cwd(), 'integration', appName);
41
+ try {
42
+ await fs.access(appPath);
43
+ if (interactive) {
44
+ const { overwrite } = await require('inquirer').prompt([{
45
+ type: 'confirm', name: 'overwrite',
46
+ message: `Directory ${appPath} already exists. Overwrite?`, default: false
47
+ }]);
48
+ if (!overwrite) {
49
+ logger.log(chalk.yellow('Wizard cancelled.')); return false;
50
+ }
51
+ } else {
52
+ logger.log(chalk.yellow(`Warning: Directory ${appPath} exists. Overwriting...`));
53
+ }
54
+ } catch (error) {
55
+ if (error.code !== 'ENOENT') throw error;
56
+ }
57
+ return true;
58
+ }
59
+
60
+ /**
61
+ * Extract session ID from response data
62
+ * @function extractSessionId
63
+ * @param {Object} responseData - Response data from API
64
+ * @returns {string} Session ID
65
+ */
66
+ function extractSessionId(responseData) {
67
+ let sessionId = responseData?.data?.sessionId || responseData?.sessionId ||
68
+ responseData?.data?.session_id || responseData?.session_id;
69
+ if (sessionId && typeof sessionId === 'object') {
70
+ sessionId = sessionId.id || sessionId.sessionId || sessionId.session_id;
71
+ }
72
+ if (!sessionId || typeof sessionId !== 'string') {
73
+ throw new Error(`Session ID not found: ${JSON.stringify(responseData, null, 2)}`);
74
+ }
75
+ return sessionId;
76
+ }
77
+
78
+ /**
79
+ * Handle mode selection step - create wizard session
80
+ * @async
81
+ * @function handleModeSelection
82
+ * @param {string} dataplaneUrl - Dataplane URL
83
+ * @param {Object} authConfig - Authentication configuration
84
+ * @param {string} [configMode] - Mode from config file
85
+ * @param {string} [systemIdOrKey] - System ID or key
86
+ * @returns {Promise<Object>} Object with mode and sessionId
87
+ */
88
+ async function handleModeSelection(dataplaneUrl, authConfig, configMode = null, systemIdOrKey = null) {
89
+ logger.log(chalk.blue('\n\uD83D\uDCCB Step 1: Create Session'));
90
+ const mode = configMode || 'create-system';
91
+ const sessionResponse = await createWizardSession(dataplaneUrl, authConfig, mode, systemIdOrKey);
92
+ if (!sessionResponse.success || !sessionResponse.data) {
93
+ const errorMsg = sessionResponse.formattedError || sessionResponse.error ||
94
+ sessionResponse.errorData?.detail || 'Unknown error';
95
+ throw new Error(`Failed to create wizard session: ${errorMsg}`);
96
+ }
97
+ const sessionId = extractSessionId(sessionResponse.data);
98
+ logger.log(chalk.green(`\u2713 Session created: ${sessionId}`));
99
+ return { mode, sessionId };
100
+ }
101
+
102
+ /**
103
+ * Handle source selection step
104
+ * @async
105
+ * @function handleSourceSelection
106
+ * @param {string} dataplaneUrl - Dataplane URL
107
+ * @param {string} sessionId - Wizard session ID
108
+ * @param {Object} authConfig - Authentication configuration
109
+ * @param {Object} [configSource] - Source config from wizard.yaml
110
+ * @returns {Promise<Object>} Object with sourceType and sourceData
111
+ */
112
+ async function handleSourceSelection(dataplaneUrl, sessionId, authConfig, configSource = null) {
113
+ logger.log(chalk.blue('\n\uD83D\uDCCB Step 2: Parse OpenAPI'));
114
+ let sourceType, sourceData = null;
115
+ const updateData = { currentStep: 1 };
116
+ if (configSource) {
117
+ sourceType = configSource.type;
118
+ if (sourceType === 'openapi-file') sourceData = configSource.filePath;
119
+ else if (sourceType === 'openapi-url') {
120
+ sourceData = configSource.url;
121
+ updateData.openapiSpec = null;
122
+ } else if (sourceType === 'mcp-server') {
123
+ sourceData = JSON.stringify({ serverUrl: configSource.serverUrl, token: configSource.token });
124
+ updateData.mcpServerUrl = configSource.serverUrl;
125
+ } else if (sourceType === 'known-platform') sourceData = configSource.platform;
126
+ }
127
+ const updateResponse = await updateWizardSession(dataplaneUrl, sessionId, authConfig, updateData);
128
+ if (!updateResponse.success) {
129
+ throw new Error(`Source selection failed: ${updateResponse.error || updateResponse.formattedError}`);
130
+ }
131
+ return { sourceType, sourceData };
132
+ }
133
+
134
+ /**
135
+ * Parse OpenAPI file or URL
136
+ * @async
137
+ * @function parseOpenApiSource
138
+ * @param {string} dataplaneUrl - Dataplane URL
139
+ * @param {Object} authConfig - Authentication configuration
140
+ * @param {string} sourceType - Source type (openapi-file or openapi-url)
141
+ * @param {string} sourceData - Source data (file path or URL)
142
+ * @returns {Promise<Object|null>} OpenAPI spec or null
143
+ */
144
+ async function parseOpenApiSource(dataplaneUrl, authConfig, sourceType, sourceData) {
145
+ const isUrl = sourceType === 'openapi-url';
146
+ const spinner = ora(`Parsing OpenAPI ${isUrl ? 'URL' : 'file'}...`).start();
147
+ try {
148
+ const parseResponse = await parseOpenApi(dataplaneUrl, authConfig, sourceData, isUrl);
149
+ spinner.stop();
150
+ if (!parseResponse.success) {
151
+ throw new Error(`OpenAPI parsing failed: ${parseResponse.error || parseResponse.formattedError}`);
152
+ }
153
+ logger.log(chalk.green(`\u2713 OpenAPI ${isUrl ? 'URL' : 'file'} parsed successfully`));
154
+ return parseResponse.data?.spec;
155
+ } catch (error) {
156
+ spinner.stop();
157
+ throw error;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Test MCP server connection
163
+ * @async
164
+ * @function testMcpServerConnection
165
+ * @param {string} dataplaneUrl - Dataplane URL
166
+ * @param {Object} authConfig - Authentication configuration
167
+ * @param {string} sourceData - MCP server details JSON string
168
+ * @returns {Promise<null>} Always returns null
169
+ */
170
+ async function testMcpServerConnection(dataplaneUrl, authConfig, sourceData) {
171
+ const mcpDetails = JSON.parse(sourceData);
172
+ const spinner = ora('Testing MCP server connection...').start();
173
+ try {
174
+ const testResponse = await testMcpConnection(dataplaneUrl, authConfig, mcpDetails.serverUrl, mcpDetails.token);
175
+ spinner.stop();
176
+ if (!testResponse.success || !testResponse.data?.connected) {
177
+ throw new Error(`MCP connection failed: ${testResponse.data?.error || 'Unable to connect'}`);
178
+ }
179
+ logger.log(chalk.green('\u2713 MCP server connection successful'));
180
+ } catch (error) {
181
+ spinner.stop();
182
+ throw error;
183
+ }
184
+ return null;
185
+ }
186
+
187
+ /**
188
+ * Handle OpenAPI parsing step
189
+ * @async
190
+ * @function handleOpenApiParsing
191
+ * @param {string} dataplaneUrl - Dataplane URL
192
+ * @param {Object} authConfig - Authentication configuration
193
+ * @param {string} sourceType - Source type
194
+ * @param {string} sourceData - Source data
195
+ * @returns {Promise<Object|null>} OpenAPI spec or null
196
+ */
197
+ async function handleOpenApiParsing(dataplaneUrl, authConfig, sourceType, sourceData) {
198
+ if (sourceType === 'openapi-file' || sourceType === 'openapi-url') {
199
+ return await parseOpenApiSource(dataplaneUrl, authConfig, sourceType, sourceData);
200
+ }
201
+ if (sourceType === 'mcp-server') {
202
+ return await testMcpServerConnection(dataplaneUrl, authConfig, sourceData);
203
+ }
204
+ if (sourceType === 'known-platform' && sourceData) {
205
+ const platformKey = String(sourceData).toUpperCase().replace(/[^A-Z0-9]/g, '_');
206
+ const filePath = process.env[`${platformKey}_OPENAPI_FILE`];
207
+ const url = process.env[`${platformKey}_OPENAPI_URL`];
208
+ if (filePath) return await parseOpenApiSource(dataplaneUrl, authConfig, 'openapi-file', filePath);
209
+ if (url) return await parseOpenApiSource(dataplaneUrl, authConfig, 'openapi-url', url);
210
+ }
211
+ return null;
212
+ }
213
+
214
+ /**
215
+ * Handle credential selection step
216
+ * @async
217
+ * @function handleCredentialSelection
218
+ * @param {string} dataplaneUrl - Dataplane URL
219
+ * @param {Object} authConfig - Authentication configuration
220
+ * @param {Object} [configCredential] - Credential config from wizard.yaml
221
+ * @returns {Promise<string|null>} Credential ID/key or null if skipped
222
+ */
223
+ async function handleCredentialSelection(dataplaneUrl, authConfig, configCredential = null) {
224
+ logger.log(chalk.blue('\n\uD83D\uDCCB Step 3: Credential Selection (Optional)'));
225
+ const selectionData = configCredential ? {
226
+ action: configCredential.action,
227
+ credentialConfig: configCredential.config,
228
+ credentialIdOrKey: configCredential.credentialIdOrKey
229
+ } : { action: 'skip' };
230
+ if (selectionData.action === 'skip') {
231
+ logger.log(chalk.gray(' Skipping credential selection'));
232
+ return null;
233
+ }
234
+ const spinner = ora('Processing credential selection...').start();
235
+ try {
236
+ const response = await credentialSelection(dataplaneUrl, authConfig, selectionData);
237
+ spinner.stop();
238
+ if (!response.success) {
239
+ logger.log(chalk.yellow(`Warning: Credential selection failed: ${response.error}`));
240
+ return null;
241
+ }
242
+ const actionText = selectionData.action === 'create' ? 'created' : 'selected';
243
+ logger.log(chalk.green(`\u2713 Credential ${actionText}`));
244
+ return response.data?.credentialIdOrKey || null;
245
+ } catch (error) {
246
+ spinner.stop();
247
+ logger.log(chalk.yellow(`Warning: Credential selection failed: ${error.message}`));
248
+ return null;
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Handle type detection step
254
+ * @async
255
+ * @function handleTypeDetection
256
+ * @param {string} dataplaneUrl - Dataplane URL
257
+ * @param {Object} authConfig - Authentication configuration
258
+ * @param {Object} openapiSpec - OpenAPI specification
259
+ * @returns {Promise<Object|null>} Detected type or null
260
+ */
261
+ async function handleTypeDetection(dataplaneUrl, authConfig, openapiSpec) {
262
+ if (!openapiSpec) return null;
263
+ logger.log(chalk.blue('\n\uD83D\uDCCB Step 4: Detect Type'));
264
+ const spinner = ora('Detecting API type...').start();
265
+ try {
266
+ const detectResponse = await detectType(dataplaneUrl, authConfig, openapiSpec);
267
+ spinner.stop();
268
+ if (detectResponse.success && detectResponse.data) {
269
+ const detectedType = detectResponse.data;
270
+ const recommendedType = detectedType.recommendedType || detectedType.apiType || 'unknown';
271
+ logger.log(chalk.green(`\u2713 API type detected: ${recommendedType}`));
272
+ return detectedType;
273
+ }
274
+ } catch (error) {
275
+ spinner.stop();
276
+ logger.log(chalk.yellow(`Warning: Type detection failed: ${error.message}`));
277
+ }
278
+ return null;
279
+ }
280
+
281
+ /**
282
+ * Build configuration preferences from configPrefs object
283
+ * @function buildConfigPreferences
284
+ * @param {Object} [configPrefs] - Preferences from wizard.yaml
285
+ * @returns {Object} Configuration preferences object
286
+ */
287
+ function buildConfigPreferences(configPrefs) {
288
+ return {
289
+ intent: configPrefs?.intent || 'general integration',
290
+ fieldOnboardingLevel: configPrefs?.fieldOnboardingLevel || 'full',
291
+ enableOpenAPIGeneration: configPrefs?.enableOpenAPIGeneration !== false,
292
+ userPreferences: {
293
+ enableMCP: configPrefs?.enableMCP || false,
294
+ enableABAC: configPrefs?.enableABAC || false,
295
+ enableRBAC: configPrefs?.enableRBAC || false
296
+ }
297
+ };
298
+ }
299
+
300
+ /**
301
+ * Build configuration payload for API call
302
+ * @function buildConfigPayload
303
+ * @param {Object} params - Parameters object
304
+ * @param {Object} params.openapiSpec - OpenAPI specification
305
+ * @param {Object} params.detectedType - Detected type info
306
+ * @param {string} params.mode - Selected mode
307
+ * @param {Object} params.prefs - Configuration preferences
308
+ * @param {string} [params.credentialIdOrKey] - Credential ID or key
309
+ * @param {string} [params.systemIdOrKey] - System ID or key
310
+ * @returns {Object} Configuration payload
311
+ */
312
+ function buildConfigPayload({ openapiSpec, detectedType, mode, prefs, credentialIdOrKey, systemIdOrKey }) {
313
+ const detectedTypeValue = detectedType?.recommendedType || detectedType?.apiType || detectedType?.selectedType || 'record-based';
314
+ const payload = {
315
+ openapiSpec,
316
+ detectedType: detectedTypeValue,
317
+ intent: prefs.intent,
318
+ mode,
319
+ fieldOnboardingLevel: prefs.fieldOnboardingLevel,
320
+ enableOpenAPIGeneration: prefs.enableOpenAPIGeneration,
321
+ userPreferences: prefs.userPreferences
322
+ };
323
+ if (credentialIdOrKey) payload.credentialIdOrKey = credentialIdOrKey;
324
+ if (systemIdOrKey) payload.systemIdOrKey = systemIdOrKey;
325
+ return payload;
326
+ }
327
+
328
+ /**
329
+ * Extract configuration from API response
330
+ * @function extractConfigurationFromResponse
331
+ * @param {Object} generateResponse - API response
332
+ * @returns {Object} Extracted configuration
333
+ */
334
+ function extractConfigurationFromResponse(generateResponse) {
335
+ const systemConfig = generateResponse.data?.systemConfig;
336
+ const datasourceConfigs = generateResponse.data?.datasourceConfigs ||
337
+ (generateResponse.data?.datasourceConfig ? [generateResponse.data.datasourceConfig] : []);
338
+ if (!systemConfig) throw new Error('System configuration not found');
339
+ return { systemConfig, datasourceConfigs, systemKey: generateResponse.data?.systemKey };
340
+ }
341
+
342
+ /**
343
+ * Handle configuration generation step
344
+ * @async
345
+ * @function handleConfigurationGeneration
346
+ * @param {string} dataplaneUrl - Dataplane URL
347
+ * @param {Object} authConfig - Authentication configuration
348
+ * @param {Object} options - Configuration options
349
+ * @param {string} options.mode - Selected mode
350
+ * @param {Object} options.openapiSpec - OpenAPI specification
351
+ * @param {Object} options.detectedType - Detected type info
352
+ * @param {Object} [options.configPrefs] - Preferences from wizard.yaml
353
+ * @param {string} [options.credentialIdOrKey] - Credential ID or key (optional)
354
+ * @param {string} [options.systemIdOrKey] - System ID or key (optional)
355
+ * @returns {Promise<Object>} Generated configuration
356
+ */
357
+ async function handleConfigurationGeneration(dataplaneUrl, authConfig, options) {
358
+ logger.log(chalk.blue('\n\uD83D\uDCCB Step 5: Generate Configuration'));
359
+ const prefs = buildConfigPreferences(options.configPrefs);
360
+ const spinner = ora('Generating configuration via AI (10-30 seconds)...').start();
361
+ try {
362
+ const configPayload = buildConfigPayload({
363
+ openapiSpec: options.openapiSpec,
364
+ detectedType: options.detectedType,
365
+ mode: options.mode,
366
+ prefs,
367
+ credentialIdOrKey: options.credentialIdOrKey,
368
+ systemIdOrKey: options.systemIdOrKey
369
+ });
370
+ const generateResponse = await generateConfig(dataplaneUrl, authConfig, configPayload);
371
+ spinner.stop();
372
+ if (!generateResponse.success) {
373
+ throw new Error(`Configuration generation failed: ${generateResponse.error || generateResponse.formattedError}`);
374
+ }
375
+ const result = extractConfigurationFromResponse(generateResponse);
376
+ const normalized = normalizeWizardConfigs(result.systemConfig, result.datasourceConfigs);
377
+ logger.log(chalk.green('\u2713 Configuration generated successfully'));
378
+ return {
379
+ systemConfig: normalized.systemConfig,
380
+ datasourceConfigs: normalized.datasourceConfigs,
381
+ systemKey: result.systemKey
382
+ };
383
+ } catch (error) {
384
+ spinner.stop();
385
+ throw error;
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Validate wizard configuration
391
+ * @async
392
+ * @function validateWizardConfiguration
393
+ * @param {string} dataplaneUrl - Dataplane URL
394
+ * @param {Object} authConfig - Authentication configuration
395
+ * @param {Object} systemConfig - System configuration
396
+ * @param {Object[]} datasourceConfigs - Datasource configurations
397
+ */
398
+ // eslint-disable-next-line max-statements
399
+ async function validateWizardConfiguration(dataplaneUrl, authConfig, systemConfig, datasourceConfigs) {
400
+ logger.log(chalk.blue('\n\uD83D\uDCCB Step 6: Validate Configuration'));
401
+ const spinner = ora('Validating configuration...').start();
402
+ try {
403
+ const configs = Array.isArray(datasourceConfigs) ? datasourceConfigs : [datasourceConfigs];
404
+ const warnings = [];
405
+ for (const datasourceConfig of configs) {
406
+ const validateResponse = await validateWizardConfig(dataplaneUrl, authConfig, systemConfig, datasourceConfig);
407
+ const isValid = validateResponse.success && (validateResponse.data?.valid || validateResponse.data?.isValid);
408
+ if (!isValid) {
409
+ const errors = validateResponse.data?.errors || validateResponse.errorData?.errors || [];
410
+ const errorDetail = validateResponse.errorData?.detail || validateResponse.errorData?.message;
411
+ const errorMsg = errors.length > 0 ? errors.map(e => e.message || e).join(', ') : errorDetail || validateResponse.error || 'Validation failed';
412
+ spinner.stop();
413
+ throw new Error(`Configuration validation failed: ${errorMsg}`);
414
+ }
415
+ if (validateResponse.data?.warnings?.length > 0) warnings.push(...validateResponse.data.warnings);
416
+ }
417
+ spinner.stop();
418
+ logger.log(chalk.green('\u2713 Configuration validated successfully'));
419
+ if (warnings.length > 0) {
420
+ logger.log(chalk.yellow('\n\u26A0 Warnings:'));
421
+ warnings.forEach(w => logger.log(chalk.yellow(` - ${w.message || w}`)));
422
+ }
423
+ } catch (error) {
424
+ spinner.stop();
425
+ throw error;
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Handle file saving step
431
+ * @async
432
+ * @function handleFileSaving
433
+ * @param {string} appName - Application name
434
+ * @param {Object} systemConfig - System configuration
435
+ * @param {Object[]} datasourceConfigs - Datasource configurations
436
+ * @param {string} systemKey - System key
437
+ * @param {string} dataplaneUrl - Dataplane URL
438
+ * @param {Object} authConfig - Authentication configuration
439
+ * @returns {Promise<Object>} Generated files information
440
+ */
441
+ async function handleFileSaving(appName, systemConfig, datasourceConfigs, systemKey, dataplaneUrl, authConfig) {
442
+ logger.log(chalk.blue('\n\uD83D\uDCCB Step 7: Save Files'));
443
+ const spinner = ora('Saving files...').start();
444
+ try {
445
+ let aiGeneratedReadme = null;
446
+ if (systemKey && dataplaneUrl && authConfig) {
447
+ try {
448
+ const docsResponse = await getDeploymentDocs(dataplaneUrl, authConfig, systemKey);
449
+ if (docsResponse.success && docsResponse.data?.content) aiGeneratedReadme = docsResponse.data.content;
450
+ } catch (e) {
451
+ logger.log(chalk.gray(` Could not fetch AI-generated README: ${e.message}`));
452
+ }
453
+ }
454
+ const generatedFiles = await generateWizardFiles(appName, systemConfig, datasourceConfigs, systemKey, { aiGeneratedReadme });
455
+ spinner.stop();
456
+ logger.log(chalk.green('\n\u2713 Wizard completed successfully!'));
457
+ logger.log(chalk.green(`\nFiles created in: ${generatedFiles.appPath}`));
458
+ logger.log(chalk.blue('\nNext steps:'));
459
+ logger.log(chalk.gray(` 1. Review the generated files in integration/${appName}/`));
460
+ logger.log(chalk.gray(' 2. Update env.template with your authentication details'));
461
+ logger.log(chalk.gray(` 3. Deploy using: ./deploy.sh or aifabrix deploy ${appName}`));
462
+ return generatedFiles;
463
+ } catch (error) {
464
+ spinner.stop();
465
+ throw error;
466
+ }
467
+ }
468
+
469
+ /**
470
+ * Setup dataplane URL and authentication
471
+ * @async
472
+ * @function setupDataplaneAndAuth
473
+ * @param {Object} options - Command options
474
+ * @param {string} appName - Application name
475
+ * @returns {Promise<Object>} Object with dataplaneUrl and authConfig
476
+ */
477
+ async function setupDataplaneAndAuth(options, appName) {
478
+ const { resolveEnvironment } = require('../core/config');
479
+ const environment = await resolveEnvironment();
480
+ const controllerUrl = await resolveControllerUrl();
481
+ let authConfig;
482
+ try {
483
+ // For wizard mode creating new external systems, use device-only auth
484
+ // since the app doesn't exist yet. Device token is sufficient for
485
+ // discovering the dataplane URL and running the wizard.
486
+ authConfig = await getDeviceOnlyAuth(controllerUrl);
487
+ } catch (error) {
488
+ // Fallback to getDeploymentAuth if device-only auth fails
489
+ // (e.g., for add-datasource mode where app might exist)
490
+ try {
491
+ authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
492
+ } catch (fallbackError) {
493
+ throw new Error(`Authentication failed: ${error.message}`);
494
+ }
495
+ }
496
+ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
497
+ let dataplaneUrl;
498
+ try {
499
+ dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
500
+ } catch (error) {
501
+ throw new Error(
502
+ `${error.message}\n\n` +
503
+ 'The dataplane URL is automatically discovered from the controller.\n' +
504
+ 'If discovery fails, ensure you are logged in and the controller is accessible:\n' +
505
+ ' aifabrix login'
506
+ );
507
+ }
508
+ return { dataplaneUrl, authConfig };
509
+ }
510
+
511
+ module.exports = {
512
+ validateAndCheckAppDirectory, extractSessionId, handleModeSelection, handleSourceSelection, handleOpenApiParsing,
513
+ handleCredentialSelection, handleTypeDetection, handleConfigurationGeneration, validateWizardConfiguration,
514
+ handleFileSaving, setupDataplaneAndAuth
515
+ };
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @fileoverview Wizard dataplane URL discovery utilities
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ const chalk = require('chalk');
8
+ const logger = require('../utils/logger');
9
+ const { getDataplaneUrl } = require('../datasource/deploy');
10
+ const { listEnvironmentApplications } = require('../api/environments.api');
11
+
12
+ /**
13
+ * Find dataplane service application key from environment applications list
14
+ * @async
15
+ * @function findDataplaneServiceAppKey
16
+ * @param {string} controllerUrl - Controller URL
17
+ * @param {string} environment - Environment key
18
+ * @param {Object} authConfig - Authentication configuration
19
+ * @returns {Promise<string|null>} Dataplane service appKey or null if not found
20
+ */
21
+ // eslint-disable-next-line complexity
22
+ async function findDataplaneServiceAppKey(controllerUrl, environment, authConfig) {
23
+ try {
24
+ const response = await listEnvironmentApplications(controllerUrl, environment, authConfig, { pageSize: 100 });
25
+ if (!response.success || !response.data) return null;
26
+ const applications = response.data.data || response.data || [];
27
+ for (const app of applications) {
28
+ const appKey = app.key || app.id;
29
+ if (!appKey) continue;
30
+ const keyLower = appKey.toLowerCase();
31
+ const appType = app.configuration?.type || app.type;
32
+ const nameLower = (app.displayName || app.name || '').toLowerCase();
33
+ if (keyLower === 'dataplane' || keyLower.includes('dataplane') || (appType === 'service' && nameLower.includes('dataplane'))) {
34
+ return appKey;
35
+ }
36
+ }
37
+ return null;
38
+ } catch (error) {
39
+ logger.log(chalk.gray(` Could not list applications: ${error.message}`));
40
+ return null;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Check if error is a "Not Found" error
46
+ * @param {Error} error - Error to check
47
+ * @returns {boolean} True if error is a "Not Found" error
48
+ */
49
+ function isNotFoundError(error) {
50
+ return error.message.includes('Not Found') ||
51
+ error.message.includes('not found') ||
52
+ error.message.includes('Application not found');
53
+ }
54
+
55
+ /**
56
+ * Create error message for missing dataplane service
57
+ * @returns {Error} Error with helpful message
58
+ */
59
+ function createDataplaneNotFoundError() {
60
+ return new Error(
61
+ 'Could not discover dataplane URL from controller. No dataplane service application found in this environment. ' +
62
+ 'Please provide the dataplane URL using --dataplane <url> flag.'
63
+ );
64
+ }
65
+
66
+ /**
67
+ * Try to get dataplane URL using fallback app key
68
+ * @async
69
+ * @param {string} controllerUrl - Controller URL
70
+ * @param {string} environment - Environment key
71
+ * @param {Object} authConfig - Authentication configuration
72
+ * @returns {Promise<string>} Dataplane URL
73
+ * @throws {Error} If dataplane URL cannot be retrieved
74
+ */
75
+ async function tryFallbackDataplaneUrl(controllerUrl, environment, authConfig) {
76
+ try {
77
+ const fallbackUrl = await getDataplaneUrl(controllerUrl, 'dataplane', environment, authConfig);
78
+ logger.log(chalk.green(`\u2713 Dataplane URL: ${fallbackUrl}`));
79
+ return fallbackUrl;
80
+ } catch (fallbackError) {
81
+ if (isNotFoundError(fallbackError)) {
82
+ throw createDataplaneNotFoundError();
83
+ }
84
+ throw fallbackError;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Discover dataplane URL from controller
90
+ * @async
91
+ * @function discoverDataplaneUrl
92
+ * @param {string} controllerUrl - Controller URL
93
+ * @param {string} environment - Environment key
94
+ * @param {Object} authConfig - Authentication configuration
95
+ * @returns {Promise<string>} Dataplane URL
96
+ * @throws {Error} If dataplane URL cannot be discovered
97
+ */
98
+ async function discoverDataplaneUrl(controllerUrl, environment, authConfig) {
99
+ logger.log(chalk.blue('\uD83C\uDF10 Getting dataplane URL from controller...'));
100
+ try {
101
+ const dataplaneAppKey = await findDataplaneServiceAppKey(controllerUrl, environment, authConfig);
102
+ if (dataplaneAppKey) {
103
+ const dataplaneUrl = await getDataplaneUrl(controllerUrl, dataplaneAppKey, environment, authConfig);
104
+ logger.log(chalk.green(`\u2713 Dataplane URL: ${dataplaneUrl}`));
105
+ return dataplaneUrl;
106
+ }
107
+ return await tryFallbackDataplaneUrl(controllerUrl, environment, authConfig);
108
+ } catch (error) {
109
+ if (error.message.includes('Could not discover dataplane URL')) {
110
+ throw error;
111
+ }
112
+ if (isNotFoundError(error) && error.message.includes('Failed to get application')) {
113
+ throw createDataplaneNotFoundError();
114
+ }
115
+ throw new Error(`Failed to discover dataplane URL: ${error.message}`);
116
+ }
117
+ }
118
+
119
+ module.exports = {
120
+ discoverDataplaneUrl,
121
+ findDataplaneServiceAppKey
122
+ };