@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,329 @@
1
+ /**
2
+ * AI Fabrix Builder API Utilities
3
+ *
4
+ * Helper functions for making API calls to the controller
5
+ * Supports both bearer token and ClientId/Secret authentication
6
+ * Supports OAuth2 Device Code Flow (RFC 8628)
7
+ *
8
+ * @fileoverview API calling utilities for AI Fabrix Builder
9
+ * @author AI Fabrix Team
10
+ * @version 2.0.0
11
+ */
12
+
13
+ /**
14
+ * Make an API call with proper error handling
15
+ * @param {string} url - API endpoint URL
16
+ * @param {Object} options - Fetch options
17
+ * @returns {Promise<Object>} Response object with success flag
18
+ */
19
+ async function makeApiCall(url, options = {}) {
20
+ try {
21
+ const response = await fetch(url, options);
22
+
23
+ if (!response.ok) {
24
+ const errorText = await response.text();
25
+ let errorMessage;
26
+ try {
27
+ const errorJson = JSON.parse(errorText);
28
+ errorMessage = errorJson.error || errorJson.message || 'Unknown error';
29
+ } catch {
30
+ errorMessage = errorText || `HTTP ${response.status}: ${response.statusText}`;
31
+ }
32
+ return {
33
+ success: false,
34
+ error: errorMessage,
35
+ status: response.status
36
+ };
37
+ }
38
+
39
+ const contentType = response.headers.get('content-type');
40
+ if (contentType && contentType.includes('application/json')) {
41
+ const data = await response.json();
42
+ return {
43
+ success: true,
44
+ data,
45
+ status: response.status
46
+ };
47
+ }
48
+
49
+ const text = await response.text();
50
+ return {
51
+ success: true,
52
+ data: text,
53
+ status: response.status
54
+ };
55
+
56
+ } catch (error) {
57
+ return {
58
+ success: false,
59
+ error: error.message,
60
+ network: true
61
+ };
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Make an authenticated API call with bearer token
67
+ * @param {string} url - API endpoint URL
68
+ * @param {Object} options - Fetch options
69
+ * @param {string} token - Bearer token
70
+ * @returns {Promise<Object>} Response object
71
+ */
72
+ async function authenticatedApiCall(url, options = {}, token) {
73
+ const headers = {
74
+ 'Content-Type': 'application/json',
75
+ ...options.headers
76
+ };
77
+
78
+ if (token) {
79
+ headers['Authorization'] = `Bearer ${token}`;
80
+ }
81
+
82
+ return makeApiCall(url, {
83
+ ...options,
84
+ headers
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Parses device code response from API
90
+ * @function parseDeviceCodeResponse
91
+ * @param {Object} response - API response object
92
+ * @returns {Object} Parsed device code response
93
+ * @throws {Error} If response is invalid
94
+ */
95
+ function parseDeviceCodeResponse(response) {
96
+ const apiResponse = response.data;
97
+ const responseData = apiResponse.data || apiResponse;
98
+
99
+ const deviceCode = responseData.deviceCode || responseData.device_code;
100
+ const userCode = responseData.userCode || responseData.user_code;
101
+ const verificationUri = responseData.verificationUri || responseData.verification_uri;
102
+ const expiresIn = responseData.expiresIn || responseData.expires_in || 600;
103
+ const interval = responseData.interval || 5;
104
+
105
+ if (!deviceCode || !userCode || !verificationUri) {
106
+ throw new Error('Invalid device code response: missing required fields');
107
+ }
108
+
109
+ return {
110
+ device_code: deviceCode,
111
+ user_code: userCode,
112
+ verification_uri: verificationUri,
113
+ expires_in: expiresIn,
114
+ interval: interval
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Initiates OAuth2 Device Code Flow
120
+ * Calls the device code endpoint to get device_code and user_code
121
+ *
122
+ * @async
123
+ * @function initiateDeviceCodeFlow
124
+ * @param {string} controllerUrl - Base URL of the controller
125
+ * @param {string} environment - Environment key (e.g., 'dev', 'tst', 'pro')
126
+ * @returns {Promise<Object>} Device code response with device_code, user_code, verification_uri, expires_in, interval
127
+ * @throws {Error} If initiation fails
128
+ */
129
+ async function initiateDeviceCodeFlow(controllerUrl, environment) {
130
+ if (!environment || typeof environment !== 'string') {
131
+ throw new Error('Environment key is required');
132
+ }
133
+
134
+ const url = `${controllerUrl}/api/v1/auth/login?environment=${encodeURIComponent(environment)}`;
135
+ const response = await makeApiCall(url, {
136
+ method: 'POST',
137
+ headers: {
138
+ 'Content-Type': 'application/json'
139
+ }
140
+ });
141
+
142
+ if (!response.success) {
143
+ throw new Error(`Device code initiation failed: ${response.error || 'Unknown error'}`);
144
+ }
145
+
146
+ return parseDeviceCodeResponse(response);
147
+ }
148
+
149
+ /**
150
+ * Checks if token has expired based on elapsed time
151
+ * @function checkTokenExpiration
152
+ * @param {number} startTime - Start time in milliseconds
153
+ * @param {number} expiresIn - Expiration time in seconds
154
+ * @throws {Error} If token has expired
155
+ */
156
+ function checkTokenExpiration(startTime, expiresIn) {
157
+ const maxWaitTime = (expiresIn + 30) * 1000;
158
+ if (Date.now() - startTime > maxWaitTime) {
159
+ throw new Error('Device code expired: Maximum polling time exceeded');
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Parses token response from API
165
+ * @function parseTokenResponse
166
+ * @param {Object} response - API response object
167
+ * @returns {Object|null} Parsed token response or null if pending
168
+ */
169
+ function parseTokenResponse(response) {
170
+ const apiResponse = response.data;
171
+ const responseData = apiResponse.data || apiResponse;
172
+
173
+ const error = responseData.error || apiResponse.error;
174
+ if (error === 'authorization_pending' || error === 'slow_down') {
175
+ return null;
176
+ }
177
+
178
+ const accessToken = responseData.accessToken || responseData.access_token;
179
+ const refreshToken = responseData.refreshToken || responseData.refresh_token;
180
+ const expiresIn = responseData.expiresIn || responseData.expires_in || 3600;
181
+
182
+ if (!accessToken) {
183
+ throw new Error('Invalid token response: missing accessToken');
184
+ }
185
+
186
+ return {
187
+ access_token: accessToken,
188
+ refresh_token: refreshToken,
189
+ expires_in: expiresIn
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Handles polling errors
195
+ * @function handlePollingErrors
196
+ * @param {string} error - Error code
197
+ * @param {number} status - HTTP status code
198
+ * @throws {Error} For fatal errors
199
+ * @returns {boolean} True if should continue polling
200
+ */
201
+ function handlePollingErrors(error, status) {
202
+ if (error === 'authorization_pending' || status === 202) {
203
+ return true;
204
+ }
205
+
206
+ // Check error field first, then status code
207
+ if (error === 'authorization_declined') {
208
+ throw new Error('Authorization declined: User denied the request');
209
+ }
210
+
211
+ if (error === 'expired_token' || status === 410) {
212
+ throw new Error('Device code expired: Please restart the authentication process');
213
+ }
214
+
215
+ if (error === 'slow_down') {
216
+ return true;
217
+ }
218
+
219
+ throw new Error(`Token polling failed: ${error}`);
220
+ }
221
+
222
+ /**
223
+ * Waits for next polling interval
224
+ * @async
225
+ * @function waitForNextPoll
226
+ * @param {number} interval - Polling interval in seconds
227
+ * @param {boolean} slowDown - Whether to slow down
228
+ */
229
+ async function waitForNextPoll(interval, slowDown) {
230
+ const waitInterval = slowDown ? interval * 2 : interval;
231
+ await new Promise(resolve => setTimeout(resolve, waitInterval * 1000));
232
+ }
233
+
234
+ /**
235
+ * Polls for token during Device Code Flow
236
+ * Continuously polls the token endpoint until user approves or flow expires
237
+ *
238
+ * @async
239
+ * @function pollDeviceCodeToken
240
+ * @param {string} controllerUrl - Base URL of the controller
241
+ * @param {string} deviceCode - Device code from initiation
242
+ * @param {number} interval - Polling interval in seconds
243
+ * @param {number} expiresIn - Expiration time in seconds
244
+ * @param {Function} [onPoll] - Optional callback called on each poll attempt
245
+ * @returns {Promise<Object>} Token response with access_token, refresh_token, expires_in
246
+ * @throws {Error} If polling fails or token is expired/declined
247
+ */
248
+ async function pollDeviceCodeToken(controllerUrl, deviceCode, interval, expiresIn, onPoll) {
249
+ if (!deviceCode || typeof deviceCode !== 'string') {
250
+ throw new Error('Device code is required');
251
+ }
252
+
253
+ const url = `${controllerUrl}/api/v1/auth/login/device/token`;
254
+ const startTime = Date.now();
255
+
256
+ // eslint-disable-next-line no-constant-condition
257
+ while (true) {
258
+ checkTokenExpiration(startTime, expiresIn);
259
+
260
+ if (onPoll) {
261
+ onPoll();
262
+ }
263
+
264
+ const response = await makeApiCall(url, {
265
+ method: 'POST',
266
+ headers: {
267
+ 'Content-Type': 'application/json'
268
+ },
269
+ body: JSON.stringify({
270
+ deviceCode: deviceCode
271
+ })
272
+ });
273
+
274
+ if (response.success) {
275
+ const tokenResponse = parseTokenResponse(response);
276
+ if (tokenResponse) {
277
+ return tokenResponse;
278
+ }
279
+
280
+ const apiResponse = response.data;
281
+ const responseData = apiResponse.data || apiResponse;
282
+ const error = responseData.error || apiResponse.error;
283
+ const slowDown = error === 'slow_down';
284
+ await waitForNextPoll(interval, slowDown);
285
+ continue;
286
+ }
287
+
288
+ const apiResponse = response.data || {};
289
+ const errorData = typeof apiResponse === 'object' ? apiResponse : {};
290
+ const error = errorData.error || response.error || 'Unknown error';
291
+ const shouldContinue = handlePollingErrors(error, response.status);
292
+
293
+ if (shouldContinue) {
294
+ const slowDown = error === 'slow_down';
295
+ await waitForNextPoll(interval, slowDown);
296
+ continue;
297
+ }
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Displays device code information to the user
303
+ * Formats user code and verification URL for easy reading
304
+ *
305
+ * @function displayDeviceCodeInfo
306
+ * @param {string} userCode - User code to display
307
+ * @param {string} verificationUri - Verification URL
308
+ * @param {Object} logger - Logger instance with log method
309
+ * @param {Object} chalk - Chalk instance for colored output
310
+ */
311
+ function displayDeviceCodeInfo(userCode, verificationUri, logger, chalk) {
312
+ logger.log(chalk.cyan('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
313
+ logger.log(chalk.cyan(' Device Code Flow Authentication'));
314
+ logger.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
315
+ logger.log(chalk.yellow('To complete authentication:'));
316
+ logger.log(chalk.gray(' 1. Visit: ') + chalk.blue.underline(verificationUri));
317
+ logger.log(chalk.gray(' 2. Enter code: ') + chalk.bold.cyan(userCode));
318
+ logger.log(chalk.gray(' 3. Approve the request\n'));
319
+ logger.log(chalk.gray('Waiting for approval...'));
320
+ }
321
+
322
+ module.exports = {
323
+ makeApiCall,
324
+ authenticatedApiCall,
325
+ initiateDeviceCodeFlow,
326
+ pollDeviceCodeToken,
327
+ displayDeviceCodeInfo
328
+ };
329
+
@@ -0,0 +1,97 @@
1
+ /**
2
+ * CLI Utility Functions
3
+ *
4
+ * Utility functions for CLI command handling and validation.
5
+ *
6
+ * @fileoverview CLI utilities for error handling and validation
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const logger = require('./logger');
12
+
13
+ /**
14
+ * Validates a command and its options
15
+ * @param {string} _command - Command name
16
+ * @param {Object} _options - Command options
17
+ * @returns {boolean} True if valid
18
+ */
19
+ function validateCommand(_command, _options) {
20
+ // TODO: Implement command validation
21
+ // TODO: Add helpful error messages for common issues
22
+ return true;
23
+ }
24
+
25
+ /**
26
+ * Formats error message based on error type
27
+ * @function formatError
28
+ * @param {Error} error - The error that occurred
29
+ * @returns {string[]} Array of error message lines
30
+ */
31
+ function formatError(error) {
32
+ const messages = [];
33
+ const errorMsg = error.message || '';
34
+
35
+ // Check for specific error patterns first (most specific to least specific)
36
+ if (errorMsg.includes('Configuration not found')) {
37
+ messages.push(` ${errorMsg}`);
38
+ } else if (errorMsg.includes('not found locally') || (errorMsg.includes('Docker image') && errorMsg.includes('not found'))) {
39
+ messages.push(' Docker image not found.');
40
+ messages.push(' Run: aifabrix build <app> first');
41
+ } else if (errorMsg.includes('Docker') && (errorMsg.includes('not running') || errorMsg.includes('not installed') || errorMsg.includes('Cannot connect'))) {
42
+ messages.push(' Docker is not running or not installed.');
43
+ messages.push(' Please start Docker Desktop and try again.');
44
+ } else if (errorMsg.includes('port')) {
45
+ messages.push(' Port conflict detected.');
46
+ messages.push(' Run "aifabrix doctor" to check which ports are in use.');
47
+ } else if (errorMsg.includes('permission')) {
48
+ messages.push(' Permission denied.');
49
+ messages.push(' Make sure you have the necessary permissions to run Docker commands.');
50
+ } else if (errorMsg.includes('Azure CLI') || errorMsg.includes('az --version')) {
51
+ messages.push(' Azure CLI is not installed.');
52
+ messages.push(' Install from: https://docs.microsoft.com/cli/azure/install-azure-cli');
53
+ messages.push(' Run: az login');
54
+ } else if (errorMsg.includes('authenticate') || errorMsg.includes('ACR')) {
55
+ messages.push(' Azure Container Registry authentication failed.');
56
+ messages.push(' Run: az acr login --name <registry-name>');
57
+ messages.push(' Or login to Azure: az login');
58
+ } else if (errorMsg.includes('Invalid ACR URL') || errorMsg.includes('Expected format')) {
59
+ messages.push(' Invalid registry URL format.');
60
+ messages.push(' Use format: *.azurecr.io (e.g., myacr.azurecr.io)');
61
+ } else if (errorMsg.includes('Registry URL is required')) {
62
+ messages.push(' Registry URL is required.');
63
+ messages.push(' Provide via --registry flag or configure in variables.yaml under image.registry');
64
+ } else {
65
+ messages.push(` ${errorMsg}`);
66
+ }
67
+
68
+ return messages;
69
+ }
70
+
71
+ /**
72
+ * Logs error messages
73
+ * @function logError
74
+ * @param {string} command - Command that failed
75
+ * @param {string[]} errorMessages - Error message lines
76
+ */
77
+ function logError(command, errorMessages) {
78
+ logger.error(`\n❌ Error in ${command} command:`);
79
+ errorMessages.forEach(msg => logger.error(msg));
80
+ logger.error('\n💡 Run "aifabrix doctor" for environment diagnostics.\n');
81
+ }
82
+
83
+ /**
84
+ * Handles command errors with user-friendly messages
85
+ * @param {Error} error - The error that occurred
86
+ * @param {string} command - Command that failed
87
+ */
88
+ function handleCommandError(error, command) {
89
+ const errorMessages = formatError(error);
90
+ logError(command, errorMessages);
91
+ }
92
+
93
+ module.exports = {
94
+ validateCommand,
95
+ handleCommandError
96
+ };
97
+
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Docker Compose Generation Utilities
3
+ *
4
+ * This module handles Docker Compose configuration generation for application running.
5
+ * Separated from app-run.js to maintain file size limits.
6
+ *
7
+ * @fileoverview Docker Compose generation utilities
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const fsSync = require('fs');
13
+ const path = require('path');
14
+ const handlebars = require('handlebars');
15
+
16
+ /**
17
+ * Loads and compiles Docker Compose template
18
+ * @param {string} language - Language type
19
+ * @returns {Function} Compiled Handlebars template
20
+ * @throws {Error} If template not found
21
+ */
22
+ function loadDockerComposeTemplate(language) {
23
+ const templatePath = path.join(__dirname, '..', '..', 'templates', language, 'docker-compose.hbs');
24
+ if (!fsSync.existsSync(templatePath)) {
25
+ throw new Error(`Docker Compose template not found for language: ${language}`);
26
+ }
27
+
28
+ const templateContent = fsSync.readFileSync(templatePath, 'utf8');
29
+ return handlebars.compile(templateContent);
30
+ }
31
+
32
+ /**
33
+ * Extracts image name from configuration (same logic as build.js)
34
+ * @param {Object} config - Application configuration
35
+ * @param {string} appName - Application name (fallback)
36
+ * @returns {string} Image name
37
+ */
38
+ function getImageName(config, appName) {
39
+ if (typeof config.image === 'string') {
40
+ return config.image.split(':')[0];
41
+ } else if (config.image?.name) {
42
+ return config.image.name;
43
+ } else if (config.app?.key) {
44
+ return config.app.key;
45
+ }
46
+ return appName;
47
+ }
48
+
49
+ /**
50
+ * Builds app configuration section
51
+ * @param {string} appName - Application name
52
+ * @param {Object} config - Application configuration
53
+ * @returns {Object} App configuration
54
+ */
55
+ function buildAppConfig(appName, config) {
56
+ return {
57
+ key: appName,
58
+ name: config.displayName || appName
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Builds image configuration section
64
+ * @param {Object} config - Application configuration
65
+ * @param {string} appName - Application name
66
+ * @returns {Object} Image configuration
67
+ */
68
+ function buildImageConfig(config, appName) {
69
+ const imageName = getImageName(config, appName);
70
+ const imageTag = config.image?.tag || 'latest';
71
+ return {
72
+ name: imageName,
73
+ tag: imageTag
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Builds health check configuration section
79
+ * @param {Object} config - Application configuration
80
+ * @returns {Object} Health check configuration
81
+ */
82
+ function buildHealthCheckConfig(config) {
83
+ return {
84
+ path: config.healthCheck?.path || '/health',
85
+ interval: config.healthCheck?.interval || 30
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Builds requires configuration section
91
+ * @param {Object} config - Application configuration
92
+ * @returns {Object} Requires configuration
93
+ */
94
+ function buildRequiresConfig(config) {
95
+ return {
96
+ requiresDatabase: config.requires?.database || config.services?.database || false,
97
+ requiresStorage: config.requires?.storage || config.services?.storage || false,
98
+ requiresRedis: config.requires?.redis || config.services?.redis || false
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Builds service configuration for template data
104
+ * @param {string} appName - Application name
105
+ * @param {Object} config - Application configuration
106
+ * @param {number} port - Application port
107
+ * @returns {Object} Service configuration
108
+ */
109
+ function buildServiceConfig(appName, config, port) {
110
+ const containerPort = config.build?.containerPort || config.port || 3000;
111
+
112
+ return {
113
+ app: buildAppConfig(appName, config),
114
+ image: buildImageConfig(config, appName),
115
+ port: containerPort,
116
+ build: {
117
+ localPort: port
118
+ },
119
+ healthCheck: buildHealthCheckConfig(config),
120
+ ...buildRequiresConfig(config)
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Builds volumes configuration for template data
126
+ * @param {string} appName - Application name
127
+ * @returns {Object} Volumes configuration
128
+ */
129
+ function buildVolumesConfig(appName) {
130
+ // Use forward slashes for Docker paths (works on both Windows and Unix)
131
+ const volumePath = path.join(process.cwd(), 'data', appName);
132
+ return {
133
+ mountVolume: volumePath.replace(/\\/g, '/')
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Builds networks configuration for template data
139
+ * @param {Object} config - Application configuration
140
+ * @returns {Object} Networks configuration
141
+ */
142
+ function buildNetworksConfig(config) {
143
+ // Get databases from requires.databases or top-level databases
144
+ const databases = config.requires?.databases || config.databases || [];
145
+ return {
146
+ databases: databases
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Generates Docker Compose configuration from template
152
+ * @param {string} appName - Application name
153
+ * @param {Object} config - Application configuration
154
+ * @param {Object} options - Run options
155
+ * @returns {Promise<string>} Generated compose content
156
+ */
157
+ async function generateDockerCompose(appName, config, options) {
158
+ const language = config.build?.language || config.language || 'typescript';
159
+ const template = loadDockerComposeTemplate(language);
160
+
161
+ const port = options.port || config.build?.localPort || config.port || 3000;
162
+
163
+ const serviceConfig = buildServiceConfig(appName, config, port);
164
+ const volumesConfig = buildVolumesConfig(appName);
165
+ const networksConfig = buildNetworksConfig(config);
166
+
167
+ // Get absolute path to .env file for docker-compose
168
+ const envFilePath = path.join(process.cwd(), 'builder', appName, '.env');
169
+ const envFileAbsolutePath = envFilePath.replace(/\\/g, '/'); // Use forward slashes for Docker
170
+
171
+ const templateData = {
172
+ ...serviceConfig,
173
+ ...volumesConfig,
174
+ ...networksConfig,
175
+ envFile: envFileAbsolutePath
176
+ };
177
+
178
+ return template(templateData);
179
+ }
180
+
181
+ module.exports = {
182
+ generateDockerCompose,
183
+ getImageName
184
+ };
185
+