@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.
- package/README.md +5 -3
- package/bin/aifabrix.js +9 -3
- package/jest.config.integration.js +30 -0
- package/lib/app-config.js +157 -0
- package/lib/app-deploy.js +233 -82
- package/lib/app-dockerfile.js +112 -0
- package/lib/app-prompts.js +244 -0
- package/lib/app-push.js +172 -0
- package/lib/app-run.js +235 -144
- package/lib/app.js +208 -274
- package/lib/audit-logger.js +2 -0
- package/lib/build.js +177 -125
- package/lib/cli.js +76 -86
- package/lib/commands/app.js +414 -0
- package/lib/commands/login.js +304 -0
- package/lib/config.js +78 -0
- package/lib/deployer.js +225 -81
- package/lib/env-reader.js +45 -30
- package/lib/generator.js +308 -191
- package/lib/github-generator.js +67 -7
- package/lib/infra.js +156 -61
- package/lib/push.js +105 -10
- package/lib/schema/application-schema.json +30 -2
- package/lib/schema/env-config.yaml +9 -1
- package/lib/schema/infrastructure-schema.json +589 -0
- package/lib/secrets.js +229 -24
- package/lib/template-validator.js +205 -0
- package/lib/templates.js +305 -170
- package/lib/utils/api.js +329 -0
- package/lib/utils/cli-utils.js +97 -0
- package/lib/utils/compose-generator.js +185 -0
- package/lib/utils/docker-build.js +173 -0
- package/lib/utils/dockerfile-utils.js +131 -0
- package/lib/utils/environment-checker.js +125 -0
- package/lib/utils/error-formatter.js +61 -0
- package/lib/utils/health-check.js +187 -0
- package/lib/utils/logger.js +53 -0
- package/lib/utils/template-helpers.js +223 -0
- package/lib/utils/variable-transformer.js +271 -0
- package/lib/validator.js +27 -112
- package/package.json +14 -10
- package/templates/README.md +75 -3
- package/templates/applications/keycloak/Dockerfile +36 -0
- package/templates/applications/keycloak/env.template +32 -0
- package/templates/applications/keycloak/rbac.yaml +37 -0
- package/templates/applications/keycloak/variables.yaml +56 -0
- package/templates/applications/miso-controller/Dockerfile +125 -0
- package/templates/applications/miso-controller/env.template +129 -0
- package/templates/applications/miso-controller/rbac.yaml +214 -0
- package/templates/applications/miso-controller/variables.yaml +56 -0
- package/templates/github/release.yaml.hbs +5 -26
- package/templates/github/steps/npm.hbs +24 -0
- package/templates/infra/compose.yaml +6 -6
- package/templates/python/docker-compose.hbs +19 -12
- package/templates/python/main.py +80 -0
- package/templates/python/requirements.txt +4 -0
- package/templates/typescript/Dockerfile.hbs +2 -2
- package/templates/typescript/docker-compose.hbs +19 -12
- package/templates/typescript/index.ts +116 -0
- package/templates/typescript/package.json +26 -0
- 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
|
+
|