@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.
Files changed (118) hide show
  1. package/README.md +9 -9
  2. package/integration/hubspot/README.md +2 -2
  3. package/integration/hubspot/hubspot-deploy-company.json +17 -14
  4. package/integration/hubspot/hubspot-deploy-contact.json +19 -16
  5. package/integration/hubspot/hubspot-deploy-deal.json +21 -18
  6. package/lib/api/types/datasources.types.js +31 -5
  7. package/lib/api/types/wizard.types.js +142 -0
  8. package/lib/api/wizard.api.js +177 -0
  9. package/lib/{app-config.js → app/config.js} +4 -4
  10. package/lib/{app-deploy.js → app/deploy.js} +8 -8
  11. package/lib/app/display.js +90 -0
  12. package/lib/{app-dockerfile.js → app/dockerfile.js} +4 -4
  13. package/lib/{app-down.js → app/down.js} +4 -4
  14. package/lib/app/helpers.js +218 -0
  15. package/lib/app/index.js +298 -0
  16. package/lib/{app-list.js → app/list.js} +6 -6
  17. package/lib/{app-push.js → app/push.js} +4 -4
  18. package/lib/{app-readme.js → app/readme.js} +34 -13
  19. package/lib/{app-register.js → app/register.js} +9 -9
  20. package/lib/{app-rotate-secret.js → app/rotate-secret.js} +10 -10
  21. package/lib/{app-run-helpers.js → app/run-helpers.js} +10 -10
  22. package/lib/{app-run.js → app/run.js} +6 -6
  23. package/lib/{build.js → build/index.js} +59 -32
  24. package/lib/build/package.json +7 -0
  25. package/lib/cli.js +245 -179
  26. package/lib/commands/app.js +3 -3
  27. package/lib/commands/datasource.js +4 -4
  28. package/lib/commands/login-credentials.js +209 -0
  29. package/lib/commands/login-device.js +254 -0
  30. package/lib/commands/login.js +67 -378
  31. package/lib/commands/logout.js +1 -1
  32. package/lib/commands/secrets-set.js +1 -1
  33. package/lib/commands/secure.js +2 -2
  34. package/lib/commands/wizard.js +498 -0
  35. package/lib/{audit-logger.js → core/audit-logger.js} +1 -1
  36. package/lib/{config.js → core/config.js} +28 -26
  37. package/lib/{diff.js → core/diff.js} +157 -72
  38. package/lib/{secrets.js → core/secrets.js} +86 -49
  39. package/lib/{templates.js → core/templates-env.js} +14 -222
  40. package/lib/core/templates.js +279 -0
  41. package/lib/{datasource-deploy.js → datasource/deploy.js} +6 -6
  42. package/lib/{datasource-diff.js → datasource/diff.js} +2 -2
  43. package/lib/datasource/list.js +223 -0
  44. package/lib/{datasource-validate.js → datasource/validate.js} +2 -2
  45. package/lib/{deployer.js → deployment/deployer.js} +48 -18
  46. package/lib/{environment-deploy.js → deployment/environment.js} +163 -84
  47. package/lib/{push.js → deployment/push.js} +1 -1
  48. package/lib/external-system/deploy-helpers.js +145 -0
  49. package/lib/{external-system-deploy.js → external-system/deploy.js} +156 -111
  50. package/lib/external-system/download-helpers.js +114 -0
  51. package/lib/{external-system-download.js → external-system/download.js} +92 -135
  52. package/lib/{external-system-generator.js → external-system/generator.js} +15 -11
  53. package/lib/external-system/test-auth.js +40 -0
  54. package/lib/external-system/test-execution.js +84 -0
  55. package/lib/external-system/test-helpers.js +109 -0
  56. package/lib/{external-system-test.js → external-system/test.js} +174 -192
  57. package/lib/{generator-builders.js → generator/builders.js} +87 -10
  58. package/lib/{generator-external.js → generator/external.js} +115 -52
  59. package/lib/{github-generator.js → generator/github.js} +116 -15
  60. package/lib/{generator-helpers.js → generator/helpers.js} +92 -42
  61. package/lib/{generator.js → generator/index.js} +49 -22
  62. package/lib/{generator-split.js → generator/split.js} +108 -55
  63. package/lib/generator/wizard-prompts.js +357 -0
  64. package/lib/generator/wizard.js +490 -0
  65. package/lib/{infra.js → infrastructure/index.js} +49 -22
  66. package/lib/schema/external-datasource.schema.json +158 -136
  67. package/lib/schema/external-system.schema.json +43 -1
  68. package/lib/utils/api.js +9 -5
  69. package/lib/utils/app-register-api.js +60 -32
  70. package/lib/utils/app-register-auth.js +172 -47
  71. package/lib/utils/app-register-config.js +130 -59
  72. package/lib/utils/app-run-containers.js +29 -8
  73. package/lib/utils/build-helpers.js +1 -1
  74. package/lib/utils/cli-utils.js +78 -30
  75. package/lib/utils/compose-generator.js +145 -65
  76. package/lib/utils/config-paths.js +2 -0
  77. package/lib/utils/deployment-errors.js +1 -1
  78. package/lib/utils/device-code.js +99 -41
  79. package/lib/utils/env-config-loader.js +1 -1
  80. package/lib/utils/env-copy.js +21 -18
  81. package/lib/utils/env-endpoints.js +115 -67
  82. package/lib/utils/env-map.js +13 -14
  83. package/lib/utils/env-ports.js +45 -25
  84. package/lib/utils/env-template.js +84 -42
  85. package/lib/utils/error-formatter.js +26 -9
  86. package/lib/utils/error-formatters/error-parser.js +90 -4
  87. package/lib/utils/error-formatters/http-status-errors.js +54 -17
  88. package/lib/utils/error-formatters/network-errors.js +103 -26
  89. package/lib/utils/external-system-display.js +184 -90
  90. package/lib/utils/external-system-validators.js +164 -42
  91. package/lib/utils/file-upload.js +109 -0
  92. package/lib/utils/health-check.js +199 -83
  93. package/lib/utils/infra-containers.js +1 -1
  94. package/lib/utils/infra-status.js +66 -15
  95. package/lib/utils/local-secrets.js +45 -25
  96. package/lib/utils/paths.js +45 -33
  97. package/lib/utils/schema-loader.js +42 -25
  98. package/lib/utils/schema-resolver.js +123 -74
  99. package/lib/utils/secrets-encryption.js +62 -25
  100. package/lib/utils/secrets-helpers.js +126 -63
  101. package/lib/utils/secrets-path.js +1 -1
  102. package/lib/utils/secrets-url.js +1 -1
  103. package/lib/utils/token-manager-refresh.js +181 -0
  104. package/lib/utils/token-manager.js +76 -123
  105. package/lib/utils/variable-transformer.js +154 -77
  106. package/lib/utils/yaml-preserve.js +41 -47
  107. package/lib/{template-validator.js → validation/template.js} +54 -23
  108. package/lib/{validate.js → validation/validate.js} +205 -125
  109. package/lib/{validator.js → validation/validator.js} +58 -39
  110. package/package.json +31 -2
  111. package/templates/external-system/deploy.ps1.hbs +34 -0
  112. package/templates/external-system/deploy.sh.hbs +34 -0
  113. package/templates/external-system/external-datasource.json.hbs +31 -12
  114. package/lib/app.js +0 -467
  115. package/lib/datasource-list.js +0 -141
  116. /package/lib/{app-prompts.js → app/prompts.js} +0 -0
  117. /package/lib/{env-reader.js → core/env-reader.js} +0 -0
  118. /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('./utils/paths');
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('./utils/token-encryption');
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(`Invalid developer-id value: "${developerId}". Must be a non-negative digit string.`);
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(`Invalid developer-id value: "${developerId}". Must contain only digits 0-9.`);
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(`Invalid developer-id value type: ${typeof developerId}. Must be a non-negative digit string.`);
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
- async function setDeveloperId(developerId) {
208
- const DEV_ID_DIGITS_REGEX = /^[0-9]+$/;
209
- const errorMsg = 'Developer ID must be a non-negative digit string or number (0 = default infra, > 0 = developer-specific)';
210
- let devIdString;
211
- if (typeof developerId === 'number') {
212
- if (!Number.isFinite(developerId) || developerId < 0) throw new Error(errorMsg);
213
- devIdString = String(developerId);
214
- } else if (typeof developerId === 'string') {
215
- if (!DEV_ID_DIGITS_REGEX.test(developerId)) throw new Error(errorMsg);
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('./utils/secrets-encryption');
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('./utils/config-tokens');
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('./utils/token-encryption').isTokenEncrypted
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('./utils/config-paths');
394
+ const { createPathConfigFunctions } = require('../utils/config-paths');
393
395
  const pathConfigFunctions = createPathConfigFunctions(getConfig, saveConfig);
394
396
  Object.assign(exportsObj, pathConfigFunctions);
395
397