@aifabrix/builder 2.22.1 → 2.31.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 (65) hide show
  1. package/jest.config.coverage.js +37 -0
  2. package/lib/api/pipeline.api.js +10 -9
  3. package/lib/app-deploy.js +36 -14
  4. package/lib/app-list.js +191 -71
  5. package/lib/app-prompts.js +77 -26
  6. package/lib/app-readme.js +123 -5
  7. package/lib/app-rotate-secret.js +101 -57
  8. package/lib/app-run-helpers.js +200 -172
  9. package/lib/app-run.js +137 -68
  10. package/lib/audit-logger.js +8 -7
  11. package/lib/build.js +161 -250
  12. package/lib/cli.js +73 -65
  13. package/lib/commands/login.js +45 -31
  14. package/lib/commands/logout.js +181 -0
  15. package/lib/commands/secrets-set.js +2 -2
  16. package/lib/commands/secure.js +61 -26
  17. package/lib/config.js +79 -45
  18. package/lib/datasource-deploy.js +89 -29
  19. package/lib/deployer.js +164 -129
  20. package/lib/diff.js +63 -21
  21. package/lib/environment-deploy.js +36 -19
  22. package/lib/external-system-deploy.js +134 -66
  23. package/lib/external-system-download.js +244 -171
  24. package/lib/external-system-test.js +199 -164
  25. package/lib/generator-external.js +145 -72
  26. package/lib/generator-helpers.js +49 -17
  27. package/lib/generator-split.js +105 -58
  28. package/lib/infra.js +101 -131
  29. package/lib/schema/application-schema.json +895 -896
  30. package/lib/schema/env-config.yaml +11 -4
  31. package/lib/template-validator.js +13 -4
  32. package/lib/utils/api.js +8 -8
  33. package/lib/utils/app-register-auth.js +36 -18
  34. package/lib/utils/app-run-containers.js +140 -0
  35. package/lib/utils/auth-headers.js +6 -6
  36. package/lib/utils/build-copy.js +60 -2
  37. package/lib/utils/build-helpers.js +94 -0
  38. package/lib/utils/cli-utils.js +177 -76
  39. package/lib/utils/compose-generator.js +12 -2
  40. package/lib/utils/config-tokens.js +151 -9
  41. package/lib/utils/deployment-errors.js +137 -69
  42. package/lib/utils/deployment-validation-helpers.js +103 -0
  43. package/lib/utils/docker-build.js +57 -0
  44. package/lib/utils/dockerfile-utils.js +13 -3
  45. package/lib/utils/env-copy.js +163 -94
  46. package/lib/utils/env-map.js +226 -86
  47. package/lib/utils/environment-checker.js +2 -2
  48. package/lib/utils/error-formatters/network-errors.js +0 -1
  49. package/lib/utils/external-system-display.js +14 -19
  50. package/lib/utils/external-system-env-helpers.js +107 -0
  51. package/lib/utils/external-system-test-helpers.js +144 -0
  52. package/lib/utils/health-check.js +10 -8
  53. package/lib/utils/infra-status.js +123 -0
  54. package/lib/utils/local-secrets.js +3 -2
  55. package/lib/utils/paths.js +228 -49
  56. package/lib/utils/schema-loader.js +125 -57
  57. package/lib/utils/token-manager.js +10 -7
  58. package/lib/utils/yaml-preserve.js +55 -16
  59. package/lib/validate.js +87 -89
  60. package/package.json +4 -4
  61. package/scripts/ci-fix.sh +19 -0
  62. package/scripts/ci-simulate.sh +19 -0
  63. package/templates/applications/miso-controller/test.yaml +1 -0
  64. package/templates/python/Dockerfile.hbs +8 -45
  65. package/templates/typescript/Dockerfile.hbs +8 -42
@@ -54,11 +54,8 @@ function displayTestResults(results, verbose = false) {
54
54
 
55
55
  if (dsResult.metadataSchemaResults) {
56
56
  const ms = dsResult.metadataSchemaResults;
57
- if (ms.valid) {
58
- logger.log(chalk.gray(' Metadata schema: ✓ Valid'));
59
- } else {
60
- logger.log(chalk.red(' Metadata schema: ✗ Invalid'));
61
- }
57
+ const statusMsg = ms.valid ? ' Metadata schema: ✓ Valid' : ' Metadata schema: ✗ Invalid';
58
+ logger.log(ms.valid ? chalk.gray(statusMsg) : chalk.red(statusMsg));
62
59
  }
63
60
  }
64
61
  }
@@ -110,20 +107,18 @@ function displayIntegrationTestResults(results, verbose = false) {
110
107
  }
111
108
  }
112
109
 
113
- if (verbose) {
114
- if (dsResult.validationResults) {
115
- const vr = dsResult.validationResults;
116
- if (vr.isValid) {
117
- logger.log(chalk.gray(' Validation: Valid'));
118
- } else {
119
- logger.log(chalk.red(' Validation: ✗ Invalid'));
120
- if (vr.errors && vr.errors.length > 0) {
121
- vr.errors.forEach(err => logger.log(chalk.red(` - ${err}`)));
122
- }
123
- }
124
- if (vr.warnings && vr.warnings.length > 0) {
125
- vr.warnings.forEach(warn => logger.log(chalk.yellow(` ⚠ ${warn}`)));
126
- }
110
+ if (verbose && dsResult.validationResults) {
111
+ const vr = dsResult.validationResults;
112
+ if (vr.isValid) {
113
+ logger.log(chalk.gray(' Validation: ✓ Valid'));
114
+ } else {
115
+ logger.log(chalk.red(' Validation: Invalid'));
116
+ }
117
+ if (vr.errors && vr.errors.length > 0) {
118
+ vr.errors.forEach(err => logger.log(chalk.red(` - ${err}`)));
119
+ }
120
+ if (vr.warnings && vr.warnings.length > 0) {
121
+ vr.warnings.forEach(warn => logger.log(chalk.yellow(` ⚠ ${warn}`)));
127
122
  }
128
123
 
129
124
  if (dsResult.fieldMappingResults) {
@@ -0,0 +1,107 @@
1
+ /**
2
+ * External System Environment Variable Helpers
3
+ *
4
+ * Helper functions for extracting environment variables from external system configurations.
5
+ * Separated from external-system-download.js to maintain file size limits.
6
+ *
7
+ * @fileoverview External system environment variable extraction helpers
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ /**
13
+ * Extract OAuth2 environment variables
14
+ * @param {Object} oauth2 - OAuth2 configuration
15
+ * @param {string} systemKey - System key
16
+ * @param {Array<string>} lines - Lines array to append to
17
+ */
18
+ function extractOAuth2EnvVars(oauth2, systemKey, lines) {
19
+ if (oauth2.clientId && oauth2.clientId.includes('{{')) {
20
+ const key = oauth2.clientId.replace(/[{}]/g, '').trim();
21
+ lines.push(`${key}=kv://secrets/${systemKey}/client-id`);
22
+ }
23
+ if (oauth2.clientSecret && oauth2.clientSecret.includes('{{')) {
24
+ const key = oauth2.clientSecret.replace(/[{}]/g, '').trim();
25
+ lines.push(`${key}=kv://secrets/${systemKey}/client-secret`);
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Extract API Key environment variables
31
+ * @param {Object} apikey - API Key configuration
32
+ * @param {string} systemKey - System key
33
+ * @param {Array<string>} lines - Lines array to append to
34
+ */
35
+ function extractApiKeyEnvVars(apikey, systemKey, lines) {
36
+ if (apikey.key && apikey.key.includes('{{')) {
37
+ const key = apikey.key.replace(/[{}]/g, '').trim();
38
+ lines.push(`${key}=kv://secrets/${systemKey}/api-key`);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Extract Basic Auth environment variables
44
+ * @param {Object} basic - Basic Auth configuration
45
+ * @param {string} systemKey - System key
46
+ * @param {Array<string>} lines - Lines array to append to
47
+ */
48
+ function extractBasicAuthEnvVars(basic, systemKey, lines) {
49
+ if (basic.username && basic.username.includes('{{')) {
50
+ const key = basic.username.replace(/[{}]/g, '').trim();
51
+ lines.push(`${key}=kv://secrets/${systemKey}/username`);
52
+ }
53
+ if (basic.password && basic.password.includes('{{')) {
54
+ const key = basic.password.replace(/[{}]/g, '').trim();
55
+ lines.push(`${key}=kv://secrets/${systemKey}/password`);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Extract authentication environment variables
61
+ * @param {Object} auth - Authentication configuration
62
+ * @param {string} systemKey - System key
63
+ * @param {Array<string>} lines - Lines array to append to
64
+ */
65
+ function extractAuthEnvVars(auth, systemKey, lines) {
66
+ // OAuth2 configuration
67
+ if (auth.type === 'oauth2' && auth.oauth2) {
68
+ extractOAuth2EnvVars(auth.oauth2, systemKey, lines);
69
+ }
70
+
71
+ // API Key configuration
72
+ if (auth.type === 'apikey' && auth.apikey) {
73
+ extractApiKeyEnvVars(auth.apikey, systemKey, lines);
74
+ }
75
+
76
+ // Basic Auth configuration
77
+ if (auth.type === 'basic' && auth.basic) {
78
+ extractBasicAuthEnvVars(auth.basic, systemKey, lines);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Extracts environment variables from authentication configuration
84
+ * @param {Object} application - External system configuration
85
+ * @returns {string} Environment variables template content
86
+ */
87
+ function generateEnvTemplate(application) {
88
+ const lines = ['# Environment variables for external system'];
89
+ lines.push(`# System: ${application.key || 'unknown'}`);
90
+ lines.push('');
91
+
92
+ if (!application.authentication) {
93
+ return lines.join('\n');
94
+ }
95
+
96
+ extractAuthEnvVars(application.authentication, application.key, lines);
97
+ return lines.join('\n');
98
+ }
99
+
100
+ module.exports = {
101
+ extractOAuth2EnvVars,
102
+ extractApiKeyEnvVars,
103
+ extractBasicAuthEnvVars,
104
+ extractAuthEnvVars,
105
+ generateEnvTemplate
106
+ };
107
+
@@ -0,0 +1,144 @@
1
+ /**
2
+ * External System Test Helper Functions
3
+ *
4
+ * Helper functions for external system testing.
5
+ * Separated from external-system-test.js to maintain file size limits.
6
+ *
7
+ * @fileoverview External system test helper functions
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const fs = require('fs').promises;
13
+ const path = require('path');
14
+ const { testDatasourceViaPipeline } = require('../api/pipeline.api');
15
+
16
+ /**
17
+ * Retry API call with exponential backoff
18
+ * @async
19
+ * @param {Function} fn - Function to retry
20
+ * @param {number} maxRetries - Maximum number of retries
21
+ * @param {number} backoffMs - Initial backoff delay in milliseconds
22
+ * @returns {Promise<any>} Function result
23
+ * @throws {Error} Last error if all retries fail
24
+ */
25
+ async function retryApiCall(fn, maxRetries = 3, backoffMs = 1000) {
26
+ let lastError;
27
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
28
+ try {
29
+ return await fn();
30
+ } catch (error) {
31
+ lastError = error;
32
+ if (attempt < maxRetries) {
33
+ const delay = backoffMs * Math.pow(2, attempt);
34
+ await new Promise(resolve => setTimeout(resolve, delay));
35
+ }
36
+ }
37
+ }
38
+ throw lastError;
39
+ }
40
+
41
+ /**
42
+ * Calls pipeline test endpoint using centralized API client
43
+ * @async
44
+ * @param {Object} params - Function parameters
45
+ * @param {string} params.systemKey - System key
46
+ * @param {string} params.datasourceKey - Datasource key
47
+ * @param {Object} params.payloadTemplate - Test payload template
48
+ * @param {string} params.dataplaneUrl - Dataplane URL
49
+ * @param {Object} params.authConfig - Authentication configuration
50
+ * @param {number} [params.timeout] - Request timeout in milliseconds (default: 30000)
51
+ * @returns {Promise<Object>} Test response
52
+ */
53
+ async function callPipelineTestEndpoint({ systemKey, datasourceKey, payloadTemplate, dataplaneUrl, authConfig, timeout = 30000 }) {
54
+ const response = await retryApiCall(async() => {
55
+ return await testDatasourceViaPipeline({
56
+ dataplaneUrl,
57
+ systemKey,
58
+ datasourceKey,
59
+ authConfig,
60
+ testData: { payloadTemplate },
61
+ options: { timeout }
62
+ });
63
+ });
64
+
65
+ if (!response.success || !response.data) {
66
+ throw new Error(`Test endpoint failed: ${response.error || response.formattedError || 'Unknown error'}`);
67
+ }
68
+
69
+ return response.data.data || response.data;
70
+ }
71
+
72
+ /**
73
+ * Load custom payload from file if provided
74
+ * @async
75
+ * @param {string} [payloadPath] - Path to custom payload file
76
+ * @returns {Promise<Object|null>} Custom payload or null
77
+ */
78
+ async function loadCustomPayload(payloadPath) {
79
+ if (!payloadPath) {
80
+ return null;
81
+ }
82
+
83
+ const absolutePath = path.isAbsolute(payloadPath) ? payloadPath : path.join(process.cwd(), payloadPath);
84
+ const payloadContent = await fs.readFile(absolutePath, 'utf8');
85
+ return JSON.parse(payloadContent);
86
+ }
87
+
88
+ /**
89
+ * Determine payload template for a datasource
90
+ * @param {Object} datasource - Datasource configuration
91
+ * @param {string} datasourceKey - Datasource key
92
+ * @param {Object|null} customPayload - Custom payload if provided
93
+ * @returns {Object|null} Payload template or null
94
+ */
95
+ function determinePayloadTemplate(datasource, datasourceKey, customPayload) {
96
+ if (customPayload) {
97
+ return customPayload;
98
+ }
99
+ if (datasource.testPayload && datasource.testPayload.payloadTemplate) {
100
+ return datasource.testPayload.payloadTemplate;
101
+ }
102
+ return null;
103
+ }
104
+
105
+ /**
106
+ * Test a single datasource via pipeline
107
+ * @async
108
+ * @param {Object} params - Test parameters
109
+ * @param {string} params.systemKey - System key
110
+ * @param {string} params.datasourceKey - Datasource key
111
+ * @param {Object} params.payloadTemplate - Payload template
112
+ * @param {string} params.dataplaneUrl - Dataplane URL
113
+ * @param {Object} params.authConfig - Authentication configuration
114
+ * @param {number} params.timeout - Request timeout
115
+ * @returns {Promise<Object>} Test result
116
+ */
117
+ async function testSingleDatasource({ systemKey, datasourceKey, payloadTemplate, dataplaneUrl, authConfig, timeout }) {
118
+ const testResponse = await callPipelineTestEndpoint({
119
+ systemKey,
120
+ datasourceKey,
121
+ payloadTemplate,
122
+ dataplaneUrl,
123
+ authConfig,
124
+ timeout
125
+ });
126
+
127
+ return {
128
+ key: datasourceKey,
129
+ skipped: false,
130
+ success: testResponse.success !== false,
131
+ validationResults: testResponse.validationResults || {},
132
+ fieldMappingResults: testResponse.fieldMappingResults || {},
133
+ endpointTestResults: testResponse.endpointTestResults || {}
134
+ };
135
+ }
136
+
137
+ module.exports = {
138
+ retryApiCall,
139
+ callPipelineTestEndpoint,
140
+ loadCustomPayload,
141
+ determinePayloadTemplate,
142
+ testSingleDatasource
143
+ };
144
+
@@ -99,15 +99,17 @@ async function getContainerPort(appName, debug = false) {
99
99
  }
100
100
  const { stdout: psOutput } = await execAsync(psCmd);
101
101
  const portMatch = psOutput.match(/:(\d+)->/);
102
- if (portMatch) {
103
- const port = parseInt(portMatch[1], 10);
104
- if (!isNaN(port) && port > 0) {
105
- if (debug) {
106
- logger.log(chalk.gray(`[DEBUG] Detected port ${port} from docker ps`));
107
- }
108
- return port;
109
- }
102
+ if (!portMatch) {
103
+ return null;
104
+ }
105
+ const port = parseInt(portMatch[1], 10);
106
+ if (isNaN(port) || port <= 0) {
107
+ return null;
108
+ }
109
+ if (debug) {
110
+ logger.log(chalk.gray(`[DEBUG] Detected port ${port} from docker ps`));
110
111
  }
112
+ return port;
111
113
  } catch (error) {
112
114
  if (debug) {
113
115
  logger.log(chalk.gray(`[DEBUG] Fallback port detection failed: ${error.message}`));
@@ -0,0 +1,123 @@
1
+ /**
2
+ * AI Fabrix Builder - Infrastructure Status Helpers
3
+ *
4
+ * Status-related helper functions for infrastructure management.
5
+ * Extracted from infra.js to reduce file size.
6
+ *
7
+ * @fileoverview Status helper functions for infrastructure management
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const { exec } = require('child_process');
13
+ const { promisify } = require('util');
14
+ const config = require('../config');
15
+ const devConfig = require('./dev-config');
16
+ const containerUtils = require('./infra-containers');
17
+
18
+ const execAsync = promisify(exec);
19
+
20
+ /**
21
+ * Gets the status of infrastructure services
22
+ * Returns detailed information about running containers
23
+ *
24
+ * @async
25
+ * @function getInfraStatus
26
+ * @returns {Promise<Object>} Status information for each service
27
+ *
28
+ * @example
29
+ * const status = await getInfraStatus();
30
+ * // Returns: { postgres: { status: 'running', port: 5432, url: 'localhost:5432' }, ... }
31
+ */
32
+ async function getInfraStatus() {
33
+ const devId = await config.getDeveloperId();
34
+ // Convert string developer ID to number for getDevPorts
35
+ const devIdNum = parseInt(devId, 10);
36
+ const ports = devConfig.getDevPorts(devIdNum);
37
+ const services = {
38
+ postgres: { port: ports.postgres, url: `localhost:${ports.postgres}` },
39
+ redis: { port: ports.redis, url: `localhost:${ports.redis}` },
40
+ pgadmin: { port: ports.pgadmin, url: `http://localhost:${ports.pgadmin}` },
41
+ 'redis-commander': { port: ports.redisCommander, url: `http://localhost:${ports.redisCommander}` }
42
+ };
43
+
44
+ const status = {};
45
+
46
+ for (const [serviceName, serviceConfig] of Object.entries(services)) {
47
+ try {
48
+ const containerName = await containerUtils.findContainer(serviceName, devId);
49
+ if (containerName) {
50
+ const { stdout } = await execAsync(`docker inspect --format='{{.State.Status}}' ${containerName}`);
51
+ // Normalize status value (trim whitespace and remove quotes)
52
+ const normalizedStatus = stdout.trim().replace(/['"]/g, '');
53
+ status[serviceName] = {
54
+ status: normalizedStatus,
55
+ port: serviceConfig.port,
56
+ url: serviceConfig.url
57
+ };
58
+ } else {
59
+ status[serviceName] = {
60
+ status: 'not running',
61
+ port: serviceConfig.port,
62
+ url: serviceConfig.url
63
+ };
64
+ }
65
+ } catch (error) {
66
+ status[serviceName] = {
67
+ status: 'not running',
68
+ port: serviceConfig.port,
69
+ url: serviceConfig.url
70
+ };
71
+ }
72
+ }
73
+
74
+ return status;
75
+ }
76
+
77
+ /**
78
+ * Gets status of running application containers
79
+ * Finds all containers matching pattern aifabrix-dev{id}-* (excluding infrastructure)
80
+ *
81
+ * @async
82
+ * @function getAppStatus
83
+ * @returns {Promise<Array>} Array of application status objects
84
+ *
85
+ * @example
86
+ * const apps = await getAppStatus();
87
+ * // Returns: [{ name: 'myapp', container: 'aifabrix-dev1-myapp', port: '3100:3000', status: 'running', url: 'http://localhost:3100' }]
88
+ */
89
+ async function getAppStatus() {
90
+ const devId = await config.getDeveloperId();
91
+ const apps = [];
92
+
93
+ try {
94
+ const filterPattern = devId === 0 ? 'aifabrix-' : `aifabrix-dev${devId}-`;
95
+ const { stdout } = await execAsync(`docker ps --filter "name=${filterPattern}" --format "{{.Names}}\t{{.Ports}}\t{{.Status}}"`);
96
+ const lines = stdout.trim().split('\n').filter(line => line.trim() !== '');
97
+ const infraContainers = devId === 0
98
+ ? ['aifabrix-postgres', 'aifabrix-redis', 'aifabrix-pgadmin', 'aifabrix-redis-commander']
99
+ : [`aifabrix-dev${devId}-postgres`, `aifabrix-dev${devId}-redis`, `aifabrix-dev${devId}-pgadmin`, `aifabrix-dev${devId}-redis-commander`];
100
+ for (const line of lines) {
101
+ const [containerName, ports, status] = line.split('\t');
102
+ if (infraContainers.includes(containerName)) continue;
103
+ const pattern = devId === 0 ? /^aifabrix-(.+)$/ : new RegExp(`^aifabrix-dev${devId}-(.+)$`);
104
+ const appNameMatch = containerName.match(pattern);
105
+ if (!appNameMatch) continue;
106
+ const appName = appNameMatch[1];
107
+ const portMatch = ports.match(/:(\d+)->\d+\//);
108
+ const hostPort = portMatch ? portMatch[1] : 'unknown';
109
+ const url = hostPort !== 'unknown' ? `http://localhost:${hostPort}` : 'unknown';
110
+ apps.push({ name: appName, container: containerName, port: ports, status: status.trim(), url: url });
111
+ }
112
+ } catch (error) {
113
+ return [];
114
+ }
115
+
116
+ return apps;
117
+ }
118
+
119
+ module.exports = {
120
+ getInfraStatus,
121
+ getAppStatus
122
+ };
123
+
@@ -11,11 +11,12 @@
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
13
  const yaml = require('js-yaml');
14
- const os = require('os');
15
14
  const logger = require('../utils/logger');
15
+ const pathsUtil = require('./paths');
16
16
 
17
17
  /**
18
18
  * Saves a secret to ~/.aifabrix/secrets.local.yaml
19
+ * Uses paths.getAifabrixHome() to respect config.yaml aifabrix-home override
19
20
  * Merges with existing secrets without overwriting other keys
20
21
  *
21
22
  * @async
@@ -37,7 +38,7 @@ async function saveLocalSecret(key, value) {
37
38
  throw new Error('Secret value is required');
38
39
  }
39
40
 
40
- const secretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.local.yaml');
41
+ const secretsPath = path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
41
42
  const secretsDir = path.dirname(secretsPath);
42
43
 
43
44
  // Create directory if needed