@aifabrix/builder 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +6 -2
  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 +334 -133
  10. package/lib/app.js +208 -274
  11. package/lib/audit-logger.js +2 -0
  12. package/lib/build.js +209 -98
  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/infrastructure-schema.json +589 -0
  25. package/lib/secrets.js +229 -24
  26. package/lib/template-validator.js +205 -0
  27. package/lib/templates.js +305 -170
  28. package/lib/utils/api.js +329 -0
  29. package/lib/utils/cli-utils.js +97 -0
  30. package/lib/utils/dockerfile-utils.js +131 -0
  31. package/lib/utils/environment-checker.js +125 -0
  32. package/lib/utils/error-formatter.js +61 -0
  33. package/lib/utils/health-check.js +187 -0
  34. package/lib/utils/logger.js +53 -0
  35. package/lib/utils/template-helpers.js +223 -0
  36. package/lib/utils/variable-transformer.js +271 -0
  37. package/lib/validator.js +27 -112
  38. package/package.json +13 -10
  39. package/templates/README.md +75 -3
  40. package/templates/applications/keycloak/Dockerfile +36 -0
  41. package/templates/applications/keycloak/env.template +32 -0
  42. package/templates/applications/keycloak/rbac.yaml +37 -0
  43. package/templates/applications/keycloak/variables.yaml +56 -0
  44. package/templates/applications/miso-controller/Dockerfile +125 -0
  45. package/templates/applications/miso-controller/env.template +129 -0
  46. package/templates/applications/miso-controller/rbac.yaml +168 -0
  47. package/templates/applications/miso-controller/variables.yaml +56 -0
  48. package/templates/github/release.yaml.hbs +5 -26
  49. package/templates/github/steps/npm.hbs +24 -0
  50. package/templates/infra/compose.yaml +6 -6
  51. package/templates/python/docker-compose.hbs +19 -12
  52. package/templates/python/main.py +80 -0
  53. package/templates/python/requirements.txt +4 -0
  54. package/templates/typescript/Dockerfile.hbs +2 -2
  55. package/templates/typescript/docker-compose.hbs +19 -12
  56. package/templates/typescript/index.ts +116 -0
  57. package/templates/typescript/package.json +26 -0
  58. package/templates/typescript/tsconfig.json +24 -0
@@ -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
+
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Logger Utility
3
+ *
4
+ * Centralized logging utility that wraps console methods
5
+ * Allows disabling eslint warnings in one place
6
+ *
7
+ * @fileoverview Logger utility for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ /* eslint-disable no-console */
13
+
14
+ /**
15
+ * Logger utility that wraps console methods
16
+ * All console statements should use this logger to avoid eslint warnings
17
+ */
18
+ const logger = {
19
+ /**
20
+ * Log informational message
21
+ * @param {...any} args - Arguments to log
22
+ */
23
+ log: (...args) => {
24
+ console.log(...args);
25
+ },
26
+
27
+ /**
28
+ * Log error message
29
+ * @param {...any} args - Arguments to log
30
+ */
31
+ error: (...args) => {
32
+ console.error(...args);
33
+ },
34
+
35
+ /**
36
+ * Log warning message
37
+ * @param {...any} args - Arguments to log
38
+ */
39
+ warn: (...args) => {
40
+ console.warn(...args);
41
+ },
42
+
43
+ /**
44
+ * Log informational message (alias for log)
45
+ * @param {...any} args - Arguments to log
46
+ */
47
+ info: (...args) => {
48
+ console.log(...args);
49
+ }
50
+ };
51
+
52
+ module.exports = logger;
53
+
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Template Helper Utilities
3
+ *
4
+ * Handles template variable loading, updating, and merging
5
+ *
6
+ * @fileoverview Template helper utilities for AI Fabrix Builder
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const fs = require('fs').promises;
12
+ const path = require('path');
13
+ const chalk = require('chalk');
14
+ const logger = require('./logger');
15
+
16
+ /**
17
+ * Loads template variables from template's variables.yaml file
18
+ * @async
19
+ * @function loadTemplateVariables
20
+ * @param {string} templateName - Template name
21
+ * @returns {Promise<Object|null>} Template variables or null if not found
22
+ */
23
+ async function loadTemplateVariables(templateName) {
24
+ if (!templateName) {
25
+ return null;
26
+ }
27
+
28
+ const yaml = require('js-yaml');
29
+ const templatePath = path.join(__dirname, '..', '..', 'templates', 'applications', templateName);
30
+ const templateVariablesPath = path.join(templatePath, 'variables.yaml');
31
+
32
+ try {
33
+ const templateContent = await fs.readFile(templateVariablesPath, 'utf8');
34
+ return yaml.load(templateContent);
35
+ } catch (error) {
36
+ // Template variables.yaml not found or invalid, continue without it
37
+ if (error.code !== 'ENOENT') {
38
+ logger.warn(chalk.yellow(`⚠️ Warning: Could not load template variables.yaml: ${error.message}`));
39
+ }
40
+ return null;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Updates app key and display name in variables
46
+ * @function updateAppMetadata
47
+ * @param {Object} variables - Variables object
48
+ * @param {string} appName - Application name
49
+ */
50
+ function updateAppMetadata(variables, appName) {
51
+ if (variables.app) {
52
+ variables.app.key = appName;
53
+ }
54
+
55
+ if (variables.app?.displayName && variables.app.displayName.toLowerCase().includes('miso')) {
56
+ variables.app.displayName = appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Updates port in variables if provided
62
+ * @function updatePort
63
+ * @param {Object} variables - Variables object
64
+ * @param {Object} options - CLI options
65
+ * @param {Object} config - Final configuration
66
+ */
67
+ function updatePort(variables, options, config) {
68
+ if (options.port && config.port && variables.port !== config.port) {
69
+ variables.port = config.port;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Updates build configuration
75
+ * @function updateBuildConfig
76
+ * @param {Object} variables - Variables object
77
+ */
78
+ function updateBuildConfig(variables) {
79
+ if (variables.build && variables.build.envOutputPath) {
80
+ variables.build.envOutputPath = null;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Updates database configuration for --app flag
86
+ * @function updateDatabaseConfig
87
+ * @param {Object} variables - Variables object
88
+ * @param {Object} options - CLI options
89
+ * @param {string} appName - Application name
90
+ */
91
+ function updateDatabaseConfig(variables, options, appName) {
92
+ if (!options.app || !variables.requires) {
93
+ return;
94
+ }
95
+
96
+ if (variables.requires.databases) {
97
+ variables.requires.databases = [{ name: appName }];
98
+ } else if (variables.requires.database && !variables.requires.databases) {
99
+ variables.requires.databases = [{ name: appName }];
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Updates variables.yaml file after copying from template
105
+ * Updates app.key, displayName, and port with actual values
106
+ * @async
107
+ * @function updateTemplateVariables
108
+ * @param {string} appPath - Application directory path
109
+ * @param {string} appName - Application name
110
+ * @param {Object} options - CLI options
111
+ * @param {Object} config - Final configuration
112
+ */
113
+ async function updateTemplateVariables(appPath, appName, options, config) {
114
+ const variablesPath = path.join(appPath, 'variables.yaml');
115
+ try {
116
+ const yaml = require('js-yaml');
117
+ const variablesContent = await fs.readFile(variablesPath, 'utf8');
118
+ const variables = yaml.load(variablesContent);
119
+
120
+ updateAppMetadata(variables, appName);
121
+ updatePort(variables, options, config);
122
+ updateBuildConfig(variables);
123
+ updateDatabaseConfig(variables, options, appName);
124
+
125
+ await fs.writeFile(variablesPath, yaml.dump(variables, { indent: 2, lineWidth: 120, noRefs: true }));
126
+ } catch (error) {
127
+ if (error.code !== 'ENOENT') {
128
+ logger.warn(chalk.yellow(`⚠️ Warning: Could not update variables.yaml: ${error.message}`));
129
+ }
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Merges port from template variables if not in options
135
+ * @function mergePort
136
+ * @param {Object} merged - Merged options object
137
+ * @param {Object} templateVariables - Template variables
138
+ */
139
+ function mergePort(merged, templateVariables) {
140
+ if (!merged.port && templateVariables.port) {
141
+ merged.port = templateVariables.port;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Merges language from template variables if not in options
147
+ * @function mergeLanguage
148
+ * @param {Object} merged - Merged options object
149
+ * @param {Object} templateVariables - Template variables
150
+ */
151
+ function mergeLanguage(merged, templateVariables) {
152
+ if (!merged.language && templateVariables.build?.language) {
153
+ merged.language = templateVariables.build.language;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Merges service requirements from template variables if not in options
159
+ * @function mergeServices
160
+ * @param {Object} merged - Merged options object
161
+ * @param {Object} templateVariables - Template variables
162
+ */
163
+ function mergeServices(merged, templateVariables) {
164
+ // Database: use template requires.database if not specified in options
165
+ if (!Object.prototype.hasOwnProperty.call(merged, 'database') &&
166
+ templateVariables.requires?.database !== undefined) {
167
+ merged.database = templateVariables.requires.database;
168
+ }
169
+
170
+ // Redis: use template requires.redis if not specified in options
171
+ if (!Object.prototype.hasOwnProperty.call(merged, 'redis') &&
172
+ templateVariables.requires?.redis !== undefined) {
173
+ merged.redis = templateVariables.requires.redis;
174
+ }
175
+
176
+ // Storage: use template requires.storage if not specified in options
177
+ if (!Object.prototype.hasOwnProperty.call(merged, 'storage') &&
178
+ templateVariables.requires?.storage !== undefined) {
179
+ merged.storage = templateVariables.requires.storage;
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Merges authentication from template variables if not in options
185
+ * @function mergeAuthentication
186
+ * @param {Object} merged - Merged options object
187
+ * @param {Object} templateVariables - Template variables
188
+ */
189
+ function mergeAuthentication(merged, templateVariables) {
190
+ if (!Object.prototype.hasOwnProperty.call(merged, 'authentication') &&
191
+ templateVariables.authentication !== undefined) {
192
+ merged.authentication = !!templateVariables.authentication;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Merges template variables into options
198
+ * @function mergeTemplateVariables
199
+ * @param {Object} options - User-provided options
200
+ * @param {Object} templateVariables - Template variables from variables.yaml
201
+ * @returns {Object} Merged options object
202
+ */
203
+ function mergeTemplateVariables(options, templateVariables) {
204
+ if (!templateVariables) {
205
+ return options;
206
+ }
207
+
208
+ const merged = { ...options };
209
+
210
+ mergePort(merged, templateVariables);
211
+ mergeLanguage(merged, templateVariables);
212
+ mergeServices(merged, templateVariables);
213
+ mergeAuthentication(merged, templateVariables);
214
+
215
+ return merged;
216
+ }
217
+
218
+ module.exports = {
219
+ loadTemplateVariables,
220
+ updateTemplateVariables,
221
+ mergeTemplateVariables
222
+ };
223
+