@aifabrix/builder 2.0.0 → 2.0.3

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 (61) hide show
  1. package/README.md +5 -3
  2. package/bin/aifabrix.js +9 -3
  3. package/jest.config.integration.js +30 -0
  4. package/lib/app-config.js +157 -0
  5. package/lib/app-deploy.js +233 -82
  6. package/lib/app-dockerfile.js +112 -0
  7. package/lib/app-prompts.js +244 -0
  8. package/lib/app-push.js +172 -0
  9. package/lib/app-run.js +235 -144
  10. package/lib/app.js +208 -274
  11. package/lib/audit-logger.js +2 -0
  12. package/lib/build.js +177 -125
  13. package/lib/cli.js +76 -86
  14. package/lib/commands/app.js +414 -0
  15. package/lib/commands/login.js +304 -0
  16. package/lib/config.js +78 -0
  17. package/lib/deployer.js +225 -81
  18. package/lib/env-reader.js +45 -30
  19. package/lib/generator.js +308 -191
  20. package/lib/github-generator.js +67 -7
  21. package/lib/infra.js +156 -61
  22. package/lib/push.js +105 -10
  23. package/lib/schema/application-schema.json +30 -2
  24. package/lib/schema/env-config.yaml +9 -1
  25. package/lib/schema/infrastructure-schema.json +589 -0
  26. package/lib/secrets.js +229 -24
  27. package/lib/template-validator.js +205 -0
  28. package/lib/templates.js +305 -170
  29. package/lib/utils/api.js +329 -0
  30. package/lib/utils/cli-utils.js +97 -0
  31. package/lib/utils/compose-generator.js +185 -0
  32. package/lib/utils/docker-build.js +173 -0
  33. package/lib/utils/dockerfile-utils.js +131 -0
  34. package/lib/utils/environment-checker.js +125 -0
  35. package/lib/utils/error-formatter.js +61 -0
  36. package/lib/utils/health-check.js +187 -0
  37. package/lib/utils/logger.js +53 -0
  38. package/lib/utils/template-helpers.js +223 -0
  39. package/lib/utils/variable-transformer.js +271 -0
  40. package/lib/validator.js +27 -112
  41. package/package.json +14 -10
  42. package/templates/README.md +75 -3
  43. package/templates/applications/keycloak/Dockerfile +36 -0
  44. package/templates/applications/keycloak/env.template +32 -0
  45. package/templates/applications/keycloak/rbac.yaml +37 -0
  46. package/templates/applications/keycloak/variables.yaml +56 -0
  47. package/templates/applications/miso-controller/Dockerfile +125 -0
  48. package/templates/applications/miso-controller/env.template +129 -0
  49. package/templates/applications/miso-controller/rbac.yaml +214 -0
  50. package/templates/applications/miso-controller/variables.yaml +56 -0
  51. package/templates/github/release.yaml.hbs +5 -26
  52. package/templates/github/steps/npm.hbs +24 -0
  53. package/templates/infra/compose.yaml +6 -6
  54. package/templates/python/docker-compose.hbs +19 -12
  55. package/templates/python/main.py +80 -0
  56. package/templates/python/requirements.txt +4 -0
  57. package/templates/typescript/Dockerfile.hbs +2 -2
  58. package/templates/typescript/docker-compose.hbs +19 -12
  59. package/templates/typescript/index.ts +116 -0
  60. package/templates/typescript/package.json +26 -0
  61. package/templates/typescript/tsconfig.json +24 -0
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Docker Build Utilities
3
+ *
4
+ * This module handles Docker image building with progress indicators.
5
+ * Separated from build.js to maintain file size limits.
6
+ *
7
+ * @fileoverview Docker build execution utilities
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const { spawn } = require('child_process');
13
+ const ora = require('ora');
14
+
15
+ /**
16
+ * Checks if error indicates Docker is not running or not installed
17
+ * @param {string} errorMessage - Error message to check
18
+ * @returns {boolean} True if Docker is not available
19
+ */
20
+ function isDockerNotAvailableError(errorMessage) {
21
+ return errorMessage.includes('docker: command not found') ||
22
+ errorMessage.includes('Cannot connect to the Docker daemon') ||
23
+ errorMessage.includes('Is the docker daemon running') ||
24
+ errorMessage.includes('Cannot connect to Docker');
25
+ }
26
+
27
+ /**
28
+ * Parses Docker build output to extract progress information
29
+ * @param {string} line - Single line of Docker build output
30
+ * @returns {string|null} Progress message or null if no progress info
31
+ */
32
+ function parseDockerBuildProgress(line) {
33
+ // Match step progress: "Step 1/10 : FROM node:20-alpine"
34
+ const stepMatch = line.match(/^Step\s+(\d+)\/(\d+)\s*:/i);
35
+ if (stepMatch) {
36
+ return `Step ${stepMatch[1]}/${stepMatch[2]}`;
37
+ }
38
+
39
+ // Match layer pulling: "Pulling from library/node"
40
+ const pullingMatch = line.match(/^Pulling\s+(.+)$/i);
41
+ if (pullingMatch) {
42
+ return `Pulling ${pullingMatch[1]}`;
43
+ }
44
+
45
+ // Match layer extracting: "Extracting [====> ]"
46
+ const extractingMatch = line.match(/^Extracting/i);
47
+ if (extractingMatch) {
48
+ return 'Extracting layers';
49
+ }
50
+
51
+ // Match build progress: " => [internal] load build context"
52
+ const buildMatch = line.match(/^=>\s*\[(.*?)\]\s*(.+)$/i);
53
+ if (buildMatch) {
54
+ return buildMatch[2].substring(0, 50);
55
+ }
56
+
57
+ // Match progress bars: "[====> ] 10.5MB/50MB"
58
+ const progressMatch = line.match(/\[.*?\]\s+([\d.]+(MB|KB|GB))\/([\d.]+(MB|KB|GB))/i);
59
+ if (progressMatch) {
60
+ return `${progressMatch[1]}/${progressMatch[3]}`;
61
+ }
62
+
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * Executes Docker build command with progress indicator
68
+ * @param {string} imageName - Image name to build
69
+ * @param {string} dockerfilePath - Path to Dockerfile
70
+ * @param {string} contextPath - Build context path
71
+ * @param {string} tag - Image tag
72
+ * @returns {Promise<void>} Resolves when build completes
73
+ * @throws {Error} If build fails
74
+ */
75
+ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag) {
76
+ const spinner = ora({
77
+ text: 'Starting Docker build...',
78
+ spinner: 'dots'
79
+ }).start();
80
+
81
+ return new Promise((resolve, reject) => {
82
+ // Use spawn for streaming output
83
+ const dockerProcess = spawn('docker', [
84
+ 'build',
85
+ '-t', `${imageName}:${tag}`,
86
+ '-f', dockerfilePath,
87
+ contextPath
88
+ ], {
89
+ shell: process.platform === 'win32'
90
+ });
91
+
92
+ let stdoutBuffer = '';
93
+ let stderrBuffer = '';
94
+ let lastProgressUpdate = Date.now();
95
+
96
+ dockerProcess.stdout.on('data', (data) => {
97
+ const output = data.toString();
98
+ stdoutBuffer += output;
99
+
100
+ // Parse progress from output lines
101
+ const lines = output.split('\n');
102
+ for (const line of lines) {
103
+ const progress = parseDockerBuildProgress(line.trim());
104
+ if (progress) {
105
+ // Update spinner text with progress (throttle updates)
106
+ const now = Date.now();
107
+ if (now - lastProgressUpdate > 200) {
108
+ spinner.text = `Building image... ${progress}`;
109
+ lastProgressUpdate = now;
110
+ }
111
+ }
112
+ }
113
+ });
114
+
115
+ dockerProcess.stderr.on('data', (data) => {
116
+ const output = data.toString();
117
+ stderrBuffer += output;
118
+
119
+ // Check for warnings vs errors
120
+ if (!output.toLowerCase().includes('warning')) {
121
+ // Parse progress from stderr too (Docker outputs progress to stderr)
122
+ const lines = output.split('\n');
123
+ for (const line of lines) {
124
+ const progress = parseDockerBuildProgress(line.trim());
125
+ if (progress) {
126
+ const now = Date.now();
127
+ if (now - lastProgressUpdate > 200) {
128
+ spinner.text = `Building image... ${progress}`;
129
+ lastProgressUpdate = now;
130
+ }
131
+ }
132
+ }
133
+ }
134
+ });
135
+
136
+ dockerProcess.on('close', (code) => {
137
+ if (code === 0) {
138
+ spinner.succeed(`Image built: ${imageName}:${tag}`);
139
+ resolve();
140
+ } else {
141
+ spinner.fail('Build failed');
142
+
143
+ const errorMessage = stderrBuffer || stdoutBuffer || 'Docker build failed';
144
+
145
+ if (isDockerNotAvailableError(errorMessage)) {
146
+ reject(new Error('Docker is not running or not installed. Please start Docker Desktop and try again.'));
147
+ } else {
148
+ // Show last few lines of error output
149
+ const errorLines = errorMessage.split('\n').filter(line => line.trim());
150
+ const lastError = errorLines.slice(-5).join('\n');
151
+ reject(new Error(`Docker build failed: ${lastError}`));
152
+ }
153
+ }
154
+ });
155
+
156
+ dockerProcess.on('error', (error) => {
157
+ spinner.fail('Build failed');
158
+ const errorMessage = error.message || String(error);
159
+
160
+ if (isDockerNotAvailableError(errorMessage)) {
161
+ reject(new Error('Docker is not running or not installed. Please start Docker Desktop and try again.'));
162
+ } else {
163
+ reject(new Error(`Docker build failed: ${errorMessage}`));
164
+ }
165
+ });
166
+ });
167
+ }
168
+
169
+ module.exports = {
170
+ executeDockerBuild,
171
+ isDockerNotAvailableError
172
+ };
173
+
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Dockerfile Utility Functions
3
+ *
4
+ * This module handles Dockerfile template loading, rendering,
5
+ * and path resolution. Separated from build.js to maintain
6
+ * file size limits and improve code organization.
7
+ *
8
+ * @fileoverview Dockerfile utility functions for AI Fabrix Builder
9
+ * @author AI Fabrix Team
10
+ * @version 2.0.0
11
+ */
12
+
13
+ const fsSync = require('fs');
14
+ const path = require('path');
15
+ const handlebars = require('handlebars');
16
+
17
+ /**
18
+ * Loads Dockerfile template for language
19
+ * @function loadDockerfileTemplate
20
+ * @param {string} language - Language ('typescript' or 'python')
21
+ * @returns {Function} Compiled Handlebars template
22
+ * @throws {Error} If template not found
23
+ */
24
+ function loadDockerfileTemplate(language) {
25
+ const templatePath = path.join(__dirname, '..', '..', 'templates', language, 'Dockerfile.hbs');
26
+
27
+ if (!fsSync.existsSync(templatePath)) {
28
+ throw new Error(`Template not found for language: ${language}`);
29
+ }
30
+
31
+ const templateContent = fsSync.readFileSync(templatePath, 'utf8');
32
+ return handlebars.compile(templateContent);
33
+ }
34
+
35
+ /**
36
+ * Renders Dockerfile with template variables
37
+ * @function renderDockerfile
38
+ * @param {Function} template - Compiled Handlebars template
39
+ * @param {Object} templateVars - Template variables
40
+ * @param {string} language - Language ('typescript' or 'python')
41
+ * @param {boolean} isAppFlag - Whether --app flag was used
42
+ * @param {string} appSourcePath - Application source path
43
+ * @returns {string} Rendered Dockerfile content
44
+ */
45
+ function renderDockerfile(template, templateVars, language, isAppFlag, appSourcePath) {
46
+ let dockerfileContent = template(templateVars);
47
+
48
+ if (!isAppFlag) {
49
+ return dockerfileContent;
50
+ }
51
+
52
+ dockerfileContent = dockerfileContent.replace(
53
+ /^COPY \. \./gm,
54
+ `COPY ${appSourcePath} .`
55
+ );
56
+
57
+ if (language === 'python') {
58
+ // Replace COPY requirements*.txt with app-specific path
59
+ dockerfileContent = dockerfileContent.replace(
60
+ /^COPY requirements\*\.txt \./gm,
61
+ `COPY ${appSourcePath}requirements*.txt ./`
62
+ );
63
+ // Also handle case where it might be COPY requirements.txt
64
+ dockerfileContent = dockerfileContent.replace(
65
+ /^COPY requirements\.txt \./gm,
66
+ `COPY ${appSourcePath}requirements.txt ./`
67
+ );
68
+ }
69
+
70
+ if (language === 'typescript') {
71
+ dockerfileContent = dockerfileContent.replace(
72
+ /^COPY package\*\.json \./gm,
73
+ `COPY ${appSourcePath}package*.json ./`
74
+ );
75
+ }
76
+
77
+ return dockerfileContent;
78
+ }
79
+
80
+ /**
81
+ * Checks for template Dockerfile in builder directory
82
+ * @function checkTemplateDockerfile
83
+ * @param {string} builderPath - Builder directory path
84
+ * @param {string} appName - Application name
85
+ * @param {boolean} forceTemplate - Force template flag
86
+ * @returns {string|null} Dockerfile path or null
87
+ */
88
+ function checkTemplateDockerfile(builderPath, appName, forceTemplate) {
89
+ const appDockerfilePath = path.join(builderPath, 'Dockerfile');
90
+ if (fsSync.existsSync(appDockerfilePath) && !forceTemplate) {
91
+ return appDockerfilePath;
92
+ }
93
+ return null;
94
+ }
95
+
96
+ /**
97
+ * Checks for custom Dockerfile from variables.yaml
98
+ * @function checkProjectDockerfile
99
+ * @param {string} builderPath - Builder directory path
100
+ * @param {string} appName - Application name
101
+ * @param {Object} buildConfig - Build configuration
102
+ * @param {string} contextPath - Build context path
103
+ * @param {boolean} forceTemplate - Force template flag
104
+ * @returns {string|null} Dockerfile path or null
105
+ */
106
+ function checkProjectDockerfile(builderPath, appName, buildConfig, contextPath, forceTemplate) {
107
+ const customDockerfile = buildConfig.dockerfile;
108
+ if (!customDockerfile || forceTemplate) {
109
+ return null;
110
+ }
111
+
112
+ const customPath = path.resolve(contextPath, customDockerfile);
113
+ if (fsSync.existsSync(customPath)) {
114
+ return customPath;
115
+ }
116
+
117
+ const builderCustomPath = path.join(process.cwd(), 'builder', appName, customDockerfile);
118
+ if (fsSync.existsSync(builderCustomPath)) {
119
+ return builderCustomPath;
120
+ }
121
+
122
+ return null;
123
+ }
124
+
125
+ module.exports = {
126
+ loadDockerfileTemplate,
127
+ renderDockerfile,
128
+ checkTemplateDockerfile,
129
+ checkProjectDockerfile
130
+ };
131
+
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Environment Checking Utilities
3
+ *
4
+ * Checks the development environment for common issues
5
+ * Validates Docker, ports, secrets, and other requirements
6
+ *
7
+ * @fileoverview Environment checking utilities for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+
16
+ /**
17
+ * Checks if Docker is installed and available
18
+ *
19
+ * @async
20
+ * @function checkDocker
21
+ * @returns {Promise<string>} 'ok' if Docker is available, 'error' otherwise
22
+ */
23
+ async function checkDocker() {
24
+ try {
25
+ const { exec } = require('child_process');
26
+ const { promisify } = require('util');
27
+ const execAsync = promisify(exec);
28
+
29
+ await execAsync('docker --version');
30
+ await execAsync('docker-compose --version');
31
+ return 'ok';
32
+ } catch (error) {
33
+ return 'error';
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Checks if required ports are available
39
+ *
40
+ * @async
41
+ * @function checkPorts
42
+ * @returns {Promise<string>} 'ok' if all ports are available, 'warning' otherwise
43
+ */
44
+ async function checkPorts() {
45
+ const requiredPorts = [5432, 6379, 5050, 8081];
46
+ const netstat = require('net');
47
+ let portIssues = 0;
48
+
49
+ for (const port of requiredPorts) {
50
+ try {
51
+ await new Promise((resolve, reject) => {
52
+ const server = netstat.createServer();
53
+ server.listen(port, () => {
54
+ server.close(resolve);
55
+ });
56
+ server.on('error', reject);
57
+ });
58
+ } catch (error) {
59
+ portIssues++;
60
+ }
61
+ }
62
+
63
+ return portIssues === 0 ? 'ok' : 'warning';
64
+ }
65
+
66
+ /**
67
+ * Checks if secrets file exists
68
+ *
69
+ * @function checkSecrets
70
+ * @returns {string} 'ok' if secrets file exists, 'missing' otherwise
71
+ */
72
+ function checkSecrets() {
73
+ const secretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
74
+ return fs.existsSync(secretsPath) ? 'ok' : 'missing';
75
+ }
76
+
77
+ /**
78
+ * Checks the development environment for common issues
79
+ * Validates Docker, ports, secrets, and other requirements
80
+ *
81
+ * @async
82
+ * @function checkEnvironment
83
+ * @returns {Promise<Object>} Environment check result
84
+ * @throws {Error} If critical issues are found
85
+ *
86
+ * @example
87
+ * const result = await checkEnvironment();
88
+ * // Returns: { docker: 'ok', ports: 'ok', secrets: 'missing', recommendations: [...] }
89
+ */
90
+ async function checkEnvironment() {
91
+ const result = {
92
+ docker: 'unknown',
93
+ ports: 'unknown',
94
+ secrets: 'unknown',
95
+ recommendations: []
96
+ };
97
+
98
+ // Check Docker
99
+ result.docker = await checkDocker();
100
+ if (result.docker === 'error') {
101
+ result.recommendations.push('Install Docker and Docker Compose');
102
+ }
103
+
104
+ // Check ports
105
+ result.ports = await checkPorts();
106
+ if (result.ports === 'warning') {
107
+ result.recommendations.push('Some required ports (5432, 6379, 5050, 8081) are in use');
108
+ }
109
+
110
+ // Check secrets
111
+ result.secrets = checkSecrets();
112
+ if (result.secrets === 'missing') {
113
+ result.recommendations.push('Create secrets file: ~/.aifabrix/secrets.yaml');
114
+ }
115
+
116
+ return result;
117
+ }
118
+
119
+ module.exports = {
120
+ checkDocker,
121
+ checkPorts,
122
+ checkSecrets,
123
+ checkEnvironment
124
+ };
125
+
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Error Formatting Utilities
3
+ *
4
+ * Formats validation errors into developer-friendly messages
5
+ * Converts technical schema errors into actionable advice
6
+ *
7
+ * @fileoverview Error formatting utilities for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ /**
13
+ * Formats a single validation error into a developer-friendly message
14
+ *
15
+ * @function formatSingleError
16
+ * @param {Object} error - Raw validation error from Ajv
17
+ * @returns {string} Formatted error message
18
+ */
19
+ function formatSingleError(error) {
20
+ const path = error.instancePath ? error.instancePath.slice(1) : 'root';
21
+ const field = path ? `Field "${path}"` : 'Configuration';
22
+
23
+ const errorMessages = {
24
+ required: `${field}: Missing required property "${error.params.missingProperty}"`,
25
+ type: `${field}: Expected ${error.params.type}, got ${typeof error.data}`,
26
+ minimum: `${field}: Value must be at least ${error.params.limit}`,
27
+ maximum: `${field}: Value must be at most ${error.params.limit}`,
28
+ minLength: `${field}: Must be at least ${error.params.limit} characters`,
29
+ maxLength: `${field}: Must be at most ${error.params.limit} characters`,
30
+ pattern: `${field}: Invalid format`,
31
+ enum: `${field}: Must be one of: ${error.params.allowedValues?.join(', ') || 'unknown'}`
32
+ };
33
+
34
+ return errorMessages[error.keyword] || `${field}: ${error.message}`;
35
+ }
36
+
37
+ /**
38
+ * Formats validation errors into developer-friendly messages
39
+ * Converts technical schema errors into actionable advice
40
+ *
41
+ * @function formatValidationErrors
42
+ * @param {Array} errors - Raw validation errors from Ajv
43
+ * @returns {Array} Formatted error messages
44
+ *
45
+ * @example
46
+ * const messages = formatValidationErrors(ajvErrors);
47
+ * // Returns: ['Port must be between 1 and 65535', 'Missing required field: displayName']
48
+ */
49
+ function formatValidationErrors(errors) {
50
+ if (!Array.isArray(errors)) {
51
+ return ['Unknown validation error'];
52
+ }
53
+
54
+ return errors.map(formatSingleError);
55
+ }
56
+
57
+ module.exports = {
58
+ formatSingleError,
59
+ formatValidationErrors
60
+ };
61
+
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Health Check Utilities
3
+ *
4
+ * Handles health check functionality for application containers
5
+ *
6
+ * @fileoverview Health check utilities for AI Fabrix Builder
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const http = require('http');
12
+ const chalk = require('chalk');
13
+ const { exec } = require('child_process');
14
+ const { promisify } = require('util');
15
+ const logger = require('./logger');
16
+
17
+ const execAsync = promisify(exec);
18
+
19
+ /**
20
+ * Checks if db-init container exists and waits for it to complete
21
+ * @async
22
+ * @function waitForDbInit
23
+ * @param {string} appName - Application name
24
+ * @throws {Error} If db-init fails
25
+ */
26
+ async function waitForDbInit(appName) {
27
+ const dbInitContainer = `aifabrix-${appName}-db-init`;
28
+ try {
29
+ const { stdout } = await execAsync(`docker ps -a --filter "name=${dbInitContainer}" --format "{{.Names}}"`);
30
+ if (stdout.trim() !== dbInitContainer) {
31
+ return;
32
+ }
33
+
34
+ const { stdout: status } = await execAsync(`docker inspect --format='{{.State.Status}}' ${dbInitContainer}`);
35
+ if (status.trim() === 'exited') {
36
+ const { stdout: exitCode } = await execAsync(`docker inspect --format='{{.State.ExitCode}}' ${dbInitContainer}`);
37
+ if (exitCode.trim() === '0') {
38
+ logger.log(chalk.green('✓ Database initialization already completed'));
39
+ } else {
40
+ logger.log(chalk.yellow(`⚠ Database initialization exited with code ${exitCode.trim()}`));
41
+ }
42
+ return;
43
+ }
44
+
45
+ logger.log(chalk.blue('Waiting for database initialization to complete...'));
46
+ const maxDbInitAttempts = 30;
47
+ for (let dbInitAttempts = 0; dbInitAttempts < maxDbInitAttempts; dbInitAttempts++) {
48
+ const { stdout: currentStatus } = await execAsync(`docker inspect --format='{{.State.Status}}' ${dbInitContainer}`);
49
+ if (currentStatus.trim() === 'exited') {
50
+ const { stdout: exitCode } = await execAsync(`docker inspect --format='{{.State.ExitCode}}' ${dbInitContainer}`);
51
+ if (exitCode.trim() === '0') {
52
+ logger.log(chalk.green('✓ Database initialization completed'));
53
+ } else {
54
+ logger.log(chalk.yellow(`⚠ Database initialization exited with code ${exitCode.trim()}`));
55
+ }
56
+ return;
57
+ }
58
+ await new Promise(resolve => setTimeout(resolve, 1000));
59
+ }
60
+ } catch (error) {
61
+ // db-init container might not exist, which is fine
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Gets container port from Docker inspect
67
+ * @async
68
+ * @function getContainerPort
69
+ * @param {string} appName - Application name
70
+ * @returns {Promise<number>} Container port
71
+ */
72
+ async function getContainerPort(appName) {
73
+ try {
74
+ const { stdout: portMapping } = await execAsync(`docker inspect --format='{{range .NetworkSettings.Ports}}{{range .}}{{.HostPort}}{{end}}{{end}}' aifabrix-${appName}`);
75
+ const ports = portMapping.trim().split('\n').filter(p => p);
76
+ if (ports.length > 0) {
77
+ return parseInt(ports[0], 10);
78
+ }
79
+ } catch (error) {
80
+ // Fall through to default
81
+ }
82
+ return 3000;
83
+ }
84
+
85
+ /**
86
+ * Parses health check response
87
+ * @function parseHealthResponse
88
+ * @param {string} data - Response data
89
+ * @param {number} statusCode - HTTP status code
90
+ * @returns {boolean} True if healthy
91
+ */
92
+ function parseHealthResponse(data, statusCode) {
93
+ try {
94
+ const health = JSON.parse(data);
95
+ if (health.status === 'UP') {
96
+ return true;
97
+ }
98
+ if (health.status === 'ok') {
99
+ return health.database === 'connected' || !health.database;
100
+ }
101
+ return false;
102
+ } catch (error) {
103
+ return statusCode === 200;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Checks health endpoint
109
+ * @async
110
+ * @function checkHealthEndpoint
111
+ * @param {string} healthCheckUrl - Health check URL
112
+ * @returns {Promise<boolean>} True if healthy
113
+ * @throws {Error} If request fails with exception
114
+ */
115
+ async function checkHealthEndpoint(healthCheckUrl) {
116
+ return new Promise((resolve, reject) => {
117
+ try {
118
+ const req = http.get(healthCheckUrl, { timeout: 5000 }, (res) => {
119
+ let data = '';
120
+ res.on('data', (chunk) => {
121
+ data += chunk;
122
+ });
123
+ res.on('end', () => {
124
+ resolve(parseHealthResponse(data, res.statusCode));
125
+ });
126
+ });
127
+ req.on('error', () => resolve(false));
128
+ req.on('timeout', () => {
129
+ req.destroy();
130
+ resolve(false);
131
+ });
132
+ } catch (error) {
133
+ // Re-throw exceptions (not just network errors)
134
+ reject(error);
135
+ }
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Waits for application health check to pass
141
+ * Checks HTTP endpoint and waits for healthy response
142
+ *
143
+ * @async
144
+ * @function waitForHealthCheck
145
+ * @param {string} appName - Application name
146
+ * @param {number} timeout - Timeout in seconds (default: 90)
147
+ * @param {number} [port] - Application port (auto-detected if not provided)
148
+ * @param {Object} [config] - Application configuration
149
+ * @returns {Promise<void>} Resolves when health check passes
150
+ * @throws {Error} If health check times out
151
+ */
152
+ async function waitForHealthCheck(appName, timeout = 90, port = null, config = null) {
153
+ await waitForDbInit(appName);
154
+
155
+ if (!port) {
156
+ port = await getContainerPort(appName);
157
+ }
158
+
159
+ const healthCheckPath = config?.healthCheck?.path || '/health';
160
+ const healthCheckUrl = `http://localhost:${port}${healthCheckPath}`;
161
+ const maxAttempts = timeout / 2;
162
+
163
+ for (let attempts = 0; attempts < maxAttempts; attempts++) {
164
+ try {
165
+ const healthCheckPassed = await checkHealthEndpoint(healthCheckUrl);
166
+ if (healthCheckPassed) {
167
+ logger.log(chalk.green('✓ Application is healthy'));
168
+ return;
169
+ }
170
+ } catch (error) {
171
+ // If exception occurs, continue retrying until timeout
172
+ // The error will be handled by timeout error below
173
+ }
174
+
175
+ if (attempts < maxAttempts - 1) {
176
+ logger.log(chalk.yellow(`Waiting for health check... (${attempts + 1}/${maxAttempts}) ${healthCheckUrl}`));
177
+ await new Promise(resolve => setTimeout(resolve, 2000));
178
+ }
179
+ }
180
+
181
+ throw new Error(`Health check timeout after ${timeout} seconds`);
182
+ }
183
+
184
+ module.exports = {
185
+ waitForHealthCheck
186
+ };
187
+