@aifabrix/builder 2.1.7 → 2.3.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.
Files changed (43) hide show
  1. package/lib/app-deploy.js +73 -29
  2. package/lib/app-list.js +132 -0
  3. package/lib/app-readme.js +11 -4
  4. package/lib/app-register.js +435 -0
  5. package/lib/app-rotate-secret.js +164 -0
  6. package/lib/app-run.js +98 -84
  7. package/lib/app.js +13 -0
  8. package/lib/audit-logger.js +195 -15
  9. package/lib/build.js +155 -42
  10. package/lib/cli.js +104 -8
  11. package/lib/commands/app.js +8 -391
  12. package/lib/commands/login.js +130 -36
  13. package/lib/commands/secure.js +260 -0
  14. package/lib/config.js +315 -4
  15. package/lib/deployer.js +221 -183
  16. package/lib/infra.js +177 -112
  17. package/lib/push.js +34 -7
  18. package/lib/secrets.js +89 -23
  19. package/lib/templates.js +1 -1
  20. package/lib/utils/api-error-handler.js +465 -0
  21. package/lib/utils/api.js +165 -16
  22. package/lib/utils/auth-headers.js +84 -0
  23. package/lib/utils/build-copy.js +162 -0
  24. package/lib/utils/cli-utils.js +49 -3
  25. package/lib/utils/compose-generator.js +57 -16
  26. package/lib/utils/deployment-errors.js +90 -0
  27. package/lib/utils/deployment-validation.js +60 -0
  28. package/lib/utils/dev-config.js +83 -0
  29. package/lib/utils/docker-build.js +24 -0
  30. package/lib/utils/env-template.js +30 -10
  31. package/lib/utils/health-check.js +18 -1
  32. package/lib/utils/infra-containers.js +101 -0
  33. package/lib/utils/local-secrets.js +0 -2
  34. package/lib/utils/secrets-encryption.js +203 -0
  35. package/lib/utils/secrets-path.js +22 -3
  36. package/lib/utils/token-manager.js +381 -0
  37. package/package.json +2 -2
  38. package/templates/applications/README.md.hbs +155 -23
  39. package/templates/applications/miso-controller/Dockerfile +7 -119
  40. package/templates/infra/compose.yaml.hbs +93 -0
  41. package/templates/python/docker-compose.hbs +25 -17
  42. package/templates/typescript/docker-compose.hbs +25 -17
  43. package/test-output.txt +0 -5431
package/lib/build.js CHANGED
@@ -19,13 +19,61 @@ const { promisify } = require('util');
19
19
  const chalk = require('chalk');
20
20
  const yaml = require('js-yaml');
21
21
  const secrets = require('./secrets');
22
+ const config = require('./config');
22
23
  const logger = require('./utils/logger');
23
24
  const validator = require('./validator');
24
25
  const dockerfileUtils = require('./utils/dockerfile-utils');
25
26
  const dockerBuild = require('./utils/docker-build');
27
+ const buildCopy = require('./utils/build-copy');
26
28
 
27
29
  const execAsync = promisify(exec);
28
30
 
31
+ /**
32
+ * Copies application template files to dev directory
33
+ * Used when apps directory doesn't exist to ensure build can proceed
34
+ * @async
35
+ * @param {string} templatePath - Path to template directory
36
+ * @param {string} devDir - Target dev directory
37
+ * @param {string} _language - Language (typescript/python) - currently unused but kept for future use
38
+ * @throws {Error} If copying fails
39
+ */
40
+ async function copyTemplateFilesToDevDir(templatePath, devDir, _language) {
41
+ if (!fsSync.existsSync(templatePath)) {
42
+ throw new Error(`Template path not found: ${templatePath}`);
43
+ }
44
+
45
+ const entries = await fs.readdir(templatePath);
46
+
47
+ // Copy only application files, skip Dockerfile and docker-compose templates
48
+ const appFiles = entries.filter(entry => {
49
+ const lowerEntry = entry.toLowerCase();
50
+ // Include .gitignore, exclude .hbs files and docker-related files
51
+ if (entry === '.gitignore') {
52
+ return true;
53
+ }
54
+ if (lowerEntry.endsWith('.hbs')) {
55
+ return false;
56
+ }
57
+ if (lowerEntry.startsWith('dockerfile') || lowerEntry.includes('docker-compose')) {
58
+ return false;
59
+ }
60
+ if (entry.startsWith('.') && entry !== '.gitignore') {
61
+ return false;
62
+ }
63
+ return true;
64
+ });
65
+
66
+ for (const entry of appFiles) {
67
+ const sourcePath = path.join(templatePath, entry);
68
+ const targetPath = path.join(devDir, entry);
69
+
70
+ const entryStats = await fs.stat(sourcePath);
71
+ if (entryStats.isFile()) {
72
+ await fs.copyFile(sourcePath, targetPath);
73
+ }
74
+ }
75
+ }
76
+
29
77
  /**
30
78
  * Loads variables.yaml configuration for an application
31
79
  * @param {string} appName - Application name
@@ -89,7 +137,6 @@ function detectLanguage(appPath) {
89
137
  const packageJsonPath = path.join(appPath, 'package.json');
90
138
  const requirementsPath = path.join(appPath, 'requirements.txt');
91
139
  const pyprojectPath = path.join(appPath, 'pyproject.toml');
92
- const dockerfilePath = path.join(appPath, 'Dockerfile');
93
140
 
94
141
  // Check for package.json (TypeScript/Node.js)
95
142
  if (fsSync.existsSync(packageJsonPath)) {
@@ -101,11 +148,6 @@ function detectLanguage(appPath) {
101
148
  return 'python';
102
149
  }
103
150
 
104
- // Check for custom Dockerfile
105
- if (fsSync.existsSync(dockerfilePath)) {
106
- throw new Error('Custom Dockerfile found. Use --force-template to regenerate from template.');
107
- }
108
-
109
151
  // Default to typescript if no indicators found
110
152
  return 'typescript';
111
153
  }
@@ -127,7 +169,7 @@ function detectLanguage(appPath) {
127
169
  * const dockerfilePath = await generateDockerfile('myapp', 'typescript', config);
128
170
  * // Returns: '~/.aifabrix/myapp/Dockerfile.typescript'
129
171
  */
130
- async function generateDockerfile(appNameOrPath, language, config, buildConfig = {}) {
172
+ async function generateDockerfile(appNameOrPath, language, config, buildConfig = {}, devDir = null) {
131
173
  let appName;
132
174
  if (appNameOrPath.includes(path.sep) || appNameOrPath.includes('/') || appNameOrPath.includes('\\')) {
133
175
  appName = path.basename(appNameOrPath);
@@ -151,12 +193,19 @@ async function generateDockerfile(appNameOrPath, language, config, buildConfig =
151
193
 
152
194
  const dockerfileContent = dockerfileUtils.renderDockerfile(template, templateVars, language, isAppFlag, appSourcePath);
153
195
 
154
- const aifabrixDir = path.join(os.homedir(), '.aifabrix', appName);
155
- if (!fsSync.existsSync(aifabrixDir)) {
156
- await fs.mkdir(aifabrixDir, { recursive: true });
196
+ // Use dev directory if provided, otherwise use default aifabrix directory
197
+ let targetDir;
198
+ if (devDir) {
199
+ targetDir = devDir;
200
+ } else {
201
+ targetDir = path.join(os.homedir(), '.aifabrix', appName);
157
202
  }
158
203
 
159
- const dockerfilePath = path.join(aifabrixDir, `Dockerfile.${language}`);
204
+ if (!fsSync.existsSync(targetDir)) {
205
+ await fs.mkdir(targetDir, { recursive: true });
206
+ }
207
+
208
+ const dockerfilePath = path.join(targetDir, 'Dockerfile');
160
209
  await fs.writeFile(dockerfilePath, dockerfileContent, 'utf8');
161
210
 
162
211
  return dockerfilePath;
@@ -175,41 +224,29 @@ async function generateDockerfile(appNameOrPath, language, config, buildConfig =
175
224
  * @returns {Promise<string>} Path to Dockerfile
176
225
  */
177
226
  async function determineDockerfile(appName, options) {
178
- const builderPath = path.join(process.cwd(), 'builder', appName);
227
+ // Use dev directory if provided, otherwise fall back to builder directory
228
+ const searchPath = options.devDir || path.join(process.cwd(), 'builder', appName);
179
229
 
180
- const templateDockerfile = dockerfileUtils.checkTemplateDockerfile(builderPath, appName, options.forceTemplate);
230
+ const templateDockerfile = dockerfileUtils.checkTemplateDockerfile(searchPath, appName, options.forceTemplate);
181
231
  if (templateDockerfile) {
182
- logger.log(chalk.green(`āœ“ Using existing Dockerfile: builder/${appName}/Dockerfile`));
232
+ const relativePath = path.relative(process.cwd(), templateDockerfile);
233
+ logger.log(chalk.green(`āœ“ Using existing Dockerfile: ${relativePath}`));
183
234
  return templateDockerfile;
184
235
  }
185
236
 
186
- const customDockerfile = dockerfileUtils.checkProjectDockerfile(builderPath, appName, options.buildConfig, options.contextPath, options.forceTemplate);
237
+ const customDockerfile = dockerfileUtils.checkProjectDockerfile(searchPath, appName, options.buildConfig, options.contextPath, options.forceTemplate);
187
238
  if (customDockerfile) {
188
239
  logger.log(chalk.green(`āœ“ Using custom Dockerfile: ${options.buildConfig.dockerfile}`));
189
240
  return customDockerfile;
190
241
  }
191
242
 
192
- const dockerfilePath = await generateDockerfile(appName, options.language, options.config, options.buildConfig);
243
+ // Generate Dockerfile in dev directory if provided
244
+ const dockerfilePath = await generateDockerfile(appName, options.language, options.config, options.buildConfig, options.devDir);
193
245
  const relativePath = path.relative(process.cwd(), dockerfilePath);
194
246
  logger.log(chalk.green(`āœ“ Generated Dockerfile from template: ${relativePath}`));
195
247
  return dockerfilePath;
196
248
  }
197
249
 
198
- /**
199
- * Prepares build context path
200
- * @param {string} appName - Application name
201
- * @param {string} contextPath - Relative context path
202
- * @returns {string} Absolute context path
203
- */
204
- function prepareBuildContext(appName, contextPath) {
205
- // Ensure contextPath is a string
206
- const context = typeof contextPath === 'string' ? contextPath : (contextPath || '');
207
- return resolveContextPath(
208
- path.join(process.cwd(), 'builder', appName),
209
- context
210
- );
211
- }
212
-
213
250
  /**
214
251
  * Loads and validates configuration for build
215
252
  * @async
@@ -309,20 +346,89 @@ async function buildApp(appName, options = {}) {
309
346
  logger.log(chalk.blue(`\nšŸ”Ø Building application: ${appName}`));
310
347
 
311
348
  // 1. Load and validate configuration
312
- const { config, imageName, buildConfig } = await loadAndValidateConfig(appName);
349
+ const { config: appConfig, imageName, buildConfig } = await loadAndValidateConfig(appName);
350
+
351
+ // 2. Get developer ID and copy files to dev-specific directory
352
+ const developerId = await config.getDeveloperId();
353
+ const directoryName = developerId === 0 ? 'applications' : `dev-${developerId}`;
354
+ logger.log(chalk.blue(`Copying files to developer-specific directory (${directoryName})...`));
355
+ const devDir = await buildCopy.copyBuilderToDevDirectory(appName, developerId);
356
+ logger.log(chalk.green(`āœ“ Files copied to: ${devDir}`));
357
+
358
+ // 2a. Check if application source files exist, if not copy from templates
359
+ const appsPath = path.join(process.cwd(), 'apps', appName);
360
+ if (fsSync.existsSync(appsPath)) {
361
+ // Copy app source files from apps directory
362
+ await buildCopy.copyAppSourceFiles(appsPath, devDir);
363
+ logger.log(chalk.green(`āœ“ Copied application source files from apps/${appName}`));
364
+ } else {
365
+ // No apps directory - check if we need to copy template files
366
+ const language = options.language || buildConfig.language || detectLanguage(devDir);
367
+ const packageJsonPath = path.join(devDir, 'package.json');
368
+ const requirementsPath = path.join(devDir, 'requirements.txt');
369
+
370
+ if (language === 'typescript' && !fsSync.existsSync(packageJsonPath)) {
371
+ // Copy TypeScript template files
372
+ const templatePath = path.join(__dirname, '..', 'templates', 'typescript');
373
+ await copyTemplateFilesToDevDir(templatePath, devDir, language);
374
+ logger.log(chalk.green(`āœ“ Generated application files from ${language} template`));
375
+ } else if (language === 'python' && !fsSync.existsSync(requirementsPath)) {
376
+ // Copy Python template files
377
+ const templatePath = path.join(__dirname, '..', 'templates', 'python');
378
+ await copyTemplateFilesToDevDir(templatePath, devDir, language);
379
+ logger.log(chalk.green(`āœ“ Generated application files from ${language} template`));
380
+ }
381
+ }
382
+
383
+ // 3. Prepare build context (use dev-specific directory)
384
+ // If buildConfig.context is relative, resolve it relative to devDir
385
+ // If buildConfig.context is '../..' (apps flag), keep it as is (it's relative to devDir)
386
+ let contextPath;
387
+
388
+ // Check if context is using old format (../appName) - these are incompatible with dev directory structure
389
+ if (buildConfig.context && buildConfig.context.startsWith('../') && buildConfig.context !== '../..') {
390
+ // Old format detected - always use devDir instead
391
+ logger.log(chalk.yellow(`āš ļø Warning: Build context uses old format: ${buildConfig.context}`));
392
+ logger.log(chalk.yellow(` Using dev directory instead: ${devDir}`));
393
+ contextPath = devDir;
394
+ } else if (buildConfig.context && buildConfig.context !== '../..') {
395
+ // Resolve relative context path from dev directory
396
+ contextPath = path.resolve(devDir, buildConfig.context);
397
+ } else if (buildConfig.context === '../..') {
398
+ // For apps flag, context is relative to devDir (which is ~/.aifabrix/<app>-dev-<id>)
399
+ // So '../..' from devDir goes to ~/.aifabrix, then we need to go to project root
400
+ // Actually, we need to keep the relative path and let Docker resolve it
401
+ // But the build context should be the project root, not devDir
402
+ contextPath = process.cwd();
403
+ } else {
404
+ // No context specified, use dev directory
405
+ contextPath = devDir;
406
+ }
407
+
408
+ // Ensure context path is absolute and normalized
409
+ contextPath = path.resolve(contextPath);
313
410
 
314
- // 2. Prepare build context
315
- const contextPath = prepareBuildContext(appName, buildConfig.context);
411
+ // Validate that context path exists (skip in test environments)
412
+ const isTestEnv = process.env.NODE_ENV === 'test' ||
413
+ process.env.JEST_WORKER_ID !== undefined ||
414
+ typeof jest !== 'undefined';
316
415
 
317
- // 3. Check if Dockerfile exists in builder/{appName}/ directory
318
- const builderPath = path.join(process.cwd(), 'builder', appName);
319
- const appDockerfilePath = path.join(builderPath, 'Dockerfile');
416
+ if (!isTestEnv && !fsSync.existsSync(contextPath)) {
417
+ throw new Error(
418
+ `Build context path does not exist: ${contextPath}\n` +
419
+ `Expected dev directory: ${devDir}\n` +
420
+ 'Please ensure files were copied correctly or update the context in variables.yaml.'
421
+ );
422
+ }
423
+
424
+ // 4. Check if Dockerfile exists in dev directory
425
+ const appDockerfilePath = path.join(devDir, 'Dockerfile');
320
426
  const hasExistingDockerfile = fsSync.existsSync(appDockerfilePath) && !options.forceTemplate;
321
427
 
322
- // 4. Determine language (skip if existing Dockerfile found)
428
+ // 5. Determine language (skip if existing Dockerfile found)
323
429
  let language = options.language || buildConfig.language;
324
430
  if (!language && !hasExistingDockerfile) {
325
- language = detectLanguage(builderPath);
431
+ language = detectLanguage(devDir);
326
432
  } else if (!language) {
327
433
  // Default language if existing Dockerfile is found (won't be used, but needed for API)
328
434
  language = 'typescript';
@@ -331,17 +437,24 @@ async function buildApp(appName, options = {}) {
331
437
  logger.log(chalk.green(`āœ“ Detected language: ${language}`));
332
438
  }
333
439
 
334
- // 5. Determine Dockerfile (needs context path to generate in correct location)
440
+ // 6. Determine Dockerfile (needs context path to generate in correct location)
441
+ // Use dev directory for Dockerfile generation
335
442
  const dockerfilePath = await determineDockerfile(appName, {
336
443
  language,
337
- config,
444
+ config: appConfig,
338
445
  buildConfig,
339
446
  contextPath,
340
- forceTemplate: options.forceTemplate
447
+ forceTemplate: options.forceTemplate,
448
+ devDir: devDir
341
449
  });
342
450
 
343
451
  // 6. Build Docker image
344
452
  const tag = options.tag || 'latest';
453
+
454
+ // Log paths for debugging
455
+ logger.log(chalk.blue(`Using Dockerfile: ${dockerfilePath}`));
456
+ logger.log(chalk.blue(`Using build context: ${contextPath}`));
457
+
345
458
  await executeBuild(imageName, dockerfilePath, contextPath, tag, options);
346
459
 
347
460
  // 7. Post-build tasks
package/lib/cli.js CHANGED
@@ -15,10 +15,13 @@ const secrets = require('./secrets');
15
15
  const generator = require('./generator');
16
16
  const validator = require('./validator');
17
17
  const keyGenerator = require('./key-generator');
18
+ const config = require('./config');
19
+ const devConfig = require('./utils/dev-config');
18
20
  const chalk = require('chalk');
19
21
  const logger = require('./utils/logger');
20
22
  const { validateCommand, handleCommandError } = require('./utils/cli-utils');
21
23
  const { handleLogin } = require('./commands/login');
24
+ const { handleSecure } = require('./commands/secure');
22
25
 
23
26
  /**
24
27
  * Sets up all CLI commands on the Commander program instance
@@ -28,11 +31,12 @@ function setupCommands(program) {
28
31
  // Authentication command
29
32
  program.command('login')
30
33
  .description('Authenticate with Miso Controller')
31
- .option('-u, --url <url>', 'Controller URL', 'http://localhost:3000')
34
+ .option('-c, --controller <url>', 'Controller URL', 'http://localhost:3000')
32
35
  .option('-m, --method <method>', 'Authentication method (device|credentials)')
33
- .option('--client-id <id>', 'Client ID (for credentials method)')
34
- .option('--client-secret <secret>', 'Client Secret (for credentials method)')
35
- .option('-e, --environment <env>', 'Environment key (for device method, e.g., dev, tst, pro)')
36
+ .option('-a, --app <app>', 'Application name (required for credentials method, reads from secrets.local.yaml)')
37
+ .option('--client-id <id>', 'Client ID (for credentials method, overrides secrets.local.yaml)')
38
+ .option('--client-secret <secret>', 'Client Secret (for credentials method, overrides secrets.local.yaml)')
39
+ .option('-e, --environment <env>', 'Environment key (updates root-level environment in config.yaml, e.g., miso, dev, tst, pro)')
36
40
  .action(async(options) => {
37
41
  try {
38
42
  await handleLogin(options);
@@ -45,9 +49,21 @@ function setupCommands(program) {
45
49
  // Infrastructure commands
46
50
  program.command('up')
47
51
  .description('Start local infrastructure services (Postgres, Redis, pgAdmin, Redis Commander)')
48
- .action(async() => {
52
+ .option('-d, --developer <id>', 'Set developer ID and start infrastructure')
53
+ .action(async(options) => {
49
54
  try {
50
- await infra.startInfra();
55
+ let developerId = null;
56
+ if (options.developer) {
57
+ const id = parseInt(options.developer, 10);
58
+ if (isNaN(id) || id < 0) {
59
+ throw new Error('Developer ID must be a non-negative number (0 = default infra, > 0 = developer-specific)');
60
+ }
61
+ await config.setDeveloperId(id);
62
+ process.env.AIFABRIX_DEVELOPERID = id.toString();
63
+ developerId = id;
64
+ logger.log(chalk.green(`āœ“ Developer ID set to ${id}`));
65
+ }
66
+ await infra.startInfra(developerId);
51
67
  } catch (error) {
52
68
  handleCommandError(error, 'up');
53
69
  process.exit(1);
@@ -138,7 +154,7 @@ function setupCommands(program) {
138
154
  program.command('deploy <app>')
139
155
  .description('Deploy to Azure via Miso Controller')
140
156
  .option('-c, --controller <url>', 'Controller URL')
141
- .option('-e, --environment <env>', 'Environment (dev, tst, pro)', 'dev')
157
+ .option('-e, --environment <env>', 'Environment (miso, dev, tst, pro)', 'dev')
142
158
  .option('--client-id <id>', 'Client ID (overrides config)')
143
159
  .option('--client-secret <secret>', 'Client Secret (overrides config)')
144
160
  .option('--poll', 'Poll for deployment status', true)
@@ -191,7 +207,7 @@ function setupCommands(program) {
191
207
  });
192
208
 
193
209
  program.command('status')
194
- .description('Show detailed infrastructure service status')
210
+ .description('Show detailed infrastructure service status and running applications')
195
211
  .action(async() => {
196
212
  try {
197
213
  const status = await infra.getInfraStatus();
@@ -207,6 +223,22 @@ function setupCommands(program) {
207
223
  logger.log(` URL: ${info.url}`);
208
224
  logger.log('');
209
225
  });
226
+
227
+ // Show running applications
228
+ const apps = await infra.getAppStatus();
229
+ if (apps.length > 0) {
230
+ logger.log('šŸ“± Running Applications\n');
231
+ apps.forEach((app) => {
232
+ const normalizedStatus = String(app.status).trim().toLowerCase();
233
+ const icon = normalizedStatus.includes('running') || normalizedStatus.includes('up') ? 'āœ…' : 'āŒ';
234
+ logger.log(`${icon} ${app.name}:`);
235
+ logger.log(` Container: ${app.container}`);
236
+ logger.log(` Port: ${app.port}`);
237
+ logger.log(` Status: ${app.status}`);
238
+ logger.log(` URL: ${app.url}`);
239
+ logger.log('');
240
+ });
241
+ }
210
242
  } catch (error) {
211
243
  handleCommandError(error, 'status');
212
244
  process.exit(1);
@@ -291,6 +323,70 @@ function setupCommands(program) {
291
323
  process.exit(1);
292
324
  }
293
325
  });
326
+
327
+ // Developer configuration commands
328
+ program.command('dev config')
329
+ .description('Show or set developer configuration')
330
+ .option('--set-id <id>', 'Set developer ID')
331
+ .action(async(cmdName, opts) => {
332
+ try {
333
+ // For commands with spaces like 'dev config', Commander.js passes command name as first arg
334
+ // Options are passed as second arg, or if only one arg, it might be the command name
335
+ const options = typeof cmdName === 'object' && cmdName !== null ? cmdName : (opts || {});
336
+ // Commander.js converts --set-id to setId in options object
337
+ const setIdValue = options.setId || options['set-id'];
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)');
342
+ }
343
+ await config.setDeveloperId(id);
344
+ process.env.AIFABRIX_DEVELOPERID = id.toString();
345
+ logger.log(chalk.green(`āœ“ Developer ID set to ${id}`));
346
+ // 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
+ logger.log('\nšŸ”§ Developer Configuration\n');
350
+ logger.log(`Developer ID: ${devId}`);
351
+ logger.log('\nPorts:');
352
+ logger.log(` App: ${ports.app}`);
353
+ logger.log(` Postgres: ${ports.postgres}`);
354
+ logger.log(` Redis: ${ports.redis}`);
355
+ logger.log(` pgAdmin: ${ports.pgadmin}`);
356
+ logger.log(` Redis Commander: ${ports.redisCommander}`);
357
+ logger.log('');
358
+ return;
359
+ }
360
+
361
+ const devId = await config.getDeveloperId();
362
+ const ports = devConfig.getDevPorts(devId);
363
+ logger.log('\nšŸ”§ Developer Configuration\n');
364
+ logger.log(`Developer ID: ${devId}`);
365
+ logger.log('\nPorts:');
366
+ logger.log(` App: ${ports.app}`);
367
+ logger.log(` Postgres: ${ports.postgres}`);
368
+ logger.log(` Redis: ${ports.redis}`);
369
+ logger.log(` pgAdmin: ${ports.pgadmin}`);
370
+ logger.log(` Redis Commander: ${ports.redisCommander}`);
371
+ logger.log('');
372
+ } catch (error) {
373
+ handleCommandError(error, 'dev config');
374
+ process.exit(1);
375
+ }
376
+ });
377
+
378
+ // Security command
379
+ program.command('secure')
380
+ .description('Encrypt secrets in secrets.local.yaml files for ISO 27001 compliance')
381
+ .option('--secrets-encryption <key>', 'Encryption key (32 bytes, hex or base64)')
382
+ .action(async(options) => {
383
+ try {
384
+ await handleSecure(options);
385
+ } catch (error) {
386
+ handleCommandError(error, 'secure');
387
+ process.exit(1);
388
+ }
389
+ });
294
390
  }
295
391
 
296
392
  module.exports = {