@aifabrix/builder 2.32.3 → 2.33.1

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 (127) 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 +12 -11
  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 +6 -2
  40. package/lib/api/types/wizard.types.js +176 -38
  41. package/lib/api/wizard.api.js +161 -23
  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 +17 -10
  46. package/lib/app/readme.js +41 -112
  47. package/lib/app/register.js +44 -9
  48. package/lib/app/rotate-secret.js +48 -31
  49. package/lib/cli.js +219 -70
  50. package/lib/commands/app.js +4 -9
  51. package/lib/commands/auth-config.js +125 -0
  52. package/lib/commands/auth-status.js +7 -8
  53. package/lib/commands/datasource.js +3 -6
  54. package/lib/commands/login-credentials.js +4 -4
  55. package/lib/commands/login-device.js +26 -17
  56. package/lib/commands/login.js +12 -10
  57. package/lib/commands/wizard-config-normalizer.js +92 -0
  58. package/lib/commands/wizard-core.js +515 -0
  59. package/lib/commands/wizard-dataplane.js +122 -0
  60. package/lib/commands/wizard-headless.js +115 -0
  61. package/lib/commands/wizard.js +110 -332
  62. package/lib/core/config.js +46 -0
  63. package/lib/core/secrets.js +3 -22
  64. package/lib/core/templates-env.js +1 -1
  65. package/lib/datasource/deploy.js +59 -23
  66. package/lib/datasource/list.js +108 -19
  67. package/lib/deployment/deployer.js +25 -0
  68. package/lib/deployment/environment.js +10 -13
  69. package/lib/external-system/delete.js +151 -0
  70. package/lib/external-system/deploy.js +53 -378
  71. package/lib/external-system/download-helpers.js +45 -65
  72. package/lib/external-system/download.js +33 -13
  73. package/lib/external-system/generator.js +11 -7
  74. package/lib/external-system/test-auth.js +4 -3
  75. package/lib/generator/builders.js +3 -1
  76. package/lib/generator/external-controller-manifest.js +157 -0
  77. package/lib/generator/external-schema-utils.js +236 -0
  78. package/lib/generator/external.js +55 -3
  79. package/lib/generator/index.js +22 -10
  80. package/lib/generator/wizard-prompts.js +33 -10
  81. package/lib/generator/wizard.js +69 -86
  82. package/lib/infrastructure/compose.js +100 -0
  83. package/lib/infrastructure/helpers.js +139 -0
  84. package/lib/infrastructure/index.js +52 -311
  85. package/lib/infrastructure/services.js +168 -0
  86. package/lib/schema/application-schema.json +23 -4
  87. package/lib/schema/external-datasource.schema.json +2 -2
  88. package/lib/schema/wizard-config.schema.json +234 -0
  89. package/lib/utils/api.js +102 -52
  90. package/lib/utils/app-existence.js +42 -0
  91. package/lib/utils/app-register-config.js +7 -2
  92. package/lib/utils/auth-config-validator.js +92 -0
  93. package/lib/utils/command-header.js +43 -0
  94. package/lib/utils/compose-generator.js +113 -70
  95. package/lib/utils/controller-url.js +65 -17
  96. package/lib/utils/dataplane-health.js +115 -0
  97. package/lib/utils/dataplane-resolver.js +29 -0
  98. package/lib/utils/dev-config.js +6 -2
  99. package/lib/utils/env-copy.js +2 -1
  100. package/lib/utils/env-ports.js +2 -1
  101. package/lib/utils/env-template.js +1 -1
  102. package/lib/utils/error-formatter.js +49 -0
  103. package/lib/utils/error-formatters/network-errors.js +13 -3
  104. package/lib/utils/external-readme.js +125 -0
  105. package/lib/utils/help-builder.js +190 -0
  106. package/lib/utils/infra-status.js +13 -3
  107. package/lib/utils/paths.js +17 -2
  108. package/lib/utils/port-resolver.js +111 -0
  109. package/lib/utils/secrets-helpers.js +3 -15
  110. package/lib/utils/secrets-utils.js +2 -2
  111. package/lib/utils/token-manager.js +9 -4
  112. package/lib/utils/variable-transformer.js +7 -2
  113. package/lib/validation/external-manifest-validator.js +202 -0
  114. package/lib/validation/validate-display.js +406 -0
  115. package/lib/validation/validate.js +159 -123
  116. package/lib/validation/validator.js +36 -3
  117. package/lib/validation/wizard-config-validator.js +267 -0
  118. package/package.json +4 -2
  119. package/templates/applications/README.md.hbs +18 -16
  120. package/templates/applications/miso-controller/env.template +1 -1
  121. package/templates/applications/miso-controller/rbac.yaml +7 -7
  122. package/templates/external-system/README.md.hbs +99 -0
  123. package/templates/github/ci.yaml.hbs +44 -1
  124. package/templates/github/release.yaml.hbs +44 -0
  125. package/templates/infra/compose.yaml.hbs +35 -0
  126. package/templates/python/docker-compose.hbs +26 -0
  127. package/templates/typescript/docker-compose.hbs +26 -0
@@ -1,29 +1,14 @@
1
1
  /**
2
- * @fileoverview Wizard command handler - interactive external system creation
2
+ * @fileoverview Wizard command handler - interactive and headless external system creation
3
3
  * @author AI Fabrix Team
4
4
  * @version 2.0.0
5
5
  */
6
6
 
7
7
  const chalk = require('chalk');
8
- const ora = require('ora');
9
- const path = require('path');
10
- const fs = require('fs').promises;
11
8
  const logger = require('../utils/logger');
12
- const config = require('../core/config');
13
- const { getDeviceOnlyAuth } = require('../utils/token-manager');
14
- const { getDataplaneUrl } = require('../datasource/deploy');
15
- const { resolveControllerUrl } = require('../utils/controller-url');
16
- const {
17
- createWizardSession,
18
- updateWizardSession,
19
- parseOpenApi,
20
- detectType,
21
- generateConfig,
22
- validateWizardConfig,
23
- getDeploymentDocs
24
- } = require('../api/wizard.api');
25
9
  const {
26
10
  promptForMode,
11
+ promptForSystemIdOrKey,
27
12
  promptForSourceType,
28
13
  promptForOpenApiFile,
29
14
  promptForOpenApiUrl,
@@ -34,99 +19,90 @@ const {
34
19
  promptForConfigReview,
35
20
  promptForAppName
36
21
  } = require('../generator/wizard-prompts');
37
- const { generateWizardFiles } = require('../generator/wizard');
22
+ const {
23
+ validateAndCheckAppDirectory,
24
+ handleOpenApiParsing,
25
+ handleCredentialSelection,
26
+ handleTypeDetection,
27
+ handleConfigurationGeneration,
28
+ validateWizardConfiguration,
29
+ handleFileSaving,
30
+ setupDataplaneAndAuth
31
+ } = require('./wizard-core');
32
+ const { handleWizardHeadless } = require('./wizard-headless');
33
+ const { createWizardSession, updateWizardSession } = require('../api/wizard.api');
38
34
 
39
35
  /**
40
- * Validate app name and check if directory exists
41
- * @async
42
- * @function validateAndCheckAppDirectory
43
- * @param {string} appName - Application name
44
- * @returns {Promise<boolean>} True if should continue, false if cancelled
45
- * @throws {Error} If validation fails
36
+ * Extract session ID from response data
37
+ * @function extractSessionId
38
+ * @param {Object} responseData - Response data from API
39
+ * @returns {string} Session ID
40
+ * @throws {Error} If session ID not found or invalid
46
41
  */
47
- async function validateAndCheckAppDirectory(appName) {
48
- if (!/^[a-z0-9-_]+$/.test(appName)) {
49
- throw new Error('Application name must contain only lowercase letters, numbers, hyphens, and underscores');
42
+ function extractSessionId(responseData) {
43
+ let sessionId = responseData?.data?.sessionId || responseData?.sessionId ||
44
+ responseData?.data?.session_id || responseData?.session_id;
45
+ if (sessionId && typeof sessionId === 'object') {
46
+ sessionId = sessionId.id || sessionId.sessionId || sessionId.session_id;
50
47
  }
51
- const appPath = path.join(process.cwd(), 'integration', appName);
52
- try {
53
- await fs.access(appPath);
54
- const { overwrite } = await require('inquirer').prompt([{
55
- type: 'confirm',
56
- name: 'overwrite',
57
- message: `Directory ${appPath} already exists. Overwrite?`,
58
- default: false
59
- }]);
60
- if (!overwrite) {
61
- logger.log(chalk.yellow('Wizard cancelled.'));
62
- return false;
63
- }
64
- } catch (error) {
65
- if (error.code !== 'ENOENT') {
66
- throw error;
67
- }
48
+ if (!sessionId || typeof sessionId !== 'string') {
49
+ throw new Error(`Session ID not found: ${JSON.stringify(responseData, null, 2)}`);
68
50
  }
69
- return true;
51
+ return sessionId;
70
52
  }
71
53
 
72
54
  /**
73
- * Handle mode selection step - create wizard session
55
+ * Handle interactive mode selection step
74
56
  * @async
75
- * @function handleModeSelection
57
+ * @function handleInteractiveModeSelection
76
58
  * @param {string} dataplaneUrl - Dataplane URL
77
59
  * @param {Object} authConfig - Authentication configuration
78
60
  * @returns {Promise<Object>} Object with mode and sessionId
79
- * @throws {Error} If mode selection fails
80
61
  */
81
- async function handleModeSelection(dataplaneUrl, authConfig) {
82
- logger.log(chalk.blue('\n📋 Step 1: Mode Selection'));
62
+ async function handleInteractiveModeSelection(dataplaneUrl, authConfig) {
63
+ logger.log(chalk.blue('\n\uD83D\uDCCB Step 1: Mode Selection'));
83
64
  const mode = await promptForMode();
84
- const sessionResponse = await createWizardSession(dataplaneUrl, authConfig, mode);
65
+ let systemIdOrKey = null;
66
+ if (mode === 'add-datasource') {
67
+ systemIdOrKey = await promptForSystemIdOrKey();
68
+ }
69
+ const sessionResponse = await createWizardSession(dataplaneUrl, authConfig, mode, systemIdOrKey);
85
70
  if (!sessionResponse.success || !sessionResponse.data) {
86
- const errorMsg = sessionResponse.formattedError ||
87
- sessionResponse.error ||
88
- sessionResponse.errorData?.detail ||
89
- sessionResponse.message ||
71
+ const errorMsg = sessionResponse.formattedError || sessionResponse.error ||
72
+ sessionResponse.errorData?.detail || sessionResponse.message ||
90
73
  (sessionResponse.status ? `HTTP ${sessionResponse.status}` : 'Unknown error');
91
74
  throw new Error(`Failed to create wizard session: ${errorMsg}`);
92
75
  }
93
- const sessionId = sessionResponse.data.data?.sessionId || sessionResponse.data.sessionId;
94
- if (!sessionId) {
95
- throw new Error('Session ID not found in response');
96
- }
97
- return { mode, sessionId };
76
+ const sessionId = extractSessionId(sessionResponse.data);
77
+ return { mode, sessionId, systemIdOrKey };
98
78
  }
99
79
 
100
80
  /**
101
- * Handle source selection step - update wizard session
81
+ * Handle interactive source selection step
102
82
  * @async
103
- * @function handleSourceSelection
83
+ * @function handleInteractiveSourceSelection
104
84
  * @param {string} dataplaneUrl - Dataplane URL
105
85
  * @param {string} sessionId - Wizard session ID
106
86
  * @param {Object} authConfig - Authentication configuration
107
87
  * @returns {Promise<Object>} Object with sourceType and sourceData
108
- * @throws {Error} If source selection fails
109
88
  */
110
- async function handleSourceSelection(dataplaneUrl, sessionId, authConfig) {
111
- logger.log(chalk.blue('\n📋 Step 2: Source Selection'));
89
+ async function handleInteractiveSourceSelection(dataplaneUrl, sessionId, authConfig) {
90
+ logger.log(chalk.blue('\n\uD83D\uDCCB Step 2: Source Selection'));
112
91
  const sourceType = await promptForSourceType();
113
92
  let sourceData = null;
114
93
  const updateData = { currentStep: 1 };
115
94
 
116
95
  if (sourceType === 'openapi-file') {
117
- const filePath = await promptForOpenApiFile();
118
- sourceData = filePath;
96
+ sourceData = await promptForOpenApiFile();
119
97
  } else if (sourceType === 'openapi-url') {
120
- const url = await promptForOpenApiUrl();
121
- sourceData = url;
122
- updateData.openapiSpec = null; // Will be set after parsing
98
+ sourceData = await promptForOpenApiUrl();
99
+ updateData.openapiSpec = null;
123
100
  } else if (sourceType === 'mcp-server') {
124
101
  const mcpDetails = await promptForMcpServer();
125
102
  sourceData = JSON.stringify(mcpDetails);
126
103
  updateData.mcpServerUrl = mcpDetails.url || null;
127
104
  } else if (sourceType === 'known-platform') {
128
- const platform = await promptForKnownPlatform();
129
- sourceData = platform;
105
+ sourceData = await promptForKnownPlatform();
130
106
  }
131
107
 
132
108
  const updateResponse = await updateWizardSession(dataplaneUrl, sessionId, authConfig, updateData);
@@ -138,185 +114,53 @@ async function handleSourceSelection(dataplaneUrl, sessionId, authConfig) {
138
114
  }
139
115
 
140
116
  /**
141
- * Parse OpenAPI file
142
- * @async
143
- * @function parseOpenApiFile
144
- * @param {string} dataplaneUrl - Dataplane URL
145
- * @param {Object} authConfig - Authentication configuration
146
- * @param {string} sourceData - Source data (file path)
147
- * @returns {Promise<Object>} OpenAPI spec
148
- * @throws {Error} If parsing fails
149
- */
150
- async function parseOpenApiFile(dataplaneUrl, authConfig, sourceData) {
151
- logger.log(chalk.blue('\n📋 Step 3: Parsing OpenAPI File'));
152
- const spinner = ora('Parsing OpenAPI file...').start();
153
- try {
154
- const parseResponse = await parseOpenApi(dataplaneUrl, authConfig, sourceData);
155
- spinner.stop();
156
- if (!parseResponse.success) {
157
- throw new Error(`OpenAPI parsing failed: ${parseResponse.error || parseResponse.formattedError}`);
158
- }
159
- logger.log(chalk.green('✓ OpenAPI file parsed successfully'));
160
- return parseResponse.data?.spec;
161
- } catch (error) {
162
- spinner.stop();
163
- throw error;
164
- }
165
- }
166
-
167
- /**
168
- * Handle OpenAPI parsing step
117
+ * Handle interactive configuration generation step
169
118
  * @async
170
- * @function handleOpenApiParsing
171
- * @param {string} dataplaneUrl - Dataplane URL
172
- * @param {Object} authConfig - Authentication configuration
173
- * @param {string} sourceType - Source type
174
- * @param {string} sourceData - Source data
175
- * @returns {Promise<Object|null>} OpenAPI spec or null
176
- * @throws {Error} If parsing fails
119
+ * @function handleInteractiveConfigGeneration
120
+ * @param {Object} options - Configuration options
121
+ * @param {string} options.dataplaneUrl - Dataplane URL
122
+ * @param {Object} options.authConfig - Authentication configuration
123
+ * @param {string} options.mode - Selected mode
124
+ * @param {Object} options.openapiSpec - OpenAPI specification
125
+ * @param {Object} options.detectedType - Detected type info
126
+ * @param {string} [options.credentialIdOrKey] - Credential ID or key (optional)
127
+ * @param {string} [options.systemIdOrKey] - System ID or key (optional)
128
+ * @returns {Promise<Object>} Generated configuration
177
129
  */
178
- async function handleOpenApiParsing(dataplaneUrl, authConfig, sourceType, sourceData) {
179
- if (sourceType === 'openapi-file') {
180
- return await parseOpenApiFile(dataplaneUrl, authConfig, sourceData);
181
- }
182
- if (sourceType === 'openapi-url') {
183
- logger.log(chalk.blue('\n📋 Step 3: Parsing OpenAPI URL'));
184
- logger.log(chalk.green('✓ OpenAPI URL processed'));
185
- return null;
186
- }
187
- return null;
188
- }
189
-
190
- /**
191
- * Handle type detection step
192
- * @async
193
- * @function handleTypeDetection
194
- * @param {string} dataplaneUrl - Dataplane URL
195
- * @param {Object} authConfig - Authentication configuration
196
- * @param {Object} openApiSpec - OpenAPI specification
197
- * @returns {Promise<Object|null>} Detected type or null
198
- */
199
- async function handleTypeDetection(dataplaneUrl, authConfig, openApiSpec) {
200
- if (!openApiSpec) {
201
- return null;
202
- }
203
-
204
- logger.log(chalk.blue('\n📋 Step 4: Detecting API Type'));
205
- const spinner = ora('Detecting API type...').start();
206
- try {
207
- const detectResponse = await detectType(dataplaneUrl, authConfig, openApiSpec);
208
- spinner.stop();
209
- if (detectResponse.success && detectResponse.data) {
210
- const detectedType = detectResponse.data;
211
- logger.log(chalk.green(`✓ API type detected: ${detectedType.apiType || 'unknown'}`));
212
- if (detectedType.category) {
213
- logger.log(chalk.gray(` Category: ${detectedType.category}`));
214
- }
215
- return detectedType;
216
- }
217
- } catch (error) {
218
- spinner.stop();
219
- logger.log(chalk.yellow(`⚠ Type detection failed: ${error.message}`));
220
- }
221
- return null;
222
- }
223
-
224
- /**
225
- * Handle configuration generation step
226
- * @async
227
- * @function handleConfigurationGeneration
228
- * @param {string} dataplaneUrl - Dataplane URL
229
- * @param {Object} authConfig - Authentication configuration
230
- * @param {string} mode - Selected mode
231
- * @param {string} sourceType - Source type
232
- * @param {Object} openApiSpec - OpenAPI specification
233
- * @returns {Promise<Object>} Generated configuration with systemConfig, datasourceConfigs, and systemKey
234
- * @throws {Error} If generation fails
235
- */
236
- async function handleConfigurationGeneration(dataplaneUrl, authConfig, mode, sourceType, openApiSpec) {
237
- logger.log(chalk.blue('\n📋 Step 5: User Preferences'));
130
+ async function handleInteractiveConfigGeneration(options) {
131
+ logger.log(chalk.blue('\n\uD83D\uDCCB Step 5: User Preferences'));
238
132
  const userIntent = await promptForUserIntent();
239
133
  const preferences = await promptForUserPreferences();
240
134
 
241
- logger.log(chalk.blue('\n📋 Step 6: Generating Configuration'));
242
- const spinner = ora('Generating configuration via AI (this may take 10-30 seconds)...').start();
243
- try {
244
- const generateResponse = await generateConfig(dataplaneUrl, authConfig, {
245
- mode,
246
- sourceType,
247
- openApiSpec,
248
- userIntent,
249
- preferences
250
- });
251
- spinner.stop();
252
- if (!generateResponse.success) {
253
- throw new Error(`Configuration generation failed: ${generateResponse.error || generateResponse.formattedError}`);
254
- }
255
-
256
- const systemConfig = generateResponse.data?.systemConfig;
257
- const datasourceConfigs = generateResponse.data?.datasourceConfigs || [];
258
- const systemKey = generateResponse.data?.systemKey;
259
-
260
- if (!systemConfig) {
261
- throw new Error('System configuration not found in generation response');
262
- }
263
-
264
- logger.log(chalk.green('✓ Configuration generated successfully'));
265
- return { systemConfig, datasourceConfigs, systemKey };
266
- } catch (error) {
267
- spinner.stop();
268
- throw error;
269
- }
135
+ const configPrefs = {
136
+ intent: userIntent,
137
+ enableMCP: preferences.mcp,
138
+ enableABAC: preferences.abac,
139
+ enableRBAC: preferences.rbac
140
+ };
141
+
142
+ return await handleConfigurationGeneration(options.dataplaneUrl, options.authConfig, {
143
+ mode: options.mode,
144
+ openapiSpec: options.openapiSpec,
145
+ detectedType: options.detectedType,
146
+ configPrefs,
147
+ credentialIdOrKey: options.credentialIdOrKey,
148
+ systemIdOrKey: options.systemIdOrKey
149
+ });
270
150
  }
271
151
 
272
152
  /**
273
- * Validate wizard configuration
274
- * @async
275
- * @function validateWizardConfiguration
276
- * @param {string} dataplaneUrl - Dataplane URL
277
- * @param {Object} authConfig - Authentication configuration
278
- * @param {Object} systemConfig - System configuration
279
- * @param {Object[]} datasourceConfigs - Datasource configurations
280
- * @throws {Error} If validation fails
281
- */
282
- async function validateWizardConfiguration(dataplaneUrl, authConfig, systemConfig, datasourceConfigs) {
283
- const validateSpinner = ora('Validating configuration...').start();
284
- try {
285
- const validateResponse = await validateWizardConfig(dataplaneUrl, authConfig, systemConfig, datasourceConfigs);
286
- validateSpinner.stop();
287
- if (!validateResponse.success || !validateResponse.data?.valid) {
288
- const errors = validateResponse.data?.errors || [];
289
- const errorMsg = errors.length > 0
290
- ? errors.map(e => e.message || e).join(', ')
291
- : validateResponse.error || validateResponse.formattedError || 'Validation failed';
292
- throw new Error(`Configuration validation failed: ${errorMsg}`);
293
- }
294
- logger.log(chalk.green('✓ Configuration validated successfully'));
295
- if (validateResponse.data?.warnings && validateResponse.data.warnings.length > 0) {
296
- logger.log(chalk.yellow('\n⚠ Warnings:'));
297
- validateResponse.data.warnings.forEach(warning => {
298
- logger.log(chalk.yellow(` - ${warning.message || warning}`));
299
- });
300
- }
301
- } catch (error) {
302
- validateSpinner.stop();
303
- throw error;
304
- }
305
- }
306
-
307
- /**
308
- * Handle configuration review and validation step
153
+ * Handle configuration review and validation step (interactive mode only)
309
154
  * @async
310
155
  * @function handleConfigurationReview
311
156
  * @param {string} dataplaneUrl - Dataplane URL
312
157
  * @param {Object} authConfig - Authentication configuration
313
158
  * @param {Object} systemConfig - System configuration
314
159
  * @param {Object[]} datasourceConfigs - Datasource configurations
315
- * @returns {Promise<Object>} Final configurations with systemConfig and datasourceConfigs
316
- * @throws {Error} If validation fails
160
+ * @returns {Promise<Object>} Final configurations
317
161
  */
318
162
  async function handleConfigurationReview(dataplaneUrl, authConfig, systemConfig, datasourceConfigs) {
319
- logger.log(chalk.blue('\n📋 Step 7: Review & Validate'));
163
+ logger.log(chalk.blue('\n\uD83D\uDCCB Step 6-7: Review & Validate'));
320
164
  const reviewResult = await promptForConfigReview(systemConfig, datasourceConfigs);
321
165
 
322
166
  if (reviewResult.action === 'cancel') {
@@ -324,97 +168,55 @@ async function handleConfigurationReview(dataplaneUrl, authConfig, systemConfig,
324
168
  return null;
325
169
  }
326
170
 
327
- // Use edited configs if user edited them
328
171
  const finalSystemConfig = reviewResult.systemConfig || systemConfig;
329
172
  const finalDatasourceConfigs = reviewResult.datasourceConfigs || datasourceConfigs;
330
173
 
331
- // Validate configuration
332
174
  await validateWizardConfiguration(dataplaneUrl, authConfig, finalSystemConfig, finalDatasourceConfigs);
333
175
 
334
176
  return { systemConfig: finalSystemConfig, datasourceConfigs: finalDatasourceConfigs };
335
177
  }
336
178
 
337
179
  /**
338
- * Handle file saving step
339
- * @async
340
- * @function handleFileSaving
341
- * @param {string} appName - Application name
342
- * @param {Object} systemConfig - System configuration
343
- * @param {Object[]} datasourceConfigs - Datasource configurations
344
- * @param {string} systemKey - System key
345
- * @param {string} dataplaneUrl - Dataplane URL
346
- * @param {Object} authConfig - Authentication configuration
347
- * @returns {Promise<Object>} Generated files information
348
- * @throws {Error} If file saving fails
349
- */
350
- async function handleFileSaving(appName, systemConfig, datasourceConfigs, systemKey, dataplaneUrl, authConfig) {
351
- logger.log(chalk.blue('\n📋 Step 8: Saving Files'));
352
- const saveSpinner = ora('Saving files...').start();
353
- try {
354
- let aiGeneratedReadme = null;
355
- if (systemKey && dataplaneUrl && authConfig) {
356
- try {
357
- const docsResponse = await getDeploymentDocs(dataplaneUrl, authConfig, systemKey);
358
- if (docsResponse.success && docsResponse.data?.content) {
359
- aiGeneratedReadme = docsResponse.data.content;
360
- logger.log(chalk.gray(' ✓ Fetched AI-generated README.md from dataplane'));
361
- }
362
- } catch (error) {
363
- logger.log(chalk.gray(` ⚠ Could not fetch AI-generated README: ${error.message}`));
364
- }
365
- }
366
- const generatedFiles = await generateWizardFiles(appName, systemConfig, datasourceConfigs, systemKey, { aiGeneratedReadme });
367
- saveSpinner.stop();
368
- logger.log(chalk.green('\n✓ Wizard completed successfully!'));
369
- logger.log(chalk.green(`\nFiles created in: ${generatedFiles.appPath}`));
370
- logger.log(chalk.blue('\nNext steps:'));
371
- [` 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)));
372
- return generatedFiles;
373
- } catch (error) {
374
- saveSpinner.stop();
375
- throw error;
376
- }
377
- }
378
-
379
- /**
380
- * Execute wizard flow steps
180
+ * Execute wizard flow steps (interactive mode)
381
181
  * @async
382
182
  * @function executeWizardFlow
383
183
  * @param {string} appName - Application name
384
184
  * @param {string} dataplaneUrl - Dataplane URL
385
185
  * @param {Object} authConfig - Authentication configuration
386
186
  * @returns {Promise<void>} Resolves when wizard flow completes
387
- * @throws {Error} If wizard flow fails
388
187
  */
389
188
  async function executeWizardFlow(appName, dataplaneUrl, authConfig) {
390
- // Step 1: Mode Selection - Create wizard session
391
- const { mode, sessionId } = await handleModeSelection(dataplaneUrl, authConfig);
189
+ // Step 1: Mode Selection
190
+ const { mode, sessionId, systemIdOrKey } = await handleInteractiveModeSelection(dataplaneUrl, authConfig);
392
191
 
393
- // Step 2: Source Selection - Update session
394
- const { sourceType, sourceData } = await handleSourceSelection(dataplaneUrl, sessionId, authConfig);
192
+ // Step 2: Source Selection
193
+ const { sourceType, sourceData } = await handleInteractiveSourceSelection(dataplaneUrl, sessionId, authConfig);
395
194
 
396
- // Step 3: Parse OpenAPI (if applicable)
397
- const openApiSpec = await handleOpenApiParsing(dataplaneUrl, authConfig, sourceType, sourceData);
195
+ // Parse OpenAPI (part of step 2)
196
+ const openapiSpec = await handleOpenApiParsing(dataplaneUrl, authConfig, sourceType, sourceData);
398
197
 
399
- // Step 4: Detect Type (optional, if OpenAPI spec available)
400
- await handleTypeDetection(dataplaneUrl, authConfig, openApiSpec);
198
+ // Step 3: Credential Selection (optional)
199
+ const credentialIdOrKey = await handleCredentialSelection(dataplaneUrl, authConfig);
401
200
 
402
- // Step 5-6: Generate Configuration
403
- const { systemConfig, datasourceConfigs, systemKey } = await handleConfigurationGeneration(
201
+ // Step 4: Detect Type
202
+ const detectedType = await handleTypeDetection(dataplaneUrl, authConfig, openapiSpec);
203
+
204
+ // Step 5: Generate Configuration
205
+ const { systemConfig, datasourceConfigs, systemKey } = await handleInteractiveConfigGeneration({
404
206
  dataplaneUrl,
405
207
  authConfig,
406
208
  mode,
407
- sourceType,
408
- openApiSpec
409
- );
209
+ openapiSpec,
210
+ detectedType,
211
+ credentialIdOrKey,
212
+ systemIdOrKey
213
+ });
410
214
 
411
- // Step 7: Review & Validate
215
+ // Step 6-7: Review & Validate
412
216
  const finalConfigs = await handleConfigurationReview(dataplaneUrl, authConfig, systemConfig, datasourceConfigs);
413
- if (!finalConfigs) {
414
- return; // User cancelled
415
- }
217
+ if (!finalConfigs) return;
416
218
 
417
- // Step 8: Save Files
219
+ // Step 7: Save Files
418
220
  await handleFileSaving(
419
221
  appName,
420
222
  finalConfigs.systemConfig,
@@ -434,11 +236,17 @@ async function executeWizardFlow(appName, dataplaneUrl, authConfig) {
434
236
  * @param {string} [options.controller] - Controller URL
435
237
  * @param {string} [options.environment] - Environment key
436
238
  * @param {string} [options.dataplane] - Dataplane URL (overrides controller lookup)
239
+ * @param {string} [options.config] - Path to wizard.yaml config file (headless mode)
437
240
  * @returns {Promise<void>} Resolves when wizard completes
438
241
  * @throws {Error} If wizard fails
439
242
  */
440
243
  async function handleWizard(options = {}) {
441
- logger.log(chalk.blue('\n🧙 AI Fabrix External System Wizard\n'));
244
+ // Check if headless mode (config file provided)
245
+ if (options.config) {
246
+ return await handleWizardHeadless(options);
247
+ }
248
+
249
+ logger.log(chalk.blue('\n\uD83E\uDDD9 AI Fabrix External System Wizard\n'));
442
250
 
443
251
  // Get or prompt for app name
444
252
  let appName = options.app;
@@ -447,7 +255,7 @@ async function handleWizard(options = {}) {
447
255
  }
448
256
 
449
257
  // Validate app name and check directory
450
- const shouldContinue = await validateAndCheckAppDirectory(appName);
258
+ const shouldContinue = await validateAndCheckAppDirectory(appName, true);
451
259
  if (!shouldContinue) {
452
260
  return;
453
261
  }
@@ -459,34 +267,4 @@ async function handleWizard(options = {}) {
459
267
  await executeWizardFlow(appName, dataplaneUrl, authConfig);
460
268
  }
461
269
 
462
- /**
463
- * Setup dataplane URL and authentication
464
- * @async
465
- * @function setupDataplaneAndAuth
466
- * @param {Object} options - Command options
467
- * @param {string} appName - Application name
468
- * @returns {Promise<Object>} Object with dataplaneUrl and authConfig
469
- * @throws {Error} If setup fails
470
- */
471
- async function setupDataplaneAndAuth(options, appName) {
472
- const configData = await config.getConfig();
473
- const environment = options.environment || 'dev';
474
- const controllerUrl = await resolveControllerUrl(options, configData);
475
- // Wizard requires device token authentication (user-level), not client credentials
476
- const authConfig = await getDeviceOnlyAuth(controllerUrl);
477
-
478
- let dataplaneUrl = options.dataplane;
479
- if (!dataplaneUrl) {
480
- logger.log(chalk.blue('🌐 Getting dataplane URL from controller...'));
481
- try {
482
- dataplaneUrl = await getDataplaneUrl(controllerUrl, 'dataplane', environment, authConfig);
483
- logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
484
- } catch (error) {
485
- const example = `aifabrix wizard -a ${appName} --dataplane https://dataplane.example.com -e ${environment} -c ${controllerUrl}`;
486
- throw new Error(`${error.message}\n\n💡 For new applications, provide the dataplane URL using:\n --dataplane <dataplane-url>\n\n Example: ${example}`);
487
- }
488
- }
489
-
490
- return { dataplaneUrl, authConfig };
491
- }
492
- module.exports = { handleWizard };
270
+ module.exports = { handleWizard, handleWizardHeadless };
@@ -95,6 +95,8 @@ function applyConfigDefaults(config) {
95
95
  if (typeof config.device !== 'object' || config.device === null) {
96
96
  config.device = {};
97
97
  }
98
+ // Ensure controller field exists (but don't set defaults)
99
+ // It will be set by login or auth config commands
98
100
  return config;
99
101
  }
100
102
 
@@ -107,6 +109,7 @@ function getDefaultConfig() {
107
109
  return {
108
110
  'developer-id': '0',
109
111
  environment: 'dev',
112
+ controller: undefined,
110
113
  environments: {},
111
114
  device: {}
112
115
  };
@@ -239,6 +242,17 @@ async function getCurrentEnvironment() {
239
242
  return config.environment || 'dev';
240
243
  }
241
244
 
245
+ /**
246
+ * Resolve environment from configuration
247
+ * Uses config.environment, defaults to 'dev'
248
+ * @async
249
+ * @function resolveEnvironment
250
+ * @returns {Promise<string>} Environment key
251
+ */
252
+ async function resolveEnvironment() {
253
+ return await getCurrentEnvironment();
254
+ }
255
+
242
256
  async function setCurrentEnvironment(environment) {
243
257
  if (!environment || typeof environment !== 'string') {
244
258
  throw new Error('Environment must be a non-empty string');
@@ -248,6 +262,35 @@ async function setCurrentEnvironment(environment) {
248
262
  await saveConfig(config);
249
263
  }
250
264
 
265
+ /**
266
+ * Set controller URL in configuration
267
+ * @async
268
+ * @function setControllerUrl
269
+ * @param {string} controllerUrl - Controller URL to save
270
+ * @returns {Promise<void>}
271
+ * @throws {Error} If controller URL is invalid
272
+ */
273
+ async function setControllerUrl(controllerUrl) {
274
+ if (!controllerUrl || typeof controllerUrl !== 'string') {
275
+ throw new Error('Controller URL is required and must be a string');
276
+ }
277
+ const normalizedUrl = normalizeControllerUrl(controllerUrl);
278
+ const config = await getConfig();
279
+ config.controller = normalizedUrl;
280
+ await saveConfig(config);
281
+ }
282
+
283
+ /**
284
+ * Get controller URL from configuration
285
+ * @async
286
+ * @function getControllerUrl
287
+ * @returns {Promise<string|null>} Controller URL or null if not set
288
+ */
289
+ async function getControllerUrl() {
290
+ const config = await getConfig();
291
+ return config.controller || null;
292
+ }
293
+
251
294
  function isTokenExpired(expiresAt) {
252
295
  if (!expiresAt) return true;
253
296
  const expirationTime = new Date(expiresAt).getTime();
@@ -354,6 +397,7 @@ const exportsObj = {
354
397
  loadDeveloperId,
355
398
  getCurrentEnvironment,
356
399
  setCurrentEnvironment,
400
+ resolveEnvironment,
357
401
  isTokenExpired,
358
402
  shouldRefreshToken,
359
403
  encryptTokenValue,
@@ -363,6 +407,8 @@ const exportsObj = {
363
407
  getSecretsPath,
364
408
  setSecretsPath,
365
409
  normalizeControllerUrl,
410
+ setControllerUrl,
411
+ getControllerUrl,
366
412
  CONFIG_DIR,
367
413
  CONFIG_FILE
368
414
  };