@aifabrix/builder 2.1.0 → 2.1.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/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(`docker images --format "{{.Repository}}:{{.Tag}}" --filter "reference=${fullImageName}"`);
44
+ const { stdout } = await execAsync(cmd);
40
45
  const lines = stdout.trim().split('\n').filter(line => line.trim() !== '');
41
- return lines.some(line => line.trim() === fullImageName);
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 { stdout } = await execAsync(`docker ps --filter "name=aifabrix-${appName}" --format "{{.Names}}"`);
55
- return stdout.trim() === `aifabrix-${appName}`;
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
- await execAsync(`docker stop aifabrix-${appName}`);
69
- await execAsync(`docker rm aifabrix-${appName}`);
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
- await execAsync(`docker-compose -f "${composePath}" up -d`, { env });
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);
package/lib/generator.js CHANGED
@@ -124,10 +124,19 @@ function buildBaseDeployment(appName, variables, filteredConfiguration) {
124
124
  function buildAuthenticationConfig(variables, rbac) {
125
125
  if (variables.authentication) {
126
126
  const auth = {
127
- type: variables.authentication.type || 'azure',
128
- enableSSO: variables.authentication.enableSSO !== undefined ? variables.authentication.enableSSO : true,
129
- requiredRoles: variables.authentication.requiredRoles || []
127
+ enableSSO: variables.authentication.enableSSO !== undefined ? variables.authentication.enableSSO : true
130
128
  };
129
+
130
+ // When enableSSO is false, default type to 'none' and requiredRoles to []
131
+ // When enableSSO is true, require type and requiredRoles
132
+ if (auth.enableSSO === false) {
133
+ auth.type = variables.authentication.type || 'none';
134
+ auth.requiredRoles = variables.authentication.requiredRoles || [];
135
+ } else {
136
+ auth.type = variables.authentication.type || 'azure';
137
+ auth.requiredRoles = variables.authentication.requiredRoles || [];
138
+ }
139
+
131
140
  if (variables.authentication.endpoints) {
132
141
  auth.endpoints = variables.authentication.endpoints;
133
142
  }
@@ -474,5 +483,6 @@ module.exports = {
474
483
  buildImageReference,
475
484
  buildHealthCheck,
476
485
  buildRequirements,
477
- buildAuthentication
486
+ buildAuthentication,
487
+ buildAuthenticationConfig
478
488
  };
@@ -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",
@@ -384,8 +428,8 @@
384
428
  },
385
429
  "authentication": {
386
430
  "type": "object",
387
- "description": "Authentication configuration",
388
- "required": ["type", "enableSSO", "requiredRoles"],
431
+ "description": "Authentication configuration. When enableSSO is false, only enableSSO is required. When enableSSO is true, type and requiredRoles are also required.",
432
+ "required": ["enableSSO"],
389
433
  "properties": {
390
434
  "type": {
391
435
  "type": "string",
@@ -422,7 +466,21 @@
422
466
  "additionalProperties": false
423
467
  }
424
468
  },
425
- "additionalProperties": false
469
+ "additionalProperties": false,
470
+ "allOf": [
471
+ {
472
+ "if": {
473
+ "properties": {
474
+ "enableSSO": {
475
+ "const": true
476
+ }
477
+ }
478
+ },
479
+ "then": {
480
+ "required": ["type", "enableSSO", "requiredRoles"]
481
+ }
482
+ }
483
+ ]
426
484
  },
427
485
  "roles": {
428
486
  "type": "array",
@@ -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 { stdout: portMapping } = await execAsync(`docker inspect --format='{{range $p, $conf := .NetworkSettings.Ports}}{{if $conf}}{{range $conf}}{{.HostPort}}{{end}}{{end}}{{end}}' aifabrix-${appName}`);
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 { stdout: psOutput } = await execAsync(`docker ps --filter "name=aifabrix-${appName}" --format "{{.Ports}}"`);
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 req = http.get(healthCheckUrl, { timeout: 5000 }, (res) => {
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
- resolve(parseHealthResponse(data, res.statusCode));
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
- req.on('error', () => resolve(false));
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
246
  // Use provided port if given, otherwise detect from Docker
175
247
  // Port provided should be the host port (CLI --port or config.port, NOT localPort)
176
- const healthCheckPort = port !== null && port !== undefined ? port : await getContainerPort(appName);
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
- const healthCheckPassed = await checkHealthEndpoint(healthCheckUrl);
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
 
@@ -55,6 +55,23 @@ function transformFlatStructure(variables, appName) {
55
55
  type: sanitizeAuthType(result.authentication.type)
56
56
  };
57
57
  }
58
+ // Handle partial authentication objects (when only enableSSO is provided)
59
+ if (result.authentication && result.authentication.enableSSO !== undefined) {
60
+ const auth = {
61
+ ...result.authentication,
62
+ enableSSO: result.authentication.enableSSO
63
+ };
64
+ // When enableSSO is false, default type to 'none' and requiredRoles to []
65
+ // When enableSSO is true, default type to 'azure' if not provided
66
+ if (auth.enableSSO === false) {
67
+ auth.type = sanitizeAuthType(result.authentication.type || 'none');
68
+ auth.requiredRoles = result.authentication.requiredRoles || [];
69
+ } else {
70
+ auth.type = sanitizeAuthType(result.authentication.type || 'azure');
71
+ auth.requiredRoles = result.authentication.requiredRoles || [];
72
+ }
73
+ result.authentication = auth;
74
+ }
58
75
 
59
76
  return result;
60
77
  }
@@ -182,10 +199,24 @@ function transformOptionalFields(variables, transformed) {
182
199
  }
183
200
 
184
201
  if (variables.authentication) {
185
- transformed.authentication = {
202
+ // Ensure authentication object has enableSSO at minimum
203
+ // Default type and requiredRoles based on enableSSO value
204
+ const auth = {
186
205
  ...variables.authentication,
187
- type: sanitizeAuthType(variables.authentication.type)
206
+ enableSSO: variables.authentication.enableSSO !== undefined ? variables.authentication.enableSSO : true
188
207
  };
208
+
209
+ // When enableSSO is false, default type to 'none' and requiredRoles to []
210
+ // When enableSSO is true, default type to 'azure' if not provided
211
+ if (auth.enableSSO === false) {
212
+ auth.type = sanitizeAuthType(variables.authentication.type || 'none');
213
+ auth.requiredRoles = variables.authentication.requiredRoles || [];
214
+ } else {
215
+ auth.type = sanitizeAuthType(variables.authentication.type || 'azure');
216
+ auth.requiredRoles = variables.authentication.requiredRoles || [];
217
+ }
218
+
219
+ transformed.authentication = auth;
189
220
  }
190
221
 
191
222
  const repository = validateRepositoryConfig(variables.repository);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -34,11 +34,7 @@ healthCheck:
34
34
 
35
35
  # Authentication
36
36
  authentication:
37
- type: keycloak
38
- enableSSO: true
39
- requiredRoles: ["aifabrix-user"]
40
- endpoints:
41
- local: "http://localhost:8082/auth/callback"
37
+ enableSSO: false
42
38
 
43
39
  # Build Configuration
44
40
  build:
@@ -1,37 +0,0 @@
1
- roles:
2
- - name: "AI Fabrix Admin"
3
- value: "aifabrix-admin"
4
- description: "Full access to all application features and configurations"
5
-
6
- - name: "AI Fabrix User"
7
- value: "aifabrix-user"
8
- description: "Basic user access to the application"
9
-
10
- - name: "AI Fabrix Developer"
11
- value: "aifabrix-developer"
12
- description: "Developer access for testing and debugging"
13
-
14
- permissions:
15
- - name: "myapp:read"
16
- roles:
17
- - "aifabrix-user"
18
- - "aifabrix-admin"
19
- - "aifabrix-developer"
20
- description: "Read access to application data"
21
-
22
- - name: "myapp:write"
23
- roles:
24
- - "aifabrix-admin"
25
- - "aifabrix-developer"
26
- description: "Create and edit application data"
27
-
28
- - name: "myapp:delete"
29
- roles:
30
- - "aifabrix-admin"
31
- description: "Delete application data"
32
-
33
- - name: "myapp:admin"
34
- roles:
35
- - "aifabrix-admin"
36
- description: "Administrative access to application configuration"
37
-