@aifabrix/builder 2.3.6 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,6 +18,9 @@ aifabrix up # Start Postgres + Redis
18
18
  aifabrix create myapp # Create your app
19
19
  aifabrix build myapp # Build Docker image
20
20
  aifabrix run myapp # Run locally
21
+ # Stop the app (optionally remove its data volume)
22
+ aifabrix down myapp
23
+ # aifabrix down myapp --volumes
21
24
  ```
22
25
 
23
26
  → [Full Guide](docs/QUICK-START.md) | [CLI Commands](docs/CLI-REFERENCE.md)
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Application Down Management
3
+ *
4
+ * Provides functionality to stop and remove a specific application's container,
5
+ * and optionally remove its associated Docker volume.
6
+ *
7
+ * @fileoverview Application stop/remove utilities for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const chalk = require('chalk');
15
+ const { exec } = require('child_process');
16
+ const logger = require('./utils/logger');
17
+ const config = require('./config');
18
+ const helpers = require('./app-run-helpers');
19
+ const { validateAppName } = require('./app-push');
20
+
21
+ /**
22
+ * Executes a shell command asynchronously.
23
+ * Wraps child_process.exec with a Promise interface.
24
+ *
25
+ * @async
26
+ * @param {string} command - Command to execute
27
+ * @param {Object} [options] - Exec options
28
+ * @returns {Promise<{stdout: string, stderr: string}>} Exec result
29
+ */
30
+ function execAsync(command, options = {}) {
31
+ return new Promise((resolve, reject) => {
32
+ exec(command, options, (error, stdout, stderr) => {
33
+ if (error) {
34
+ return reject(error);
35
+ }
36
+ resolve({ stdout, stderr });
37
+ });
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Computes the Docker volume name for an application, based on developer ID.
43
+ * Dev 0: aifabrix_{app}_data
44
+ * Dev > 0: aifabrix_dev{developerId}_{app}_data
45
+ *
46
+ * @param {string} appName - Application name
47
+ * @param {number|string} developerId - Developer ID
48
+ * @returns {string} Docker volume name
49
+ */
50
+ function getAppVolumeName(appName, developerId) {
51
+ const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
52
+ if (idNum === 0) {
53
+ return `aifabrix_${appName}_data`;
54
+ }
55
+ return `aifabrix_dev${developerId}_${appName}_data`;
56
+ }
57
+
58
+ /**
59
+ * Stops and removes a specific application's container.
60
+ * If --volumes is passed, attempts to remove the app's named Docker volume.
61
+ *
62
+ * This does NOT delete any files under builder/<app> or apps/<app>.
63
+ *
64
+ * @async
65
+ * @function downApp
66
+ * @param {string} appName - Application name
67
+ * @param {Object} options - Options
68
+ * @param {boolean} [options.volumes=false] - Remove Docker volume data
69
+ * @returns {Promise<void>} Resolves when operation completes
70
+ * @throws {Error} If validation fails or Docker operations error (other than missing volume)
71
+ *
72
+ * @example
73
+ * await downApp('myapp', { volumes: true });
74
+ */
75
+ async function downApp(appName, options = {}) {
76
+ try {
77
+ // Input validation
78
+ if (!appName || typeof appName !== 'string') {
79
+ throw new Error('Application name is required and must be a string');
80
+ }
81
+ validateAppName(appName);
82
+
83
+ const volumes = !!options.volumes;
84
+
85
+ // Load developer ID
86
+ const developerId = await config.getDeveloperId();
87
+
88
+ // Stop and remove container (idempotent handling in helper logs when not running)
89
+ await helpers.stopAndRemoveContainer(appName, developerId, false);
90
+
91
+ // Optionally remove named Docker volume (ignore if it doesn't exist)
92
+ if (volumes) {
93
+ const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
94
+ // Primary expected name
95
+ const primaryName = getAppVolumeName(appName, developerId);
96
+ // Legacy name used when devId \"0\" was treated as string in templates
97
+ const legacyDev0Name = idNum === 0 ? `aifabrix_dev0_${appName}_data` : null;
98
+ // Build list of candidates, unique and non-empty
99
+ const candidates = Array.from(
100
+ new Set([primaryName, legacyDev0Name].filter(Boolean))
101
+ );
102
+
103
+ for (const volumeName of candidates) {
104
+ logger.log(chalk.yellow(`Removing volume ${volumeName}...`));
105
+ try {
106
+ await execAsync(`docker volume rm -f ${volumeName}`);
107
+ logger.log(chalk.green(`✓ Volume ${volumeName} removed`));
108
+ } catch (volErr) {
109
+ // Swallow errors for missing volume; provide neutral message
110
+ logger.log(chalk.gray(`Volume ${volumeName} not found or already removed`));
111
+ }
112
+ }
113
+ }
114
+ } catch (error) {
115
+ // Provide meaningful error while avoiding sensitive info
116
+ throw new Error(`Failed to stop application: ${error.message}`);
117
+ }
118
+ }
119
+
120
+ module.exports = {
121
+ downApp
122
+ };
123
+
package/lib/app.js CHANGED
@@ -23,6 +23,7 @@ const { generateDockerfileForApp } = require('./app-dockerfile');
23
23
  const { loadTemplateVariables, updateTemplateVariables, mergeTemplateVariables } = require('./utils/template-helpers');
24
24
  const logger = require('./utils/logger');
25
25
  const auditLogger = require('./audit-logger');
26
+ const { downApp } = require('./app-down');
26
27
 
27
28
  /**
28
29
  * Displays success message after app creation
@@ -170,11 +171,11 @@ async function updateVariablesForAppFlag(appPath, appName) {
170
171
 
171
172
  if (variables.build) {
172
173
  variables.build.context = '../..';
173
- variables.build.envOutputPath = `apps/${appName}/.env`;
174
+ variables.build.envOutputPath = `../../apps/${appName}/.env`;
174
175
  } else {
175
176
  variables.build = {
176
177
  context: '../..',
177
- envOutputPath: `apps/${appName}/.env`
178
+ envOutputPath: `../../apps/${appName}/.env`
178
179
  };
179
180
  }
180
181
 
@@ -403,6 +404,7 @@ module.exports = {
403
404
  createApp,
404
405
  buildApp,
405
406
  runApp,
407
+ downApp,
406
408
  detectLanguage,
407
409
  generateDockerfile,
408
410
  generateDockerfileForApp,
package/lib/build.js CHANGED
@@ -25,6 +25,7 @@ const validator = require('./validator');
25
25
  const dockerfileUtils = require('./utils/dockerfile-utils');
26
26
  const dockerBuild = require('./utils/docker-build');
27
27
  const buildCopy = require('./utils/build-copy');
28
+ const { buildDevImageName } = require('./utils/image-name');
28
29
 
29
30
  const execAsync = promisify(exec);
30
31
 
@@ -306,18 +307,10 @@ async function executeBuild(imageName, dockerfilePath, contextPath, tag, options
306
307
 
307
308
  async function postBuildTasks(appName, buildConfig) {
308
309
  try {
309
- const envPath = await secrets.generateEnvFile(appName, buildConfig.secrets);
310
+ const envPath = await secrets.generateEnvFile(appName, buildConfig.secrets, 'docker');
310
311
  logger.log(chalk.green(`✓ Generated .env file: ${envPath}`));
311
- if (buildConfig.envOutputPath) {
312
- const builderPath = path.join(process.cwd(), 'builder', appName);
313
- const outputPath = path.resolve(builderPath, buildConfig.envOutputPath);
314
- const outputDir = path.dirname(outputPath);
315
- if (!fsSync.existsSync(outputDir)) {
316
- await fs.mkdir(outputDir, { recursive: true });
317
- }
318
- await fs.copyFile(envPath, outputPath);
319
- logger.log(chalk.green(`✓ Copied .env to: ${buildConfig.envOutputPath}`));
320
- }
312
+ // Note: processEnvVariables is already called by generateEnvFile to generate local .env
313
+ // at the envOutputPath, so we don't need to manually copy the docker .env file
321
314
  } catch (error) {
322
315
  logger.log(chalk.yellow(`⚠️ Warning: Could not generate .env file: ${error.message}`));
323
316
  }
@@ -355,6 +348,7 @@ async function buildApp(appName, options = {}) {
355
348
  logger.log(chalk.blue(`Copying files to developer-specific directory (${directoryName})...`));
356
349
  const devDir = await buildCopy.copyBuilderToDevDirectory(appName, developerId);
357
350
  logger.log(chalk.green(`✓ Files copied to: ${devDir}`));
351
+ const effectiveImageName = buildDevImageName(imageName, developerId);
358
352
 
359
353
  // 2a. Check if application source files exist, if not copy from templates
360
354
  const appsPath = path.join(process.cwd(), 'apps', appName);
@@ -456,7 +450,18 @@ async function buildApp(appName, options = {}) {
456
450
  logger.log(chalk.blue(`Using Dockerfile: ${dockerfilePath}`));
457
451
  logger.log(chalk.blue(`Using build context: ${contextPath}`));
458
452
 
459
- await executeBuild(imageName, dockerfilePath, contextPath, tag, options);
453
+ await executeBuild(effectiveImageName, dockerfilePath, contextPath, tag, options);
454
+ // Back-compat: also tag the built dev image as the base image name
455
+ try {
456
+ // Use runtime promisify so tests can capture this call reliably
457
+ const { promisify } = require('util');
458
+ const { exec } = require('child_process');
459
+ const run = promisify(exec);
460
+ await run(`docker tag ${effectiveImageName}:${tag} ${imageName}:${tag}`);
461
+ logger.log(chalk.green(`✓ Tagged image: ${imageName}:${tag}`));
462
+ } catch (err) {
463
+ logger.log(chalk.yellow(`⚠️ Warning: Could not create compatibility tag ${imageName}:${tag} - ${err.message}`));
464
+ }
460
465
 
461
466
  // 7. Post-build tasks
462
467
  await postBuildTasks(appName, buildConfig);
@@ -475,5 +480,6 @@ module.exports = {
475
480
  executeDockerBuild: dockerBuild.executeDockerBuild,
476
481
  detectLanguage,
477
482
  generateDockerfile,
478
- buildApp
483
+ buildApp,
484
+ postBuildTasks
479
485
  };
package/lib/cli.js CHANGED
@@ -70,15 +70,21 @@ function setupCommands(program) {
70
70
  }
71
71
  });
72
72
 
73
- program.command('down')
74
- .description('Stop and remove local infrastructure services')
73
+ program.command('down [app]')
74
+ .description('Stop and remove local infrastructure services or a specific application')
75
75
  .option('-v, --volumes', 'Remove volumes (deletes all data)')
76
- .action(async(options) => {
76
+ .action(async(appName, options) => {
77
77
  try {
78
- if (options.volumes) {
79
- await infra.stopInfraWithVolumes();
78
+ // If app name is provided, stop/remove that application (optionally volumes)
79
+ if (typeof appName === 'string' && appName.trim().length > 0) {
80
+ await app.downApp(appName, { volumes: !!options.volumes });
80
81
  } else {
81
- await infra.stopInfra();
82
+ // Otherwise, stop/remove infrastructure
83
+ if (options.volumes) {
84
+ await infra.stopInfraWithVolumes();
85
+ } else {
86
+ await infra.stopInfra();
87
+ }
82
88
  }
83
89
  } catch (error) {
84
90
  handleCommandError(error, 'down');
@@ -340,11 +346,12 @@ function setupCommands(program) {
340
346
  if (!digitsOnly) {
341
347
  throw new Error('Developer ID must be a non-negative digit string (0 = default infra, > 0 = developer-specific)');
342
348
  }
343
- // Convert to number for setDeveloperId and getDevPorts (they expect numbers)
344
- const devIdNum = parseInt(setIdValue, 10);
345
- await config.setDeveloperId(devIdNum);
349
+ // Preserve the original string value to maintain leading zeros (e.g., "01")
350
+ await config.setDeveloperId(setIdValue);
346
351
  process.env.AIFABRIX_DEVELOPERID = setIdValue;
347
352
  logger.log(chalk.green(`✓ Developer ID set to ${setIdValue}`));
353
+ // Convert to number only for getDevPorts (which requires a number)
354
+ const devIdNum = parseInt(setIdValue, 10);
348
355
  // Use the ID we just set instead of reading from file to avoid race conditions
349
356
  const ports = devConfig.getDevPorts(devIdNum);
350
357
  logger.log('\n🔧 Developer Configuration\n');
@@ -355,6 +362,24 @@ function setupCommands(program) {
355
362
  logger.log(` Redis: ${ports.redis}`);
356
363
  logger.log(` pgAdmin: ${ports.pgadmin}`);
357
364
  logger.log(` Redis Commander: ${ports.redisCommander}`);
365
+
366
+ // Display configuration variables if set
367
+ const aifabrixHome = await config.getAifabrixHomeOverride();
368
+ const aifabrixSecrets = await config.getAifabrixSecretsPath();
369
+ const aifabrixEnvConfig = await config.getAifabrixEnvConfigPath();
370
+
371
+ if (aifabrixHome || aifabrixSecrets || aifabrixEnvConfig) {
372
+ logger.log('\nConfiguration:');
373
+ if (aifabrixHome) {
374
+ logger.log(` aifabrix-home: ${aifabrixHome}`);
375
+ }
376
+ if (aifabrixSecrets) {
377
+ logger.log(` aifabrix-secrets: ${aifabrixSecrets}`);
378
+ }
379
+ if (aifabrixEnvConfig) {
380
+ logger.log(` aifabrix-env-config: ${aifabrixEnvConfig}`);
381
+ }
382
+ }
358
383
  logger.log('');
359
384
  return;
360
385
  }
@@ -371,6 +396,24 @@ function setupCommands(program) {
371
396
  logger.log(` Redis: ${ports.redis}`);
372
397
  logger.log(` pgAdmin: ${ports.pgadmin}`);
373
398
  logger.log(` Redis Commander: ${ports.redisCommander}`);
399
+
400
+ // Display configuration variables if set
401
+ const aifabrixHome = await config.getAifabrixHomeOverride();
402
+ const aifabrixSecrets = await config.getAifabrixSecretsPath();
403
+ const aifabrixEnvConfig = await config.getAifabrixEnvConfigPath();
404
+
405
+ if (aifabrixHome || aifabrixSecrets || aifabrixEnvConfig) {
406
+ logger.log('\nConfiguration:');
407
+ if (aifabrixHome) {
408
+ logger.log(` aifabrix-home: ${aifabrixHome}`);
409
+ }
410
+ if (aifabrixSecrets) {
411
+ logger.log(` aifabrix-secrets: ${aifabrixSecrets}`);
412
+ }
413
+ if (aifabrixEnvConfig) {
414
+ logger.log(` aifabrix-env-config: ${aifabrixEnvConfig}`);
415
+ }
416
+ }
374
417
  logger.log('');
375
418
  } catch (error) {
376
419
  handleCommandError(error, 'dev config');
package/lib/config.js CHANGED
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * AI Fabrix Builder Configuration Management
3
- *
4
3
  * Manages stored authentication configuration for CLI
5
4
  * Stores controller URL and auth tokens securely
6
5
  *
@@ -13,14 +12,15 @@ const fs = require('fs').promises;
13
12
  const path = require('path');
14
13
  const yaml = require('js-yaml');
15
14
  const os = require('os');
16
- const paths = require('./utils/paths');
15
+ // Avoid importing paths here to prevent circular dependency.
16
+ // Config location is always under OS home at ~/.aifabrix/config.yaml
17
17
 
18
18
  // Default (for tests and constants): always reflects OS home
19
19
  const CONFIG_DIR = path.join(os.homedir(), '.aifabrix');
20
20
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.yaml');
21
21
 
22
- // Runtime (respects AIFABRIX_HOME override)
23
- const RUNTIME_CONFIG_DIR = paths.getAifabrixHome();
22
+ // Runtime config directory (always under OS home)
23
+ const RUNTIME_CONFIG_DIR = path.join(os.homedir(), '.aifabrix');
24
24
  const RUNTIME_CONFIG_FILE = path.join(RUNTIME_CONFIG_DIR, 'config.yaml');
25
25
 
26
26
  // Cache for developer ID - loaded when getConfig() is first called
@@ -368,7 +368,8 @@ async function setSecretsEncryptionKey(key) {
368
368
  */
369
369
  async function getSecretsPath() {
370
370
  const config = await getConfig();
371
- return config['secrets-path'] || null;
371
+ // Backward compatibility: prefer new key, fallback to legacy
372
+ return config['aifabrix-secrets'] || config['secrets-path'] || null;
372
373
  }
373
374
 
374
375
  /**
@@ -382,7 +383,77 @@ async function setSecretsPath(secretsPath) {
382
383
  }
383
384
 
384
385
  const config = await getConfig();
385
- config['secrets-path'] = secretsPath;
386
+ // Store under new canonical key
387
+ config['aifabrix-secrets'] = secretsPath;
388
+ await saveConfig(config);
389
+ }
390
+
391
+ /**
392
+ * Get aifabrix-home override from configuration
393
+ * @returns {Promise<string|null>} Home override path or null if not set
394
+ */
395
+ async function getAifabrixHomeOverride() {
396
+ const config = await getConfig();
397
+ return config['aifabrix-home'] || null;
398
+ }
399
+
400
+ /**
401
+ * Set aifabrix-home override in configuration
402
+ * @param {string} homePath - Base directory path for AI Fabrix files
403
+ * @returns {Promise<void>}
404
+ */
405
+ async function setAifabrixHomeOverride(homePath) {
406
+ if (!homePath || typeof homePath !== 'string') {
407
+ throw new Error('Home path is required and must be a string');
408
+ }
409
+ const config = await getConfig();
410
+ config['aifabrix-home'] = homePath;
411
+ await saveConfig(config);
412
+ }
413
+
414
+ /**
415
+ * Get aifabrix-secrets path from configuration (canonical)
416
+ * @returns {Promise<string|null>} Secrets path or null if not set
417
+ */
418
+ async function getAifabrixSecretsPath() {
419
+ const config = await getConfig();
420
+ return config['aifabrix-secrets'] || null;
421
+ }
422
+
423
+ /**
424
+ * Set aifabrix-secrets path in configuration (canonical)
425
+ * @param {string} secretsPath - Path to default secrets file
426
+ * @returns {Promise<void>}
427
+ */
428
+ async function setAifabrixSecretsPath(secretsPath) {
429
+ if (!secretsPath || typeof secretsPath !== 'string') {
430
+ throw new Error('Secrets path is required and must be a string');
431
+ }
432
+ const config = await getConfig();
433
+ config['aifabrix-secrets'] = secretsPath;
434
+ await saveConfig(config);
435
+ }
436
+
437
+ /**
438
+ * Get aifabrix-env-config path from configuration
439
+ * @returns {Promise<string|null>} Env config path or null if not set
440
+ */
441
+ async function getAifabrixEnvConfigPath() {
442
+ const config = await getConfig();
443
+ return config['aifabrix-env-config'] || null;
444
+ }
445
+
446
+ /**
447
+ * Set aifabrix-env-config path in configuration
448
+ * @param {string} envConfigPath - Path to user env-config file
449
+ * @returns {Promise<void>}
450
+ */
451
+ async function setAifabrixEnvConfigPath(envConfigPath) {
452
+ if (!envConfigPath || typeof envConfigPath !== 'string') {
453
+ throw new Error('Env config path is required and must be a string');
454
+ }
455
+ const config = await getConfig();
456
+ config['aifabrix-env-config'] = envConfigPath;
386
457
  await saveConfig(config);
387
458
  }
388
459
 
@@ -405,6 +476,12 @@ const exportsObj = {
405
476
  setSecretsEncryptionKey,
406
477
  getSecretsPath,
407
478
  setSecretsPath,
479
+ getAifabrixHomeOverride,
480
+ setAifabrixHomeOverride,
481
+ getAifabrixSecretsPath,
482
+ setAifabrixSecretsPath,
483
+ getAifabrixEnvConfigPath,
484
+ setAifabrixEnvConfigPath,
408
485
  CONFIG_DIR,
409
486
  CONFIG_FILE
410
487
  };
@@ -419,6 +496,4 @@ Object.defineProperty(exportsObj, 'developerId', {
419
496
  enumerable: true,
420
497
  configurable: true
421
498
  });
422
-
423
499
  module.exports = exportsObj;
424
-
package/lib/env-reader.js CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  const fs = require('fs').promises;
9
9
  const path = require('path');
10
+ const { getCanonicalSecretName } = require('./secrets');
10
11
 
11
12
  /**
12
13
  * Read existing .env file from application folder
@@ -147,8 +148,8 @@ function generateSecretsFromEnv(envVars) {
147
148
 
148
149
  Object.entries(envVars).forEach(([key, value]) => {
149
150
  if (detectSensitiveValue(key, value)) {
150
- // Convert key to secret name format
151
- const secretName = key.toLowerCase().replace(/[^a-z0-9]/g, '-');
151
+ // Use centralized resolver for canonical secret names
152
+ const secretName = getCanonicalSecretName(key);
152
153
  secrets[secretName] = value;
153
154
  }
154
155
  });
package/lib/generator.js CHANGED
@@ -185,9 +185,6 @@ function validateBuildFields(build) {
185
185
  if (build.envOutputPath) {
186
186
  buildConfig.envOutputPath = build.envOutputPath;
187
187
  }
188
- if (build.secrets && typeof build.secrets === 'string') {
189
- buildConfig.secrets = build.secrets;
190
- }
191
188
  if (build.dockerfile && build.dockerfile.trim()) {
192
189
  buildConfig.dockerfile = build.dockerfile;
193
190
  }
@@ -210,12 +207,6 @@ function validateDeploymentFields(deployment) {
210
207
  if (deployment.controllerUrl && deployment.controllerUrl.trim() && deployment.controllerUrl.startsWith('https://')) {
211
208
  deploymentConfig.controllerUrl = deployment.controllerUrl;
212
209
  }
213
- if (deployment.clientId && deployment.clientId.trim()) {
214
- deploymentConfig.clientId = deployment.clientId;
215
- }
216
- if (deployment.clientSecret && deployment.clientSecret.trim()) {
217
- deploymentConfig.clientSecret = deployment.clientSecret;
218
- }
219
210
 
220
211
  return Object.keys(deploymentConfig).length > 0 ? deploymentConfig : null;
221
212
  }
package/lib/infra.js CHANGED
@@ -23,7 +23,22 @@ const dockerUtils = require('./utils/docker');
23
23
  const paths = require('./utils/paths');
24
24
 
25
25
  // Register Handlebars helper for equality check
26
- handlebars.registerHelper('eq', (a, b) => a === b);
26
+ // Handles both strict equality and numeric string comparisons
27
+ // Treats null/undefined as equivalent to "0" (default infrastructure)
28
+ handlebars.registerHelper('eq', (a, b) => {
29
+ // Handle null/undefined - treat as "0" for default infrastructure
30
+ if (a === null || a === undefined) a = '0';
31
+ if (b === null || b === undefined) b = '0';
32
+
33
+ // If both are numeric strings or one is number and other is numeric string, compare as numbers
34
+ const aNum = typeof a === 'string' && /^\d+$/.test(a) ? parseInt(a, 10) : a;
35
+ const bNum = typeof b === 'string' && /^\d+$/.test(b) ? parseInt(b, 10) : b;
36
+ // Use numeric comparison if both are numbers, otherwise strict equality
37
+ if (typeof aNum === 'number' && typeof bNum === 'number') {
38
+ return aNum === bNum;
39
+ }
40
+ return a === b;
41
+ });
27
42
  const execAsync = promisify(exec);
28
43
 
29
44
  /**
@@ -118,12 +133,25 @@ async function startInfra(developerId = null) {
118
133
  }
119
134
 
120
135
  // Generate compose file from template
136
+ // Register helper right before compiling to ensure it's available
137
+ handlebars.registerHelper('eq', (a, b) => {
138
+ if (a === null || a === undefined) a = '0';
139
+ if (b === null || b === undefined) b = '0';
140
+ const aNum = typeof a === 'string' && /^\d+$/.test(a) ? parseInt(a, 10) : a;
141
+ const bNum = typeof b === 'string' && /^\d+$/.test(b) ? parseInt(b, 10) : b;
142
+ if (typeof aNum === 'number' && typeof bNum === 'number') {
143
+ return aNum === bNum;
144
+ }
145
+ return a === b;
146
+ });
121
147
  const templateContent = fs.readFileSync(templatePath, 'utf8');
122
148
  const template = handlebars.compile(templateContent);
123
149
  // Dev 0: infra-aifabrix-network, Dev > 0: infra-dev{id}-aifabrix-network
124
150
  const networkName = idNum === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
151
+ // Pass string devId to preserve leading zeros (e.g., "01") in container names
152
+ // The eq helper will handle numeric comparisons correctly
125
153
  const composeContent = template({
126
- devId: idNum,
154
+ devId: devId,
127
155
  postgresPort: ports.postgres,
128
156
  redisPort: ports.redis,
129
157
  pgadminPort: ports.pgadmin,
@@ -466,7 +494,6 @@ async function getAppStatus() {
466
494
 
467
495
  return apps;
468
496
  }
469
-
470
497
  module.exports = {
471
498
  startInfra, stopInfra, stopInfraWithVolumes, checkInfraHealth,
472
499
  getInfraStatus, getAppStatus, restartService, ensureAdminSecrets
@@ -633,11 +633,6 @@
633
633
  "description": "Path where .env file is copied for local development (relative to builder/)",
634
634
  "pattern": "^[^/].*"
635
635
  },
636
- "secrets": {
637
- "type": "string",
638
- "description": "Path to secrets file (defaults to ~/.aifabrix/secrets.yaml if empty)",
639
- "pattern": "^[^/].*"
640
- },
641
636
  "localPort": {
642
637
  "type": "integer",
643
638
  "description": "Port for local development (different from Docker port)",
@@ -676,16 +671,6 @@
676
671
  "type": "string",
677
672
  "description": "Controller API URL for deployment",
678
673
  "pattern": "^https://.*$"
679
- },
680
- "clientId": {
681
- "type": "string",
682
- "description": "Pipeline ClientId for automated deployment",
683
- "pattern": "^[a-z0-9-]+$"
684
- },
685
- "clientSecret": {
686
- "type": "string",
687
- "description": "Pipeline ClientSecret (use kv:// reference)",
688
- "pattern": "^(kv://.*|.+)$"
689
674
  }
690
675
  },
691
676
  "additionalProperties": false
@@ -4,20 +4,20 @@
4
4
  environments:
5
5
  docker:
6
6
  DB_HOST: postgres
7
+ DB_PORT: 5432
7
8
  REDIS_HOST: redis
9
+ REDIS_PORT: 6379
8
10
  MISO_HOST: miso-controller
11
+ MISO_PORT: 3000
9
12
  KEYCLOAK_HOST: keycloak
10
- MORI_HOST: mori-controller
11
- OPENWEBUI_HOST: openwebui
12
- FLOWISE_HOST: flowise
13
- DATAPLANE_HOST: dataplane
13
+ KEYCLOAK_PORT: 8082
14
14
 
15
15
  local:
16
16
  DB_HOST: localhost
17
+ DB_PORT: 5432
17
18
  REDIS_HOST: localhost
19
+ REDIS_PORT: 6379
18
20
  MISO_HOST: localhost
21
+ MISO_PORT: 3010
19
22
  KEYCLOAK_HOST: localhost
20
- MORI_HOST: localhost
21
- OPENWEBUI_HOST: localhost
22
- FLOWISE_HOST: localhost
23
- DATAPLANE_HOST: localhost
23
+ KEYCLOAK_PORT: 8082