@aifabrix/builder 2.0.0 → 2.0.3
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/README.md +5 -3
- package/bin/aifabrix.js +9 -3
- package/jest.config.integration.js +30 -0
- package/lib/app-config.js +157 -0
- package/lib/app-deploy.js +233 -82
- package/lib/app-dockerfile.js +112 -0
- package/lib/app-prompts.js +244 -0
- package/lib/app-push.js +172 -0
- package/lib/app-run.js +235 -144
- package/lib/app.js +208 -274
- package/lib/audit-logger.js +2 -0
- package/lib/build.js +177 -125
- package/lib/cli.js +76 -86
- package/lib/commands/app.js +414 -0
- package/lib/commands/login.js +304 -0
- package/lib/config.js +78 -0
- package/lib/deployer.js +225 -81
- package/lib/env-reader.js +45 -30
- package/lib/generator.js +308 -191
- package/lib/github-generator.js +67 -7
- package/lib/infra.js +156 -61
- package/lib/push.js +105 -10
- package/lib/schema/application-schema.json +30 -2
- package/lib/schema/env-config.yaml +9 -1
- package/lib/schema/infrastructure-schema.json +589 -0
- package/lib/secrets.js +229 -24
- package/lib/template-validator.js +205 -0
- package/lib/templates.js +305 -170
- package/lib/utils/api.js +329 -0
- package/lib/utils/cli-utils.js +97 -0
- package/lib/utils/compose-generator.js +185 -0
- package/lib/utils/docker-build.js +173 -0
- package/lib/utils/dockerfile-utils.js +131 -0
- package/lib/utils/environment-checker.js +125 -0
- package/lib/utils/error-formatter.js +61 -0
- package/lib/utils/health-check.js +187 -0
- package/lib/utils/logger.js +53 -0
- package/lib/utils/template-helpers.js +223 -0
- package/lib/utils/variable-transformer.js +271 -0
- package/lib/validator.js +27 -112
- package/package.json +14 -10
- package/templates/README.md +75 -3
- package/templates/applications/keycloak/Dockerfile +36 -0
- package/templates/applications/keycloak/env.template +32 -0
- package/templates/applications/keycloak/rbac.yaml +37 -0
- package/templates/applications/keycloak/variables.yaml +56 -0
- package/templates/applications/miso-controller/Dockerfile +125 -0
- package/templates/applications/miso-controller/env.template +129 -0
- package/templates/applications/miso-controller/rbac.yaml +214 -0
- package/templates/applications/miso-controller/variables.yaml +56 -0
- package/templates/github/release.yaml.hbs +5 -26
- package/templates/github/steps/npm.hbs +24 -0
- package/templates/infra/compose.yaml +6 -6
- package/templates/python/docker-compose.hbs +19 -12
- package/templates/python/main.py +80 -0
- package/templates/python/requirements.txt +4 -0
- package/templates/typescript/Dockerfile.hbs +2 -2
- package/templates/typescript/docker-compose.hbs +19 -12
- package/templates/typescript/index.ts +116 -0
- package/templates/typescript/package.json +26 -0
- package/templates/typescript/tsconfig.json +24 -0
package/lib/app-run.js
CHANGED
|
@@ -15,24 +15,30 @@ const path = require('path');
|
|
|
15
15
|
const net = require('net');
|
|
16
16
|
const chalk = require('chalk');
|
|
17
17
|
const yaml = require('js-yaml');
|
|
18
|
-
const handlebars = require('handlebars');
|
|
19
18
|
const { exec } = require('child_process');
|
|
20
19
|
const { promisify } = require('util');
|
|
21
20
|
const validator = require('./validator');
|
|
22
21
|
const infra = require('./infra');
|
|
23
22
|
const secrets = require('./secrets');
|
|
23
|
+
const logger = require('./utils/logger');
|
|
24
|
+
const { waitForHealthCheck } = require('./utils/health-check');
|
|
25
|
+
const composeGenerator = require('./utils/compose-generator');
|
|
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}
|
|
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(
|
|
35
|
+
async function checkImageExists(imageName, tag = 'latest') {
|
|
33
36
|
try {
|
|
34
|
-
const
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
+
logger.log(chalk.gray(`Container aifabrix-${appName} was not running`));
|
|
68
74
|
}
|
|
69
75
|
}
|
|
70
76
|
|
|
@@ -84,87 +90,227 @@ async function checkPortAvailable(port) {
|
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
/**
|
|
87
|
-
*
|
|
93
|
+
* Extracts image name from configuration (same logic as build.js)
|
|
94
|
+
* @param {Object} config - Application configuration
|
|
95
|
+
* @param {string} appName - Application name (fallback)
|
|
96
|
+
* @returns {string} Image name
|
|
97
|
+
*/
|
|
98
|
+
function getImageName(config, appName) {
|
|
99
|
+
return composeGenerator.getImageName(config, appName);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validates app name and loads configuration
|
|
104
|
+
* @async
|
|
105
|
+
* @param {string} appName - Application name
|
|
106
|
+
* @returns {Promise<Object>} Application configuration
|
|
107
|
+
* @throws {Error} If validation fails
|
|
108
|
+
*/
|
|
109
|
+
async function validateAppConfiguration(appName) {
|
|
110
|
+
// Validate app name
|
|
111
|
+
if (!appName || typeof appName !== 'string') {
|
|
112
|
+
throw new Error('Application name is required');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check if we're running from inside the builder directory
|
|
116
|
+
const currentDir = process.cwd();
|
|
117
|
+
const normalizedPath = currentDir.replace(/\\/g, '/');
|
|
118
|
+
const expectedBuilderPath = `builder/${appName}`;
|
|
119
|
+
|
|
120
|
+
// If inside builder/{appName}, suggest moving to project root
|
|
121
|
+
if (normalizedPath.endsWith(expectedBuilderPath)) {
|
|
122
|
+
const projectRoot = path.resolve(currentDir, '../..');
|
|
123
|
+
throw new Error(
|
|
124
|
+
'You\'re running from inside the builder directory.\n' +
|
|
125
|
+
`Current directory: ${currentDir}\n` +
|
|
126
|
+
'Please change to the project root and try again:\n' +
|
|
127
|
+
` cd ${projectRoot}\n` +
|
|
128
|
+
` aifabrix run ${appName}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Load and validate app configuration
|
|
133
|
+
const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
|
|
134
|
+
if (!fsSync.existsSync(configPath)) {
|
|
135
|
+
const expectedDir = path.join(currentDir, 'builder', appName);
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Application configuration not found: ${configPath}\n` +
|
|
138
|
+
`Current directory: ${currentDir}\n` +
|
|
139
|
+
`Expected location: ${expectedDir}\n` +
|
|
140
|
+
'Make sure you\'re running from the project root (where \'builder\' directory exists)\n' +
|
|
141
|
+
`Run 'aifabrix create ${appName}' first if configuration doesn't exist`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const configContent = fsSync.readFileSync(configPath, 'utf8');
|
|
146
|
+
const config = yaml.load(configContent);
|
|
147
|
+
|
|
148
|
+
// Validate configuration
|
|
149
|
+
const validation = await validator.validateApplication(appName);
|
|
150
|
+
if (!validation.valid) {
|
|
151
|
+
const allErrors = [];
|
|
152
|
+
|
|
153
|
+
if (validation.variables && validation.variables.errors && validation.variables.errors.length > 0) {
|
|
154
|
+
allErrors.push('variables.yaml:');
|
|
155
|
+
allErrors.push(...validation.variables.errors.map(err => ` ${err}`));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (validation.rbac && validation.rbac.errors && validation.rbac.errors.length > 0) {
|
|
159
|
+
allErrors.push('rbac.yaml:');
|
|
160
|
+
allErrors.push(...validation.rbac.errors.map(err => ` ${err}`));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (validation.env && validation.env.errors && validation.env.errors.length > 0) {
|
|
164
|
+
allErrors.push('env.template:');
|
|
165
|
+
allErrors.push(...validation.env.errors.map(err => ` ${err}`));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (allErrors.length === 0) {
|
|
169
|
+
throw new Error('Configuration validation failed');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
throw new Error(`Configuration validation failed:\n${allErrors.join('\n')}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return config;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Checks prerequisites: Docker image and infrastructure
|
|
180
|
+
* @async
|
|
88
181
|
* @param {string} appName - Application name
|
|
89
182
|
* @param {Object} config - Application configuration
|
|
90
|
-
* @
|
|
91
|
-
* @returns {Promise<string>} Generated compose content
|
|
183
|
+
* @throws {Error} If prerequisites are not met
|
|
92
184
|
*/
|
|
93
|
-
async function
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
185
|
+
async function checkPrerequisites(appName, config) {
|
|
186
|
+
// Extract image name from configuration (same logic as build process)
|
|
187
|
+
const imageName = getImageName(config, appName);
|
|
188
|
+
const imageTag = config.image?.tag || 'latest';
|
|
189
|
+
const fullImageName = `${imageName}:${imageTag}`;
|
|
190
|
+
|
|
191
|
+
// Check if Docker image exists
|
|
192
|
+
logger.log(chalk.blue(`Checking if image ${fullImageName} exists...`));
|
|
193
|
+
const imageExists = await checkImageExists(imageName, imageTag);
|
|
194
|
+
if (!imageExists) {
|
|
195
|
+
throw new Error(`Docker image ${fullImageName} not found\nRun 'aifabrix build ${appName}' first`);
|
|
98
196
|
}
|
|
197
|
+
logger.log(chalk.green(`✓ Image ${fullImageName} found`));
|
|
99
198
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
};
|
|
199
|
+
// Check infrastructure health
|
|
200
|
+
logger.log(chalk.blue('Checking infrastructure health...'));
|
|
201
|
+
const infraHealth = await infra.checkInfraHealth();
|
|
202
|
+
const unhealthyServices = Object.entries(infraHealth)
|
|
203
|
+
.filter(([_, status]) => status !== 'healthy')
|
|
204
|
+
.map(([service, _]) => service);
|
|
128
205
|
|
|
129
|
-
|
|
206
|
+
if (unhealthyServices.length > 0) {
|
|
207
|
+
throw new Error(`Infrastructure services not healthy: ${unhealthyServices.join(', ')}\nRun 'aifabrix up' first`);
|
|
208
|
+
}
|
|
209
|
+
logger.log(chalk.green('✓ Infrastructure is running'));
|
|
130
210
|
}
|
|
131
211
|
|
|
132
212
|
/**
|
|
133
|
-
*
|
|
213
|
+
* Prepares environment: ensures .env file and generates Docker Compose
|
|
214
|
+
* @async
|
|
134
215
|
* @param {string} appName - Application name
|
|
135
|
-
* @param {
|
|
216
|
+
* @param {Object} config - Application configuration
|
|
217
|
+
* @param {Object} options - Run options
|
|
218
|
+
* @returns {Promise<string>} Path to generated compose file
|
|
136
219
|
*/
|
|
137
|
-
async function
|
|
138
|
-
|
|
139
|
-
|
|
220
|
+
async function prepareEnvironment(appName, config, options) {
|
|
221
|
+
// Ensure .env file exists with 'docker' environment context (for running in Docker)
|
|
222
|
+
const envPath = path.join(process.cwd(), 'builder', appName, '.env');
|
|
223
|
+
if (!fsSync.existsSync(envPath)) {
|
|
224
|
+
logger.log(chalk.yellow('Generating .env file from template...'));
|
|
225
|
+
await secrets.generateEnvFile(appName, null, 'docker');
|
|
226
|
+
} else {
|
|
227
|
+
// Re-generate with 'docker' context to ensure correct hostnames for Docker
|
|
228
|
+
logger.log(chalk.blue('Updating .env file for Docker environment...'));
|
|
229
|
+
await secrets.generateEnvFile(appName, null, 'docker');
|
|
230
|
+
}
|
|
140
231
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
}
|
|
232
|
+
// Also ensure .env file in apps/ directory is updated (for Docker build context)
|
|
233
|
+
const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
|
|
234
|
+
if (fsSync.existsSync(variablesPath)) {
|
|
235
|
+
const variablesContent = fsSync.readFileSync(variablesPath, 'utf8');
|
|
236
|
+
const variables = yaml.load(variablesContent);
|
|
237
|
+
|
|
238
|
+
if (variables?.build?.envOutputPath && variables.build.envOutputPath !== null) {
|
|
239
|
+
// The generateEnvFile already copies to apps/, but ensure it's using docker context
|
|
240
|
+
logger.log(chalk.blue('Ensuring .env file in apps/ directory is updated for Docker...'));
|
|
241
|
+
await secrets.generateEnvFile(appName, null, 'docker');
|
|
162
242
|
}
|
|
163
243
|
}
|
|
164
244
|
|
|
165
|
-
|
|
245
|
+
// Generate Docker Compose configuration
|
|
246
|
+
logger.log(chalk.blue('Generating Docker Compose configuration...'));
|
|
247
|
+
const composeContent = await composeGenerator.generateDockerCompose(appName, config, options);
|
|
248
|
+
// Write compose file to temporary location
|
|
249
|
+
const tempComposePath = path.join(process.cwd(), 'builder', appName, 'docker-compose.yaml');
|
|
250
|
+
await fs.writeFile(tempComposePath, composeContent);
|
|
251
|
+
|
|
252
|
+
return tempComposePath;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Starts the container and waits for health check
|
|
257
|
+
* @async
|
|
258
|
+
* @param {string} appName - Application name
|
|
259
|
+
* @param {string} composePath - Path to Docker Compose file
|
|
260
|
+
* @param {number} port - Application port
|
|
261
|
+
* @throws {Error} If container fails to start or become healthy
|
|
262
|
+
*/
|
|
263
|
+
async function startContainer(appName, composePath, port, config = null) {
|
|
264
|
+
logger.log(chalk.blue(`Starting ${appName}...`));
|
|
265
|
+
|
|
266
|
+
// Ensure ADMIN_SECRETS_PATH is set for db-init service
|
|
267
|
+
const adminSecretsPath = await infra.ensureAdminSecrets();
|
|
268
|
+
|
|
269
|
+
// Load POSTGRES_PASSWORD from admin-secrets.env
|
|
270
|
+
const adminSecretsContent = fsSync.readFileSync(adminSecretsPath, 'utf8');
|
|
271
|
+
const postgresPasswordMatch = adminSecretsContent.match(/^POSTGRES_PASSWORD=(.+)$/m);
|
|
272
|
+
const postgresPassword = postgresPasswordMatch ? postgresPasswordMatch[1] : '';
|
|
273
|
+
|
|
274
|
+
// Set environment variables for docker-compose
|
|
275
|
+
const env = {
|
|
276
|
+
...process.env,
|
|
277
|
+
ADMIN_SECRETS_PATH: adminSecretsPath,
|
|
278
|
+
POSTGRES_PASSWORD: postgresPassword
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
await execAsync(`docker-compose -f "${composePath}" up -d`, { env });
|
|
282
|
+
logger.log(chalk.green(`✓ Container aifabrix-${appName} started`));
|
|
283
|
+
|
|
284
|
+
// Wait for health check
|
|
285
|
+
const healthCheckPath = config?.healthCheck?.path || '/health';
|
|
286
|
+
const healthCheckUrl = `http://localhost:${port}${healthCheckPath}`;
|
|
287
|
+
logger.log(chalk.blue(`Waiting for application to be healthy at ${healthCheckUrl}...`));
|
|
288
|
+
await waitForHealthCheck(appName, 90, port, config);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Displays run status after successful start
|
|
293
|
+
* @param {string} appName - Application name
|
|
294
|
+
* @param {number} port - Application port
|
|
295
|
+
* @param {Object} config - Application configuration
|
|
296
|
+
*/
|
|
297
|
+
function displayRunStatus(appName, port, config) {
|
|
298
|
+
const healthCheckPath = config?.healthCheck?.path || '/health';
|
|
299
|
+
const healthCheckUrl = `http://localhost:${port}${healthCheckPath}`;
|
|
300
|
+
|
|
301
|
+
logger.log(chalk.green(`\n✓ App running at http://localhost:${port}`));
|
|
302
|
+
logger.log(chalk.blue(`Health check: ${healthCheckUrl}`));
|
|
303
|
+
logger.log(chalk.gray(`Container: aifabrix-${appName}`));
|
|
166
304
|
}
|
|
167
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Waits for container health check to pass
|
|
308
|
+
* @param {string} appName - Application name
|
|
309
|
+
* @param {number} timeout - Timeout in seconds
|
|
310
|
+
* @param {number} port - Application port (optional, will be detected if not provided)
|
|
311
|
+
* @param {Object} config - Application configuration (optional)
|
|
312
|
+
*/
|
|
313
|
+
|
|
168
314
|
/**
|
|
169
315
|
* Runs the application locally using Docker
|
|
170
316
|
* Starts container with proper port mapping and environment
|
|
@@ -183,50 +329,16 @@ async function waitForHealthCheck(appName, timeout = 60) {
|
|
|
183
329
|
*/
|
|
184
330
|
async function runApp(appName, options = {}) {
|
|
185
331
|
try {
|
|
186
|
-
// Validate app name
|
|
187
|
-
|
|
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`));
|
|
332
|
+
// Validate app name and load configuration
|
|
333
|
+
const config = await validateAppConfiguration(appName);
|
|
213
334
|
|
|
214
|
-
// Check infrastructure
|
|
215
|
-
|
|
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'));
|
|
335
|
+
// Check prerequisites: image and infrastructure
|
|
336
|
+
await checkPrerequisites(appName, config);
|
|
225
337
|
|
|
226
338
|
// Check if container is already running
|
|
227
339
|
const containerRunning = await checkContainerRunning(appName);
|
|
228
340
|
if (containerRunning) {
|
|
229
|
-
|
|
341
|
+
logger.log(chalk.yellow(`Container aifabrix-${appName} is already running`));
|
|
230
342
|
await stopAndRemoveContainer(appName);
|
|
231
343
|
}
|
|
232
344
|
|
|
@@ -237,42 +349,21 @@ async function runApp(appName, options = {}) {
|
|
|
237
349
|
throw new Error(`Port ${port} is already in use. Try --port <alternative>`);
|
|
238
350
|
}
|
|
239
351
|
|
|
240
|
-
//
|
|
241
|
-
const
|
|
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);
|
|
352
|
+
// Prepare environment: ensure .env file and generate Docker Compose
|
|
353
|
+
const tempComposePath = await prepareEnvironment(appName, config, options);
|
|
253
354
|
|
|
254
355
|
try {
|
|
255
|
-
// Start container
|
|
256
|
-
|
|
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);
|
|
356
|
+
// Start container and wait for health check
|
|
357
|
+
await startContainer(appName, tempComposePath, port, config);
|
|
263
358
|
|
|
264
359
|
// Display success message
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
await fs.unlink(tempComposePath);
|
|
273
|
-
} catch (error) {
|
|
274
|
-
// Ignore cleanup errors
|
|
275
|
-
}
|
|
360
|
+
displayRunStatus(appName, port, config);
|
|
361
|
+
|
|
362
|
+
} catch (error) {
|
|
363
|
+
// Keep the compose file for debugging - don't delete on error
|
|
364
|
+
logger.log(chalk.yellow(`\n⚠️ Compose file preserved at: ${tempComposePath}`));
|
|
365
|
+
logger.log(chalk.yellow(' Review the file to debug issues'));
|
|
366
|
+
throw error;
|
|
276
367
|
}
|
|
277
368
|
|
|
278
369
|
} catch (error) {
|
|
@@ -286,6 +377,6 @@ module.exports = {
|
|
|
286
377
|
checkContainerRunning,
|
|
287
378
|
stopAndRemoveContainer,
|
|
288
379
|
checkPortAvailable,
|
|
289
|
-
generateDockerCompose,
|
|
380
|
+
generateDockerCompose: composeGenerator.generateDockerCompose,
|
|
290
381
|
waitForHealthCheck
|
|
291
382
|
};
|