@aifabrix/builder 2.7.0 → 2.9.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 (47) hide show
  1. package/.cursor/rules/project-rules.mdc +680 -0
  2. package/integration/hubspot/README.md +136 -0
  3. package/integration/hubspot/env.template +9 -0
  4. package/integration/hubspot/hubspot-deploy-company.json +200 -0
  5. package/integration/hubspot/hubspot-deploy-contact.json +228 -0
  6. package/integration/hubspot/hubspot-deploy-deal.json +248 -0
  7. package/integration/hubspot/hubspot-deploy.json +91 -0
  8. package/integration/hubspot/variables.yaml +17 -0
  9. package/lib/app-config.js +13 -2
  10. package/lib/app-deploy.js +9 -3
  11. package/lib/app-dockerfile.js +14 -1
  12. package/lib/app-prompts.js +177 -13
  13. package/lib/app-push.js +16 -1
  14. package/lib/app-register.js +37 -5
  15. package/lib/app-rotate-secret.js +10 -0
  16. package/lib/app-run.js +19 -0
  17. package/lib/app.js +70 -25
  18. package/lib/audit-logger.js +9 -4
  19. package/lib/build.js +25 -13
  20. package/lib/cli.js +109 -2
  21. package/lib/commands/login.js +40 -3
  22. package/lib/config.js +121 -114
  23. package/lib/datasource-deploy.js +14 -20
  24. package/lib/environment-deploy.js +305 -0
  25. package/lib/external-system-deploy.js +345 -0
  26. package/lib/external-system-download.js +431 -0
  27. package/lib/external-system-generator.js +190 -0
  28. package/lib/external-system-test.js +446 -0
  29. package/lib/generator-builders.js +323 -0
  30. package/lib/generator.js +200 -292
  31. package/lib/schema/application-schema.json +830 -800
  32. package/lib/schema/external-datasource.schema.json +868 -46
  33. package/lib/schema/external-system.schema.json +98 -80
  34. package/lib/schema/infrastructure-schema.json +1 -1
  35. package/lib/templates.js +32 -1
  36. package/lib/utils/cli-utils.js +4 -4
  37. package/lib/utils/device-code.js +10 -2
  38. package/lib/utils/external-system-display.js +159 -0
  39. package/lib/utils/external-system-validators.js +245 -0
  40. package/lib/utils/paths.js +151 -1
  41. package/lib/utils/schema-resolver.js +7 -2
  42. package/lib/utils/token-encryption.js +68 -0
  43. package/lib/validator.js +52 -5
  44. package/package.json +1 -1
  45. package/tatus +181 -0
  46. package/templates/external-system/external-datasource.json.hbs +55 -0
  47. package/templates/external-system/external-system.json.hbs +37 -0
@@ -0,0 +1,305 @@
1
+ /**
2
+ * AI Fabrix Builder Environment Deployment Module
3
+ *
4
+ * Handles environment deployment/setup in Miso Controller.
5
+ * Sets up environment infrastructure before applications can be deployed.
6
+ *
7
+ * @fileoverview Environment deployment for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const chalk = require('chalk');
13
+ const logger = require('./utils/logger');
14
+ const config = require('./config');
15
+ const { validateControllerUrl, validateEnvironmentKey } = require('./utils/deployment-validation');
16
+ const { getOrRefreshDeviceToken } = require('./utils/token-manager');
17
+ const { authenticatedApiCall } = require('./utils/api');
18
+ const { handleDeploymentErrors } = require('./utils/deployment-errors');
19
+ const auditLogger = require('./audit-logger');
20
+
21
+ /**
22
+ * Validates environment deployment prerequisites
23
+ * @param {string} envKey - Environment key
24
+ * @param {string} controllerUrl - Controller URL
25
+ * @throws {Error} If prerequisites are not met
26
+ */
27
+ function validateEnvironmentPrerequisites(envKey, controllerUrl) {
28
+ if (!envKey || typeof envKey !== 'string') {
29
+ throw new Error('Environment key is required');
30
+ }
31
+
32
+ if (!controllerUrl || typeof controllerUrl !== 'string') {
33
+ throw new Error('Controller URL is required');
34
+ }
35
+
36
+ // Validate environment key format
37
+ validateEnvironmentKey(envKey);
38
+
39
+ // Validate controller URL
40
+ validateControllerUrl(controllerUrl);
41
+ }
42
+
43
+ /**
44
+ * Gets authentication for environment deployment
45
+ * Uses device token (not app-specific client credentials)
46
+ * @async
47
+ * @param {string} controllerUrl - Controller URL
48
+ * @returns {Promise<Object>} Authentication configuration
49
+ * @throws {Error} If authentication is not available
50
+ */
51
+ async function getEnvironmentAuth(controllerUrl) {
52
+ const validatedUrl = validateControllerUrl(controllerUrl);
53
+
54
+ // Get or refresh device token
55
+ const deviceToken = await getOrRefreshDeviceToken(validatedUrl);
56
+
57
+ if (!deviceToken || !deviceToken.token) {
58
+ throw new Error('Device token is required for environment deployment. Run "aifabrix login" first to authenticate.');
59
+ }
60
+
61
+ return {
62
+ type: 'device',
63
+ token: deviceToken.token,
64
+ controller: deviceToken.controller || validatedUrl
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Sends environment deployment request to controller
70
+ * @async
71
+ * @param {string} controllerUrl - Controller URL
72
+ * @param {string} envKey - Environment key
73
+ * @param {Object} authConfig - Authentication configuration
74
+ * @param {Object} options - Deployment options
75
+ * @returns {Promise<Object>} Deployment result
76
+ * @throws {Error} If deployment fails
77
+ */
78
+ async function sendEnvironmentDeployment(controllerUrl, envKey, authConfig, options = {}) {
79
+ const validatedUrl = validateControllerUrl(controllerUrl);
80
+ const validatedEnvKey = validateEnvironmentKey(envKey);
81
+
82
+ // Build environment deployment request
83
+ const deploymentRequest = {
84
+ key: validatedEnvKey,
85
+ displayName: `${validatedEnvKey.charAt(0).toUpperCase() + validatedEnvKey.slice(1)} Environment`,
86
+ description: `${validatedEnvKey.charAt(0).toUpperCase() + validatedEnvKey.slice(1)} environment for application deployments`
87
+ };
88
+
89
+ // Add configuration if provided
90
+ if (options.config) {
91
+ // TODO: Load and parse config file if provided
92
+ // For now, just include the config path in description
93
+ deploymentRequest.description += ` (config: ${options.config})`;
94
+ }
95
+
96
+ // API endpoint: POST /api/v1/environments/{env}/deploy
97
+ // Alternative: POST /api/v1/environments/deploy with environment in body
98
+ const endpoint = `${validatedUrl}/api/v1/environments/${validatedEnvKey}/deploy`;
99
+
100
+ // Log deployment attempt for audit
101
+ await auditLogger.logDeploymentAttempt(validatedEnvKey, validatedUrl, options);
102
+
103
+ try {
104
+ const response = await authenticatedApiCall(
105
+ endpoint,
106
+ {
107
+ method: 'POST',
108
+ body: JSON.stringify(deploymentRequest)
109
+ },
110
+ authConfig.token
111
+ );
112
+
113
+ if (!response.success) {
114
+ const error = new Error(response.formattedError || response.error || 'Environment deployment failed');
115
+ error.status = response.status;
116
+ error.data = response.errorData;
117
+ throw error;
118
+ }
119
+
120
+ // Handle response structure
121
+ const responseData = response.data || {};
122
+ return {
123
+ success: true,
124
+ environment: validatedEnvKey,
125
+ deploymentId: responseData.deploymentId || responseData.id,
126
+ status: responseData.status || 'initiated',
127
+ url: responseData.url || `${validatedUrl}/environments/${validatedEnvKey}`,
128
+ message: responseData.message
129
+ };
130
+ } catch (error) {
131
+ // Use unified error handler
132
+ await handleDeploymentErrors(error, validatedEnvKey, validatedUrl, false);
133
+ throw error;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Polls environment deployment status
139
+ * @async
140
+ * @param {string} deploymentId - Deployment ID
141
+ * @param {string} controllerUrl - Controller URL
142
+ * @param {string} envKey - Environment key
143
+ * @param {Object} authConfig - Authentication configuration
144
+ * @param {Object} options - Polling options
145
+ * @returns {Promise<Object>} Final deployment status
146
+ */
147
+ async function pollEnvironmentStatus(deploymentId, controllerUrl, envKey, authConfig, options = {}) {
148
+ const validatedUrl = validateControllerUrl(controllerUrl);
149
+ const validatedEnvKey = validateEnvironmentKey(envKey);
150
+
151
+ const pollInterval = options.pollInterval || 5000;
152
+ const maxAttempts = options.maxAttempts || 60;
153
+ const statusEndpoint = `${validatedUrl}/api/v1/environments/${validatedEnvKey}/status`;
154
+
155
+ logger.log(chalk.blue(`⏳ Polling environment status (${pollInterval}ms intervals)...`));
156
+
157
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
158
+ try {
159
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
160
+
161
+ const response = await authenticatedApiCall(
162
+ statusEndpoint,
163
+ {
164
+ method: 'GET'
165
+ },
166
+ authConfig.token
167
+ );
168
+
169
+ if (response.success && response.data) {
170
+ const status = response.data.status || response.data.ready;
171
+ const isReady = status === 'ready' || status === 'completed' || response.data.ready === true;
172
+
173
+ if (isReady) {
174
+ return {
175
+ success: true,
176
+ environment: validatedEnvKey,
177
+ status: 'ready',
178
+ message: 'Environment is ready for application deployments'
179
+ };
180
+ }
181
+
182
+ // Check for terminal failure states
183
+ if (status === 'failed' || status === 'error') {
184
+ throw new Error(`Environment deployment failed: ${response.data.message || 'Unknown error'}`);
185
+ }
186
+ }
187
+ } catch (error) {
188
+ // If it's a terminal error (not a timeout), throw it
189
+ if (error.message && error.message.includes('failed')) {
190
+ throw error;
191
+ }
192
+ // Otherwise, continue polling
193
+ }
194
+
195
+ if (attempt < maxAttempts) {
196
+ logger.log(chalk.gray(` Attempt ${attempt}/${maxAttempts}...`));
197
+ }
198
+ }
199
+
200
+ // Timeout
201
+ throw new Error(`Environment deployment timeout after ${maxAttempts} attempts. Check controller logs for status.`);
202
+ }
203
+
204
+ /**
205
+ * Displays environment deployment results
206
+ * @param {Object} result - Deployment result
207
+ */
208
+ function displayDeploymentResults(result) {
209
+ logger.log(chalk.green('\n✅ Environment deployed successfully'));
210
+ logger.log(chalk.blue(` Environment: ${result.environment}`));
211
+ logger.log(chalk.blue(` Status: ${result.status === 'ready' ? '✅ ready' : result.status}`));
212
+ if (result.url) {
213
+ logger.log(chalk.blue(` URL: ${result.url}`));
214
+ }
215
+ if (result.deploymentId) {
216
+ logger.log(chalk.blue(` Deployment ID: ${result.deploymentId}`));
217
+ }
218
+ logger.log(chalk.green('\n✓ Environment is ready for application deployments'));
219
+ }
220
+
221
+ /**
222
+ * Deploys/setups an environment in the controller
223
+ * @async
224
+ * @function deployEnvironment
225
+ * @param {string} envKey - Environment key (miso, dev, tst, pro)
226
+ * @param {Object} options - Deployment options
227
+ * @param {string} options.controller - Controller URL (required)
228
+ * @param {string} [options.config] - Environment configuration file (optional)
229
+ * @param {boolean} [options.skipValidation] - Skip validation checks
230
+ * @param {boolean} [options.poll] - Poll for deployment status (default: true)
231
+ * @param {boolean} [options.noPoll] - Do not poll for status
232
+ * @returns {Promise<Object>} Deployment result
233
+ * @throws {Error} If deployment fails
234
+ *
235
+ * @example
236
+ * await deployEnvironment('dev', { controller: 'https://controller.aifabrix.ai' });
237
+ */
238
+ async function deployEnvironment(envKey, options = {}) {
239
+ try {
240
+ // 1. Input validation
241
+ if (!envKey || typeof envKey !== 'string' || envKey.trim().length === 0) {
242
+ throw new Error('Environment key is required');
243
+ }
244
+
245
+ const controllerUrl = options.controller || options['controller-url'];
246
+ if (!controllerUrl) {
247
+ throw new Error('Controller URL is required. Use --controller flag');
248
+ }
249
+
250
+ // 2. Validate prerequisites
251
+ if (!options.skipValidation) {
252
+ validateEnvironmentPrerequisites(envKey, controllerUrl);
253
+ }
254
+
255
+ // 3. Update root-level environment in config.yaml
256
+ await config.setCurrentEnvironment(envKey);
257
+
258
+ // 4. Get authentication (device token)
259
+ logger.log(chalk.blue(`\n📋 Deploying environment '${envKey}' to ${controllerUrl}...`));
260
+ const authConfig = await getEnvironmentAuth(controllerUrl);
261
+ logger.log(chalk.green('✓ Environment validated'));
262
+ logger.log(chalk.green('✓ Authentication successful'));
263
+
264
+ // 5. Send environment deployment request
265
+ logger.log(chalk.blue('\n🚀 Deploying environment infrastructure...'));
266
+ const validatedControllerUrl = validateControllerUrl(authConfig.controller);
267
+ const result = await sendEnvironmentDeployment(validatedControllerUrl, envKey, authConfig, options);
268
+
269
+ logger.log(chalk.blue(`📤 Sending deployment request to ${validatedControllerUrl}/api/v1/environments/${envKey}/deploy...`));
270
+
271
+ // 6. Poll for status if enabled
272
+ const shouldPoll = options.poll !== false && !options.noPoll;
273
+ if (shouldPoll && result.deploymentId) {
274
+ const pollResult = await pollEnvironmentStatus(
275
+ result.deploymentId,
276
+ validatedControllerUrl,
277
+ envKey,
278
+ authConfig,
279
+ {
280
+ pollInterval: 5000,
281
+ maxAttempts: 60
282
+ }
283
+ );
284
+ result.status = pollResult.status;
285
+ result.message = pollResult.message;
286
+ }
287
+
288
+ // 7. Display results
289
+ displayDeploymentResults(result);
290
+
291
+ return result;
292
+ } catch (error) {
293
+ // Error handling is done in sendEnvironmentDeployment and pollEnvironmentStatus
294
+ // Re-throw with context
295
+ if (error._logged !== true) {
296
+ logger.error(chalk.red(`\n❌ Environment deployment failed: ${error.message}`));
297
+ }
298
+ throw error;
299
+ }
300
+ }
301
+
302
+ module.exports = {
303
+ deployEnvironment
304
+ };
305
+
@@ -0,0 +1,345 @@
1
+ /**
2
+ * External System Deployment Module
3
+ *
4
+ * Handles deployment of external systems and datasources via pipeline API
5
+ * for external type applications.
6
+ *
7
+ * @fileoverview External system deployment for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const fs = require('fs').promises;
13
+ const fsSync = require('fs');
14
+ const path = require('path');
15
+ const yaml = require('js-yaml');
16
+ const chalk = require('chalk');
17
+ const { authenticatedApiCall } = require('./utils/api');
18
+ const { getDeploymentAuth } = require('./utils/token-manager');
19
+ const { getConfig } = require('./config');
20
+ const logger = require('./utils/logger');
21
+ const { getDataplaneUrl } = require('./datasource-deploy');
22
+ const { detectAppType, getDeployJsonPath } = require('./utils/paths');
23
+ const { generateExternalSystemApplicationSchema } = require('./generator');
24
+
25
+ /**
26
+ * Loads variables.yaml for an application
27
+ * @async
28
+ * @function loadVariablesYaml
29
+ * @param {string} appName - Application name
30
+ * @returns {Promise<Object>} Variables configuration
31
+ * @throws {Error} If file cannot be loaded
32
+ */
33
+ async function loadVariablesYaml(appName) {
34
+ // Detect app type and get correct path (integration or builder)
35
+ const { appPath } = await detectAppType(appName);
36
+ const variablesPath = path.join(appPath, 'variables.yaml');
37
+ const content = await fs.readFile(variablesPath, 'utf8');
38
+ return yaml.load(content);
39
+ }
40
+
41
+ /**
42
+ * Validates external system files exist
43
+ * @async
44
+ * @function validateExternalSystemFiles
45
+ * @param {string} appName - Application name
46
+ * @returns {Promise<Object>} Validation result with file paths
47
+ * @throws {Error} If validation fails
48
+ */
49
+ async function validateExternalSystemFiles(appName) {
50
+ const variables = await loadVariablesYaml(appName);
51
+
52
+ if (!variables.externalIntegration) {
53
+ throw new Error('externalIntegration block not found in variables.yaml');
54
+ }
55
+
56
+ // Detect app type and get correct path (integration or builder)
57
+ const { appPath } = await detectAppType(appName);
58
+
59
+ // For new structure, files are in same folder (schemaBasePath is usually './')
60
+ // For backward compatibility, support old schemas/ subfolder
61
+ const schemaBasePath = variables.externalIntegration.schemaBasePath || './';
62
+ const schemasPath = path.isAbsolute(schemaBasePath)
63
+ ? schemaBasePath
64
+ : path.join(appPath, schemaBasePath);
65
+
66
+ // Validate system files
67
+ const systemFiles = [];
68
+ if (variables.externalIntegration.systems && variables.externalIntegration.systems.length > 0) {
69
+ for (const systemFile of variables.externalIntegration.systems) {
70
+ // Try new naming first: <app-name>-deploy.json in same folder
71
+ const newSystemPath = getDeployJsonPath(appName, 'external', true);
72
+ if (fsSync.existsSync(newSystemPath)) {
73
+ systemFiles.push(newSystemPath);
74
+ } else {
75
+ // Fall back to specified path
76
+ const systemPath = path.join(schemasPath, systemFile);
77
+ try {
78
+ await fs.access(systemPath);
79
+ systemFiles.push(systemPath);
80
+ } catch {
81
+ throw new Error(`External system file not found: ${systemPath} (also checked: ${newSystemPath})`);
82
+ }
83
+ }
84
+ }
85
+ } else {
86
+ throw new Error('No external system files specified in externalIntegration.systems');
87
+ }
88
+
89
+ // Validate datasource files (naming: <app-name>-deploy-<datasource-key>.json)
90
+ const datasourceFiles = [];
91
+ if (variables.externalIntegration.dataSources && variables.externalIntegration.dataSources.length > 0) {
92
+ for (const datasourceFile of variables.externalIntegration.dataSources) {
93
+ // Try same folder first (new structure)
94
+ const datasourcePath = path.join(appPath, datasourceFile);
95
+ try {
96
+ await fs.access(datasourcePath);
97
+ datasourceFiles.push(datasourcePath);
98
+ } catch {
99
+ // Fall back to schemaBasePath
100
+ const fallbackPath = path.join(schemasPath, datasourceFile);
101
+ try {
102
+ await fs.access(fallbackPath);
103
+ datasourceFiles.push(fallbackPath);
104
+ } catch {
105
+ throw new Error(`External datasource file not found: ${datasourcePath} or ${fallbackPath}`);
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ // Extract systemKey from system file (remove -deploy.json suffix if present)
112
+ const systemFileName = path.basename(systemFiles[0], '.json');
113
+ const systemKey = systemFileName.replace(/-deploy$/, '');
114
+
115
+ return {
116
+ systemFiles,
117
+ datasourceFiles,
118
+ systemKey
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Deploys external system to dataplane (build step - deploy, not publish)
124
+ * @async
125
+ * @function buildExternalSystem
126
+ * @param {string} appName - Application name
127
+ * @param {Object} options - Deployment options
128
+ * @returns {Promise<void>} Resolves when deployment completes
129
+ * @throws {Error} If deployment fails
130
+ */
131
+ async function buildExternalSystem(appName, options = {}) {
132
+ try {
133
+ logger.log(chalk.blue(`\n🔨 Building external system: ${appName}`));
134
+
135
+ // Validate files
136
+ const { systemFiles, datasourceFiles, systemKey } = await validateExternalSystemFiles(appName);
137
+
138
+ // Get authentication
139
+ const config = await getConfig();
140
+ const environment = options.environment || 'dev';
141
+ const controllerUrl = options.controller || config.deployment?.controllerUrl || 'http://localhost:3000';
142
+ const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
143
+
144
+ if (!authConfig.token && !authConfig.clientId) {
145
+ throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
146
+ }
147
+
148
+ // Get dataplane URL from controller
149
+ logger.log(chalk.blue('🌐 Getting dataplane URL from controller...'));
150
+ const dataplaneUrl = await getDataplaneUrl(controllerUrl, appName, environment, authConfig);
151
+ logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
152
+
153
+ // Deploy external system
154
+ logger.log(chalk.blue(`Deploying external system: ${systemKey}...`));
155
+ const systemContent = await fs.readFile(systemFiles[0], 'utf8');
156
+ const systemJson = JSON.parse(systemContent);
157
+
158
+ const systemResponse = await authenticatedApiCall(
159
+ `${dataplaneUrl}/api/v1/pipeline/deploy`,
160
+ {
161
+ method: 'POST',
162
+ body: JSON.stringify(systemJson)
163
+ },
164
+ authConfig.token
165
+ );
166
+
167
+ if (!systemResponse.success) {
168
+ throw new Error(`Failed to deploy external system: ${systemResponse.error || systemResponse.formattedError}`);
169
+ }
170
+
171
+ logger.log(chalk.green(`✓ External system deployed: ${systemKey}`));
172
+
173
+ // Deploy datasources
174
+ for (const datasourceFile of datasourceFiles) {
175
+ const datasourceName = path.basename(datasourceFile, '.json');
176
+ logger.log(chalk.blue(`Deploying datasource: ${datasourceName}...`));
177
+
178
+ const datasourceContent = await fs.readFile(datasourceFile, 'utf8');
179
+ const datasourceJson = JSON.parse(datasourceContent);
180
+
181
+ const datasourceResponse = await authenticatedApiCall(
182
+ `${dataplaneUrl}/api/v1/pipeline/${systemKey}/deploy`,
183
+ {
184
+ method: 'POST',
185
+ body: JSON.stringify(datasourceJson)
186
+ },
187
+ authConfig.token
188
+ );
189
+
190
+ if (!datasourceResponse.success) {
191
+ throw new Error(`Failed to deploy datasource ${datasourceName}: ${datasourceResponse.error || datasourceResponse.formattedError}`);
192
+ }
193
+
194
+ logger.log(chalk.green(`✓ Datasource deployed: ${datasourceName}`));
195
+ }
196
+
197
+ logger.log(chalk.green('\n✅ External system built successfully!'));
198
+ logger.log(chalk.blue(`System: ${systemKey}`));
199
+ logger.log(chalk.blue(`Datasources: ${datasourceFiles.length}`));
200
+ } catch (error) {
201
+ throw new Error(`Failed to build external system: ${error.message}`);
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Publishes external system to dataplane using application-level workflow
207
+ * Uses upload → validate → publish workflow for atomic deployment
208
+ * @async
209
+ * @function deployExternalSystem
210
+ * @param {string} appName - Application name
211
+ * @param {Object} options - Deployment options
212
+ * @param {string} [options.environment] - Environment (dev, tst, pro)
213
+ * @param {string} [options.controller] - Controller URL
214
+ * @param {boolean} [options.skipValidation] - Skip validation step and go straight to publish
215
+ * @param {boolean} [options.generateMcpContract] - Generate MCP contract (default: true)
216
+ * @returns {Promise<void>} Resolves when deployment completes
217
+ * @throws {Error} If deployment fails
218
+ */
219
+ async function deployExternalSystem(appName, options = {}) {
220
+ try {
221
+ logger.log(chalk.blue(`\n🚀 Publishing external system: ${appName}`));
222
+
223
+ // Validate files
224
+ const { systemFiles: _systemFiles, datasourceFiles, systemKey } = await validateExternalSystemFiles(appName);
225
+
226
+ // Generate application-schema.json structure
227
+ logger.log(chalk.blue('📋 Generating application schema...'));
228
+ const applicationSchema = await generateExternalSystemApplicationSchema(appName);
229
+ logger.log(chalk.green('✓ Application schema generated'));
230
+
231
+ // Get authentication
232
+ const config = await getConfig();
233
+ const environment = options.environment || 'dev';
234
+ const controllerUrl = options.controller || config.deployment?.controllerUrl || 'http://localhost:3000';
235
+ const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
236
+
237
+ if (!authConfig.token && !authConfig.clientId) {
238
+ throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
239
+ }
240
+
241
+ // Get dataplane URL from controller
242
+ logger.log(chalk.blue('🌐 Getting dataplane URL from controller...'));
243
+ const dataplaneUrl = await getDataplaneUrl(controllerUrl, appName, environment, authConfig);
244
+ logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
245
+
246
+ // Step 1: Upload application
247
+ logger.log(chalk.blue('📤 Uploading application configuration...'));
248
+ const uploadResponse = await authenticatedApiCall(
249
+ `${dataplaneUrl}/api/v1/pipeline/upload`,
250
+ {
251
+ method: 'POST',
252
+ body: JSON.stringify(applicationSchema)
253
+ },
254
+ authConfig.token
255
+ );
256
+
257
+ if (!uploadResponse.success || !uploadResponse.data) {
258
+ throw new Error(`Failed to upload application: ${uploadResponse.error || uploadResponse.formattedError || 'Unknown error'}`);
259
+ }
260
+
261
+ const uploadData = uploadResponse.data.data || uploadResponse.data;
262
+ const uploadId = uploadData.uploadId || uploadData.id;
263
+
264
+ if (!uploadId) {
265
+ throw new Error('Upload ID not found in upload response');
266
+ }
267
+
268
+ logger.log(chalk.green(`✓ Upload successful (ID: ${uploadId})`));
269
+
270
+ // Step 2: Validate upload (optional, can be skipped)
271
+ if (!options.skipValidation) {
272
+ logger.log(chalk.blue('🔍 Validating upload...'));
273
+ const validateResponse = await authenticatedApiCall(
274
+ `${dataplaneUrl}/api/v1/pipeline/upload/${uploadId}/validate`,
275
+ {
276
+ method: 'POST'
277
+ },
278
+ authConfig.token
279
+ );
280
+
281
+ if (!validateResponse.success || !validateResponse.data) {
282
+ throw new Error(`Validation failed: ${validateResponse.error || validateResponse.formattedError || 'Unknown error'}`);
283
+ }
284
+
285
+ const validateData = validateResponse.data.data || validateResponse.data;
286
+
287
+ // Display changes
288
+ if (validateData.changes && validateData.changes.length > 0) {
289
+ logger.log(chalk.blue('\n📋 Changes to be published:'));
290
+ for (const change of validateData.changes) {
291
+ const changeType = change.type || 'unknown';
292
+ const changeEntity = change.entity || change.key || 'unknown';
293
+ const emoji = changeType === 'new' ? '➕' : changeType === 'modified' ? '✏️' : '🗑️';
294
+ logger.log(chalk.gray(` ${emoji} ${changeType}: ${changeEntity}`));
295
+ }
296
+ }
297
+
298
+ if (validateData.summary) {
299
+ logger.log(chalk.blue(`\n📊 Summary: ${validateData.summary}`));
300
+ }
301
+
302
+ logger.log(chalk.green('✓ Validation successful'));
303
+ } else {
304
+ logger.log(chalk.yellow('⚠ Skipping validation step'));
305
+ }
306
+
307
+ // Step 3: Publish application
308
+ const generateMcpContract = options.generateMcpContract !== false; // Default to true
309
+ logger.log(chalk.blue(`📢 Publishing application (MCP contract: ${generateMcpContract ? 'enabled' : 'disabled'})...`));
310
+
311
+ const publishResponse = await authenticatedApiCall(
312
+ `${dataplaneUrl}/api/v1/pipeline/upload/${uploadId}/publish?generateMcpContract=${generateMcpContract}`,
313
+ {
314
+ method: 'POST'
315
+ },
316
+ authConfig.token
317
+ );
318
+
319
+ if (!publishResponse.success || !publishResponse.data) {
320
+ throw new Error(`Failed to publish application: ${publishResponse.error || publishResponse.formattedError || 'Unknown error'}`);
321
+ }
322
+
323
+ const publishData = publishResponse.data.data || publishResponse.data;
324
+
325
+ logger.log(chalk.green('\n✅ External system published successfully!'));
326
+ logger.log(chalk.blue(`System: ${systemKey}`));
327
+ if (publishData.systems && publishData.systems.length > 0) {
328
+ logger.log(chalk.blue(`Published systems: ${publishData.systems.length}`));
329
+ }
330
+ if (publishData.dataSources && publishData.dataSources.length > 0) {
331
+ logger.log(chalk.blue(`Published datasources: ${publishData.dataSources.length}`));
332
+ } else {
333
+ logger.log(chalk.blue(`Datasources: ${datasourceFiles.length}`));
334
+ }
335
+ } catch (error) {
336
+ throw new Error(`Failed to deploy external system: ${error.message}`);
337
+ }
338
+ }
339
+
340
+ module.exports = {
341
+ buildExternalSystem,
342
+ deployExternalSystem,
343
+ validateExternalSystemFiles
344
+ };
345
+