@aifabrix/builder 2.3.0 → 2.3.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
@@ -9,390 +9,13 @@
9
9
  * @version 2.0.0
10
10
  */
11
11
 
12
- const fs = require('fs').promises;
13
- const fsSync = require('fs');
14
- const path = require('path');
15
12
  const chalk = require('chalk');
16
- const yaml = require('js-yaml');
17
- const { exec } = require('child_process');
18
- const { promisify } = require('util');
19
- const validator = require('./validator');
20
- const infra = require('./infra');
21
- const secrets = require('./secrets');
22
13
  const config = require('./config');
23
- const buildCopy = require('./utils/build-copy');
24
14
  const logger = require('./utils/logger');
25
- const { waitForHealthCheck, checkPortAvailable } = require('./utils/health-check');
15
+ const { checkPortAvailable, waitForHealthCheck } = require('./utils/health-check');
26
16
  const composeGenerator = require('./utils/compose-generator');
27
-
28
- const execAsync = promisify(exec);
29
-
30
- /**
31
- * Checks if Docker image exists for the application
32
- * @param {string} imageName - Image name (can include repository prefix)
33
- * @param {string} tag - Image tag (default: latest)
34
- * @param {boolean} [debug=false] - Enable debug logging
35
- * @returns {Promise<boolean>} True if image exists
36
- */
37
- async function checkImageExists(imageName, tag = 'latest', debug = false) {
38
- try {
39
- const fullImageName = `${imageName}:${tag}`;
40
- const cmd = `docker images --format "{{.Repository}}:{{.Tag}}" --filter "reference=${fullImageName}"`;
41
- if (debug) {
42
- logger.log(chalk.gray(`[DEBUG] Executing: ${cmd}`));
43
- }
44
- // Use Docker's native filtering for cross-platform compatibility (Windows-safe)
45
- const { stdout } = await execAsync(cmd);
46
- const lines = stdout.trim().split('\n').filter(line => line.trim() !== '');
47
- const exists = lines.some(line => line.trim() === fullImageName);
48
- if (debug) {
49
- logger.log(chalk.gray(`[DEBUG] Image ${fullImageName} exists: ${exists}`));
50
- }
51
- return exists;
52
- } catch (error) {
53
- if (debug) {
54
- logger.log(chalk.gray(`[DEBUG] Image check failed: ${error.message}`));
55
- }
56
- return false;
57
- }
58
- }
59
-
60
- /**
61
- * Checks if container is already running
62
- * @param {string} appName - Application name
63
- * @param {number} developerId - Developer ID (0 = default infra, > 0 = developer-specific)
64
- * @param {boolean} [debug=false] - Enable debug logging
65
- * @returns {Promise<boolean>} True if container is running
66
- */
67
- async function checkContainerRunning(appName, developerId, debug = false) {
68
- try {
69
- // Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
70
- const containerName = developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${developerId}-${appName}`;
71
- const cmd = `docker ps --filter "name=${containerName}" --format "{{.Names}}"`;
72
- if (debug) {
73
- logger.log(chalk.gray(`[DEBUG] Executing: ${cmd}`));
74
- }
75
- const { stdout } = await execAsync(cmd);
76
- const isRunning = stdout.trim() === containerName;
77
- if (debug) {
78
- logger.log(chalk.gray(`[DEBUG] Container ${containerName} running: ${isRunning}`));
79
- if (isRunning) {
80
- // Get container status details
81
- const statusCmd = `docker ps --filter "name=${containerName}" --format "{{.Status}}"`;
82
- const { stdout: status } = await execAsync(statusCmd);
83
- const portsCmd = `docker ps --filter "name=${containerName}" --format "{{.Ports}}"`;
84
- const { stdout: ports } = await execAsync(portsCmd);
85
- logger.log(chalk.gray(`[DEBUG] Container status: ${status.trim()}`));
86
- logger.log(chalk.gray(`[DEBUG] Container ports: ${ports.trim()}`));
87
- }
88
- }
89
- return isRunning;
90
- } catch (error) {
91
- if (debug) {
92
- logger.log(chalk.gray(`[DEBUG] Container check failed: ${error.message}`));
93
- }
94
- return false;
95
- }
96
- }
97
-
98
- /**
99
- * Stops and removes existing container
100
- * @param {string} appName - Application name
101
- * @param {number} developerId - Developer ID (0 = default infra, > 0 = developer-specific)
102
- * @param {boolean} [debug=false] - Enable debug logging
103
- */
104
- async function stopAndRemoveContainer(appName, developerId, debug = false) {
105
- try {
106
- // Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
107
- const containerName = developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${developerId}-${appName}`;
108
- logger.log(chalk.yellow(`Stopping existing container ${containerName}...`));
109
- const stopCmd = `docker stop ${containerName}`;
110
- if (debug) {
111
- logger.log(chalk.gray(`[DEBUG] Executing: ${stopCmd}`));
112
- }
113
- await execAsync(stopCmd);
114
- const rmCmd = `docker rm ${containerName}`;
115
- if (debug) {
116
- logger.log(chalk.gray(`[DEBUG] Executing: ${rmCmd}`));
117
- }
118
- await execAsync(rmCmd);
119
- logger.log(chalk.green(`✓ Container ${containerName} stopped and removed`));
120
- } catch (error) {
121
- if (debug) {
122
- logger.log(chalk.gray(`[DEBUG] Stop/remove container error: ${error.message}`));
123
- }
124
- // Container might not exist, which is fine
125
- // Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
126
- const containerName = developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${developerId}-${appName}`;
127
- logger.log(chalk.gray(`Container ${containerName} was not running`));
128
- }
129
- }
130
-
131
- /**
132
- * Validates app name and loads configuration
133
- * @async
134
- * @param {string} appName - Application name
135
- * @returns {Promise<Object>} Application configuration
136
- * @throws {Error} If validation fails
137
- */
138
- async function validateAppConfiguration(appName) {
139
- // Validate app name
140
- if (!appName || typeof appName !== 'string') {
141
- throw new Error('Application name is required');
142
- }
143
-
144
- // Check if we're running from inside the builder directory
145
- const currentDir = process.cwd();
146
- const normalizedPath = currentDir.replace(/\\/g, '/');
147
- const expectedBuilderPath = `builder/${appName}`;
148
-
149
- // If inside builder/{appName}, suggest moving to project root
150
- if (normalizedPath.endsWith(expectedBuilderPath)) {
151
- const projectRoot = path.resolve(currentDir, '../..');
152
- throw new Error(
153
- 'You\'re running from inside the builder directory.\n' +
154
- `Current directory: ${currentDir}\n` +
155
- 'Please change to the project root and try again:\n' +
156
- ` cd ${projectRoot}\n` +
157
- ` aifabrix run ${appName}`
158
- );
159
- }
160
-
161
- // Load and validate app configuration
162
- const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
163
- if (!fsSync.existsSync(configPath)) {
164
- const expectedDir = path.join(currentDir, 'builder', appName);
165
- throw new Error(
166
- `Application configuration not found: ${configPath}\n` +
167
- `Current directory: ${currentDir}\n` +
168
- `Expected location: ${expectedDir}\n` +
169
- 'Make sure you\'re running from the project root (where \'builder\' directory exists)\n' +
170
- `Run 'aifabrix create ${appName}' first if configuration doesn't exist`
171
- );
172
- }
173
-
174
- const configContent = fsSync.readFileSync(configPath, 'utf8');
175
- const config = yaml.load(configContent);
176
-
177
- // Validate configuration
178
- const validation = await validator.validateApplication(appName);
179
- if (!validation.valid) {
180
- const allErrors = [];
181
-
182
- if (validation.variables && validation.variables.errors && validation.variables.errors.length > 0) {
183
- allErrors.push('variables.yaml:');
184
- allErrors.push(...validation.variables.errors.map(err => ` ${err}`));
185
- }
186
-
187
- if (validation.rbac && validation.rbac.errors && validation.rbac.errors.length > 0) {
188
- allErrors.push('rbac.yaml:');
189
- allErrors.push(...validation.rbac.errors.map(err => ` ${err}`));
190
- }
191
-
192
- if (validation.env && validation.env.errors && validation.env.errors.length > 0) {
193
- allErrors.push('env.template:');
194
- allErrors.push(...validation.env.errors.map(err => ` ${err}`));
195
- }
196
-
197
- if (allErrors.length === 0) {
198
- throw new Error('Configuration validation failed');
199
- }
200
-
201
- throw new Error(`Configuration validation failed:\n${allErrors.join('\n')}`);
202
- }
203
-
204
- return config;
205
- }
206
-
207
- /**
208
- * Checks prerequisites: Docker image and infrastructure
209
- * @async
210
- * @param {string} appName - Application name
211
- * @param {Object} config - Application configuration
212
- * @param {boolean} [debug=false] - Enable debug logging
213
- * @throws {Error} If prerequisites are not met
214
- */
215
- async function checkPrerequisites(appName, config, debug = false) {
216
- // Extract image name from configuration (same logic as build process)
217
- const imageName = composeGenerator.getImageName(config, appName);
218
- const imageTag = config.image?.tag || 'latest';
219
- const fullImageName = `${imageName}:${imageTag}`;
220
-
221
- if (debug) {
222
- logger.log(chalk.gray(`[DEBUG] Image name: ${imageName}, tag: ${imageTag}`));
223
- }
224
-
225
- // Check if Docker image exists
226
- logger.log(chalk.blue(`Checking if image ${fullImageName} exists...`));
227
- const imageExists = await checkImageExists(imageName, imageTag, debug);
228
- if (!imageExists) {
229
- throw new Error(`Docker image ${fullImageName} not found\nRun 'aifabrix build ${appName}' first`);
230
- }
231
- logger.log(chalk.green(`✓ Image ${fullImageName} found`));
232
-
233
- // Check infrastructure health
234
- logger.log(chalk.blue('Checking infrastructure health...'));
235
- const infraHealth = await infra.checkInfraHealth();
236
- if (debug) {
237
- logger.log(chalk.gray(`[DEBUG] Infrastructure health: ${JSON.stringify(infraHealth, null, 2)}`));
238
- }
239
- const unhealthyServices = Object.entries(infraHealth)
240
- .filter(([_, status]) => status !== 'healthy')
241
- .map(([service, _]) => service);
242
-
243
- if (unhealthyServices.length > 0) {
244
- throw new Error(`Infrastructure services not healthy: ${unhealthyServices.join(', ')}\nRun 'aifabrix up' first`);
245
- }
246
- logger.log(chalk.green('✓ Infrastructure is running'));
247
- }
248
- /**
249
- * Prepares environment: ensures .env file and generates Docker Compose
250
- * @async
251
- * @param {string} appName - Application name
252
- * @param {Object} appConfig - Application configuration
253
- * @param {Object} options - Run options
254
- * @returns {Promise<string>} Path to generated compose file
255
- */
256
- async function prepareEnvironment(appName, appConfig, options) {
257
- // Get developer ID and dev-specific directory
258
- const developerId = await config.getDeveloperId();
259
- const devDir = buildCopy.getDevDirectory(appName, developerId);
260
-
261
- // Ensure dev directory exists (should exist from build, but check anyway)
262
- if (!fsSync.existsSync(devDir)) {
263
- await buildCopy.copyBuilderToDevDirectory(appName, developerId);
264
- }
265
-
266
- // Ensure .env file exists with 'docker' environment context (for running in Docker)
267
- // Generate in builder directory first, then copy to dev directory
268
- const builderEnvPath = path.join(process.cwd(), 'builder', appName, '.env');
269
- if (!fsSync.existsSync(builderEnvPath)) {
270
- logger.log(chalk.yellow('Generating .env file from template...'));
271
- await secrets.generateEnvFile(appName, null, 'docker');
272
- } else {
273
- // Re-generate with 'docker' context to ensure correct hostnames for Docker
274
- logger.log(chalk.blue('Updating .env file for Docker environment...'));
275
- await secrets.generateEnvFile(appName, null, 'docker');
276
- }
277
-
278
- // Copy .env to dev directory
279
- const devEnvPath = path.join(devDir, '.env');
280
- if (fsSync.existsSync(builderEnvPath)) {
281
- await fs.copyFile(builderEnvPath, devEnvPath);
282
- }
283
-
284
- // Also ensure .env file in apps/ directory is updated (for Docker build context)
285
- const variablesPath = path.join(devDir, 'variables.yaml');
286
- if (fsSync.existsSync(variablesPath)) {
287
- const variablesContent = fsSync.readFileSync(variablesPath, 'utf8');
288
- const variables = yaml.load(variablesContent);
289
-
290
- if (variables?.build?.envOutputPath && variables.build.envOutputPath !== null) {
291
- // The generateEnvFile already copies to apps/, but ensure it's using docker context
292
- logger.log(chalk.blue('Ensuring .env file in apps/ directory is updated for Docker...'));
293
- await secrets.generateEnvFile(appName, null, 'docker');
294
- // Copy .env to dev directory again after regeneration
295
- if (fsSync.existsSync(builderEnvPath)) {
296
- await fs.copyFile(builderEnvPath, devEnvPath);
297
- }
298
- }
299
- }
300
-
301
- // Generate Docker Compose configuration
302
- logger.log(chalk.blue('Generating Docker Compose configuration...'));
303
- const composeOptions = { ...options };
304
- if (!composeOptions.port) {
305
- const basePort = appConfig.port || 3000;
306
- composeOptions.port = developerId === 0 ? basePort : basePort + (developerId * 100);
307
- }
308
- const composeContent = await composeGenerator.generateDockerCompose(appName, appConfig, composeOptions);
309
-
310
- // Write compose file to dev-specific directory (devDir already defined above)
311
- // Ensure dev directory exists (should exist from build, but check anyway)
312
- if (!fsSync.existsSync(devDir)) {
313
- await buildCopy.copyBuilderToDevDirectory(appName, developerId);
314
- }
315
- const tempComposePath = path.join(devDir, 'docker-compose.yaml');
316
- await fs.writeFile(tempComposePath, composeContent);
317
-
318
- return tempComposePath;
319
- }
320
-
321
- /**
322
- * Starts the container and waits for health check
323
- * @async
324
- * @param {string} appName - Application name
325
- * @param {string} composePath - Path to Docker Compose file
326
- * @param {number} port - Application port
327
- * @param {Object} config - Application configuration
328
- * @param {boolean} [debug=false] - Enable debug logging
329
- * @throws {Error} If container fails to start or become healthy
330
- */
331
- async function startContainer(appName, composePath, port, config = null, debug = false) {
332
- logger.log(chalk.blue(`Starting ${appName}...`));
333
-
334
- // Ensure ADMIN_SECRETS_PATH is set for db-init service
335
- const adminSecretsPath = await infra.ensureAdminSecrets();
336
- if (debug) {
337
- logger.log(chalk.gray(`[DEBUG] Admin secrets path: ${adminSecretsPath}`));
338
- }
339
-
340
- // Load POSTGRES_PASSWORD from admin-secrets.env
341
- const adminSecretsContent = fsSync.readFileSync(adminSecretsPath, 'utf8');
342
- const postgresPasswordMatch = adminSecretsContent.match(/^POSTGRES_PASSWORD=(.+)$/m);
343
- const postgresPassword = postgresPasswordMatch ? postgresPasswordMatch[1] : '';
344
-
345
- // Set environment variables for docker-compose
346
- const env = {
347
- ...process.env,
348
- ADMIN_SECRETS_PATH: adminSecretsPath,
349
- POSTGRES_PASSWORD: postgresPassword
350
- };
351
-
352
- if (debug) {
353
- logger.log(chalk.gray(`[DEBUG] Environment variables: ADMIN_SECRETS_PATH=${adminSecretsPath}, POSTGRES_PASSWORD=${postgresPassword ? '***' : '(not set)'}`));
354
- }
355
-
356
- const composeCmd = `docker-compose -f "${composePath}" up -d`;
357
- if (debug) {
358
- logger.log(chalk.gray(`[DEBUG] Executing: ${composeCmd}`));
359
- logger.log(chalk.gray(`[DEBUG] Compose file: ${composePath}`));
360
- }
361
- await execAsync(composeCmd, { env });
362
- // Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
363
- const containerName = config.developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${config.developerId}-${appName}`;
364
- logger.log(chalk.green(`✓ Container ${containerName} started`));
365
- if (debug) {
366
- // Get container status after start
367
- const statusCmd = `docker ps --filter "name=${containerName}" --format "{{.Status}}"`;
368
- const { stdout: status } = await execAsync(statusCmd);
369
- const portsCmd = `docker ps --filter "name=${containerName}" --format "{{.Ports}}"`;
370
- const { stdout: ports } = await execAsync(portsCmd);
371
- logger.log(chalk.gray(`[DEBUG] Container status: ${status.trim()}`));
372
- logger.log(chalk.gray(`[DEBUG] Container ports: ${ports.trim()}`));
373
- }
374
-
375
- // Wait for health check using host port (CLI --port or dev-specific port, NOT localPort)
376
- const healthCheckPath = config?.healthCheck?.path || '/health';
377
- logger.log(chalk.blue(`Waiting for application to be healthy at http://localhost:${port}${healthCheckPath}...`));
378
- await waitForHealthCheck(appName, 90, port, config, debug);
379
- }
380
- /**
381
- * Displays run status after successful start
382
- * @param {string} appName - Application name
383
- * @param {number} port - Application port
384
- * @param {Object} config - Application configuration (with developerId property)
385
- */
386
- async function displayRunStatus(appName, port, config) {
387
- // Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
388
- const containerName = config.developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${config.developerId}-${appName}`;
389
- const healthCheckPath = config?.healthCheck?.path || '/health';
390
- const healthCheckUrl = `http://localhost:${port}${healthCheckPath}`;
391
-
392
- logger.log(chalk.green(`\n✓ App running at http://localhost:${port}`));
393
- logger.log(chalk.blue(`Health check: ${healthCheckUrl}`));
394
- logger.log(chalk.gray(`Container: ${containerName}`));
395
- }
17
+ // Helper functions extracted to reduce file size and complexity
18
+ const helpers = require('./app-run-helpers');
396
19
 
397
20
  /**
398
21
  * Runs the application locally using Docker
@@ -421,7 +44,7 @@ async function runApp(appName, options = {}) {
421
44
 
422
45
  try {
423
46
  // Validate app name and load configuration
424
- const appConfig = await validateAppConfiguration(appName);
47
+ const appConfig = await helpers.validateAppConfiguration(appName);
425
48
 
426
49
  // Load developer ID once from config module - it's now cached and available as config.developerId
427
50
  // Developer ID: 0 = default infra, > 0 = developer-specific
@@ -433,21 +56,23 @@ async function runApp(appName, options = {}) {
433
56
  }
434
57
 
435
58
  // Check prerequisites: image and infrastructure
436
- await checkPrerequisites(appName, appConfig, debug);
59
+ await helpers.checkPrerequisites(appName, appConfig, debug);
437
60
 
438
61
  // Check if container is already running
439
- const containerRunning = await checkContainerRunning(appName, appConfig.developerId, debug);
62
+ const containerRunning = await helpers.checkContainerRunning(appName, appConfig.developerId, debug);
440
63
  if (containerRunning) {
441
64
  // Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
442
- const containerName = appConfig.developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${appConfig.developerId}-${appName}`;
65
+ const idNum2 = typeof appConfig.developerId === 'string' ? parseInt(appConfig.developerId, 10) : appConfig.developerId;
66
+ const containerName = idNum2 === 0 ? `aifabrix-${appName}` : `aifabrix-dev${appConfig.developerId}-${appName}`;
443
67
  logger.log(chalk.yellow(`Container ${containerName} is already running`));
444
- await stopAndRemoveContainer(appName, appConfig.developerId, debug);
68
+ await helpers.stopAndRemoveContainer(appName, appConfig.developerId, debug);
445
69
  }
446
70
 
447
71
  // Calculate host port: use dev-specific port offset if not overridden
448
72
  // IMPORTANT: Container port (for Dockerfile) stays unchanged from appConfig.port
449
73
  const basePort = appConfig.port || 3000;
450
- const hostPort = options.port || (appConfig.developerId === 0 ? basePort : basePort + (appConfig.developerId * 100));
74
+ const idNum3 = typeof appConfig.developerId === 'string' ? parseInt(appConfig.developerId, 10) : appConfig.developerId;
75
+ const hostPort = options.port || (idNum3 === 0 ? basePort : basePort + (idNum3 * 100));
451
76
  if (debug) {
452
77
  logger.log(chalk.gray(`[DEBUG] Host port: ${hostPort} (${options.port ? 'CLI override' : 'dev-specific'}), Container port: ${appConfig.build?.containerPort || appConfig.port || 3000} (unchanged)`));
453
78
  }
@@ -460,17 +85,17 @@ async function runApp(appName, options = {}) {
460
85
  }
461
86
 
462
87
  // Prepare environment: ensure .env file and generate Docker Compose
463
- const tempComposePath = await prepareEnvironment(appName, appConfig, options);
88
+ const tempComposePath = await helpers.prepareEnvironment(appName, appConfig, options);
464
89
  if (debug) {
465
90
  logger.log(chalk.gray(`[DEBUG] Compose file generated: ${tempComposePath}`));
466
91
  }
467
92
 
468
93
  try {
469
94
  // Start container and wait for health check
470
- await startContainer(appName, tempComposePath, hostPort, appConfig, debug);
95
+ await helpers.startContainer(appName, tempComposePath, hostPort, appConfig, debug);
471
96
 
472
97
  // Display success message
473
- await displayRunStatus(appName, hostPort, appConfig);
98
+ await helpers.displayRunStatus(appName, hostPort, appConfig);
474
99
 
475
100
  } catch (error) {
476
101
  // Keep the compose file for debugging - don't delete on error
@@ -491,9 +116,9 @@ async function runApp(appName, options = {}) {
491
116
  }
492
117
  module.exports = {
493
118
  runApp,
494
- checkImageExists,
495
- checkContainerRunning,
496
- stopAndRemoveContainer,
119
+ checkImageExists: helpers.checkImageExists,
120
+ checkContainerRunning: helpers.checkContainerRunning,
121
+ stopAndRemoveContainer: helpers.stopAndRemoveContainer,
497
122
  checkPortAvailable,
498
123
  generateDockerCompose: composeGenerator.generateDockerCompose,
499
124
  waitForHealthCheck
package/lib/build.js CHANGED
@@ -350,7 +350,8 @@ async function buildApp(appName, options = {}) {
350
350
 
351
351
  // 2. Get developer ID and copy files to dev-specific directory
352
352
  const developerId = await config.getDeveloperId();
353
- const directoryName = developerId === 0 ? 'applications' : `dev-${developerId}`;
353
+ const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
354
+ const directoryName = idNum === 0 ? 'applications' : `dev-${developerId}`;
354
355
  logger.log(chalk.blue(`Copying files to developer-specific directory (${directoryName})...`));
355
356
  const devDir = await buildCopy.copyBuilderToDevDirectory(appName, developerId);
356
357
  logger.log(chalk.green(`✓ Files copied to: ${devDir}`));
package/lib/cli.js CHANGED
@@ -336,18 +336,19 @@ function setupCommands(program) {
336
336
  // Commander.js converts --set-id to setId in options object
337
337
  const setIdValue = options.setId || options['set-id'];
338
338
  if (setIdValue) {
339
- const id = parseInt(setIdValue, 10);
340
- if (isNaN(id) || id < 0) {
341
- throw new Error('Developer ID must be a non-negative number (0 = default infra, > 0 = developer-specific)');
339
+ const digitsOnly = /^[0-9]+$/.test(setIdValue);
340
+ if (!digitsOnly) {
341
+ throw new Error('Developer ID must be a non-negative digit string (0 = default infra, > 0 = developer-specific)');
342
342
  }
343
- await config.setDeveloperId(id);
344
- process.env.AIFABRIX_DEVELOPERID = id.toString();
345
- logger.log(chalk.green(`✓ Developer ID set to ${id}`));
343
+ // Convert to number for setDeveloperId and getDevPorts (they expect numbers)
344
+ const devIdNum = parseInt(setIdValue, 10);
345
+ await config.setDeveloperId(devIdNum);
346
+ process.env.AIFABRIX_DEVELOPERID = setIdValue;
347
+ logger.log(chalk.green(`✓ Developer ID set to ${setIdValue}`));
346
348
  // Use the ID we just set instead of reading from file to avoid race conditions
347
- const devId = id;
348
- const ports = devConfig.getDevPorts(devId);
349
+ const ports = devConfig.getDevPorts(devIdNum);
349
350
  logger.log('\n🔧 Developer Configuration\n');
350
- logger.log(`Developer ID: ${devId}`);
351
+ logger.log(`Developer ID: ${setIdValue}`);
351
352
  logger.log('\nPorts:');
352
353
  logger.log(` App: ${ports.app}`);
353
354
  logger.log(` Postgres: ${ports.postgres}`);
@@ -359,7 +360,9 @@ function setupCommands(program) {
359
360
  }
360
361
 
361
362
  const devId = await config.getDeveloperId();
362
- const ports = devConfig.getDevPorts(devId);
363
+ // Convert string developer ID to number for getDevPorts
364
+ const devIdNum = parseInt(devId, 10);
365
+ const ports = devConfig.getDevPorts(devIdNum);
363
366
  logger.log('\n🔧 Developer Configuration\n');
364
367
  logger.log(`Developer ID: ${devId}`);
365
368
  logger.log('\nPorts:');
@@ -17,7 +17,8 @@ const inquirer = require('inquirer');
17
17
  const chalk = require('chalk');
18
18
  const logger = require('../utils/logger');
19
19
  const { setSecretsEncryptionKey, getSecretsEncryptionKey } = require('../config');
20
- const { encryptSecret, isEncrypted, validateEncryptionKey } = require('../utils/secrets-encryption');
20
+ const { validateEncryptionKey } = require('../utils/secrets-encryption');
21
+ const { encryptYamlValues } = require('../utils/yaml-preserve');
21
22
 
22
23
  /**
23
24
  * Finds all secrets.local.yaml files to encrypt
@@ -92,7 +93,8 @@ async function findSecretsFiles() {
92
93
 
93
94
  /**
94
95
  * Encrypts all non-encrypted values in a secrets file
95
- * Preserves YAML structure and comments
96
+ * Preserves YAML structure, comments, and formatting
97
+ * Skips URLs (http:// and https://) as they are not secrets
96
98
  *
97
99
  * @async
98
100
  * @function encryptSecretsFile
@@ -102,45 +104,25 @@ async function findSecretsFiles() {
102
104
  */
103
105
  async function encryptSecretsFile(filePath, encryptionKey) {
104
106
  const content = fs.readFileSync(filePath, 'utf8');
105
- const secrets = yaml.load(content);
106
107
 
107
- if (!secrets || typeof secrets !== 'object') {
108
- throw new Error(`Invalid secrets file format: ${filePath}`);
109
- }
110
-
111
- let encryptedCount = 0;
112
- let totalCount = 0;
113
- const updatedSecrets = {};
114
-
115
- for (const [key, value] of Object.entries(secrets)) {
116
- totalCount++;
117
- if (typeof value === 'string' && value.trim() !== '') {
118
- if (isEncrypted(value)) {
119
- // Already encrypted, keep as-is
120
- updatedSecrets[key] = value;
121
- } else {
122
- // Encrypt the value
123
- updatedSecrets[key] = encryptSecret(value, encryptionKey);
124
- encryptedCount++;
125
- }
126
- } else {
127
- // Non-string or empty value, keep as-is
128
- updatedSecrets[key] = value;
108
+ // Validate that file contains valid YAML structure (optional check)
109
+ try {
110
+ const secrets = yaml.load(content);
111
+ if (!secrets || typeof secrets !== 'object') {
112
+ throw new Error(`Invalid secrets file format: ${filePath}`);
129
113
  }
114
+ } catch (error) {
115
+ // If YAML parsing fails, still try to encrypt (might have syntax issues but could be fixable)
116
+ // The line-by-line parser will handle it gracefully
130
117
  }
131
118
 
132
- // Write back to file with same formatting
133
- // Use yaml.dump with appropriate options to preserve structure
134
- const yamlContent = yaml.dump(updatedSecrets, {
135
- indent: 2,
136
- lineWidth: -1,
137
- noRefs: true,
138
- sortKeys: false
139
- });
119
+ // Use line-by-line encryption to preserve comments and formatting
120
+ const result = encryptYamlValues(content, encryptionKey);
140
121
 
141
- fs.writeFileSync(filePath, yamlContent, { mode: 0o600 });
122
+ // Write back to file preserving all formatting
123
+ fs.writeFileSync(filePath, result.content, { mode: 0o600 });
142
124
 
143
- return { encrypted: encryptedCount, total: totalCount };
125
+ return { encrypted: result.encrypted, total: result.total };
144
126
  }
145
127
 
146
128
  /**