@aifabrix/builder 2.1.7 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/utils/api.js CHANGED
@@ -10,6 +10,29 @@
10
10
  * @version 2.0.0
11
11
  */
12
12
 
13
+ const { parseErrorResponse } = require('./api-error-handler');
14
+ const auditLogger = require('../audit-logger');
15
+
16
+ /**
17
+ * Logs API request performance metrics and errors to audit log
18
+ * @param {string} url - API endpoint URL
19
+ * @param {Object} options - Fetch options
20
+ * @param {number} statusCode - HTTP status code
21
+ * @param {number} duration - Request duration in milliseconds
22
+ * @param {boolean} success - Whether the request was successful
23
+ * @param {Object} errorInfo - Error information (if failed)
24
+ */
25
+ async function logApiPerformance(url, options, statusCode, duration, success, errorInfo = {}) {
26
+ // Log all API calls (both success and failure) to audit log for troubleshooting
27
+ // This helps track what API calls were made when errors occur
28
+ try {
29
+ await auditLogger.logApiCall(url, options, statusCode, duration, success, errorInfo);
30
+ } catch (logError) {
31
+ // Don't fail the API call if audit logging fails
32
+ // Silently continue - audit logging should never break functionality
33
+ }
34
+ }
35
+
13
36
  /**
14
37
  * Make an API call with proper error handling
15
38
  * @param {string} url - API endpoint URL
@@ -17,25 +40,45 @@
17
40
  * @returns {Promise<Object>} Response object with success flag
18
41
  */
19
42
  async function makeApiCall(url, options = {}) {
43
+ const startTime = Date.now();
44
+
20
45
  try {
21
46
  const response = await fetch(url, options);
47
+ const duration = Date.now() - startTime;
22
48
 
23
49
  if (!response.ok) {
24
50
  const errorText = await response.text();
25
- let errorMessage;
51
+ let errorData;
26
52
  try {
27
- const errorJson = JSON.parse(errorText);
28
- errorMessage = errorJson.error || errorJson.message || 'Unknown error';
53
+ errorData = JSON.parse(errorText);
29
54
  } catch {
30
- errorMessage = errorText || `HTTP ${response.status}: ${response.statusText}`;
55
+ errorData = errorText || `HTTP ${response.status}: ${response.statusText}`;
31
56
  }
57
+
58
+ // Parse error using error handler
59
+ const parsedError = parseErrorResponse(errorData, response.status, false);
60
+
61
+ // Log error to audit log
62
+ await logApiPerformance(url, options, response.status, duration, false, {
63
+ errorType: parsedError.type,
64
+ errorMessage: parsedError.message,
65
+ errorData: parsedError.data,
66
+ correlationId: parsedError.data?.correlationId
67
+ });
68
+
32
69
  return {
33
70
  success: false,
34
- error: errorMessage,
71
+ error: parsedError.message,
72
+ errorData: parsedError.data,
73
+ errorType: parsedError.type,
74
+ formattedError: parsedError.formatted,
35
75
  status: response.status
36
76
  };
37
77
  }
38
78
 
79
+ // Log successful API call to audit log
80
+ await logApiPerformance(url, options, response.status, duration, true);
81
+
39
82
  const contentType = response.headers.get('content-type');
40
83
  if (contentType && contentType.includes('application/json')) {
41
84
  const data = await response.json();
@@ -54,16 +97,48 @@ async function makeApiCall(url, options = {}) {
54
97
  };
55
98
 
56
99
  } catch (error) {
100
+ const duration = Date.now() - startTime;
101
+
102
+ // Parse network error using error handler
103
+ const parsedError = parseErrorResponse(error.message, 0, true);
104
+
105
+ // Log network error to audit log
106
+ await logApiPerformance(url, options, 0, duration, false, {
107
+ errorType: parsedError.type,
108
+ errorMessage: parsedError.message,
109
+ network: true
110
+ });
111
+
57
112
  return {
58
113
  success: false,
59
- error: error.message,
114
+ error: parsedError.message,
115
+ errorData: parsedError.data,
116
+ errorType: parsedError.type,
117
+ formattedError: parsedError.formatted,
60
118
  network: true
61
119
  };
62
120
  }
63
121
  }
64
122
 
123
+ /**
124
+ * Extract controller URL from API endpoint URL
125
+ * @param {string} url - Full API endpoint URL
126
+ * @returns {string} Controller base URL
127
+ */
128
+ function extractControllerUrl(url) {
129
+ try {
130
+ const urlObj = new URL(url);
131
+ return `${urlObj.protocol}//${urlObj.host}`;
132
+ } catch {
133
+ // If URL parsing fails, try to extract manually
134
+ const match = url.match(/^(https?:\/\/[^/]+)/);
135
+ return match ? match[1] : url;
136
+ }
137
+ }
138
+
65
139
  /**
66
140
  * Make an authenticated API call with bearer token
141
+ * Automatically refreshes device token on 401 errors if refresh token is available
67
142
  * @param {string} url - API endpoint URL
68
143
  * @param {Object} options - Fetch options
69
144
  * @param {string} token - Bearer token
@@ -79,33 +154,63 @@ async function authenticatedApiCall(url, options = {}, token) {
79
154
  headers['Authorization'] = `Bearer ${token}`;
80
155
  }
81
156
 
82
- return makeApiCall(url, {
157
+ const response = await makeApiCall(url, {
83
158
  ...options,
84
159
  headers
85
160
  });
161
+
162
+ // Handle 401 errors with automatic token refresh for device tokens
163
+ if (!response.success && response.status === 401) {
164
+ try {
165
+ // Extract controller URL from request URL
166
+ const controllerUrl = extractControllerUrl(url);
167
+
168
+ // Try to get and refresh device token
169
+ const { getOrRefreshDeviceToken } = require('./token-manager');
170
+ const refreshedToken = await getOrRefreshDeviceToken(controllerUrl);
171
+
172
+ if (refreshedToken && refreshedToken.token) {
173
+ // Retry request with new token
174
+ headers['Authorization'] = `Bearer ${refreshedToken.token}`;
175
+ return makeApiCall(url, {
176
+ ...options,
177
+ headers
178
+ });
179
+ }
180
+ } catch (refreshError) {
181
+ // Refresh failed, return original 401 error
182
+ // This allows the caller to handle the authentication error
183
+ }
184
+ }
185
+
186
+ return response;
86
187
  }
87
188
 
88
189
  /**
89
190
  * Parses device code response from API
191
+ * Matches OpenAPI DeviceCodeResponse schema (camelCase)
90
192
  * @function parseDeviceCodeResponse
91
193
  * @param {Object} response - API response object
92
194
  * @returns {Object} Parsed device code response
93
195
  * @throws {Error} If response is invalid
94
196
  */
95
197
  function parseDeviceCodeResponse(response) {
198
+ // OpenAPI spec: { success: boolean, data: DeviceCodeResponse, timestamp: string }
96
199
  const apiResponse = response.data;
97
200
  const responseData = apiResponse.data || apiResponse;
98
201
 
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;
202
+ // OpenAPI spec uses camelCase: deviceCode, userCode, verificationUri, expiresIn, interval
203
+ const deviceCode = responseData.deviceCode;
204
+ const userCode = responseData.userCode;
205
+ const verificationUri = responseData.verificationUri;
206
+ const expiresIn = responseData.expiresIn || 600;
103
207
  const interval = responseData.interval || 5;
104
208
 
105
209
  if (!deviceCode || !userCode || !verificationUri) {
106
210
  throw new Error('Invalid device code response: missing required fields');
107
211
  }
108
212
 
213
+ // Return in snake_case for internal consistency (used by existing code)
109
214
  return {
110
215
  device_code: deviceCode,
111
216
  user_code: userCode,
@@ -122,7 +227,7 @@ function parseDeviceCodeResponse(response) {
122
227
  * @async
123
228
  * @function initiateDeviceCodeFlow
124
229
  * @param {string} controllerUrl - Base URL of the controller
125
- * @param {string} environment - Environment key (e.g., 'dev', 'tst', 'pro')
230
+ * @param {string} environment - Environment key (e.g., 'miso', 'dev', 'tst', 'pro')
126
231
  * @returns {Promise<Object>} Device code response with device_code, user_code, verification_uri, expires_in, interval
127
232
  * @throws {Error} If initiation fails
128
233
  */
@@ -162,11 +267,13 @@ function checkTokenExpiration(startTime, expiresIn) {
162
267
 
163
268
  /**
164
269
  * Parses token response from API
270
+ * Matches OpenAPI DeviceCodeTokenResponse schema (camelCase)
165
271
  * @function parseTokenResponse
166
272
  * @param {Object} response - API response object
167
273
  * @returns {Object|null} Parsed token response or null if pending
168
274
  */
169
275
  function parseTokenResponse(response) {
276
+ // OpenAPI spec: { success: boolean, data: DeviceCodeTokenResponse, timestamp: string }
170
277
  const apiResponse = response.data;
171
278
  const responseData = apiResponse.data || apiResponse;
172
279
 
@@ -175,14 +282,16 @@ function parseTokenResponse(response) {
175
282
  return null;
176
283
  }
177
284
 
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;
285
+ // OpenAPI spec uses camelCase: accessToken, refreshToken, expiresIn
286
+ const accessToken = responseData.accessToken;
287
+ const refreshToken = responseData.refreshToken;
288
+ const expiresIn = responseData.expiresIn || 3600;
181
289
 
182
290
  if (!accessToken) {
183
291
  throw new Error('Invalid token response: missing accessToken');
184
292
  }
185
293
 
294
+ // Return in snake_case for internal consistency (used by existing code)
186
295
  return {
187
296
  access_token: accessToken,
188
297
  refresh_token: refreshToken,
@@ -319,11 +428,51 @@ function displayDeviceCodeInfo(userCode, verificationUri, logger, chalk) {
319
428
  logger.log(chalk.gray('Waiting for approval...'));
320
429
  }
321
430
 
431
+ /**
432
+ * Refresh device code access token using refresh token
433
+ * Uses OpenAPI /api/v1/auth/login/device/refresh endpoint
434
+ *
435
+ * @async
436
+ * @function refreshDeviceToken
437
+ * @param {string} controllerUrl - Base URL of the controller
438
+ * @param {string} refreshToken - Refresh token from previous authentication
439
+ * @returns {Promise<Object>} Token response with access_token, refresh_token, expires_in
440
+ * @throws {Error} If refresh fails or refresh token is invalid/expired
441
+ */
442
+ async function refreshDeviceToken(controllerUrl, refreshToken) {
443
+ if (!refreshToken || typeof refreshToken !== 'string') {
444
+ throw new Error('Refresh token is required');
445
+ }
446
+
447
+ const url = `${controllerUrl}/api/v1/auth/login/device/refresh`;
448
+ const response = await makeApiCall(url, {
449
+ method: 'POST',
450
+ headers: {
451
+ 'Content-Type': 'application/json'
452
+ },
453
+ body: JSON.stringify({ refreshToken })
454
+ });
455
+
456
+ if (!response.success) {
457
+ const errorMsg = response.error || 'Unknown error';
458
+ throw new Error(`Failed to refresh token: ${errorMsg}`);
459
+ }
460
+
461
+ // Parse response using existing parseTokenResponse function
462
+ const tokenResponse = parseTokenResponse(response);
463
+ if (!tokenResponse) {
464
+ throw new Error('Invalid refresh token response');
465
+ }
466
+
467
+ return tokenResponse;
468
+ }
469
+
322
470
  module.exports = {
323
471
  makeApiCall,
324
472
  authenticatedApiCall,
325
473
  initiateDeviceCodeFlow,
326
474
  pollDeviceCodeToken,
327
- displayDeviceCodeInfo
475
+ displayDeviceCodeInfo,
476
+ refreshDeviceToken
328
477
  };
329
478
 
@@ -0,0 +1,84 @@
1
+ /**
2
+ * AI Fabrix Builder Authentication Headers Utilities
3
+ *
4
+ * Creates authentication headers for API requests
5
+ *
6
+ * @fileoverview Authentication header utilities for AI Fabrix Builder
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ /**
12
+ * Creates authentication headers using Bearer token
13
+ *
14
+ * @param {string} token - Authentication token
15
+ * @returns {Object} Headers object with authentication
16
+ * @throws {Error} If token is missing
17
+ */
18
+ function createBearerTokenHeaders(token) {
19
+ if (!token) {
20
+ throw new Error('Authentication token is required');
21
+ }
22
+ return {
23
+ 'Authorization': `Bearer ${token}`
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Creates authentication headers for Client Credentials flow (legacy support)
29
+ *
30
+ * @param {string} clientId - Application client ID
31
+ * @param {string} clientSecret - Application client secret
32
+ * @returns {Object} Headers object with authentication
33
+ * @throws {Error} If credentials are missing
34
+ */
35
+ function createClientCredentialsHeaders(clientId, clientSecret) {
36
+ if (!clientId || !clientSecret) {
37
+ throw new Error('Client ID and Client Secret are required for authentication');
38
+ }
39
+ return {
40
+ 'x-client-id': clientId,
41
+ 'x-client-secret': clientSecret
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Creates authentication headers based on auth configuration
47
+ * Supports both Bearer token and client credentials authentication
48
+ *
49
+ * @param {Object} authConfig - Authentication configuration
50
+ * @param {string} authConfig.type - Auth type: 'bearer' or 'credentials'
51
+ * @param {string} [authConfig.token] - Bearer token (for type 'bearer')
52
+ * @param {string} [authConfig.clientId] - Client ID (for type 'credentials')
53
+ * @param {string} [authConfig.clientSecret] - Client secret (for type 'credentials')
54
+ * @returns {Object} Headers object with authentication
55
+ * @throws {Error} If auth config is invalid
56
+ */
57
+ function createAuthHeaders(authConfig) {
58
+ if (!authConfig || !authConfig.type) {
59
+ throw new Error('Authentication configuration is required');
60
+ }
61
+
62
+ if (authConfig.type === 'bearer') {
63
+ if (!authConfig.token) {
64
+ throw new Error('Bearer token is required for bearer authentication');
65
+ }
66
+ return createBearerTokenHeaders(authConfig.token);
67
+ }
68
+
69
+ if (authConfig.type === 'credentials') {
70
+ if (!authConfig.clientId || !authConfig.clientSecret) {
71
+ throw new Error('Client ID and Client Secret are required for credentials authentication');
72
+ }
73
+ return createClientCredentialsHeaders(authConfig.clientId, authConfig.clientSecret);
74
+ }
75
+
76
+ throw new Error(`Invalid authentication type: ${authConfig.type}. Must be 'bearer' or 'credentials'`);
77
+ }
78
+
79
+ module.exports = {
80
+ createBearerTokenHeaders,
81
+ createClientCredentialsHeaders,
82
+ createAuthHeaders
83
+ };
84
+
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Build Copy Utilities
3
+ *
4
+ * This module handles copying builder files to developer-specific
5
+ * directories for isolated builds. Ensures each developer has their
6
+ * own build directory to prevent conflicts.
7
+ *
8
+ * @fileoverview Build copy utilities for developer isolation
9
+ * @author AI Fabrix Team
10
+ * @version 2.0.0
11
+ */
12
+
13
+ const fs = require('fs').promises;
14
+ const fsSync = require('fs');
15
+ const path = require('path');
16
+ const os = require('os');
17
+
18
+ /**
19
+ * Copies all files from builder directory to developer-specific directory
20
+ * Preserves directory structure and skips hidden files
21
+ *
22
+ * @async
23
+ * @function copyBuilderToDevDirectory
24
+ * @param {string} appName - Application name
25
+ * @param {number} developerId - Developer ID
26
+ * @returns {Promise<string>} Path to developer-specific directory
27
+ * @throws {Error} If copying fails
28
+ *
29
+ * @example
30
+ * const devPath = await copyBuilderToDevDirectory('myapp', 1);
31
+ * // Returns: '~/.aifabrix/applications-dev-1/myapp-dev-1'
32
+ */
33
+ async function copyBuilderToDevDirectory(appName, developerId) {
34
+ const builderPath = path.join(process.cwd(), 'builder', appName);
35
+
36
+ // Ensure builder directory exists
37
+ if (!fsSync.existsSync(builderPath)) {
38
+ throw new Error(`Builder directory not found: ${builderPath}\nRun 'aifabrix create ${appName}' first`);
39
+ }
40
+
41
+ // Get base directory (applications or applications-dev-{id})
42
+ const baseDir = developerId === 0
43
+ ? path.join(os.homedir(), '.aifabrix', 'applications')
44
+ : path.join(os.homedir(), '.aifabrix', `applications-dev-${developerId}`);
45
+
46
+ // Clear base directory before copying (delete all files)
47
+ if (fsSync.existsSync(baseDir)) {
48
+ const entries = await fs.readdir(baseDir);
49
+ for (const entry of entries) {
50
+ const entryPath = path.join(baseDir, entry);
51
+ const stats = await fs.stat(entryPath);
52
+ if (stats.isDirectory()) {
53
+ await fs.rm(entryPath, { recursive: true, force: true });
54
+ } else {
55
+ await fs.unlink(entryPath);
56
+ }
57
+ }
58
+ }
59
+
60
+ // Get target directory using getDevDirectory()
61
+ const devDir = getDevDirectory(appName, developerId);
62
+
63
+ // Create target directory
64
+ await fs.mkdir(devDir, { recursive: true });
65
+
66
+ // Copy files based on developer ID
67
+ if (developerId === 0) {
68
+ // Dev 0: Copy contents from builder/{appName}/ directly to applications/
69
+ await copyDirectory(builderPath, devDir);
70
+ } else {
71
+ // Dev > 0: Copy builder/{appName}/ to applications-dev-{id}/{appName}-dev-{id}/
72
+ await copyDirectory(builderPath, devDir);
73
+ }
74
+
75
+ return devDir;
76
+ }
77
+
78
+ /**
79
+ * Recursively copies directory contents
80
+ * @async
81
+ * @param {string} sourceDir - Source directory
82
+ * @param {string} targetDir - Target directory
83
+ * @throws {Error} If copying fails
84
+ */
85
+ async function copyDirectory(sourceDir, targetDir) {
86
+ // Ensure target directory exists
87
+ await fs.mkdir(targetDir, { recursive: true });
88
+
89
+ const entries = await fs.readdir(sourceDir);
90
+
91
+ for (const entry of entries) {
92
+ // Skip hidden files and directories (but allow .env, .gitignore, etc.)
93
+ if (entry.startsWith('.') && entry !== '.env' && entry !== '.gitignore') {
94
+ continue;
95
+ }
96
+
97
+ const sourcePath = path.join(sourceDir, entry);
98
+ const targetPath = path.join(targetDir, entry);
99
+
100
+ const stats = await fs.stat(sourcePath);
101
+
102
+ if (stats.isDirectory()) {
103
+ // Recursively copy subdirectories
104
+ await copyDirectory(sourcePath, targetPath);
105
+ } else if (stats.isFile()) {
106
+ // Copy file
107
+ await fs.copyFile(sourcePath, targetPath);
108
+ }
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Gets developer-specific directory path for an application
114
+ * @param {string} appName - Application name
115
+ * @param {number} developerId - Developer ID
116
+ * @returns {string} Path to developer-specific directory
117
+ */
118
+ function getDevDirectory(appName, developerId) {
119
+ if (developerId === 0) {
120
+ // Dev 0: all apps go directly to applications/ (no subdirectory)
121
+ return path.join(os.homedir(), '.aifabrix', 'applications');
122
+ }
123
+ // Dev > 0: apps go to applications-dev-{id}/{appName}-dev-{id}/
124
+ return path.join(os.homedir(), '.aifabrix', `applications-dev-${developerId}`, `${appName}-dev-${developerId}`);
125
+
126
+ }
127
+
128
+ /**
129
+ * Checks if developer-specific directory exists
130
+ * @param {string} appName - Application name
131
+ * @param {number} developerId - Developer ID
132
+ * @returns {boolean} True if directory exists
133
+ */
134
+ function devDirectoryExists(appName, developerId) {
135
+ const devDir = getDevDirectory(appName, developerId);
136
+ return fsSync.existsSync(devDir);
137
+ }
138
+
139
+ module.exports = {
140
+ copyBuilderToDevDirectory,
141
+ getDevDirectory,
142
+ devDirectoryExists
143
+ };
144
+
@@ -30,6 +30,19 @@ function validateCommand(_command, _options) {
30
30
  */
31
31
  function formatError(error) {
32
32
  const messages = [];
33
+
34
+ // If error has formatted message (from API error handler), use it directly
35
+ if (error.formatted) {
36
+ // Split formatted message into lines and add proper indentation
37
+ const lines = error.formatted.split('\n');
38
+ lines.forEach(line => {
39
+ if (line.trim()) {
40
+ messages.push(` ${line}`);
41
+ }
42
+ });
43
+ return messages;
44
+ }
45
+
33
46
  const errorMsg = error.message || '';
34
47
 
35
48
  // Check for specific error patterns first (most specific to least specific)
@@ -61,6 +74,14 @@ function formatError(error) {
61
74
  } else if (errorMsg.includes('Registry URL is required')) {
62
75
  messages.push(' Registry URL is required.');
63
76
  messages.push(' Provide via --registry flag or configure in variables.yaml under image.registry');
77
+ } else if (errorMsg.includes('Deployment failed after')) {
78
+ // Handle deployment retry errors - extract the actual error message
79
+ const match = errorMsg.match(/Deployment failed after \d+ attempts: (.+)/);
80
+ if (match) {
81
+ messages.push(` ${match[1]}`);
82
+ } else {
83
+ messages.push(` ${errorMsg}`);
84
+ }
64
85
  } else {
65
86
  messages.push(` ${errorMsg}`);
66
87
  }
@@ -13,6 +13,8 @@ const fsSync = require('fs');
13
13
  const fs = require('fs').promises;
14
14
  const path = require('path');
15
15
  const handlebars = require('handlebars');
16
+ const config = require('../config');
17
+ const buildCopy = require('./build-copy');
16
18
 
17
19
  // Register Handlebars helper for quoting PostgreSQL identifiers
18
20
  // PostgreSQL requires identifiers with hyphens or special characters to be quoted
@@ -39,14 +41,27 @@ handlebars.registerHelper('pgUser', (dbName) => {
39
41
 
40
42
  // Helper to generate old user name format (for migration - drops old users with hyphens)
41
43
  // This is used to drop legacy users that were created with hyphens before the fix
44
+ // Returns unquoted name (quotes should be added in template where needed)
42
45
  handlebars.registerHelper('pgUserOld', (dbName) => {
43
46
  if (!dbName) {
44
47
  return '';
45
48
  }
46
49
  // Old format: database name + _user (preserving hyphens)
47
50
  const userName = `${String(dbName)}_user`;
48
- // Return SafeString to prevent HTML escaping
49
- return new handlebars.SafeString(`"${userName.replace(/"/g, '""')}"`);
51
+ // Return unquoted name - template will add quotes where needed
52
+ return new handlebars.SafeString(userName);
53
+ });
54
+
55
+ // Helper to generate unquoted PostgreSQL user name (for SQL WHERE clauses)
56
+ // Returns the user name without quotes for use in SQL queries
57
+ handlebars.registerHelper('pgUserName', (dbName) => {
58
+ if (!dbName) {
59
+ return '';
60
+ }
61
+ // Replace hyphens with underscores in user name
62
+ const userName = `${String(dbName).replace(/-/g, '_')}_user`;
63
+ // Return unquoted name for SQL queries
64
+ return new handlebars.SafeString(userName);
50
65
  });
51
66
 
52
67
  /**
@@ -144,8 +159,9 @@ function buildRequiresConfig(config) {
144
159
  * @returns {Object} Service configuration
145
160
  */
146
161
  function buildServiceConfig(appName, config, port) {
147
- // Container port: build.containerPort > config.port
148
- const containerPortValue = config.build?.containerPort || config.port || port;
162
+ // Container port: build.containerPort > config.port (NEVER use host port parameter)
163
+ // Container port should remain unchanged regardless of developer ID
164
+ const containerPortValue = config.build?.containerPort || config.port || 3000;
149
165
 
150
166
  // Host port: use port parameter (already calculated from CLI --port or config.port in generateDockerCompose)
151
167
  // Note: build.localPort is ONLY used for .env file PORT variable (for local PC dev), NOT for Docker Compose
@@ -277,24 +293,34 @@ async function readDatabasePasswords(envPath, databases, appKey) {
277
293
  /**
278
294
  * Generates Docker Compose configuration from template
279
295
  * @param {string} appName - Application name
280
- * @param {Object} config - Application configuration
296
+ * @param {Object} appConfig - Application configuration
281
297
  * @param {Object} options - Run options
282
298
  * @returns {Promise<string>} Generated compose content
283
299
  */
284
- async function generateDockerCompose(appName, config, options) {
285
- const language = config.build?.language || config.language || 'typescript';
300
+ async function generateDockerCompose(appName, appConfig, options) {
301
+ const language = appConfig.build?.language || appConfig.language || 'typescript';
286
302
  const template = loadDockerComposeTemplate(language);
287
303
 
288
304
  // Use options.port if provided, otherwise use config.port
289
305
  // (localPort will be handled in buildServiceConfig)
290
- const port = options.port || config.port || 3000;
291
-
292
- const serviceConfig = buildServiceConfig(appName, config, port);
306
+ const port = options.port || appConfig.port || 3000;
307
+
308
+ // Get developer ID and network name
309
+ const devId = await config.getDeveloperId();
310
+ // Dev 0: infra-aifabrix-network (no dev-0 suffix)
311
+ // Dev > 0: infra-dev{id}-aifabrix-network
312
+ const networkName = devId === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
313
+ // Dev 0: aifabrix-{appName} (no dev-0 suffix)
314
+ // Dev > 0: aifabrix-dev{id}-{appName}
315
+ const containerName = devId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${devId}-${appName}`;
316
+
317
+ const serviceConfig = buildServiceConfig(appName, appConfig, port);
293
318
  const volumesConfig = buildVolumesConfig(appName);
294
- const networksConfig = buildNetworksConfig(config);
319
+ const networksConfig = buildNetworksConfig(appConfig);
295
320
 
296
- // Get absolute path to .env file for docker-compose
297
- const envFilePath = path.join(process.cwd(), 'builder', appName, '.env');
321
+ // Get absolute path to .env file for docker-compose (use dev-specific directory)
322
+ const devDir = buildCopy.getDevDirectory(appName, devId);
323
+ const envFilePath = path.join(devDir, '.env');
298
324
  const envFileAbsolutePath = envFilePath.replace(/\\/g, '/'); // Use forward slashes for Docker
299
325
 
300
326
  // Read database passwords from .env file
@@ -306,7 +332,10 @@ async function generateDockerCompose(appName, config, options) {
306
332
  ...volumesConfig,
307
333
  ...networksConfig,
308
334
  envFile: envFileAbsolutePath,
309
- databasePasswords: databasePasswords
335
+ databasePasswords: databasePasswords,
336
+ devId: devId,
337
+ networkName: networkName,
338
+ containerName: containerName
310
339
  };
311
340
 
312
341
  return template(templateData);