@aifabrix/builder 2.0.8 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/app-config.js +2 -0
- package/lib/app-readme.js +107 -0
- package/lib/app-run.js +121 -18
- package/lib/app.js +1 -0
- package/lib/cli.js +1 -0
- package/lib/schema/application-schema.json +46 -2
- package/lib/utils/health-check.js +107 -13
- package/package.json +1 -1
- package/templates/applications/README.md.hbs +70 -0
package/lib/app-config.js
CHANGED
|
@@ -13,6 +13,7 @@ const path = require('path');
|
|
|
13
13
|
const chalk = require('chalk');
|
|
14
14
|
const { generateVariablesYaml, generateEnvTemplate, generateRbacYaml } = require('./templates');
|
|
15
15
|
const { generateEnvTemplate: generateEnvTemplateFromReader } = require('./env-reader');
|
|
16
|
+
const { generateReadmeMdFile } = require('./app-readme');
|
|
16
17
|
const logger = require('./utils/logger');
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -146,6 +147,7 @@ async function generateConfigFiles(appPath, appName, config, existingEnv) {
|
|
|
146
147
|
await generateEnvTemplateFile(appPath, config, existingEnv);
|
|
147
148
|
await generateRbacYamlFile(appPath, appName, config);
|
|
148
149
|
await generateDeployJsonFile(appPath, appName, config);
|
|
150
|
+
await generateReadmeMdFile(appPath, appName, config);
|
|
149
151
|
} catch (error) {
|
|
150
152
|
throw new Error(`Failed to generate configuration files: ${error.message}`);
|
|
151
153
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application README.md Generation
|
|
3
|
+
*
|
|
4
|
+
* Generates README.md files for applications based on configuration
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview README.md generation for AI Fabrix Builder
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs').promises;
|
|
12
|
+
const fsSync = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const handlebars = require('handlebars');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Checks if a file exists
|
|
18
|
+
* @async
|
|
19
|
+
* @param {string} filePath - Path to file
|
|
20
|
+
* @returns {Promise<boolean>} True if file exists
|
|
21
|
+
*/
|
|
22
|
+
async function fileExists(filePath) {
|
|
23
|
+
try {
|
|
24
|
+
await fs.access(filePath);
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Formats application name for display (capitalize first letter of each word)
|
|
33
|
+
* @param {string} appName - Application name
|
|
34
|
+
* @returns {string} Formatted display name
|
|
35
|
+
*/
|
|
36
|
+
function formatAppDisplayName(appName) {
|
|
37
|
+
return appName
|
|
38
|
+
.split('-')
|
|
39
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
40
|
+
.join(' ');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Loads and compiles README.md template
|
|
45
|
+
* @returns {Function} Compiled Handlebars template
|
|
46
|
+
* @throws {Error} If template not found
|
|
47
|
+
*/
|
|
48
|
+
function loadReadmeTemplate() {
|
|
49
|
+
const templatePath = path.join(__dirname, '..', 'templates', 'applications', 'README.md.hbs');
|
|
50
|
+
if (!fsSync.existsSync(templatePath)) {
|
|
51
|
+
throw new Error(`README template not found at ${templatePath}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const templateContent = fsSync.readFileSync(templatePath, 'utf8');
|
|
55
|
+
return handlebars.compile(templateContent);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generates README.md content for an application using Handlebars template
|
|
60
|
+
* @param {string} appName - Application name
|
|
61
|
+
* @param {Object} config - Application configuration
|
|
62
|
+
* @returns {string} README.md content
|
|
63
|
+
*/
|
|
64
|
+
function generateReadmeMd(appName, config) {
|
|
65
|
+
const template = loadReadmeTemplate();
|
|
66
|
+
const displayName = formatAppDisplayName(appName);
|
|
67
|
+
const imageName = `aifabrix/${appName}`;
|
|
68
|
+
const port = config.port || 3000;
|
|
69
|
+
|
|
70
|
+
const context = {
|
|
71
|
+
appName,
|
|
72
|
+
displayName,
|
|
73
|
+
imageName,
|
|
74
|
+
port,
|
|
75
|
+
hasDatabase: config.database || false,
|
|
76
|
+
hasRedis: config.redis || false,
|
|
77
|
+
hasStorage: config.storage || false,
|
|
78
|
+
hasAuthentication: config.authentication || false
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return template(context);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Generates README.md file if it doesn't exist
|
|
86
|
+
* @async
|
|
87
|
+
* @function generateReadmeMdFile
|
|
88
|
+
* @param {string} appPath - Path to application directory
|
|
89
|
+
* @param {string} appName - Application name
|
|
90
|
+
* @param {Object} config - Application configuration
|
|
91
|
+
* @returns {Promise<void>} Resolves when README.md is generated or skipped
|
|
92
|
+
* @throws {Error} If file generation fails
|
|
93
|
+
*/
|
|
94
|
+
async function generateReadmeMdFile(appPath, appName, config) {
|
|
95
|
+
const readmePath = path.join(appPath, 'README.md');
|
|
96
|
+
if (!(await fileExists(readmePath))) {
|
|
97
|
+
const readmeContent = generateReadmeMd(appName, config);
|
|
98
|
+
await fs.writeFile(readmePath, readmeContent);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = {
|
|
103
|
+
generateReadmeMdFile,
|
|
104
|
+
generateReadmeMd,
|
|
105
|
+
formatAppDisplayName
|
|
106
|
+
};
|
|
107
|
+
|
package/lib/app-run.js
CHANGED
|
@@ -30,16 +30,28 @@ const execAsync = promisify(exec);
|
|
|
30
30
|
* Checks if Docker image exists for the application
|
|
31
31
|
* @param {string} imageName - Image name (can include repository prefix)
|
|
32
32
|
* @param {string} tag - Image tag (default: latest)
|
|
33
|
+
* @param {boolean} [debug=false] - Enable debug logging
|
|
33
34
|
* @returns {Promise<boolean>} True if image exists
|
|
34
35
|
*/
|
|
35
|
-
async function checkImageExists(imageName, tag = 'latest') {
|
|
36
|
+
async function checkImageExists(imageName, tag = 'latest', debug = false) {
|
|
36
37
|
try {
|
|
37
38
|
const fullImageName = `${imageName}:${tag}`;
|
|
39
|
+
const cmd = `docker images --format "{{.Repository}}:{{.Tag}}" --filter "reference=${fullImageName}"`;
|
|
40
|
+
if (debug) {
|
|
41
|
+
logger.log(chalk.gray(`[DEBUG] Executing: ${cmd}`));
|
|
42
|
+
}
|
|
38
43
|
// Use Docker's native filtering for cross-platform compatibility (Windows-safe)
|
|
39
|
-
const { stdout } = await execAsync(
|
|
44
|
+
const { stdout } = await execAsync(cmd);
|
|
40
45
|
const lines = stdout.trim().split('\n').filter(line => line.trim() !== '');
|
|
41
|
-
|
|
46
|
+
const exists = lines.some(line => line.trim() === fullImageName);
|
|
47
|
+
if (debug) {
|
|
48
|
+
logger.log(chalk.gray(`[DEBUG] Image ${fullImageName} exists: ${exists}`));
|
|
49
|
+
}
|
|
50
|
+
return exists;
|
|
42
51
|
} catch (error) {
|
|
52
|
+
if (debug) {
|
|
53
|
+
logger.log(chalk.gray(`[DEBUG] Image check failed: ${error.message}`));
|
|
54
|
+
}
|
|
43
55
|
return false;
|
|
44
56
|
}
|
|
45
57
|
}
|
|
@@ -47,13 +59,34 @@ async function checkImageExists(imageName, tag = 'latest') {
|
|
|
47
59
|
/**
|
|
48
60
|
* Checks if container is already running
|
|
49
61
|
* @param {string} appName - Application name
|
|
62
|
+
* @param {boolean} [debug=false] - Enable debug logging
|
|
50
63
|
* @returns {Promise<boolean>} True if container is running
|
|
51
64
|
*/
|
|
52
|
-
async function checkContainerRunning(appName) {
|
|
65
|
+
async function checkContainerRunning(appName, debug = false) {
|
|
53
66
|
try {
|
|
54
|
-
const
|
|
55
|
-
|
|
67
|
+
const cmd = `docker ps --filter "name=aifabrix-${appName}" --format "{{.Names}}"`;
|
|
68
|
+
if (debug) {
|
|
69
|
+
logger.log(chalk.gray(`[DEBUG] Executing: ${cmd}`));
|
|
70
|
+
}
|
|
71
|
+
const { stdout } = await execAsync(cmd);
|
|
72
|
+
const isRunning = stdout.trim() === `aifabrix-${appName}`;
|
|
73
|
+
if (debug) {
|
|
74
|
+
logger.log(chalk.gray(`[DEBUG] Container aifabrix-${appName} running: ${isRunning}`));
|
|
75
|
+
if (isRunning) {
|
|
76
|
+
// Get container status details
|
|
77
|
+
const statusCmd = `docker ps --filter "name=aifabrix-${appName}" --format "{{.Status}}"`;
|
|
78
|
+
const { stdout: status } = await execAsync(statusCmd);
|
|
79
|
+
const portsCmd = `docker ps --filter "name=aifabrix-${appName}" --format "{{.Ports}}"`;
|
|
80
|
+
const { stdout: ports } = await execAsync(portsCmd);
|
|
81
|
+
logger.log(chalk.gray(`[DEBUG] Container status: ${status.trim()}`));
|
|
82
|
+
logger.log(chalk.gray(`[DEBUG] Container ports: ${ports.trim()}`));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return isRunning;
|
|
56
86
|
} catch (error) {
|
|
87
|
+
if (debug) {
|
|
88
|
+
logger.log(chalk.gray(`[DEBUG] Container check failed: ${error.message}`));
|
|
89
|
+
}
|
|
57
90
|
return false;
|
|
58
91
|
}
|
|
59
92
|
}
|
|
@@ -61,14 +94,26 @@ async function checkContainerRunning(appName) {
|
|
|
61
94
|
/**
|
|
62
95
|
* Stops and removes existing container
|
|
63
96
|
* @param {string} appName - Application name
|
|
97
|
+
* @param {boolean} [debug=false] - Enable debug logging
|
|
64
98
|
*/
|
|
65
|
-
async function stopAndRemoveContainer(appName) {
|
|
99
|
+
async function stopAndRemoveContainer(appName, debug = false) {
|
|
66
100
|
try {
|
|
67
101
|
logger.log(chalk.yellow(`Stopping existing container aifabrix-${appName}...`));
|
|
68
|
-
|
|
69
|
-
|
|
102
|
+
const stopCmd = `docker stop aifabrix-${appName}`;
|
|
103
|
+
if (debug) {
|
|
104
|
+
logger.log(chalk.gray(`[DEBUG] Executing: ${stopCmd}`));
|
|
105
|
+
}
|
|
106
|
+
await execAsync(stopCmd);
|
|
107
|
+
const rmCmd = `docker rm aifabrix-${appName}`;
|
|
108
|
+
if (debug) {
|
|
109
|
+
logger.log(chalk.gray(`[DEBUG] Executing: ${rmCmd}`));
|
|
110
|
+
}
|
|
111
|
+
await execAsync(rmCmd);
|
|
70
112
|
logger.log(chalk.green(`✓ Container aifabrix-${appName} stopped and removed`));
|
|
71
113
|
} catch (error) {
|
|
114
|
+
if (debug) {
|
|
115
|
+
logger.log(chalk.gray(`[DEBUG] Stop/remove container error: ${error.message}`));
|
|
116
|
+
}
|
|
72
117
|
// Container might not exist, which is fine
|
|
73
118
|
logger.log(chalk.gray(`Container aifabrix-${appName} was not running`));
|
|
74
119
|
}
|
|
@@ -180,17 +225,22 @@ async function validateAppConfiguration(appName) {
|
|
|
180
225
|
* @async
|
|
181
226
|
* @param {string} appName - Application name
|
|
182
227
|
* @param {Object} config - Application configuration
|
|
228
|
+
* @param {boolean} [debug=false] - Enable debug logging
|
|
183
229
|
* @throws {Error} If prerequisites are not met
|
|
184
230
|
*/
|
|
185
|
-
async function checkPrerequisites(appName, config) {
|
|
231
|
+
async function checkPrerequisites(appName, config, debug = false) {
|
|
186
232
|
// Extract image name from configuration (same logic as build process)
|
|
187
233
|
const imageName = getImageName(config, appName);
|
|
188
234
|
const imageTag = config.image?.tag || 'latest';
|
|
189
235
|
const fullImageName = `${imageName}:${imageTag}`;
|
|
190
236
|
|
|
237
|
+
if (debug) {
|
|
238
|
+
logger.log(chalk.gray(`[DEBUG] Image name: ${imageName}, tag: ${imageTag}`));
|
|
239
|
+
}
|
|
240
|
+
|
|
191
241
|
// Check if Docker image exists
|
|
192
242
|
logger.log(chalk.blue(`Checking if image ${fullImageName} exists...`));
|
|
193
|
-
const imageExists = await checkImageExists(imageName, imageTag);
|
|
243
|
+
const imageExists = await checkImageExists(imageName, imageTag, debug);
|
|
194
244
|
if (!imageExists) {
|
|
195
245
|
throw new Error(`Docker image ${fullImageName} not found\nRun 'aifabrix build ${appName}' first`);
|
|
196
246
|
}
|
|
@@ -199,6 +249,9 @@ async function checkPrerequisites(appName, config) {
|
|
|
199
249
|
// Check infrastructure health
|
|
200
250
|
logger.log(chalk.blue('Checking infrastructure health...'));
|
|
201
251
|
const infraHealth = await infra.checkInfraHealth();
|
|
252
|
+
if (debug) {
|
|
253
|
+
logger.log(chalk.gray(`[DEBUG] Infrastructure health: ${JSON.stringify(infraHealth, null, 2)}`));
|
|
254
|
+
}
|
|
202
255
|
const unhealthyServices = Object.entries(infraHealth)
|
|
203
256
|
.filter(([_, status]) => status !== 'healthy')
|
|
204
257
|
.map(([service, _]) => service);
|
|
@@ -258,13 +311,18 @@ async function prepareEnvironment(appName, config, options) {
|
|
|
258
311
|
* @param {string} appName - Application name
|
|
259
312
|
* @param {string} composePath - Path to Docker Compose file
|
|
260
313
|
* @param {number} port - Application port
|
|
314
|
+
* @param {Object} config - Application configuration
|
|
315
|
+
* @param {boolean} [debug=false] - Enable debug logging
|
|
261
316
|
* @throws {Error} If container fails to start or become healthy
|
|
262
317
|
*/
|
|
263
|
-
async function startContainer(appName, composePath, port, config = null) {
|
|
318
|
+
async function startContainer(appName, composePath, port, config = null, debug = false) {
|
|
264
319
|
logger.log(chalk.blue(`Starting ${appName}...`));
|
|
265
320
|
|
|
266
321
|
// Ensure ADMIN_SECRETS_PATH is set for db-init service
|
|
267
322
|
const adminSecretsPath = await infra.ensureAdminSecrets();
|
|
323
|
+
if (debug) {
|
|
324
|
+
logger.log(chalk.gray(`[DEBUG] Admin secrets path: ${adminSecretsPath}`));
|
|
325
|
+
}
|
|
268
326
|
|
|
269
327
|
// Load POSTGRES_PASSWORD from admin-secrets.env
|
|
270
328
|
const adminSecretsContent = fsSync.readFileSync(adminSecretsPath, 'utf8');
|
|
@@ -278,14 +336,33 @@ async function startContainer(appName, composePath, port, config = null) {
|
|
|
278
336
|
POSTGRES_PASSWORD: postgresPassword
|
|
279
337
|
};
|
|
280
338
|
|
|
281
|
-
|
|
339
|
+
if (debug) {
|
|
340
|
+
logger.log(chalk.gray(`[DEBUG] Environment variables: ADMIN_SECRETS_PATH=${adminSecretsPath}, POSTGRES_PASSWORD=${postgresPassword ? '***' : '(not set)'}`));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const composeCmd = `docker-compose -f "${composePath}" up -d`;
|
|
344
|
+
if (debug) {
|
|
345
|
+
logger.log(chalk.gray(`[DEBUG] Executing: ${composeCmd}`));
|
|
346
|
+
logger.log(chalk.gray(`[DEBUG] Compose file: ${composePath}`));
|
|
347
|
+
}
|
|
348
|
+
await execAsync(composeCmd, { env });
|
|
282
349
|
logger.log(chalk.green(`✓ Container aifabrix-${appName} started`));
|
|
283
350
|
|
|
351
|
+
if (debug) {
|
|
352
|
+
// Get container status after start
|
|
353
|
+
const statusCmd = `docker ps --filter "name=aifabrix-${appName}" --format "{{.Status}}"`;
|
|
354
|
+
const { stdout: status } = await execAsync(statusCmd);
|
|
355
|
+
const portsCmd = `docker ps --filter "name=aifabrix-${appName}" --format "{{.Ports}}"`;
|
|
356
|
+
const { stdout: ports } = await execAsync(portsCmd);
|
|
357
|
+
logger.log(chalk.gray(`[DEBUG] Container status: ${status.trim()}`));
|
|
358
|
+
logger.log(chalk.gray(`[DEBUG] Container ports: ${ports.trim()}`));
|
|
359
|
+
}
|
|
360
|
+
|
|
284
361
|
// Wait for health check using host port
|
|
285
362
|
// Port is the host port (CLI --port or config.port, NOT localPort)
|
|
286
363
|
const healthCheckPath = config?.healthCheck?.path || '/health';
|
|
287
364
|
logger.log(chalk.blue(`Waiting for application to be healthy at http://localhost:${port}${healthCheckPath}...`));
|
|
288
|
-
await waitForHealthCheck(appName, 90, port, config);
|
|
365
|
+
await waitForHealthCheck(appName, 90, port, config, debug);
|
|
289
366
|
}
|
|
290
367
|
|
|
291
368
|
/**
|
|
@@ -320,6 +397,7 @@ function displayRunStatus(appName, port, config) {
|
|
|
320
397
|
* @param {string} appName - Name of the application to run
|
|
321
398
|
* @param {Object} options - Run options
|
|
322
399
|
* @param {number} [options.port] - Override local port
|
|
400
|
+
* @param {boolean} [options.debug] - Enable debug output
|
|
323
401
|
* @returns {Promise<void>} Resolves when app is running
|
|
324
402
|
* @throws {Error} If run fails or app is not built
|
|
325
403
|
*
|
|
@@ -328,34 +406,53 @@ function displayRunStatus(appName, port, config) {
|
|
|
328
406
|
* // Application is now running on localhost:3001
|
|
329
407
|
*/
|
|
330
408
|
async function runApp(appName, options = {}) {
|
|
409
|
+
const debug = options.debug || false;
|
|
410
|
+
|
|
411
|
+
if (debug) {
|
|
412
|
+
logger.log(chalk.gray(`[DEBUG] Starting run process for: ${appName}`));
|
|
413
|
+
logger.log(chalk.gray(`[DEBUG] Options: ${JSON.stringify(options, null, 2)}`));
|
|
414
|
+
}
|
|
415
|
+
|
|
331
416
|
try {
|
|
332
417
|
// Validate app name and load configuration
|
|
333
418
|
const config = await validateAppConfiguration(appName);
|
|
419
|
+
if (debug) {
|
|
420
|
+
logger.log(chalk.gray(`[DEBUG] Configuration loaded: port=${config.port || 'default'}, healthCheck.path=${config.healthCheck?.path || '/health'}`));
|
|
421
|
+
}
|
|
334
422
|
|
|
335
423
|
// Check prerequisites: image and infrastructure
|
|
336
|
-
await checkPrerequisites(appName, config);
|
|
424
|
+
await checkPrerequisites(appName, config, debug);
|
|
337
425
|
|
|
338
426
|
// Check if container is already running
|
|
339
|
-
const containerRunning = await checkContainerRunning(appName);
|
|
427
|
+
const containerRunning = await checkContainerRunning(appName, debug);
|
|
340
428
|
if (containerRunning) {
|
|
341
429
|
logger.log(chalk.yellow(`Container aifabrix-${appName} is already running`));
|
|
342
|
-
await stopAndRemoveContainer(appName);
|
|
430
|
+
await stopAndRemoveContainer(appName, debug);
|
|
343
431
|
}
|
|
344
432
|
|
|
345
433
|
// Check port availability
|
|
346
434
|
// Host port: CLI --port if provided, otherwise port from variables.yaml (NOT localPort)
|
|
347
435
|
const port = options.port || config.port || 3000;
|
|
436
|
+
if (debug) {
|
|
437
|
+
logger.log(chalk.gray(`[DEBUG] Port selection: ${port} (${options.port ? 'CLI override' : config.port ? 'config.port' : 'default'})`));
|
|
438
|
+
}
|
|
348
439
|
const portAvailable = await checkPortAvailable(port);
|
|
440
|
+
if (debug) {
|
|
441
|
+
logger.log(chalk.gray(`[DEBUG] Port ${port} available: ${portAvailable}`));
|
|
442
|
+
}
|
|
349
443
|
if (!portAvailable) {
|
|
350
444
|
throw new Error(`Port ${port} is already in use. Try --port <alternative>`);
|
|
351
445
|
}
|
|
352
446
|
|
|
353
447
|
// Prepare environment: ensure .env file and generate Docker Compose
|
|
354
448
|
const tempComposePath = await prepareEnvironment(appName, config, options);
|
|
449
|
+
if (debug) {
|
|
450
|
+
logger.log(chalk.gray(`[DEBUG] Compose file generated: ${tempComposePath}`));
|
|
451
|
+
}
|
|
355
452
|
|
|
356
453
|
try {
|
|
357
454
|
// Start container and wait for health check
|
|
358
|
-
await startContainer(appName, tempComposePath, port, config);
|
|
455
|
+
await startContainer(appName, tempComposePath, port, config, debug);
|
|
359
456
|
|
|
360
457
|
// Display success message
|
|
361
458
|
displayRunStatus(appName, port, config);
|
|
@@ -364,10 +461,16 @@ async function runApp(appName, options = {}) {
|
|
|
364
461
|
// Keep the compose file for debugging - don't delete on error
|
|
365
462
|
logger.log(chalk.yellow(`\n⚠️ Compose file preserved at: ${tempComposePath}`));
|
|
366
463
|
logger.log(chalk.yellow(' Review the file to debug issues'));
|
|
464
|
+
if (debug) {
|
|
465
|
+
logger.log(chalk.gray(`[DEBUG] Error during container start: ${error.message}`));
|
|
466
|
+
}
|
|
367
467
|
throw error;
|
|
368
468
|
}
|
|
369
469
|
|
|
370
470
|
} catch (error) {
|
|
471
|
+
if (debug) {
|
|
472
|
+
logger.log(chalk.gray(`[DEBUG] Run failed: ${error.message}`));
|
|
473
|
+
}
|
|
371
474
|
throw new Error(`Failed to run application: ${error.message}`);
|
|
372
475
|
}
|
|
373
476
|
}
|
package/lib/app.js
CHANGED
|
@@ -361,6 +361,7 @@ async function generateDockerfile(appPath, language, config) {
|
|
|
361
361
|
* @param {string} appName - Name of the application to run
|
|
362
362
|
* @param {Object} options - Run options
|
|
363
363
|
* @param {number} [options.port] - Override local port
|
|
364
|
+
* @param {boolean} [options.debug] - Enable debug output
|
|
364
365
|
* @returns {Promise<void>} Resolves when app is running
|
|
365
366
|
* @throws {Error} If run fails or app is not built
|
|
366
367
|
*
|
package/lib/cli.js
CHANGED
|
@@ -111,6 +111,7 @@ function setupCommands(program) {
|
|
|
111
111
|
program.command('run <app>')
|
|
112
112
|
.description('Run application locally')
|
|
113
113
|
.option('-p, --port <port>', 'Override local port')
|
|
114
|
+
.option('-d, --debug', 'Enable debug output with detailed container information')
|
|
114
115
|
.action(async(appName, options) => {
|
|
115
116
|
try {
|
|
116
117
|
await app.runApp(appName, options);
|
|
@@ -284,7 +284,7 @@
|
|
|
284
284
|
},
|
|
285
285
|
"healthCheck": {
|
|
286
286
|
"type": "object",
|
|
287
|
-
"description": "Health check configuration",
|
|
287
|
+
"description": "Health check configuration. The health check endpoint must return HTTP 200 and a JSON response with one of the following formats: {\"status\": \"UP\"} (Keycloak format), {\"status\": \"ok\"} (standard format, optionally with {\"database\": \"connected\"}), {\"status\": \"healthy\"} (alternative format), or {\"success\": true} (success-based format). For non-JSON responses, HTTP 200 status code is sufficient.",
|
|
288
288
|
"required": ["path", "interval"],
|
|
289
289
|
"properties": {
|
|
290
290
|
"path": {
|
|
@@ -320,7 +320,51 @@
|
|
|
320
320
|
"maximum": 600
|
|
321
321
|
}
|
|
322
322
|
},
|
|
323
|
-
"additionalProperties": false
|
|
323
|
+
"additionalProperties": false,
|
|
324
|
+
"examples": [
|
|
325
|
+
{
|
|
326
|
+
"path": "/health",
|
|
327
|
+
"interval": 30
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"path": "/api/health",
|
|
331
|
+
"interval": 60,
|
|
332
|
+
"probePath": "/api/health",
|
|
333
|
+
"probeRequestType": "GET",
|
|
334
|
+
"probeProtocol": "Http",
|
|
335
|
+
"probeIntervalInSeconds": 120
|
|
336
|
+
}
|
|
337
|
+
],
|
|
338
|
+
"responseFormats": {
|
|
339
|
+
"description": "Valid health check response formats",
|
|
340
|
+
"formats": [
|
|
341
|
+
{
|
|
342
|
+
"format": "Keycloak",
|
|
343
|
+
"example": "{\"status\": \"UP\", \"checks\": []}",
|
|
344
|
+
"validation": "status === 'UP'"
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
"format": "Standard",
|
|
348
|
+
"example": "{\"status\": \"ok\", \"database\": \"connected\"}",
|
|
349
|
+
"validation": "status === 'ok' && (database === 'connected' || !database)"
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
"format": "Alternative",
|
|
353
|
+
"example": "{\"status\": \"healthy\", \"service\": \"dataplane\"}",
|
|
354
|
+
"validation": "status === 'healthy'"
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
"format": "Success-based",
|
|
358
|
+
"example": "{\"success\": true, \"message\": \"Service is running\"}",
|
|
359
|
+
"validation": "success === true"
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
"format": "Non-JSON",
|
|
363
|
+
"example": "OK",
|
|
364
|
+
"validation": "HTTP status code === 200"
|
|
365
|
+
}
|
|
366
|
+
]
|
|
367
|
+
}
|
|
324
368
|
},
|
|
325
369
|
"frontDoorRouting": {
|
|
326
370
|
"type": "object",
|
|
@@ -67,37 +67,61 @@ async function waitForDbInit(appName) {
|
|
|
67
67
|
* @async
|
|
68
68
|
* @function getContainerPort
|
|
69
69
|
* @param {string} appName - Application name
|
|
70
|
+
* @param {boolean} [debug=false] - Enable debug logging
|
|
70
71
|
* @returns {Promise<number>} Container port
|
|
71
72
|
*/
|
|
72
|
-
async function getContainerPort(appName) {
|
|
73
|
+
async function getContainerPort(appName, debug = false) {
|
|
73
74
|
try {
|
|
74
75
|
// Try to get the actual mapped host port from Docker
|
|
75
76
|
// First try docker inspect for the container port mapping
|
|
76
|
-
const
|
|
77
|
+
const inspectCmd = `docker inspect --format='{{range $p, $conf := .NetworkSettings.Ports}}{{if $conf}}{{range $conf}}{{.HostPort}}{{end}}{{end}}{{end}}' aifabrix-${appName}`;
|
|
78
|
+
if (debug) {
|
|
79
|
+
logger.log(chalk.gray(`[DEBUG] Executing: ${inspectCmd}`));
|
|
80
|
+
}
|
|
81
|
+
const { stdout: portMapping } = await execAsync(inspectCmd);
|
|
77
82
|
const ports = portMapping.trim().split('\n').filter(p => p && p !== '');
|
|
78
83
|
if (ports.length > 0) {
|
|
79
84
|
const port = parseInt(ports[0], 10);
|
|
80
85
|
if (!isNaN(port) && port > 0) {
|
|
86
|
+
if (debug) {
|
|
87
|
+
logger.log(chalk.gray(`[DEBUG] Detected port ${port} from docker inspect`));
|
|
88
|
+
}
|
|
81
89
|
return port;
|
|
82
90
|
}
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
// Fallback: try docker ps to get port mapping (format: "0.0.0.0:3010->3000/tcp")
|
|
86
94
|
try {
|
|
87
|
-
const
|
|
95
|
+
const psCmd = `docker ps --filter "name=aifabrix-${appName}" --format "{{.Ports}}"`;
|
|
96
|
+
if (debug) {
|
|
97
|
+
logger.log(chalk.gray(`[DEBUG] Fallback: Executing: ${psCmd}`));
|
|
98
|
+
}
|
|
99
|
+
const { stdout: psOutput } = await execAsync(psCmd);
|
|
88
100
|
const portMatch = psOutput.match(/:(\d+)->/);
|
|
89
101
|
if (portMatch) {
|
|
90
102
|
const port = parseInt(portMatch[1], 10);
|
|
91
103
|
if (!isNaN(port) && port > 0) {
|
|
104
|
+
if (debug) {
|
|
105
|
+
logger.log(chalk.gray(`[DEBUG] Detected port ${port} from docker ps`));
|
|
106
|
+
}
|
|
92
107
|
return port;
|
|
93
108
|
}
|
|
94
109
|
}
|
|
95
110
|
} catch (error) {
|
|
111
|
+
if (debug) {
|
|
112
|
+
logger.log(chalk.gray(`[DEBUG] Fallback port detection failed: ${error.message}`));
|
|
113
|
+
}
|
|
96
114
|
// Fall through
|
|
97
115
|
}
|
|
98
116
|
} catch (error) {
|
|
117
|
+
if (debug) {
|
|
118
|
+
logger.log(chalk.gray(`[DEBUG] Port detection failed: ${error.message}`));
|
|
119
|
+
}
|
|
99
120
|
// Fall through to default
|
|
100
121
|
}
|
|
122
|
+
if (debug) {
|
|
123
|
+
logger.log(chalk.gray('[DEBUG] Using default port 3000'));
|
|
124
|
+
}
|
|
101
125
|
return 3000;
|
|
102
126
|
}
|
|
103
127
|
|
|
@@ -117,6 +141,12 @@ function parseHealthResponse(data, statusCode) {
|
|
|
117
141
|
if (health.status === 'ok') {
|
|
118
142
|
return health.database === 'connected' || !health.database;
|
|
119
143
|
}
|
|
144
|
+
if (health.status === 'healthy') {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
if (health.success === true) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
120
150
|
return false;
|
|
121
151
|
} catch (error) {
|
|
122
152
|
return statusCode === 200;
|
|
@@ -128,27 +158,68 @@ function parseHealthResponse(data, statusCode) {
|
|
|
128
158
|
* @async
|
|
129
159
|
* @function checkHealthEndpoint
|
|
130
160
|
* @param {string} healthCheckUrl - Health check URL
|
|
161
|
+
* @param {boolean} [debug=false] - Enable debug logging
|
|
131
162
|
* @returns {Promise<boolean>} True if healthy
|
|
132
163
|
* @throws {Error} If request fails with exception
|
|
133
164
|
*/
|
|
134
|
-
async function checkHealthEndpoint(healthCheckUrl) {
|
|
165
|
+
async function checkHealthEndpoint(healthCheckUrl, debug = false) {
|
|
135
166
|
return new Promise((resolve, reject) => {
|
|
136
167
|
try {
|
|
137
|
-
const
|
|
168
|
+
const urlObj = new URL(healthCheckUrl);
|
|
169
|
+
const options = {
|
|
170
|
+
hostname: urlObj.hostname,
|
|
171
|
+
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
172
|
+
path: urlObj.pathname + urlObj.search,
|
|
173
|
+
method: 'GET',
|
|
174
|
+
timeout: 5000
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
if (debug) {
|
|
178
|
+
logger.log(chalk.gray(`[DEBUG] Health check request: ${healthCheckUrl}`));
|
|
179
|
+
logger.log(chalk.gray(`[DEBUG] Request options: ${JSON.stringify(options, null, 2)}`));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const req = http.request(options, (res) => {
|
|
138
183
|
let data = '';
|
|
184
|
+
if (debug) {
|
|
185
|
+
logger.log(chalk.gray(`[DEBUG] Response status code: ${res.statusCode}`));
|
|
186
|
+
logger.log(chalk.gray(`[DEBUG] Response headers: ${JSON.stringify(res.headers, null, 2)}`));
|
|
187
|
+
}
|
|
139
188
|
res.on('data', (chunk) => {
|
|
140
189
|
data += chunk;
|
|
141
190
|
});
|
|
142
191
|
res.on('end', () => {
|
|
143
|
-
|
|
192
|
+
if (debug) {
|
|
193
|
+
const truncatedData = data.length > 200 ? data.substring(0, 200) + '...' : data;
|
|
194
|
+
logger.log(chalk.gray(`[DEBUG] Response body: ${truncatedData}`));
|
|
195
|
+
}
|
|
196
|
+
const isHealthy = parseHealthResponse(data, res.statusCode);
|
|
197
|
+
if (debug) {
|
|
198
|
+
logger.log(chalk.gray(`[DEBUG] Health check result: ${isHealthy ? 'healthy' : 'unhealthy'}`));
|
|
199
|
+
}
|
|
200
|
+
resolve(isHealthy);
|
|
144
201
|
});
|
|
145
202
|
});
|
|
146
|
-
|
|
203
|
+
|
|
204
|
+
req.on('error', (error) => {
|
|
205
|
+
if (debug) {
|
|
206
|
+
logger.log(chalk.gray(`[DEBUG] Health check request error: ${error.message}`));
|
|
207
|
+
}
|
|
208
|
+
resolve(false);
|
|
209
|
+
});
|
|
147
210
|
req.on('timeout', () => {
|
|
211
|
+
if (debug) {
|
|
212
|
+
logger.log(chalk.gray('[DEBUG] Health check request timeout after 5 seconds'));
|
|
213
|
+
}
|
|
148
214
|
req.destroy();
|
|
149
215
|
resolve(false);
|
|
150
216
|
});
|
|
217
|
+
|
|
218
|
+
req.end();
|
|
151
219
|
} catch (error) {
|
|
220
|
+
if (debug) {
|
|
221
|
+
logger.log(chalk.gray(`[DEBUG] Health check exception: ${error.message}`));
|
|
222
|
+
}
|
|
152
223
|
// Re-throw exceptions (not just network errors)
|
|
153
224
|
reject(error);
|
|
154
225
|
}
|
|
@@ -165,28 +236,47 @@ async function checkHealthEndpoint(healthCheckUrl) {
|
|
|
165
236
|
* @param {number} timeout - Timeout in seconds (default: 90)
|
|
166
237
|
* @param {number} [port] - Application port (auto-detected if not provided)
|
|
167
238
|
* @param {Object} [config] - Application configuration
|
|
239
|
+
* @param {boolean} [debug=false] - Enable debug logging
|
|
168
240
|
* @returns {Promise<void>} Resolves when health check passes
|
|
169
241
|
* @throws {Error} If health check times out
|
|
170
242
|
*/
|
|
171
|
-
async function waitForHealthCheck(appName, timeout = 90, port = null, config = null) {
|
|
243
|
+
async function waitForHealthCheck(appName, timeout = 90, port = null, config = null, debug = false) {
|
|
172
244
|
await waitForDbInit(appName);
|
|
173
245
|
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
const healthCheckPort = port
|
|
246
|
+
// Use provided port if given, otherwise detect from Docker
|
|
247
|
+
// Port provided should be the host port (CLI --port or config.port, NOT localPort)
|
|
248
|
+
const healthCheckPort = port !== null && port !== undefined ? port : await getContainerPort(appName, debug);
|
|
249
|
+
|
|
250
|
+
if (debug) {
|
|
251
|
+
logger.log(chalk.gray(`[DEBUG] Health check port: ${healthCheckPort} (${port !== null && port !== undefined ? 'provided' : 'auto-detected'})`));
|
|
252
|
+
}
|
|
177
253
|
|
|
178
254
|
const healthCheckPath = config?.healthCheck?.path || '/health';
|
|
179
255
|
const healthCheckUrl = `http://localhost:${healthCheckPort}${healthCheckPath}`;
|
|
180
256
|
const maxAttempts = timeout / 2;
|
|
181
257
|
|
|
258
|
+
if (debug) {
|
|
259
|
+
logger.log(chalk.gray(`[DEBUG] Health check URL: ${healthCheckUrl}`));
|
|
260
|
+
logger.log(chalk.gray(`[DEBUG] Timeout: ${timeout} seconds, Max attempts: ${maxAttempts}`));
|
|
261
|
+
}
|
|
262
|
+
|
|
182
263
|
for (let attempts = 0; attempts < maxAttempts; attempts++) {
|
|
183
264
|
try {
|
|
184
|
-
|
|
265
|
+
if (debug) {
|
|
266
|
+
logger.log(chalk.gray(`[DEBUG] Health check attempt ${attempts + 1}/${maxAttempts}`));
|
|
267
|
+
}
|
|
268
|
+
const healthCheckPassed = await checkHealthEndpoint(healthCheckUrl, debug);
|
|
185
269
|
if (healthCheckPassed) {
|
|
186
270
|
logger.log(chalk.green('✓ Application is healthy'));
|
|
271
|
+
if (debug) {
|
|
272
|
+
logger.log(chalk.gray(`[DEBUG] Health check passed after ${attempts + 1} attempt(s)`));
|
|
273
|
+
}
|
|
187
274
|
return;
|
|
188
275
|
}
|
|
189
276
|
} catch (error) {
|
|
277
|
+
if (debug) {
|
|
278
|
+
logger.log(chalk.gray(`[DEBUG] Health check exception on attempt ${attempts + 1}: ${error.message}`));
|
|
279
|
+
}
|
|
190
280
|
// If exception occurs, continue retrying until timeout
|
|
191
281
|
// The error will be handled by timeout error below
|
|
192
282
|
}
|
|
@@ -197,10 +287,14 @@ async function waitForHealthCheck(appName, timeout = 90, port = null, config = n
|
|
|
197
287
|
}
|
|
198
288
|
}
|
|
199
289
|
|
|
290
|
+
if (debug) {
|
|
291
|
+
logger.log(chalk.gray(`[DEBUG] Health check failed after ${maxAttempts} attempts`));
|
|
292
|
+
}
|
|
200
293
|
throw new Error(`Health check timeout after ${timeout} seconds`);
|
|
201
294
|
}
|
|
202
295
|
|
|
203
296
|
module.exports = {
|
|
204
|
-
waitForHealthCheck
|
|
297
|
+
waitForHealthCheck,
|
|
298
|
+
checkHealthEndpoint
|
|
205
299
|
};
|
|
206
300
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# {{displayName}} Builder
|
|
2
|
+
|
|
3
|
+
Build, run, and deploy {{displayName}} using `@aifabrix/builder`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @aifabrix/builder
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Build
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
aifabrix build {{appName}}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Builds Docker image: `{{imageName}}:latest`
|
|
18
|
+
|
|
19
|
+
## Run Locally
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
aifabrix run {{appName}}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Access:** http://localhost:{{port}}
|
|
26
|
+
|
|
27
|
+
**Logs:**
|
|
28
|
+
```bash
|
|
29
|
+
docker logs {{appName}} -f
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Stop:**
|
|
33
|
+
```bash
|
|
34
|
+
docker stop {{appName}}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Push to Azure Container Registry
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
aifabrix push {{appName}} --action acr --tag latest
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Note:** ACR push requires `az login` or ACR credentials in `variables.yaml`
|
|
44
|
+
|
|
45
|
+
## Prerequisites
|
|
46
|
+
|
|
47
|
+
- `@aifabrix/builder` installed globally
|
|
48
|
+
- Docker Desktop running
|
|
49
|
+
- Infrastructure running (`aifabrix up`)
|
|
50
|
+
{{#if hasDatabase}}
|
|
51
|
+
- PostgreSQL database
|
|
52
|
+
{{/if}}
|
|
53
|
+
{{#if hasRedis}}
|
|
54
|
+
- Redis
|
|
55
|
+
{{/if}}
|
|
56
|
+
{{#if hasStorage}}
|
|
57
|
+
- File storage configured
|
|
58
|
+
{{/if}}
|
|
59
|
+
{{#if hasAuthentication}}
|
|
60
|
+
- Authentication/RBAC configured
|
|
61
|
+
{{/if}}
|
|
62
|
+
|
|
63
|
+
## Troubleshooting
|
|
64
|
+
|
|
65
|
+
**Build fails:** Check Docker is running and `variables.yaml` → `build.secrets` path is correct
|
|
66
|
+
|
|
67
|
+
**Can't connect:** Verify infrastructure is running (`aifabrix up`){{#if hasDatabase}} and PostgreSQL is accessible{{/if}}
|
|
68
|
+
|
|
69
|
+
**Port in use:** Change `build.localPort` in `variables.yaml` (default: {{port}})
|
|
70
|
+
|