@aifabrix/builder 2.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 eSystems Nordic Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # 🧱 @aifabrix/builder
2
+
3
+ Local development infrastructure + Azure deployment tool.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @aifabrix/builder
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ aifabrix up # Start Postgres + Redis
15
+ aifabrix create myapp # Create your app
16
+ aifabrix build myapp # Build Docker image
17
+ aifabrix run myapp # Run locally
18
+ ```
19
+
20
+ → [Full Guide](docs/QUICK-START.md) | [CLI Commands](docs/CLI-REFERENCE.md)
21
+
22
+ ## What You Get
23
+
24
+ - **Local Postgres + Redis infrastructure** - Runs in Docker
25
+ - **Auto-generated Dockerfiles** - TypeScript and Python templates
26
+ - **Environment variable management** - Secret resolution with kv:// references
27
+ - **Azure deployment pipeline** - Push to ACR and deploy via controller
28
+
29
+ ## Optional Platform Apps
30
+
31
+ Want authentication or deployment controller?
32
+
33
+ ```bash
34
+ # Keycloak for authentication
35
+ aifabrix create keycloak --port 8082 --database --template platform
36
+ aifabrix build keycloak
37
+ aifabrix run keycloak
38
+
39
+ # Miso Controller for Azure deployments
40
+ aifabrix create miso-controller --port 3000 --database --redis --template platform
41
+ aifabrix build miso-controller
42
+ aifabrix run miso-controller
43
+ ```
44
+
45
+ → [Infrastructure Guide](docs/INFRASTRUCTURE.md)
46
+
47
+ ## Documentation
48
+
49
+ - [Quick Start](docs/QUICK-START.md) - Get running in 5 minutes
50
+ - [Infrastructure](docs/INFRASTRUCTURE.md) - What runs and why
51
+ - [Configuration](docs/CONFIGURATION.md) - Config file reference
52
+ - [Building](docs/BUILDING.md) - Build process explained
53
+ - [Running](docs/RUNNING.md) - Run apps locally
54
+ - [Deploying](docs/DEPLOYING.md) - Deploy to Azure
55
+ - [CLI Reference](docs/CLI-REFERENCE.md) - All commands
56
+
57
+ ## How It Works
58
+
59
+ 1. **Infrastructure** - Minimal baseline (Postgres + Redis)
60
+ 2. **Create** - Generate config files for your app
61
+ 3. **Build** - Auto-detect runtime and build Docker image
62
+ 4. **Run** - Start locally, connected to infrastructure
63
+ 5. **Deploy** - Push to ACR and deploy via controller
64
+
65
+ ## Requirements
66
+
67
+ - **Docker Desktop** - For running containers
68
+ - **Node.js 18+** - For running the CLI
69
+ - **Azure CLI** - For deploying to Azure (optional)
70
+
71
+ ## License
72
+
73
+ Ā© eSystems Nordic Ltd 2025 - All Rights Reserved
74
+
75
+ `@aifabrix/builder` is part of the AI Fabrix platform.
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * AI Fabrix Builder CLI Entry Point
5
+ *
6
+ * This is the main entry point for the @aifabrix/builder CLI tool.
7
+ * It initializes Commander.js and delegates command handling to lib/cli.js
8
+ *
9
+ * Usage: aifabrix <command> [options]
10
+ *
11
+ * @fileoverview CLI entry point for AI Fabrix Builder SDK
12
+ * @author AI Fabrix Team
13
+ * @version 2.0.0
14
+ */
15
+
16
+ const { Command } = require('commander');
17
+ const cli = require('../lib/cli');
18
+
19
+ /**
20
+ * Initialize and configure the CLI
21
+ * Sets up command parsing, help text, and version information
22
+ */
23
+ function initializeCLI() {
24
+ const program = new Command();
25
+
26
+ program.name('aifabrix')
27
+ .version('2.0.0')
28
+ .description('AI Fabrix Local Fabric & Deployment SDK');
29
+
30
+ // Delegate command setup to lib/cli.js
31
+ cli.setupCommands(program);
32
+
33
+ // Parse command line arguments
34
+ program.parse();
35
+ }
36
+
37
+ // TODO: Add error handling for CLI initialization
38
+ // TODO: Add graceful shutdown handling
39
+ // TODO: Add telemetry/analytics hooks (opt-in)
40
+
41
+ // Initialize CLI when this file is executed directly
42
+ if (require.main === module) {
43
+ try {
44
+ initializeCLI();
45
+ } catch (error) {
46
+ console.error('āŒ Failed to initialize CLI:', error.message);
47
+ process.exit(1);
48
+ }
49
+ }
50
+
51
+ module.exports = { initializeCLI };
@@ -0,0 +1,209 @@
1
+ /**
2
+ * AI Fabrix Builder Application Deployment Module
3
+ *
4
+ * Handles deployment to Miso Controller with manifest generation
5
+ * and orchestration. Includes push to Azure Container Registry.
6
+ *
7
+ * @fileoverview Application deployment for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const fs = require('fs').promises;
13
+ const path = require('path');
14
+ const yaml = require('js-yaml');
15
+ const chalk = require('chalk');
16
+ const pushUtils = require('./push');
17
+
18
+ /**
19
+ * Validate application name format
20
+ * @param {string} appName - Application name to validate
21
+ * @throws {Error} If app name is invalid
22
+ */
23
+ function validateAppName(appName) {
24
+ if (!appName || typeof appName !== 'string') {
25
+ throw new Error('Application name is required');
26
+ }
27
+
28
+ // App name should be lowercase, alphanumeric with dashes, 3-40 characters
29
+ const nameRegex = /^[a-z0-9-]{3,40}$/;
30
+ if (!nameRegex.test(appName)) {
31
+ throw new Error('Application name must be 3-40 characters, lowercase letters, numbers, and dashes only');
32
+ }
33
+
34
+ // Cannot start or end with dash
35
+ if (appName.startsWith('-') || appName.endsWith('-')) {
36
+ throw new Error('Application name cannot start or end with a dash');
37
+ }
38
+
39
+ // Cannot have consecutive dashes
40
+ if (appName.includes('--')) {
41
+ throw new Error('Application name cannot have consecutive dashes');
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Pushes application image to Azure Container Registry
47
+ * @async
48
+ * @function pushApp
49
+ * @param {string} appName - Name of the application
50
+ * @param {Object} options - Push options (registry, tag)
51
+ * @returns {Promise<void>} Resolves when push is complete
52
+ */
53
+ async function pushApp(appName, options = {}) {
54
+ try {
55
+ // Validate app name
56
+ validateAppName(appName);
57
+
58
+ const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
59
+ let config;
60
+ try {
61
+ config = yaml.load(await fs.readFile(configPath, 'utf8'));
62
+ } catch (error) {
63
+ throw new Error(`Failed to load configuration: ${configPath}\nRun 'aifabrix create ${appName}' first`);
64
+ }
65
+
66
+ const registry = options.registry || config.image?.registry;
67
+ if (!registry) {
68
+ throw new Error('Registry URL is required. Provide via --registry flag or configure in variables.yaml under image.registry');
69
+ }
70
+
71
+ if (!pushUtils.validateRegistryURL(registry)) {
72
+ throw new Error(`Invalid registry URL format: ${registry}. Expected format: *.azurecr.io`);
73
+ }
74
+
75
+ const tags = options.tag ? options.tag.split(',').map(t => t.trim()) : ['latest'];
76
+
77
+ if (!await pushUtils.checkLocalImageExists(appName, 'latest')) {
78
+ throw new Error(`Docker image ${appName}:latest not found locally.\nRun 'aifabrix build ${appName}' first`);
79
+ }
80
+
81
+ if (!await pushUtils.checkAzureCLIInstalled()) {
82
+ throw new Error('Azure CLI is not installed. Install from: https://docs.microsoft.com/cli/azure/install-azure-cli');
83
+ }
84
+
85
+ if (await pushUtils.checkACRAuthentication(registry)) {
86
+ console.log(chalk.green(`āœ“ Already authenticated with ${registry}`));
87
+ } else {
88
+ await pushUtils.authenticateACR(registry);
89
+ }
90
+
91
+ await Promise.all(tags.map(async(tag) => {
92
+ await pushUtils.tagImage(`${appName}:latest`, `${registry}/${appName}:${tag}`);
93
+ await pushUtils.pushImage(`${registry}/${appName}:${tag}`);
94
+ }));
95
+
96
+ console.log(chalk.green(`\nāœ“ Successfully pushed ${tags.length} tag(s) to ${registry}`));
97
+ console.log(chalk.gray(`Image: ${registry}/${appName}:*`));
98
+ console.log(chalk.gray(`Tags: ${tags.join(', ')}`));
99
+
100
+ } catch (error) {
101
+ throw new Error(`Failed to push application: ${error.message}`);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Deploys application to Miso Controller
107
+ * Orchestrates manifest generation, key creation, and deployment
108
+ *
109
+ * @async
110
+ * @function deployApp
111
+ * @param {string} appName - Name of the application to deploy
112
+ * @param {Object} options - Deployment options
113
+ * @param {string} options.controller - Controller URL (required)
114
+ * @param {string} [options.environment] - Target environment (dev/tst/pro)
115
+ * @param {boolean} [options.poll] - Poll for deployment status
116
+ * @param {number} [options.pollInterval] - Polling interval in milliseconds
117
+ * @returns {Promise<Object>} Deployment result
118
+ * @throws {Error} If deployment fails
119
+ *
120
+ * @example
121
+ * await deployApp('myapp', { controller: 'https://controller.aifabrix.ai', environment: 'dev' });
122
+ */
123
+ async function deployApp(appName, options = {}) {
124
+ try {
125
+ // 1. Input validation
126
+ if (!appName || typeof appName !== 'string') {
127
+ throw new Error('App name is required');
128
+ }
129
+
130
+ validateAppName(appName);
131
+
132
+ if (!options.controller) {
133
+ throw new Error('Controller URL is required (--controller flag required)');
134
+ }
135
+
136
+ // 2. Load application configuration
137
+ const builderPath = path.join(process.cwd(), 'builder', appName);
138
+ try {
139
+ await fs.access(builderPath);
140
+ } catch (error) {
141
+ if (error.code === 'ENOENT') {
142
+ throw new Error(`Application '${appName}' not found in builder/. Run 'aifabrix create ${appName}' first`);
143
+ }
144
+ throw error;
145
+ }
146
+
147
+ // 3. Generate deployment manifest
148
+ console.log(chalk.blue(`\nšŸ“‹ Generating deployment manifest for ${appName}...`));
149
+ const generator = require('./generator');
150
+ const manifestPath = await generator.generateDeployJson(appName);
151
+ const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
152
+
153
+ // 4. Validate manifest
154
+ const validation = generator.validateDeploymentJson(manifest);
155
+ if (!validation.valid) {
156
+ console.log(chalk.red('\nāŒ Validation failed:'));
157
+ validation.errors.forEach(error => console.log(chalk.red(` • ${error}`)));
158
+ throw new Error('Deployment manifest validation failed');
159
+ }
160
+
161
+ if (validation.warnings.length > 0) {
162
+ console.log(chalk.yellow('\nāš ļø Warnings:'));
163
+ validation.warnings.forEach(warning => console.log(chalk.yellow(` • ${warning}`)));
164
+ }
165
+
166
+ // 5. Display deployment info
167
+ console.log(chalk.green(`āœ“ Manifest generated: ${manifestPath}`));
168
+ console.log(chalk.blue(` Key: ${manifest.key}`));
169
+ console.log(chalk.blue(` Display Name: ${manifest.displayName}`));
170
+ console.log(chalk.blue(` Image: ${manifest.image}`));
171
+ console.log(chalk.blue(` Port: ${manifest.port}`));
172
+
173
+ // 6. Deploy to controller
174
+ console.log(chalk.blue(`\nšŸš€ Deploying to ${options.controller}...`));
175
+ const deployer = require('./deployer');
176
+ const result = await deployer.deployToController(manifest, options.controller, {
177
+ environment: options.environment,
178
+ poll: options.poll !== false, // Poll by default
179
+ pollInterval: options.pollInterval || 5000,
180
+ pollMaxAttempts: options.pollMaxAttempts || 60
181
+ });
182
+
183
+ // 7. Display results
184
+ console.log(chalk.green('\nāœ… Deployment initiated successfully'));
185
+ if (result.deploymentUrl) {
186
+ console.log(chalk.blue(` URL: ${result.deploymentUrl}`));
187
+ }
188
+ if (result.deploymentId) {
189
+ console.log(chalk.blue(` Deployment ID: ${result.deploymentId}`));
190
+ }
191
+ if (result.status) {
192
+ const statusIcon = result.status.status === 'completed' ? 'āœ…' :
193
+ result.status.status === 'failed' ? 'āŒ' : 'ā³';
194
+ console.log(chalk.blue(` Status: ${statusIcon} ${result.status.status}`));
195
+ }
196
+
197
+ return result;
198
+
199
+ } catch (error) {
200
+ throw new Error(`Failed to deploy application: ${error.message}`);
201
+ }
202
+ }
203
+
204
+ module.exports = {
205
+ pushApp,
206
+ deployApp,
207
+ validateAppName
208
+ };
209
+
package/lib/app-run.js ADDED
@@ -0,0 +1,291 @@
1
+ /**
2
+ * AI Fabrix Builder Application Run Management
3
+ *
4
+ * This module handles application running with Docker containers.
5
+ * Includes Docker orchestration, health checking, and port management.
6
+ *
7
+ * @fileoverview Application run management for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const fs = require('fs').promises;
13
+ const fsSync = require('fs');
14
+ const path = require('path');
15
+ const net = require('net');
16
+ const chalk = require('chalk');
17
+ const yaml = require('js-yaml');
18
+ const handlebars = require('handlebars');
19
+ const { exec } = require('child_process');
20
+ const { promisify } = require('util');
21
+ const validator = require('./validator');
22
+ const infra = require('./infra');
23
+ const secrets = require('./secrets');
24
+
25
+ const execAsync = promisify(exec);
26
+
27
+ /**
28
+ * Checks if Docker image exists for the application
29
+ * @param {string} appName - Application name
30
+ * @returns {Promise<boolean>} True if image exists
31
+ */
32
+ async function checkImageExists(appName) {
33
+ try {
34
+ const { stdout } = await execAsync(`docker images --format "{{.Repository}}:{{.Tag}}" | grep "^${appName}:latest$"`);
35
+ return stdout.trim() === `${appName}:latest`;
36
+ } catch (error) {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Checks if container is already running
43
+ * @param {string} appName - Application name
44
+ * @returns {Promise<boolean>} True if container is running
45
+ */
46
+ async function checkContainerRunning(appName) {
47
+ try {
48
+ const { stdout } = await execAsync(`docker ps --filter "name=aifabrix-${appName}" --format "{{.Names}}"`);
49
+ return stdout.trim() === `aifabrix-${appName}`;
50
+ } catch (error) {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Stops and removes existing container
57
+ * @param {string} appName - Application name
58
+ */
59
+ async function stopAndRemoveContainer(appName) {
60
+ try {
61
+ console.log(chalk.yellow(`Stopping existing container aifabrix-${appName}...`));
62
+ await execAsync(`docker stop aifabrix-${appName}`);
63
+ await execAsync(`docker rm aifabrix-${appName}`);
64
+ console.log(chalk.green(`āœ“ Container aifabrix-${appName} stopped and removed`));
65
+ } catch (error) {
66
+ // Container might not exist, which is fine
67
+ console.log(chalk.gray(`Container aifabrix-${appName} was not running`));
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Checks if port is available
73
+ * @param {number} port - Port number to check
74
+ * @returns {Promise<boolean>} True if port is available
75
+ */
76
+ async function checkPortAvailable(port) {
77
+ return new Promise((resolve) => {
78
+ const server = net.createServer();
79
+ server.listen(port, () => {
80
+ server.close(() => resolve(true));
81
+ });
82
+ server.on('error', () => resolve(false));
83
+ });
84
+ }
85
+
86
+ /**
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
92
+ */
93
+ async function generateDockerCompose(appName, config, options) {
94
+ const language = config.build?.language || config.language || 'typescript';
95
+ const templatePath = path.join(__dirname, '..', 'templates', language, 'docker-compose.hbs');
96
+ if (!fsSync.existsSync(templatePath)) {
97
+ throw new Error(`Docker Compose template not found for language: ${language}`);
98
+ }
99
+
100
+ const templateContent = fsSync.readFileSync(templatePath, 'utf8');
101
+ const template = handlebars.compile(templateContent);
102
+
103
+ const port = options.port || config.build?.localPort || config.port || 3000;
104
+
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,
115
+ build: {
116
+ localPort: port
117
+ },
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 || []
127
+ };
128
+
129
+ return template(templateData);
130
+ }
131
+
132
+ /**
133
+ * Waits for container health check to pass
134
+ * @param {string} appName - Application name
135
+ * @param {number} timeout - Timeout in seconds
136
+ */
137
+ async function waitForHealthCheck(appName, timeout = 60) {
138
+ const maxAttempts = timeout / 2; // Check every 2 seconds
139
+ let attempts = 0;
140
+
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
+ }
162
+ }
163
+ }
164
+
165
+ throw new Error(`Health check timeout after ${timeout} seconds`);
166
+ }
167
+
168
+ /**
169
+ * Runs the application locally using Docker
170
+ * Starts container with proper port mapping and environment
171
+ *
172
+ * @async
173
+ * @function runApp
174
+ * @param {string} appName - Name of the application to run
175
+ * @param {Object} options - Run options
176
+ * @param {number} [options.port] - Override local port
177
+ * @returns {Promise<void>} Resolves when app is running
178
+ * @throws {Error} If run fails or app is not built
179
+ *
180
+ * @example
181
+ * await runApp('myapp', { port: 3001 });
182
+ * // Application is now running on localhost:3001
183
+ */
184
+ async function runApp(appName, options = {}) {
185
+ 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
+ }
205
+
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'));
225
+
226
+ // Check if container is already running
227
+ const containerRunning = await checkContainerRunning(appName);
228
+ if (containerRunning) {
229
+ console.log(chalk.yellow(`Container aifabrix-${appName} is already running`));
230
+ await stopAndRemoveContainer(appName);
231
+ }
232
+
233
+ // Check port availability
234
+ const port = options.port || config.build?.localPort || config.port || 3000;
235
+ const portAvailable = await checkPortAvailable(port);
236
+ if (!portAvailable) {
237
+ throw new Error(`Port ${port} is already in use. Try --port <alternative>`);
238
+ }
239
+
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);
253
+
254
+ 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);
263
+
264
+ // 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
+ }
276
+ }
277
+
278
+ } catch (error) {
279
+ throw new Error(`Failed to run application: ${error.message}`);
280
+ }
281
+ }
282
+
283
+ module.exports = {
284
+ runApp,
285
+ checkImageExists,
286
+ checkContainerRunning,
287
+ stopAndRemoveContainer,
288
+ checkPortAvailable,
289
+ generateDockerCompose,
290
+ waitForHealthCheck
291
+ };