@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.
- package/README.md +6 -2
- 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 +334 -133
- package/lib/app.js +208 -274
- package/lib/audit-logger.js +2 -0
- package/lib/build.js +209 -98
- 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/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/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 +13 -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 +168 -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
|
@@ -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}
|
|
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,337 @@ async function checkPortAvailable(port) {
|
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
/**
|
|
87
|
-
*
|
|
88
|
-
* @param {string}
|
|
89
|
-
* @
|
|
90
|
-
* @
|
|
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
|
-
|
|
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
|
-
|
|
105
|
+
return handlebars.compile(templateContent);
|
|
106
|
+
}
|
|
102
107
|
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
*
|
|
258
|
+
* Validates app name and loads configuration
|
|
259
|
+
* @async
|
|
134
260
|
* @param {string} appName - Application name
|
|
135
|
-
* @
|
|
261
|
+
* @returns {Promise<Object>} Application configuration
|
|
262
|
+
* @throws {Error} If validation fails
|
|
136
263
|
*/
|
|
137
|
-
async function
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
207
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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);
|
|
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
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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) {
|