@aifabrix/builder 2.22.2 → 2.31.1

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 (63) hide show
  1. package/jest.config.coverage.js +37 -0
  2. package/lib/api/pipeline.api.js +10 -9
  3. package/lib/app-deploy.js +36 -14
  4. package/lib/app-list.js +191 -71
  5. package/lib/app-prompts.js +77 -26
  6. package/lib/app-readme.js +123 -5
  7. package/lib/app-rotate-secret.js +210 -80
  8. package/lib/app-run-helpers.js +200 -172
  9. package/lib/app-run.js +137 -68
  10. package/lib/audit-logger.js +8 -7
  11. package/lib/build.js +161 -250
  12. package/lib/cli.js +73 -65
  13. package/lib/commands/login.js +45 -31
  14. package/lib/commands/logout.js +181 -0
  15. package/lib/commands/secure.js +59 -24
  16. package/lib/config.js +79 -45
  17. package/lib/datasource-deploy.js +89 -29
  18. package/lib/deployer.js +164 -129
  19. package/lib/diff.js +63 -21
  20. package/lib/environment-deploy.js +36 -19
  21. package/lib/external-system-deploy.js +134 -66
  22. package/lib/external-system-download.js +244 -171
  23. package/lib/external-system-test.js +199 -164
  24. package/lib/generator-external.js +145 -72
  25. package/lib/generator-helpers.js +49 -17
  26. package/lib/generator-split.js +105 -58
  27. package/lib/infra.js +101 -131
  28. package/lib/schema/application-schema.json +895 -896
  29. package/lib/schema/env-config.yaml +11 -4
  30. package/lib/template-validator.js +13 -4
  31. package/lib/utils/api.js +8 -8
  32. package/lib/utils/app-register-auth.js +36 -18
  33. package/lib/utils/app-run-containers.js +140 -0
  34. package/lib/utils/auth-headers.js +6 -6
  35. package/lib/utils/build-copy.js +60 -2
  36. package/lib/utils/build-helpers.js +94 -0
  37. package/lib/utils/cli-utils.js +177 -76
  38. package/lib/utils/compose-generator.js +12 -2
  39. package/lib/utils/config-tokens.js +151 -9
  40. package/lib/utils/deployment-errors.js +137 -69
  41. package/lib/utils/deployment-validation-helpers.js +103 -0
  42. package/lib/utils/docker-build.js +57 -0
  43. package/lib/utils/dockerfile-utils.js +13 -3
  44. package/lib/utils/env-copy.js +163 -94
  45. package/lib/utils/env-map.js +226 -86
  46. package/lib/utils/error-formatters/network-errors.js +0 -1
  47. package/lib/utils/external-system-display.js +14 -19
  48. package/lib/utils/external-system-env-helpers.js +107 -0
  49. package/lib/utils/external-system-test-helpers.js +144 -0
  50. package/lib/utils/health-check.js +10 -8
  51. package/lib/utils/infra-status.js +123 -0
  52. package/lib/utils/paths.js +228 -49
  53. package/lib/utils/schema-loader.js +125 -57
  54. package/lib/utils/token-manager.js +3 -3
  55. package/lib/utils/yaml-preserve.js +55 -16
  56. package/lib/validate.js +87 -89
  57. package/package.json +7 -5
  58. package/scripts/ci-fix.sh +19 -0
  59. package/scripts/ci-simulate.sh +19 -0
  60. package/scripts/install-local.js +210 -0
  61. package/templates/applications/miso-controller/test.yaml +1 -0
  62. package/templates/python/Dockerfile.hbs +8 -45
  63. package/templates/typescript/Dockerfile.hbs +8 -42
@@ -25,119 +25,21 @@ const logger = require('./utils/logger');
25
25
  const { waitForHealthCheck } = require('./utils/health-check');
26
26
  const composeGenerator = require('./utils/compose-generator');
27
27
  const dockerUtils = require('./utils/docker');
28
+ const containerHelpers = require('./utils/app-run-containers');
28
29
 
29
30
  const execAsync = promisify(exec);
30
31
 
31
- /**
32
- * Checks if Docker image exists for the application
33
- * @param {string} imageName - Image name (can include repository prefix)
34
- * @param {string} tag - Image tag (default: latest)
35
- * @param {boolean} [debug=false] - Enable debug logging
36
- * @returns {Promise<boolean>} True if image exists
37
- */
38
- async function checkImageExists(imageName, tag = 'latest', debug = false) {
39
- try {
40
- const fullImageName = `${imageName}:${tag}`;
41
- const cmd = `docker images --format "{{.Repository}}:{{.Tag}}" --filter "reference=${fullImageName}"`;
42
- if (debug) {
43
- logger.log(chalk.gray(`[DEBUG] Executing: ${cmd}`));
44
- }
45
- const { stdout } = await execAsync(cmd);
46
- const lines = stdout.trim().split('\n').filter(line => line.trim() !== '');
47
- const exists = lines.some(line => line.trim() === fullImageName);
48
- if (debug) {
49
- logger.log(chalk.gray(`[DEBUG] Image ${fullImageName} exists: ${exists}`));
50
- }
51
- return exists;
52
- } catch (error) {
53
- if (debug) {
54
- logger.log(chalk.gray(`[DEBUG] Image check failed: ${error.message}`));
55
- }
56
- return false;
57
- }
58
- }
59
-
60
- /**
61
- * Checks if container is already running
62
- * @param {string} appName - Application name
63
- * @param {number|string} developerId - Developer ID (0 = default infra, > 0 = developer-specific; string allowed)
64
- * @param {boolean} [debug=false] - Enable debug logging
65
- * @returns {Promise<boolean>} True if container is running
66
- */
67
- async function checkContainerRunning(appName, developerId, debug = false) {
68
- try {
69
- const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
70
- const containerName = idNum === 0 ? `aifabrix-${appName}` : `aifabrix-dev${developerId}-${appName}`;
71
- const cmd = `docker ps --filter "name=${containerName}" --format "{{.Names}}"`;
72
- if (debug) {
73
- logger.log(chalk.gray(`[DEBUG] Executing: ${cmd}`));
74
- }
75
- const { stdout } = await execAsync(cmd);
76
- const isRunning = stdout.trim() === containerName;
77
- if (debug) {
78
- logger.log(chalk.gray(`[DEBUG] Container ${containerName} running: ${isRunning}`));
79
- if (isRunning) {
80
- const statusCmd = `docker ps --filter "name=${containerName}" --format "{{.Status}}"`;
81
- const { stdout: status } = await execAsync(statusCmd);
82
- const portsCmd = `docker ps --filter "name=${containerName}" --format "{{.Ports}}"`;
83
- const { stdout: ports } = await execAsync(portsCmd);
84
- logger.log(chalk.gray(`[DEBUG] Container status: ${status.trim()}`));
85
- logger.log(chalk.gray(`[DEBUG] Container ports: ${ports.trim()}`));
86
- }
87
- }
88
- return isRunning;
89
- } catch (error) {
90
- if (debug) {
91
- logger.log(chalk.gray(`[DEBUG] Container check failed: ${error.message}`));
92
- }
93
- return false;
94
- }
95
- }
32
+ // Re-export container helper functions
33
+ const checkImageExists = containerHelpers.checkImageExists;
34
+ const checkContainerRunning = containerHelpers.checkContainerRunning;
35
+ const stopAndRemoveContainer = containerHelpers.stopAndRemoveContainer;
96
36
 
97
37
  /**
98
- * Stops and removes existing container
38
+ * Check if running from builder directory
99
39
  * @param {string} appName - Application name
100
- * @param {number|string} developerId - Developer ID (0 = default infra, > 0 = developer-specific; string allowed)
101
- * @param {boolean} [debug=false] - Enable debug logging
40
+ * @throws {Error} If running from builder directory
102
41
  */
103
- async function stopAndRemoveContainer(appName, developerId, debug = false) {
104
- try {
105
- const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
106
- const containerName = idNum === 0 ? `aifabrix-${appName}` : `aifabrix-dev${developerId}-${appName}`;
107
- logger.log(chalk.yellow(`Stopping existing container ${containerName}...`));
108
- const stopCmd = `docker stop ${containerName}`;
109
- if (debug) {
110
- logger.log(chalk.gray(`[DEBUG] Executing: ${stopCmd}`));
111
- }
112
- await execAsync(stopCmd);
113
- const rmCmd = `docker rm ${containerName}`;
114
- if (debug) {
115
- logger.log(chalk.gray(`[DEBUG] Executing: ${rmCmd}`));
116
- }
117
- await execAsync(rmCmd);
118
- logger.log(chalk.green(`✓ Container ${containerName} stopped and removed`));
119
- } catch (error) {
120
- if (debug) {
121
- logger.log(chalk.gray(`[DEBUG] Stop/remove container error: ${error.message}`));
122
- }
123
- const idNum2 = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
124
- const containerName = idNum2 === 0 ? `aifabrix-${appName}` : `aifabrix-dev${developerId}-${appName}`;
125
- logger.log(chalk.gray(`Container ${containerName} was not running`));
126
- }
127
- }
128
-
129
- /**
130
- * Validates app name and loads configuration
131
- * @async
132
- * @param {string} appName - Application name
133
- * @returns {Promise<Object>} Application configuration
134
- * @throws {Error} If validation fails
135
- */
136
- async function validateAppConfiguration(appName) {
137
- if (!appName || typeof appName !== 'string') {
138
- throw new Error('Application name is required');
139
- }
140
-
42
+ function checkBuilderDirectory(appName) {
141
43
  const currentDir = process.cwd();
142
44
  const normalizedPath = currentDir.replace(/\\/g, '/');
143
45
  const expectedBuilderPath = `builder/${appName}`;
@@ -152,8 +54,17 @@ async function validateAppConfiguration(appName) {
152
54
  ` aifabrix run ${appName}`
153
55
  );
154
56
  }
57
+ }
155
58
 
156
- const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
59
+ /**
60
+ * Load and validate config file exists
61
+ * @param {string} appName - Application name
62
+ * @returns {Object} Application configuration
63
+ * @throws {Error} If config file not found
64
+ */
65
+ function loadAppConfig(appName) {
66
+ const currentDir = process.cwd();
67
+ const configPath = path.join(currentDir, 'builder', appName, 'variables.yaml');
157
68
  if (!fsSync.existsSync(configPath)) {
158
69
  const expectedDir = path.join(currentDir, 'builder', appName);
159
70
  throw new Error(
@@ -166,29 +77,58 @@ async function validateAppConfiguration(appName) {
166
77
  }
167
78
 
168
79
  const configContent = fsSync.readFileSync(configPath, 'utf8');
169
- const appConfig = yaml.load(configContent);
80
+ return yaml.load(configContent);
81
+ }
170
82
 
171
- const validation = await validator.validateApplication(appName);
172
- if (!validation.valid) {
173
- const allErrors = [];
83
+ /**
84
+ * Format validation errors
85
+ * @param {Object} validation - Validation result
86
+ * @returns {Array<string>} Formatted error messages
87
+ */
88
+ function formatValidationErrors(validation) {
89
+ const allErrors = [];
174
90
 
175
- if (validation.variables && validation.variables.errors && validation.variables.errors.length > 0) {
176
- allErrors.push('variables.yaml:');
177
- allErrors.push(...validation.variables.errors.map(err => ` ${err}`));
178
- }
179
- if (validation.rbac && validation.rbac.errors && validation.rbac.errors.length > 0) {
180
- allErrors.push('rbac.yaml:');
181
- allErrors.push(...validation.rbac.errors.map(err => ` ${err}`));
182
- }
183
- if (validation.env && validation.env.errors && validation.env.errors.length > 0) {
184
- allErrors.push('env.template:');
185
- allErrors.push(...validation.env.errors.map(err => ` ${err}`));
186
- }
91
+ if (validation.variables && validation.variables.errors && validation.variables.errors.length > 0) {
92
+ allErrors.push('variables.yaml:');
93
+ allErrors.push(...validation.variables.errors.map(err => ` ${err}`));
94
+ }
95
+ if (validation.rbac && validation.rbac.errors && validation.rbac.errors.length > 0) {
96
+ allErrors.push('rbac.yaml:');
97
+ allErrors.push(...validation.rbac.errors.map(err => ` ${err}`));
98
+ }
99
+ if (validation.env && validation.env.errors && validation.env.errors.length > 0) {
100
+ allErrors.push('env.template:');
101
+ allErrors.push(...validation.env.errors.map(err => ` ${err}`));
102
+ }
103
+
104
+ return allErrors;
105
+ }
106
+
107
+ /**
108
+ * Validates app name and loads configuration
109
+ * @async
110
+ * @param {string} appName - Application name
111
+ * @returns {Promise<Object>} Application configuration
112
+ * @throws {Error} If validation fails
113
+ */
114
+ async function validateAppConfiguration(appName) {
115
+ if (!appName || typeof appName !== 'string') {
116
+ throw new Error('Application name is required');
117
+ }
187
118
 
119
+ // Check if running from builder directory
120
+ checkBuilderDirectory(appName);
121
+
122
+ // Load config
123
+ const appConfig = loadAppConfig(appName);
124
+
125
+ // Validate configuration
126
+ const validation = await validator.validateApplication(appName);
127
+ if (!validation.valid) {
128
+ const allErrors = formatValidationErrors(validation);
188
129
  if (allErrors.length === 0) {
189
130
  throw new Error('Configuration validation failed');
190
131
  }
191
-
192
132
  throw new Error(`Configuration validation failed:\n${allErrors.join('\n')}`);
193
133
  }
194
134
 
@@ -235,22 +175,27 @@ async function checkPrerequisites(appName, appConfig, debug = false) {
235
175
  }
236
176
 
237
177
  /**
238
- * Prepares environment: ensures .env file and generates Docker Compose
178
+ * Ensure dev directory exists
239
179
  * @async
240
180
  * @param {string} appName - Application name
241
- * @param {Object} appConfig - Application configuration
242
- * @param {Object} options - Run options
243
- * @returns {Promise<string>} Path to generated compose file
181
+ * @param {string} developerId - Developer ID
182
+ * @returns {Promise<string>} Dev directory path
244
183
  */
245
- async function prepareEnvironment(appName, appConfig, options) {
246
- const developerId = await config.getDeveloperId();
184
+ async function ensureDevDirectory(appName, developerId) {
247
185
  const devDir = buildCopy.getDevDirectory(appName, developerId);
248
-
249
186
  if (!fsSync.existsSync(devDir)) {
250
187
  await buildCopy.copyBuilderToDevDirectory(appName, developerId);
251
188
  }
189
+ return devDir;
190
+ }
252
191
 
253
- const builderEnvPath = path.join(process.cwd(), 'builder', appName, '.env');
192
+ /**
193
+ * Generate or update .env file for Docker
194
+ * @async
195
+ * @param {string} appName - Application name
196
+ * @param {string} builderEnvPath - Path to builder .env file
197
+ */
198
+ async function ensureEnvFile(appName, builderEnvPath) {
254
199
  if (!fsSync.existsSync(builderEnvPath)) {
255
200
  logger.log(chalk.yellow('Generating .env file from template...'));
256
201
  await secrets.generateEnvFile(appName, null, 'docker');
@@ -258,60 +203,113 @@ async function prepareEnvironment(appName, appConfig, options) {
258
203
  logger.log(chalk.blue('Updating .env file for Docker environment...'));
259
204
  await secrets.generateEnvFile(appName, null, 'docker');
260
205
  }
206
+ }
261
207
 
262
- const devEnvPath = path.join(devDir, '.env');
208
+ /**
209
+ * Copy .env file to dev directory
210
+ * @async
211
+ * @param {string} builderEnvPath - Path to builder .env file
212
+ * @param {string} devEnvPath - Path to dev .env file
213
+ */
214
+ async function copyEnvToDev(builderEnvPath, devEnvPath) {
263
215
  if (fsSync.existsSync(builderEnvPath)) {
264
216
  await fs.copyFile(builderEnvPath, devEnvPath);
265
217
  }
218
+ }
266
219
 
267
- const variablesPath = path.join(devDir, 'variables.yaml');
268
- if (fsSync.existsSync(variablesPath)) {
269
- const variablesContent = fsSync.readFileSync(variablesPath, 'utf8');
270
- const variables = yaml.load(variablesContent);
271
-
272
- if (variables?.build?.envOutputPath && variables.build.envOutputPath !== null) {
273
- logger.log(chalk.blue('Ensuring .env file in apps/ directory is updated for Docker...'));
274
- await secrets.generateEnvFile(appName, null, 'docker');
275
- if (fsSync.existsSync(builderEnvPath)) {
276
- await fs.copyFile(builderEnvPath, devEnvPath);
277
- }
278
- }
220
+ /**
221
+ * Handle envOutputPath configuration
222
+ * @async
223
+ * @param {string} appName - Application name
224
+ * @param {string} variablesPath - Path to variables.yaml
225
+ * @param {string} builderEnvPath - Path to builder .env file
226
+ * @param {string} devEnvPath - Path to dev .env file
227
+ */
228
+ async function handleEnvOutputPath(appName, variablesPath, builderEnvPath, devEnvPath) {
229
+ if (!fsSync.existsSync(variablesPath)) {
230
+ return;
279
231
  }
280
232
 
281
- logger.log(chalk.blue('Generating Docker Compose configuration...'));
282
- const composeOptions = { ...options };
283
- if (!composeOptions.port) {
284
- const basePort = appConfig.port || 3000;
285
- const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
286
- composeOptions.port = idNum === 0 ? basePort : basePort + (idNum * 100);
233
+ const variablesContent = fsSync.readFileSync(variablesPath, 'utf8');
234
+ const variables = yaml.load(variablesContent);
235
+
236
+ if (variables?.build?.envOutputPath && variables.build.envOutputPath !== null) {
237
+ logger.log(chalk.blue('Ensuring .env file in apps/ directory is updated for Docker...'));
238
+ await secrets.generateEnvFile(appName, null, 'docker');
239
+ await copyEnvToDev(builderEnvPath, devEnvPath);
287
240
  }
288
- const composeContent = await composeGenerator.generateDockerCompose(appName, appConfig, composeOptions);
241
+ }
289
242
 
290
- if (!fsSync.existsSync(devDir)) {
291
- await buildCopy.copyBuilderToDevDirectory(appName, developerId);
243
+ /**
244
+ * Calculate compose port from options or app config
245
+ * @param {Object} options - Run options
246
+ * @param {Object} appConfig - Application configuration
247
+ * @param {string} developerId - Developer ID
248
+ * @returns {number} Port number
249
+ */
250
+ function calculateComposePort(options, appConfig, developerId) {
251
+ if (options.port) {
252
+ return options.port;
292
253
  }
254
+ const basePort = appConfig.port || 3000;
255
+ const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
256
+ return idNum === 0 ? basePort : basePort + (idNum * 100);
257
+ }
258
+
259
+ /**
260
+ * Generate and write Docker Compose file
261
+ * @async
262
+ * @param {string} appName - Application name
263
+ * @param {Object} appConfig - Application configuration
264
+ * @param {Object} composeOptions - Compose options
265
+ * @param {string} devDir - Dev directory path
266
+ * @returns {Promise<string>} Path to compose file
267
+ */
268
+ async function generateComposeFile(appName, appConfig, composeOptions, devDir) {
269
+ logger.log(chalk.blue('Generating Docker Compose configuration...'));
270
+ const composeContent = await composeGenerator.generateDockerCompose(appName, appConfig, composeOptions);
293
271
  const tempComposePath = path.join(devDir, 'docker-compose.yaml');
294
272
  await fs.writeFile(tempComposePath, composeContent);
295
-
296
273
  return tempComposePath;
297
274
  }
298
275
 
299
276
  /**
300
- * Starts the container and waits for health check
277
+ * Prepares environment: ensures .env file and generates Docker Compose
301
278
  * @async
302
279
  * @param {string} appName - Application name
303
- * @param {string} composePath - Path to Docker Compose file
304
- * @param {number} port - Application port
305
280
  * @param {Object} appConfig - Application configuration
306
- * @param {boolean} [debug=false] - Enable debug logging
307
- * @throws {Error} If container fails to start or become healthy
281
+ * @param {Object} options - Run options
282
+ * @returns {Promise<string>} Path to generated compose file
308
283
  */
309
- async function startContainer(appName, composePath, port, appConfig = null, debug = false) {
310
- logger.log(chalk.blue(`Starting ${appName}...`));
284
+ async function prepareEnvironment(appName, appConfig, options) {
285
+ const developerId = await config.getDeveloperId();
286
+ const devDir = await ensureDevDirectory(appName, developerId);
311
287
 
312
- // Ensure Docker + Compose available and determine correct compose command
313
- const composeCmdBase = await dockerUtils.ensureDockerAndCompose().then(() => dockerUtils.getComposeCommand());
288
+ // Generate/update .env file
289
+ const builderEnvPath = path.join(process.cwd(), 'builder', appName, '.env');
290
+ await ensureEnvFile(appName, builderEnvPath);
314
291
 
292
+ // Copy .env to dev directory
293
+ const devEnvPath = path.join(devDir, '.env');
294
+ await copyEnvToDev(builderEnvPath, devEnvPath);
295
+
296
+ // Handle envOutputPath if configured
297
+ const variablesPath = path.join(devDir, 'variables.yaml');
298
+ await handleEnvOutputPath(appName, variablesPath, builderEnvPath, devEnvPath);
299
+
300
+ // Generate Docker Compose
301
+ const composeOptions = { ...options };
302
+ composeOptions.port = calculateComposePort(composeOptions, appConfig, developerId);
303
+ return await generateComposeFile(appName, appConfig, composeOptions, devDir);
304
+ }
305
+
306
+ /**
307
+ * Prepare environment variables from admin secrets
308
+ * @async
309
+ * @param {boolean} debug - Enable debug logging
310
+ * @returns {Promise<Object>} Environment variables object
311
+ */
312
+ async function prepareContainerEnv(debug) {
315
313
  const adminSecretsPath = await infra.ensureAdminSecrets();
316
314
  if (debug) {
317
315
  logger.log(chalk.gray(`[DEBUG] Admin secrets path: ${adminSecretsPath}`));
@@ -331,25 +329,55 @@ async function startContainer(appName, composePath, port, appConfig = null, debu
331
329
  logger.log(chalk.gray(`[DEBUG] Environment variables: ADMIN_SECRETS_PATH=${adminSecretsPath}, POSTGRES_PASSWORD=${postgresPassword ? '***' : '(not set)'}`));
332
330
  }
333
331
 
332
+ return env;
333
+ }
334
+
335
+ /**
336
+ * Execute docker-compose up command
337
+ * @async
338
+ * @param {string} composeCmdBase - Base compose command
339
+ * @param {string} composePath - Path to compose file
340
+ * @param {Object} env - Environment variables
341
+ * @param {boolean} debug - Enable debug logging
342
+ */
343
+ async function executeComposeUp(composeCmdBase, composePath, env, debug) {
334
344
  const composeCmd = `${composeCmdBase} -f "${composePath}" up -d`;
335
345
  if (debug) {
336
346
  logger.log(chalk.gray(`[DEBUG] Executing: ${composeCmd}`));
337
347
  logger.log(chalk.gray(`[DEBUG] Compose file: ${composePath}`));
338
348
  }
339
349
  await execAsync(composeCmd, { env });
350
+ }
340
351
 
352
+ /**
353
+ * Starts the container and waits for health check
354
+ * @async
355
+ * @param {string} appName - Application name
356
+ * @param {string} composePath - Path to Docker Compose file
357
+ * @param {number} port - Application port
358
+ * @param {Object} appConfig - Application configuration
359
+ * @param {boolean} [debug=false] - Enable debug logging
360
+ * @throws {Error} If container fails to start or become healthy
361
+ */
362
+ async function startContainer(appName, composePath, port, appConfig = null, debug = false) {
363
+ logger.log(chalk.blue(`Starting ${appName}...`));
364
+
365
+ // Ensure Docker + Compose available and determine correct compose command
366
+ const composeCmdBase = await dockerUtils.ensureDockerAndCompose().then(() => dockerUtils.getComposeCommand());
367
+
368
+ // Prepare environment variables
369
+ const env = await prepareContainerEnv(debug);
370
+
371
+ // Execute compose up
372
+ await executeComposeUp(composeCmdBase, composePath, env, debug);
373
+
374
+ // Get container name and log status
341
375
  const idNum = typeof appConfig.developerId === 'string' ? parseInt(appConfig.developerId, 10) : appConfig.developerId;
342
376
  const containerName = idNum === 0 ? `aifabrix-${appName}` : `aifabrix-dev${appConfig.developerId}-${appName}`;
343
377
  logger.log(chalk.green(`✓ Container ${containerName} started`));
344
- if (debug) {
345
- const statusCmd = `docker ps --filter "name=${containerName}" --format "{{.Status}}"`;
346
- const { stdout: status } = await execAsync(statusCmd);
347
- const portsCmd = `docker ps --filter "name=${containerName}" --format "{{.Ports}}"`;
348
- const { stdout: ports } = await execAsync(portsCmd);
349
- logger.log(chalk.gray(`[DEBUG] Container status: ${status.trim()}`));
350
- logger.log(chalk.gray(`[DEBUG] Container ports: ${ports.trim()}`));
351
- }
378
+ await containerHelpers.logContainerStatus(containerName, debug);
352
379
 
380
+ // Wait for health check
353
381
  const healthCheckPath = appConfig?.healthCheck?.path || '/health';
354
382
  logger.log(chalk.blue(`Waiting for application to be healthy at http://localhost:${port}${healthCheckPath}...`));
355
383
  await waitForHealthCheck(appName, 90, port, appConfig, debug);