@aifabrix/builder 2.1.7 → 2.3.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.
Files changed (43) hide show
  1. package/lib/app-deploy.js +73 -29
  2. package/lib/app-list.js +132 -0
  3. package/lib/app-readme.js +11 -4
  4. package/lib/app-register.js +435 -0
  5. package/lib/app-rotate-secret.js +164 -0
  6. package/lib/app-run.js +98 -84
  7. package/lib/app.js +13 -0
  8. package/lib/audit-logger.js +195 -15
  9. package/lib/build.js +155 -42
  10. package/lib/cli.js +104 -8
  11. package/lib/commands/app.js +8 -391
  12. package/lib/commands/login.js +130 -36
  13. package/lib/commands/secure.js +260 -0
  14. package/lib/config.js +315 -4
  15. package/lib/deployer.js +221 -183
  16. package/lib/infra.js +177 -112
  17. package/lib/push.js +34 -7
  18. package/lib/secrets.js +89 -23
  19. package/lib/templates.js +1 -1
  20. package/lib/utils/api-error-handler.js +465 -0
  21. package/lib/utils/api.js +165 -16
  22. package/lib/utils/auth-headers.js +84 -0
  23. package/lib/utils/build-copy.js +162 -0
  24. package/lib/utils/cli-utils.js +49 -3
  25. package/lib/utils/compose-generator.js +57 -16
  26. package/lib/utils/deployment-errors.js +90 -0
  27. package/lib/utils/deployment-validation.js +60 -0
  28. package/lib/utils/dev-config.js +83 -0
  29. package/lib/utils/docker-build.js +24 -0
  30. package/lib/utils/env-template.js +30 -10
  31. package/lib/utils/health-check.js +18 -1
  32. package/lib/utils/infra-containers.js +101 -0
  33. package/lib/utils/local-secrets.js +0 -2
  34. package/lib/utils/secrets-encryption.js +203 -0
  35. package/lib/utils/secrets-path.js +22 -3
  36. package/lib/utils/token-manager.js +381 -0
  37. package/package.json +2 -2
  38. package/templates/applications/README.md.hbs +155 -23
  39. package/templates/applications/miso-controller/Dockerfile +7 -119
  40. package/templates/infra/compose.yaml.hbs +93 -0
  41. package/templates/python/docker-compose.hbs +25 -17
  42. package/templates/typescript/docker-compose.hbs +25 -17
  43. package/test-output.txt +0 -5431
@@ -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,36 +293,61 @@ 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
- // Read database passwords from .env file
326
+ // Read database passwords from .env file only if database is required
301
327
  const databases = networksConfig.databases || [];
302
- const databasePasswords = await readDatabasePasswords(envFilePath, databases, appName);
328
+ const requiresDatabase = serviceConfig.requiresDatabase || false;
329
+ let databasePasswords;
330
+
331
+ if (requiresDatabase || databases.length > 0) {
332
+ // Only read passwords if database is actually required
333
+ databasePasswords = await readDatabasePasswords(envFilePath, databases, appName);
334
+ } else {
335
+ // Return empty passwords when database is not required
336
+ databasePasswords = {
337
+ map: {},
338
+ array: []
339
+ };
340
+ }
303
341
 
304
342
  const templateData = {
305
343
  ...serviceConfig,
306
344
  ...volumesConfig,
307
345
  ...networksConfig,
308
346
  envFile: envFileAbsolutePath,
309
- databasePasswords: databasePasswords
347
+ databasePasswords: databasePasswords,
348
+ devId: devId,
349
+ networkName: networkName,
350
+ containerName: containerName
310
351
  };
311
352
 
312
353
  return template(templateData);
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Deployment Error Handling Utilities
3
+ *
4
+ * Handles deployment errors with security-aware messages and audit logging.
5
+ *
6
+ * @fileoverview Deployment error handling functions
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const auditLogger = require('../audit-logger');
12
+ const { parseErrorResponse } = require('./api-error-handler');
13
+
14
+ /**
15
+ * Handles deployment errors with security-aware messages
16
+ *
17
+ * @param {Error} error - Error to handle
18
+ * @returns {Object} Structured error information
19
+ */
20
+ function handleDeploymentError(error) {
21
+ const safeError = {
22
+ message: error.message,
23
+ code: error.code || 'UNKNOWN',
24
+ timeout: error.code === 'ECONNABORTED',
25
+ status: error.status || error.response?.status,
26
+ data: error.data || error.response?.data
27
+ };
28
+
29
+ // Mask sensitive information in error messages
30
+ safeError.message = auditLogger.maskSensitiveData(safeError.message);
31
+
32
+ return safeError;
33
+ }
34
+
35
+ /**
36
+ * Unified error handler for deployment errors
37
+ * Handles audit logging, error formatting, and user-friendly messages
38
+ * @param {Error} error - Error object
39
+ * @param {string} appName - Application name for audit logging
40
+ * @param {string} url - Controller URL for audit logging
41
+ * @param {boolean} [alreadyLogged=false] - Whether error has already been logged
42
+ * @throws {Error} User-friendly error message
43
+ */
44
+ async function handleDeploymentErrors(error, appName, url, alreadyLogged = false) {
45
+ // Log to audit log if not already logged
46
+ if (!alreadyLogged) {
47
+ try {
48
+ await auditLogger.logDeploymentFailure(appName, url, error);
49
+ } catch (logError) {
50
+ // Don't fail if audit logging fails, but log to console
51
+ // eslint-disable-next-line no-console
52
+ console.error(`[AUDIT LOG ERROR] Failed to log deployment failure: ${logError.message}`);
53
+ }
54
+ }
55
+
56
+ const safeError = handleDeploymentError(error);
57
+
58
+ // Extract error data from axios response
59
+ let errorData = safeError.data;
60
+ if (error.response && error.response.data !== undefined) {
61
+ errorData = error.response.data;
62
+ }
63
+
64
+ // Ensure errorData is not undefined before parsing
65
+ // If errorData is undefined, use the error message instead
66
+ const errorResponse = errorData !== undefined ? errorData : safeError.message;
67
+
68
+ // Determine if this is a network error
69
+ const isNetworkError = safeError.code === 'ECONNREFUSED' ||
70
+ safeError.code === 'ENOTFOUND' ||
71
+ safeError.code === 'ECONNABORTED' ||
72
+ safeError.timeout;
73
+
74
+ // Parse error using error handler
75
+ const parsedError = parseErrorResponse(errorResponse, safeError.status || 0, isNetworkError);
76
+
77
+ // Throw clean error message (without emoji) - CLI will format it
78
+ const formattedError = new Error(parsedError.message);
79
+ formattedError.formatted = parsedError.formatted;
80
+ formattedError.status = safeError.status;
81
+ formattedError.data = parsedError.data;
82
+ formattedError._logged = true; // Mark as logged to prevent double-logging
83
+ throw formattedError;
84
+ }
85
+
86
+ module.exports = {
87
+ handleDeploymentError,
88
+ handleDeploymentErrors
89
+ };
90
+
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Deployment Validation Utilities
3
+ *
4
+ * Validates deployment configuration inputs with ISO 27001 security measures.
5
+ *
6
+ * @fileoverview Deployment validation functions
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ /**
12
+ * Validates and sanitizes controller URL
13
+ * Enforces HTTPS-only communication for security
14
+ *
15
+ * @param {string} url - Controller URL to validate
16
+ * @throws {Error} If URL is invalid or uses HTTP
17
+ */
18
+ function validateControllerUrl(url) {
19
+ if (!url || typeof url !== 'string') {
20
+ throw new Error('Controller URL is required and must be a string');
21
+ }
22
+
23
+ // Must use HTTPS for security (allow http://localhost for local development)
24
+ if (!url.startsWith('https://') && !url.startsWith('http://localhost')) {
25
+ throw new Error('Controller URL must use HTTPS (https://) or http://localhost');
26
+ }
27
+
28
+ // Basic URL format validation
29
+ const urlPattern = /^(https?):\/\/[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*(localhost)?(:[0-9]+)?(\/.*)?$/;
30
+ if (!urlPattern.test(url)) {
31
+ throw new Error('Invalid controller URL format');
32
+ }
33
+
34
+ // Remove trailing slash if present
35
+ return url.replace(/\/$/, '');
36
+ }
37
+
38
+ /**
39
+ * Validates environment key
40
+ * @param {string} envKey - Environment key to validate
41
+ * @throws {Error} If environment key is invalid
42
+ */
43
+ function validateEnvironmentKey(envKey) {
44
+ if (!envKey || typeof envKey !== 'string') {
45
+ throw new Error('Environment key is required and must be a string');
46
+ }
47
+
48
+ const validEnvironments = ['miso', 'dev', 'tst', 'pro'];
49
+ if (!validEnvironments.includes(envKey.toLowerCase())) {
50
+ throw new Error(`Invalid environment key: ${envKey}. Must be one of: ${validEnvironments.join(', ')}`);
51
+ }
52
+
53
+ return envKey.toLowerCase();
54
+ }
55
+
56
+ module.exports = {
57
+ validateControllerUrl,
58
+ validateEnvironmentKey
59
+ };
60
+
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Developer Configuration Utilities
3
+ *
4
+ * This module provides utilities for calculating developer-specific ports
5
+ * based on developer ID. Ports are offset by (developer-id * 100).
6
+ *
7
+ * @fileoverview Developer configuration and port calculation utilities
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ /**
13
+ * Base ports for infrastructure and applications
14
+ * These are the default ports before any developer offset
15
+ */
16
+ const BASE_PORTS = {
17
+ app: 3000,
18
+ postgres: 5432,
19
+ redis: 6379,
20
+ pgadmin: 5050,
21
+ redisCommander: 8081
22
+ };
23
+
24
+ /**
25
+ * Calculates developer-specific ports based on developer ID
26
+ * Formula: basePort + (developerId * 100)
27
+ * Developer ID: 0 = default infra (base ports), > 0 = developer-specific (offset ports)
28
+ *
29
+ * @function getDevPorts
30
+ * @param {number} developerId - Developer ID (0 = default infra, 1, 2, 3, etc. = developer-specific)
31
+ * @returns {Object} Object with calculated ports for all services
32
+ *
33
+ * @example
34
+ * const ports = getDevPorts(0); // Default infra
35
+ * // Returns: { app: 3000, postgres: 5432, redis: 6379, pgadmin: 5050, redisCommander: 8081 }
36
+ * const ports = getDevPorts(1); // Developer-specific
37
+ * // Returns: { app: 3100, postgres: 5532, redis: 6479, pgadmin: 5150, redisCommander: 8181 }
38
+ */
39
+ function getDevPorts(developerId) {
40
+ // Validate type first - must be a number
41
+ if (typeof developerId !== 'number') {
42
+ throw new Error('Developer ID must be a positive number');
43
+ }
44
+
45
+ // Handle NaN, undefined, null - throw error (don't default)
46
+ if (isNaN(developerId) || developerId === undefined || developerId === null) {
47
+ throw new Error('Developer ID must be a positive number');
48
+ }
49
+
50
+ if (developerId < 0 || !Number.isInteger(developerId)) {
51
+ throw new Error('Developer ID must be a positive number');
52
+ }
53
+
54
+ // Developer ID 0 = default infra (base ports, no offset)
55
+ if (developerId === 0) {
56
+ return { ...BASE_PORTS };
57
+ }
58
+
59
+ // Developer ID > 0 = developer-specific (add offset)
60
+ const offset = developerId * 100;
61
+
62
+ return {
63
+ app: BASE_PORTS.app + offset,
64
+ postgres: BASE_PORTS.postgres + offset,
65
+ redis: BASE_PORTS.redis + offset,
66
+ pgadmin: BASE_PORTS.pgadmin + offset,
67
+ redisCommander: BASE_PORTS.redisCommander + offset
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Gets base ports (for reference/documentation)
73
+ * @returns {Object} Base ports object
74
+ */
75
+ function getBasePorts() {
76
+ return { ...BASE_PORTS };
77
+ }
78
+
79
+ module.exports = {
80
+ getDevPorts,
81
+ getBasePorts
82
+ };
83
+
@@ -78,6 +78,30 @@ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag) {
78
78
  spinner: 'dots'
79
79
  }).start();
80
80
 
81
+ // Ensure paths are absolute and normalized
82
+ const fsSync = require('fs');
83
+ const path = require('path');
84
+
85
+ dockerfilePath = path.resolve(dockerfilePath);
86
+ contextPath = path.resolve(contextPath);
87
+
88
+ // Validate paths exist (skip in test environments)
89
+ const isTestEnv = process.env.NODE_ENV === 'test' ||
90
+ process.env.JEST_WORKER_ID !== undefined ||
91
+ typeof jest !== 'undefined';
92
+
93
+ if (!isTestEnv) {
94
+ if (!fsSync.existsSync(dockerfilePath)) {
95
+ spinner.fail('Build failed');
96
+ throw new Error(`Dockerfile not found: ${dockerfilePath}`);
97
+ }
98
+
99
+ if (!fsSync.existsSync(contextPath)) {
100
+ spinner.fail('Build failed');
101
+ throw new Error(`Build context path does not exist: ${contextPath}`);
102
+ }
103
+ }
104
+
81
105
  return new Promise((resolve, reject) => {
82
106
  // Use spawn for streaming output
83
107
  const dockerProcess = spawn('docker', [
@@ -15,14 +15,15 @@ const chalk = require('chalk');
15
15
  const logger = require('../utils/logger');
16
16
 
17
17
  /**
18
- * Updates env.template to add MISO_CLIENTID and MISO_CLIENTSECRET entries
18
+ * Updates env.template to add MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL entries
19
19
  * @async
20
20
  * @param {string} appKey - Application key
21
21
  * @param {string} clientIdKey - Secret key for client ID (e.g., 'myapp-client-idKeyVault')
22
22
  * @param {string} clientSecretKey - Secret key for client secret (e.g., 'myapp-client-secretKeyVault')
23
+ * @param {string} controllerUrl - Controller URL (e.g., 'http://localhost:3010' or 'https://controller.aifabrix.ai')
23
24
  * @returns {Promise<void>} Resolves when template is updated
24
25
  */
25
- async function updateEnvTemplate(appKey, clientIdKey, clientSecretKey) {
26
+ async function updateEnvTemplate(appKey, clientIdKey, clientSecretKey, controllerUrl) {
26
27
  const envTemplatePath = path.join(process.cwd(), 'builder', appKey, 'env.template');
27
28
 
28
29
  if (!fsSync.existsSync(envTemplatePath)) {
@@ -33,19 +34,39 @@ async function updateEnvTemplate(appKey, clientIdKey, clientSecretKey) {
33
34
  try {
34
35
  let content = await fs.readFile(envTemplatePath, 'utf8');
35
36
 
36
- // Check if MISO_CLIENTID already exists
37
+ // Check if entries already exist
37
38
  const hasClientId = /^MISO_CLIENTID\s*=/m.test(content);
38
39
  const hasClientSecret = /^MISO_CLIENTSECRET\s*=/m.test(content);
40
+ const hasControllerUrl = /^MISO_CONTROLLER_URL\s*=/m.test(content);
39
41
 
40
- if (hasClientId && hasClientSecret) {
41
- // Update existing entries
42
+ // Update existing entries
43
+ if (hasClientId) {
42
44
  content = content.replace(/^MISO_CLIENTID\s*=.*$/m, `MISO_CLIENTID=kv://${clientIdKey}`);
45
+ }
46
+
47
+ if (hasClientSecret) {
43
48
  content = content.replace(/^MISO_CLIENTSECRET\s*=.*$/m, `MISO_CLIENTSECRET=kv://${clientSecretKey}`);
44
- } else {
45
- // Add new section if not present
49
+ }
50
+
51
+ if (hasControllerUrl) {
52
+ content = content.replace(/^MISO_CONTROLLER_URL\s*=.*$/m, `MISO_CONTROLLER_URL=${controllerUrl}`);
53
+ }
54
+
55
+ // Add missing entries
56
+ if (!hasClientId || !hasClientSecret || !hasControllerUrl) {
57
+ const missingEntries = [];
58
+ if (!hasClientId) {
59
+ missingEntries.push(`MISO_CLIENTID=kv://${clientIdKey}`);
60
+ }
61
+ if (!hasClientSecret) {
62
+ missingEntries.push(`MISO_CLIENTSECRET=kv://${clientSecretKey}`);
63
+ }
64
+ if (!hasControllerUrl) {
65
+ missingEntries.push(`MISO_CONTROLLER_URL=${controllerUrl}`);
66
+ }
67
+
46
68
  const misoSection = `# MISO Application Client Credentials (per application)
47
- MISO_CLIENTID=kv://${clientIdKey}
48
- MISO_CLIENTSECRET=kv://${clientSecretKey}
69
+ ${missingEntries.join('\n')}
49
70
  `;
50
71
 
51
72
  // Try to find a good place to insert (after last section or at end)
@@ -60,7 +81,6 @@ MISO_CLIENTSECRET=kv://${clientSecretKey}
60
81
  }
61
82
 
62
83
  await fs.writeFile(envTemplatePath, content, 'utf8');
63
- logger.log(chalk.green(`✓ Updated env.template for ${appKey}`));
64
84
  } catch (error) {
65
85
  logger.warn(chalk.yellow(`⚠️ Could not update env.template: ${error.message}`));
66
86
  }
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  const http = require('http');
12
+ const net = require('net');
12
13
  const chalk = require('chalk');
13
14
  const { exec } = require('child_process');
14
15
  const { promisify } = require('util');
@@ -300,8 +301,24 @@ async function waitForHealthCheck(appName, timeout = 90, port = null, config = n
300
301
  throw new Error(`Health check timeout after ${timeout} seconds`);
301
302
  }
302
303
 
304
+ /**
305
+ * Checks if port is available
306
+ * @param {number} port - Port number to check
307
+ * @returns {Promise<boolean>} True if port is available
308
+ */
309
+ async function checkPortAvailable(port) {
310
+ return new Promise((resolve) => {
311
+ const server = net.createServer();
312
+ server.listen(port, () => {
313
+ server.close(() => resolve(true));
314
+ });
315
+ server.on('error', () => resolve(false));
316
+ });
317
+ }
318
+
303
319
  module.exports = {
304
320
  waitForHealthCheck,
305
- checkHealthEndpoint
321
+ checkHealthEndpoint,
322
+ checkPortAvailable
306
323
  };
307
324
 
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Infrastructure Container Utilities
3
+ *
4
+ * This module provides helper functions for finding and checking
5
+ * infrastructure containers. Used by the main infra.js module.
6
+ *
7
+ * @fileoverview Container utilities for infrastructure management
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const { exec } = require('child_process');
13
+ const { promisify } = require('util');
14
+ const config = require('../config');
15
+
16
+ const execAsync = promisify(exec);
17
+
18
+ /**
19
+ * Finds container by name pattern
20
+ * @private
21
+ * @async
22
+ * @param {string} serviceName - Service name
23
+ * @param {number} [devId] - Developer ID (optional, will be loaded from config if not provided)
24
+ * @returns {Promise<string|null>} Container name or null if not found
25
+ */
26
+ async function findContainer(serviceName, devId = null) {
27
+ try {
28
+ const developerId = devId || await config.getDeveloperId();
29
+ // Dev 0: aifabrix-{serviceName}, Dev > 0: aifabrix-dev{id}-{serviceName}
30
+ const containerNamePattern = developerId === 0
31
+ ? `aifabrix-${serviceName}`
32
+ : `aifabrix-dev${developerId}-${serviceName}`;
33
+ let { stdout } = await execAsync(`docker ps --filter "name=${containerNamePattern}" --format "{{.Names}}"`);
34
+ let containerName = stdout.trim();
35
+ if (!containerName) {
36
+ // Fallback to old naming patterns for backward compatibility
37
+ ({ stdout } = await execAsync(`docker ps --filter "name=infra-${serviceName}" --format "{{.Names}}"`));
38
+ containerName = stdout.trim();
39
+ if (!containerName) {
40
+ ({ stdout } = await execAsync(`docker ps --filter "name=aifabrix-${serviceName}" --format "{{.Names}}"`));
41
+ containerName = stdout.trim();
42
+ }
43
+ }
44
+ return containerName;
45
+ } catch (error) {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Checks health status for a service with health checks
52
+ * @private
53
+ * @async
54
+ * @param {string} serviceName - Service name
55
+ * @param {number} [devId] - Developer ID (optional, will be loaded from config if not provided)
56
+ * @returns {Promise<string>} Health status
57
+ */
58
+ async function checkServiceWithHealthCheck(serviceName, devId = null) {
59
+ try {
60
+ const containerName = await findContainer(serviceName, devId);
61
+ if (!containerName) {
62
+ return 'unknown';
63
+ }
64
+ const { stdout } = await execAsync(`docker inspect --format='{{.State.Health.Status}}' ${containerName}`);
65
+ const status = stdout.trim().replace(/['"]/g, '');
66
+ // Accept both 'healthy' and 'starting' as healthy (starting means it's initializing)
67
+ return (status === 'healthy' || status === 'starting') ? 'healthy' : status;
68
+ } catch (error) {
69
+ return 'unknown';
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Checks health status for a service without health checks
75
+ * @private
76
+ * @async
77
+ * @param {string} serviceName - Service name
78
+ * @param {number} [devId] - Developer ID (optional, will be loaded from config if not provided)
79
+ * @returns {Promise<string>} Health status
80
+ */
81
+ async function checkServiceWithoutHealthCheck(serviceName, devId = null) {
82
+ try {
83
+ const containerName = await findContainer(serviceName, devId);
84
+ if (!containerName) {
85
+ return 'unknown';
86
+ }
87
+ const { stdout } = await execAsync(`docker inspect --format='{{.State.Status}}' ${containerName}`);
88
+ const status = stdout.trim().replace(/['"]/g, '');
89
+ // Treat 'running' or 'healthy' as 'healthy' for services without health checks
90
+ return (status === 'running' || status === 'healthy') ? 'healthy' : 'unhealthy';
91
+ } catch (error) {
92
+ return 'unknown';
93
+ }
94
+ }
95
+
96
+ module.exports = {
97
+ findContainer,
98
+ checkServiceWithHealthCheck,
99
+ checkServiceWithoutHealthCheck
100
+ };
101
+
@@ -12,7 +12,6 @@ const fs = require('fs');
12
12
  const path = require('path');
13
13
  const yaml = require('js-yaml');
14
14
  const os = require('os');
15
- const chalk = require('chalk');
16
15
  const logger = require('../utils/logger');
17
16
 
18
17
  /**
@@ -76,7 +75,6 @@ async function saveLocalSecret(key, value) {
76
75
  });
77
76
 
78
77
  fs.writeFileSync(secretsPath, yamlContent, { mode: 0o600 });
79
- logger.log(chalk.green(`✓ Saved secret ${key} to ${secretsPath}`));
80
78
  }
81
79
 
82
80
  /**