@aifabrix/builder 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +6 -2
  2. package/bin/aifabrix.js +9 -3
  3. package/jest.config.integration.js +30 -0
  4. package/lib/app-config.js +157 -0
  5. package/lib/app-deploy.js +233 -82
  6. package/lib/app-dockerfile.js +112 -0
  7. package/lib/app-prompts.js +244 -0
  8. package/lib/app-push.js +172 -0
  9. package/lib/app-run.js +334 -133
  10. package/lib/app.js +208 -274
  11. package/lib/audit-logger.js +2 -0
  12. package/lib/build.js +209 -98
  13. package/lib/cli.js +76 -86
  14. package/lib/commands/app.js +414 -0
  15. package/lib/commands/login.js +304 -0
  16. package/lib/config.js +78 -0
  17. package/lib/deployer.js +225 -81
  18. package/lib/env-reader.js +45 -30
  19. package/lib/generator.js +308 -191
  20. package/lib/github-generator.js +67 -7
  21. package/lib/infra.js +156 -61
  22. package/lib/push.js +105 -10
  23. package/lib/schema/application-schema.json +30 -2
  24. package/lib/schema/infrastructure-schema.json +589 -0
  25. package/lib/secrets.js +229 -24
  26. package/lib/template-validator.js +205 -0
  27. package/lib/templates.js +305 -170
  28. package/lib/utils/api.js +329 -0
  29. package/lib/utils/cli-utils.js +97 -0
  30. package/lib/utils/dockerfile-utils.js +131 -0
  31. package/lib/utils/environment-checker.js +125 -0
  32. package/lib/utils/error-formatter.js +61 -0
  33. package/lib/utils/health-check.js +187 -0
  34. package/lib/utils/logger.js +53 -0
  35. package/lib/utils/template-helpers.js +223 -0
  36. package/lib/utils/variable-transformer.js +271 -0
  37. package/lib/validator.js +27 -112
  38. package/package.json +13 -10
  39. package/templates/README.md +75 -3
  40. package/templates/applications/keycloak/Dockerfile +36 -0
  41. package/templates/applications/keycloak/env.template +32 -0
  42. package/templates/applications/keycloak/rbac.yaml +37 -0
  43. package/templates/applications/keycloak/variables.yaml +56 -0
  44. package/templates/applications/miso-controller/Dockerfile +125 -0
  45. package/templates/applications/miso-controller/env.template +129 -0
  46. package/templates/applications/miso-controller/rbac.yaml +168 -0
  47. package/templates/applications/miso-controller/variables.yaml +56 -0
  48. package/templates/github/release.yaml.hbs +5 -26
  49. package/templates/github/steps/npm.hbs +24 -0
  50. package/templates/infra/compose.yaml +6 -6
  51. package/templates/python/docker-compose.hbs +19 -12
  52. package/templates/python/main.py +80 -0
  53. package/templates/python/requirements.txt +4 -0
  54. package/templates/typescript/Dockerfile.hbs +2 -2
  55. package/templates/typescript/docker-compose.hbs +19 -12
  56. package/templates/typescript/index.ts +116 -0
  57. package/templates/typescript/package.json +26 -0
  58. package/templates/typescript/tsconfig.json +24 -0
package/lib/app-run.js CHANGED
@@ -21,18 +21,24 @@ const { promisify } = require('util');
21
21
  const validator = require('./validator');
22
22
  const infra = require('./infra');
23
23
  const secrets = require('./secrets');
24
+ const logger = require('./utils/logger');
25
+ const { waitForHealthCheck } = require('./utils/health-check');
24
26
 
25
27
  const execAsync = promisify(exec);
26
28
 
27
29
  /**
28
30
  * Checks if Docker image exists for the application
29
- * @param {string} appName - Application name
31
+ * @param {string} imageName - Image name (can include repository prefix)
32
+ * @param {string} tag - Image tag (default: latest)
30
33
  * @returns {Promise<boolean>} True if image exists
31
34
  */
32
- async function checkImageExists(appName) {
35
+ async function checkImageExists(imageName, tag = 'latest') {
33
36
  try {
34
- const { stdout } = await execAsync(`docker images --format "{{.Repository}}:{{.Tag}}" | grep "^${appName}:latest$"`);
35
- return stdout.trim() === `${appName}:latest`;
37
+ const fullImageName = `${imageName}:${tag}`;
38
+ // Use Docker's native filtering for cross-platform compatibility (Windows-safe)
39
+ const { stdout } = await execAsync(`docker images --format "{{.Repository}}:{{.Tag}}" --filter "reference=${fullImageName}"`);
40
+ const lines = stdout.trim().split('\n').filter(line => line.trim() !== '');
41
+ return lines.some(line => line.trim() === fullImageName);
36
42
  } catch (error) {
37
43
  return false;
38
44
  }
@@ -58,13 +64,13 @@ async function checkContainerRunning(appName) {
58
64
  */
59
65
  async function stopAndRemoveContainer(appName) {
60
66
  try {
61
- console.log(chalk.yellow(`Stopping existing container aifabrix-${appName}...`));
67
+ logger.log(chalk.yellow(`Stopping existing container aifabrix-${appName}...`));
62
68
  await execAsync(`docker stop aifabrix-${appName}`);
63
69
  await execAsync(`docker rm aifabrix-${appName}`);
64
- console.log(chalk.green(`✓ Container aifabrix-${appName} stopped and removed`));
70
+ logger.log(chalk.green(`✓ Container aifabrix-${appName} stopped and removed`));
65
71
  } catch (error) {
66
72
  // Container might not exist, which is fine
67
- console.log(chalk.gray(`Container aifabrix-${appName} was not running`));
73
+ logger.log(chalk.gray(`Container aifabrix-${appName} was not running`));
68
74
  }
69
75
  }
70
76
 
@@ -84,87 +90,337 @@ async function checkPortAvailable(port) {
84
90
  }
85
91
 
86
92
  /**
87
- * Generates Docker Compose configuration from template
88
- * @param {string} appName - Application name
89
- * @param {Object} config - Application configuration
90
- * @param {Object} options - Run options
91
- * @returns {Promise<string>} Generated compose content
93
+ * Loads and compiles Docker Compose template
94
+ * @param {string} language - Language type
95
+ * @returns {Function} Compiled Handlebars template
96
+ * @throws {Error} If template not found
92
97
  */
93
- async function generateDockerCompose(appName, config, options) {
94
- const language = config.build?.language || config.language || 'typescript';
98
+ function loadDockerComposeTemplate(language) {
95
99
  const templatePath = path.join(__dirname, '..', 'templates', language, 'docker-compose.hbs');
96
100
  if (!fsSync.existsSync(templatePath)) {
97
101
  throw new Error(`Docker Compose template not found for language: ${language}`);
98
102
  }
99
103
 
100
104
  const templateContent = fsSync.readFileSync(templatePath, 'utf8');
101
- const template = handlebars.compile(templateContent);
105
+ return handlebars.compile(templateContent);
106
+ }
102
107
 
103
- const port = options.port || config.build?.localPort || config.port || 3000;
108
+ /**
109
+ * Extracts image name from configuration (same logic as build.js)
110
+ * @param {Object} config - Application configuration
111
+ * @param {string} appName - Application name (fallback)
112
+ * @returns {string} Image name
113
+ */
114
+ function getImageName(config, appName) {
115
+ if (typeof config.image === 'string') {
116
+ return config.image.split(':')[0];
117
+ } else if (config.image?.name) {
118
+ return config.image.name;
119
+ } else if (config.app?.key) {
120
+ return config.app.key;
121
+ }
122
+ return appName;
123
+ }
104
124
 
105
- const templateData = {
106
- app: {
107
- key: appName,
108
- name: config.displayName || appName
109
- },
110
- image: {
111
- name: appName,
112
- tag: 'latest'
113
- },
114
- port: config.port || 3000,
125
+ /**
126
+ * Builds app configuration section
127
+ * @param {string} appName - Application name
128
+ * @param {Object} config - Application configuration
129
+ * @returns {Object} App configuration
130
+ */
131
+ function buildAppConfig(appName, config) {
132
+ return {
133
+ key: appName,
134
+ name: config.displayName || appName
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Builds image configuration section
140
+ * @param {Object} config - Application configuration
141
+ * @param {string} appName - Application name
142
+ * @returns {Object} Image configuration
143
+ */
144
+ function buildImageConfig(config, appName) {
145
+ const imageName = getImageName(config, appName);
146
+ const imageTag = config.image?.tag || 'latest';
147
+ return {
148
+ name: imageName,
149
+ tag: imageTag
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Builds health check configuration section
155
+ * @param {Object} config - Application configuration
156
+ * @returns {Object} Health check configuration
157
+ */
158
+ function buildHealthCheckConfig(config) {
159
+ return {
160
+ path: config.healthCheck?.path || '/health',
161
+ interval: config.healthCheck?.interval || 30
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Builds requires configuration section
167
+ * @param {Object} config - Application configuration
168
+ * @returns {Object} Requires configuration
169
+ */
170
+ function buildRequiresConfig(config) {
171
+ return {
172
+ requiresDatabase: config.requires?.database || config.services?.database || false,
173
+ requiresStorage: config.requires?.storage || config.services?.storage || false,
174
+ requiresRedis: config.requires?.redis || config.services?.redis || false
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Builds service configuration for template data
180
+ * @param {string} appName - Application name
181
+ * @param {Object} config - Application configuration
182
+ * @param {number} port - Application port
183
+ * @returns {Object} Service configuration
184
+ */
185
+ function buildServiceConfig(appName, config, port) {
186
+ const containerPort = config.build?.containerPort || config.port || 3000;
187
+
188
+ return {
189
+ app: buildAppConfig(appName, config),
190
+ image: buildImageConfig(config, appName),
191
+ port: containerPort,
115
192
  build: {
116
193
  localPort: port
117
194
  },
118
- healthCheck: {
119
- path: config.healthCheck?.path || '/health',
120
- interval: config.healthCheck?.interval || 30
121
- },
122
- requiresDatabase: config.services?.database || false,
123
- requiresStorage: config.services?.storage || false,
124
- requiresRedis: config.services?.redis || false,
125
- mountVolume: path.join(process.cwd(), 'data', appName),
126
- databases: config.databases || []
195
+ healthCheck: buildHealthCheckConfig(config),
196
+ ...buildRequiresConfig(config)
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Builds volumes configuration for template data
202
+ * @param {string} appName - Application name
203
+ * @returns {Object} Volumes configuration
204
+ */
205
+ function buildVolumesConfig(appName) {
206
+ // Use forward slashes for Docker paths (works on both Windows and Unix)
207
+ const volumePath = path.join(process.cwd(), 'data', appName);
208
+ return {
209
+ mountVolume: volumePath.replace(/\\/g, '/')
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Builds networks configuration for template data
215
+ * @param {Object} config - Application configuration
216
+ * @returns {Object} Networks configuration
217
+ */
218
+ function buildNetworksConfig(config) {
219
+ // Get databases from requires.databases or top-level databases
220
+ const databases = config.requires?.databases || config.databases || [];
221
+ return {
222
+ databases: databases
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Generates Docker Compose configuration from template
228
+ * @param {string} appName - Application name
229
+ * @param {Object} config - Application configuration
230
+ * @param {Object} options - Run options
231
+ * @returns {Promise<string>} Generated compose content
232
+ */
233
+ async function generateDockerCompose(appName, config, options) {
234
+ const language = config.build?.language || config.language || 'typescript';
235
+ const template = loadDockerComposeTemplate(language);
236
+
237
+ const port = options.port || config.build?.localPort || config.port || 3000;
238
+
239
+ const serviceConfig = buildServiceConfig(appName, config, port);
240
+ const volumesConfig = buildVolumesConfig(appName);
241
+ const networksConfig = buildNetworksConfig(config);
242
+
243
+ // Get absolute path to .env file for docker-compose
244
+ const envFilePath = path.join(process.cwd(), 'builder', appName, '.env');
245
+ const envFileAbsolutePath = envFilePath.replace(/\\/g, '/'); // Use forward slashes for Docker
246
+
247
+ const templateData = {
248
+ ...serviceConfig,
249
+ ...volumesConfig,
250
+ ...networksConfig,
251
+ envFile: envFileAbsolutePath
127
252
  };
128
253
 
129
254
  return template(templateData);
130
255
  }
131
256
 
132
257
  /**
133
- * Waits for container health check to pass
258
+ * Validates app name and loads configuration
259
+ * @async
134
260
  * @param {string} appName - Application name
135
- * @param {number} timeout - Timeout in seconds
261
+ * @returns {Promise<Object>} Application configuration
262
+ * @throws {Error} If validation fails
136
263
  */
137
- async function waitForHealthCheck(appName, timeout = 60) {
138
- const maxAttempts = timeout / 2; // Check every 2 seconds
139
- let attempts = 0;
264
+ async function validateAppConfiguration(appName) {
265
+ // Validate app name
266
+ if (!appName || typeof appName !== 'string') {
267
+ throw new Error('Application name is required');
268
+ }
140
269
 
141
- while (attempts < maxAttempts) {
142
- try {
143
- const { stdout } = await execAsync(`docker inspect --format='{{.State.Health.Status}}' aifabrix-${appName}`);
144
- const status = stdout.trim();
145
-
146
- if (status === 'healthy') {
147
- return;
148
- } else if (status === 'unhealthy') {
149
- throw new Error(`Container aifabrix-${appName} is unhealthy`);
150
- }
151
-
152
- attempts++;
153
- if (attempts < maxAttempts) {
154
- console.log(chalk.yellow(`Waiting for health check... (${attempts}/${maxAttempts})`));
155
- await new Promise(resolve => setTimeout(resolve, 2000));
156
- }
157
- } catch (error) {
158
- attempts++;
159
- if (attempts < maxAttempts) {
160
- await new Promise(resolve => setTimeout(resolve, 2000));
161
- }
270
+ // Load and validate app configuration
271
+ const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
272
+ if (!fsSync.existsSync(configPath)) {
273
+ throw new Error(`Application configuration not found: ${configPath}\nRun 'aifabrix create ${appName}' first`);
274
+ }
275
+
276
+ const configContent = fsSync.readFileSync(configPath, 'utf8');
277
+ const config = yaml.load(configContent);
278
+
279
+ // Validate configuration
280
+ const validation = await validator.validateApplication(appName);
281
+ if (!validation.valid) {
282
+ throw new Error(`Configuration validation failed:\n${validation.variables.errors.join('\n')}`);
283
+ }
284
+
285
+ return config;
286
+ }
287
+
288
+ /**
289
+ * Checks prerequisites: Docker image and infrastructure
290
+ * @async
291
+ * @param {string} appName - Application name
292
+ * @param {Object} config - Application configuration
293
+ * @throws {Error} If prerequisites are not met
294
+ */
295
+ async function checkPrerequisites(appName, config) {
296
+ // Extract image name from configuration (same logic as build process)
297
+ const imageName = getImageName(config, appName);
298
+ const imageTag = config.image?.tag || 'latest';
299
+ const fullImageName = `${imageName}:${imageTag}`;
300
+
301
+ // Check if Docker image exists
302
+ logger.log(chalk.blue(`Checking if image ${fullImageName} exists...`));
303
+ const imageExists = await checkImageExists(imageName, imageTag);
304
+ if (!imageExists) {
305
+ throw new Error(`Docker image ${fullImageName} not found\nRun 'aifabrix build ${appName}' first`);
306
+ }
307
+ logger.log(chalk.green(`✓ Image ${fullImageName} found`));
308
+
309
+ // Check infrastructure health
310
+ logger.log(chalk.blue('Checking infrastructure health...'));
311
+ const infraHealth = await infra.checkInfraHealth();
312
+ const unhealthyServices = Object.entries(infraHealth)
313
+ .filter(([_, status]) => status !== 'healthy')
314
+ .map(([service, _]) => service);
315
+
316
+ if (unhealthyServices.length > 0) {
317
+ throw new Error(`Infrastructure services not healthy: ${unhealthyServices.join(', ')}\nRun 'aifabrix up' first`);
318
+ }
319
+ logger.log(chalk.green('✓ Infrastructure is running'));
320
+ }
321
+
322
+ /**
323
+ * Prepares environment: ensures .env file and generates Docker Compose
324
+ * @async
325
+ * @param {string} appName - Application name
326
+ * @param {Object} config - Application configuration
327
+ * @param {Object} options - Run options
328
+ * @returns {Promise<string>} Path to generated compose file
329
+ */
330
+ async function prepareEnvironment(appName, config, options) {
331
+ // Ensure .env file exists with 'docker' environment context (for running in Docker)
332
+ const envPath = path.join(process.cwd(), 'builder', appName, '.env');
333
+ if (!fsSync.existsSync(envPath)) {
334
+ logger.log(chalk.yellow('Generating .env file from template...'));
335
+ await secrets.generateEnvFile(appName, null, 'docker');
336
+ } else {
337
+ // Re-generate with 'docker' context to ensure correct hostnames for Docker
338
+ logger.log(chalk.blue('Updating .env file for Docker environment...'));
339
+ await secrets.generateEnvFile(appName, null, 'docker');
340
+ }
341
+
342
+ // Also ensure .env file in apps/ directory is updated (for Docker build context)
343
+ const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
344
+ if (fsSync.existsSync(variablesPath)) {
345
+ const variablesContent = fsSync.readFileSync(variablesPath, 'utf8');
346
+ const variables = yaml.load(variablesContent);
347
+
348
+ if (variables?.build?.envOutputPath && variables.build.envOutputPath !== null) {
349
+ // The generateEnvFile already copies to apps/, but ensure it's using docker context
350
+ logger.log(chalk.blue('Ensuring .env file in apps/ directory is updated for Docker...'));
351
+ await secrets.generateEnvFile(appName, null, 'docker');
162
352
  }
163
353
  }
164
354
 
165
- throw new Error(`Health check timeout after ${timeout} seconds`);
355
+ // Generate Docker Compose configuration
356
+ logger.log(chalk.blue('Generating Docker Compose configuration...'));
357
+ const composeContent = await generateDockerCompose(appName, config, options);
358
+ // Write compose file to temporary location
359
+ const tempComposePath = path.join(process.cwd(), 'builder', appName, 'docker-compose.yaml');
360
+ await fs.writeFile(tempComposePath, composeContent);
361
+
362
+ return tempComposePath;
166
363
  }
167
364
 
365
+ /**
366
+ * Starts the container and waits for health check
367
+ * @async
368
+ * @param {string} appName - Application name
369
+ * @param {string} composePath - Path to Docker Compose file
370
+ * @param {number} port - Application port
371
+ * @throws {Error} If container fails to start or become healthy
372
+ */
373
+ async function startContainer(appName, composePath, port, config = null) {
374
+ logger.log(chalk.blue(`Starting ${appName}...`));
375
+
376
+ // Ensure ADMIN_SECRETS_PATH is set for db-init service
377
+ const adminSecretsPath = await infra.ensureAdminSecrets();
378
+
379
+ // Load POSTGRES_PASSWORD from admin-secrets.env
380
+ const adminSecretsContent = fsSync.readFileSync(adminSecretsPath, 'utf8');
381
+ const postgresPasswordMatch = adminSecretsContent.match(/^POSTGRES_PASSWORD=(.+)$/m);
382
+ const postgresPassword = postgresPasswordMatch ? postgresPasswordMatch[1] : '';
383
+
384
+ // Set environment variables for docker-compose
385
+ const env = {
386
+ ...process.env,
387
+ ADMIN_SECRETS_PATH: adminSecretsPath,
388
+ POSTGRES_PASSWORD: postgresPassword
389
+ };
390
+
391
+ await execAsync(`docker-compose -f "${composePath}" up -d`, { env });
392
+ logger.log(chalk.green(`✓ Container aifabrix-${appName} started`));
393
+
394
+ // Wait for health check
395
+ const healthCheckPath = config?.healthCheck?.path || '/health';
396
+ const healthCheckUrl = `http://localhost:${port}${healthCheckPath}`;
397
+ logger.log(chalk.blue(`Waiting for application to be healthy at ${healthCheckUrl}...`));
398
+ await waitForHealthCheck(appName, 90, port, config);
399
+ }
400
+
401
+ /**
402
+ * Displays run status after successful start
403
+ * @param {string} appName - Application name
404
+ * @param {number} port - Application port
405
+ * @param {Object} config - Application configuration
406
+ */
407
+ function displayRunStatus(appName, port, config) {
408
+ const healthCheckPath = config?.healthCheck?.path || '/health';
409
+ const healthCheckUrl = `http://localhost:${port}${healthCheckPath}`;
410
+
411
+ logger.log(chalk.green(`\n✓ App running at http://localhost:${port}`));
412
+ logger.log(chalk.blue(`Health check: ${healthCheckUrl}`));
413
+ logger.log(chalk.gray(`Container: aifabrix-${appName}`));
414
+ }
415
+
416
+ /**
417
+ * Waits for container health check to pass
418
+ * @param {string} appName - Application name
419
+ * @param {number} timeout - Timeout in seconds
420
+ * @param {number} port - Application port (optional, will be detected if not provided)
421
+ * @param {Object} config - Application configuration (optional)
422
+ */
423
+
168
424
  /**
169
425
  * Runs the application locally using Docker
170
426
  * Starts container with proper port mapping and environment
@@ -183,50 +439,16 @@ async function waitForHealthCheck(appName, timeout = 60) {
183
439
  */
184
440
  async function runApp(appName, options = {}) {
185
441
  try {
186
- // Validate app name
187
- if (!appName || typeof appName !== 'string') {
188
- throw new Error('Application name is required');
189
- }
190
-
191
- // Load and validate app configuration
192
- const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
193
- if (!fsSync.existsSync(configPath)) {
194
- throw new Error(`Application configuration not found: ${configPath}\nRun 'aifabrix create ${appName}' first`);
195
- }
196
-
197
- const configContent = fsSync.readFileSync(configPath, 'utf8');
198
- const config = yaml.load(configContent);
199
-
200
- // Validate configuration
201
- const validation = await validator.validateApplication(appName);
202
- if (!validation.valid) {
203
- throw new Error(`Configuration validation failed:\n${validation.variables.errors.join('\n')}`);
204
- }
442
+ // Validate app name and load configuration
443
+ const config = await validateAppConfiguration(appName);
205
444
 
206
- // Check if Docker image exists
207
- console.log(chalk.blue(`Checking if image ${appName}:latest exists...`));
208
- const imageExists = await checkImageExists(appName);
209
- if (!imageExists) {
210
- throw new Error(`Docker image ${appName}:latest not found\nRun 'aifabrix build ${appName}' first`);
211
- }
212
- console.log(chalk.green(`✓ Image ${appName}:latest found`));
213
-
214
- // Check infrastructure health
215
- console.log(chalk.blue('Checking infrastructure health...'));
216
- const infraHealth = await infra.checkInfraHealth();
217
- const unhealthyServices = Object.entries(infraHealth)
218
- .filter(([_, status]) => status !== 'healthy')
219
- .map(([service, _]) => service);
220
-
221
- if (unhealthyServices.length > 0) {
222
- throw new Error(`Infrastructure services not healthy: ${unhealthyServices.join(', ')}\nRun 'aifabrix up' first`);
223
- }
224
- console.log(chalk.green('✓ Infrastructure is running'));
445
+ // Check prerequisites: image and infrastructure
446
+ await checkPrerequisites(appName, config);
225
447
 
226
448
  // Check if container is already running
227
449
  const containerRunning = await checkContainerRunning(appName);
228
450
  if (containerRunning) {
229
- console.log(chalk.yellow(`Container aifabrix-${appName} is already running`));
451
+ logger.log(chalk.yellow(`Container aifabrix-${appName} is already running`));
230
452
  await stopAndRemoveContainer(appName);
231
453
  }
232
454
 
@@ -237,42 +459,21 @@ async function runApp(appName, options = {}) {
237
459
  throw new Error(`Port ${port} is already in use. Try --port <alternative>`);
238
460
  }
239
461
 
240
- // Ensure .env file exists
241
- const envPath = path.join(process.cwd(), 'builder', appName, '.env');
242
- if (!fsSync.existsSync(envPath)) {
243
- console.log(chalk.yellow('Generating .env file from template...'));
244
- await secrets.generateEnvFile(appName);
245
- }
246
-
247
- // Generate Docker Compose configuration
248
- console.log(chalk.blue('Generating Docker Compose configuration...'));
249
- const composeContent = await generateDockerCompose(appName, config, options);
250
- // Write compose file to temporary location
251
- const tempComposePath = path.join(process.cwd(), 'builder', appName, 'docker-compose.yaml');
252
- await fs.writeFile(tempComposePath, composeContent);
462
+ // Prepare environment: ensure .env file and generate Docker Compose
463
+ const tempComposePath = await prepareEnvironment(appName, config, options);
253
464
 
254
465
  try {
255
- // Start container
256
- console.log(chalk.blue(`Starting ${appName}...`));
257
- await execAsync(`docker-compose -f "${tempComposePath}" up -d`);
258
- console.log(chalk.green(`✓ Container aifabrix-${appName} started`));
259
-
260
- // Wait for health check
261
- console.log(chalk.blue('Waiting for application to be healthy...'));
262
- await waitForHealthCheck(appName);
466
+ // Start container and wait for health check
467
+ await startContainer(appName, tempComposePath, port, config);
263
468
 
264
469
  // Display success message
265
- console.log(chalk.green(`\n✓ App running at http://localhost:${port}`));
266
- console.log(chalk.gray(`Container: aifabrix-${appName}`));
267
- console.log(chalk.gray('Health check: /health'));
268
-
269
- } finally {
270
- // Clean up temporary compose file
271
- try {
272
- await fs.unlink(tempComposePath);
273
- } catch (error) {
274
- // Ignore cleanup errors
275
- }
470
+ displayRunStatus(appName, port, config);
471
+
472
+ } catch (error) {
473
+ // Keep the compose file for debugging - don't delete on error
474
+ logger.log(chalk.yellow(`\n⚠️ Compose file preserved at: ${tempComposePath}`));
475
+ logger.log(chalk.yellow(' Review the file to debug issues'));
476
+ throw error;
276
477
  }
277
478
 
278
479
  } catch (error) {