@aifabrix/builder 2.0.0 → 2.0.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.
Files changed (58) hide show
  1. package/README.md +6 -2
  2. package/bin/aifabrix.js +9 -3
  3. package/jest.config.integration.js +30 -0
  4. package/lib/app-config.js +157 -0
  5. package/lib/app-deploy.js +233 -82
  6. package/lib/app-dockerfile.js +112 -0
  7. package/lib/app-prompts.js +244 -0
  8. package/lib/app-push.js +172 -0
  9. package/lib/app-run.js +334 -133
  10. package/lib/app.js +208 -274
  11. package/lib/audit-logger.js +2 -0
  12. package/lib/build.js +209 -98
  13. package/lib/cli.js +76 -86
  14. package/lib/commands/app.js +414 -0
  15. package/lib/commands/login.js +304 -0
  16. package/lib/config.js +78 -0
  17. package/lib/deployer.js +225 -81
  18. package/lib/env-reader.js +45 -30
  19. package/lib/generator.js +308 -191
  20. package/lib/github-generator.js +67 -7
  21. package/lib/infra.js +156 -61
  22. package/lib/push.js +105 -10
  23. package/lib/schema/application-schema.json +30 -2
  24. package/lib/schema/infrastructure-schema.json +589 -0
  25. package/lib/secrets.js +229 -24
  26. package/lib/template-validator.js +205 -0
  27. package/lib/templates.js +305 -170
  28. package/lib/utils/api.js +329 -0
  29. package/lib/utils/cli-utils.js +97 -0
  30. package/lib/utils/dockerfile-utils.js +131 -0
  31. package/lib/utils/environment-checker.js +125 -0
  32. package/lib/utils/error-formatter.js +61 -0
  33. package/lib/utils/health-check.js +187 -0
  34. package/lib/utils/logger.js +53 -0
  35. package/lib/utils/template-helpers.js +223 -0
  36. package/lib/utils/variable-transformer.js +271 -0
  37. package/lib/validator.js +27 -112
  38. package/package.json +13 -10
  39. package/templates/README.md +75 -3
  40. package/templates/applications/keycloak/Dockerfile +36 -0
  41. package/templates/applications/keycloak/env.template +32 -0
  42. package/templates/applications/keycloak/rbac.yaml +37 -0
  43. package/templates/applications/keycloak/variables.yaml +56 -0
  44. package/templates/applications/miso-controller/Dockerfile +125 -0
  45. package/templates/applications/miso-controller/env.template +129 -0
  46. package/templates/applications/miso-controller/rbac.yaml +168 -0
  47. package/templates/applications/miso-controller/variables.yaml +56 -0
  48. package/templates/github/release.yaml.hbs +5 -26
  49. package/templates/github/steps/npm.hbs +24 -0
  50. package/templates/infra/compose.yaml +6 -6
  51. package/templates/python/docker-compose.hbs +19 -12
  52. package/templates/python/main.py +80 -0
  53. package/templates/python/requirements.txt +4 -0
  54. package/templates/typescript/Dockerfile.hbs +2 -2
  55. package/templates/typescript/docker-compose.hbs +19 -12
  56. package/templates/typescript/index.ts +116 -0
  57. package/templates/typescript/package.json +26 -0
  58. package/templates/typescript/tsconfig.json +24 -0
package/lib/build.js CHANGED
@@ -13,13 +13,15 @@
13
13
  const fs = require('fs').promises;
14
14
  const fsSync = require('fs');
15
15
  const path = require('path');
16
+ const os = require('os');
16
17
  const { exec } = require('child_process');
17
18
  const { promisify } = require('util');
18
19
  const chalk = require('chalk');
19
20
  const yaml = require('js-yaml');
20
- const handlebars = require('handlebars');
21
- const validator = require('./validator');
22
21
  const secrets = require('./secrets');
22
+ const logger = require('./utils/logger');
23
+ const validator = require('./validator');
24
+ const dockerfileUtils = require('./utils/dockerfile-utils');
23
25
 
24
26
  const execAsync = promisify(exec);
25
27
 
@@ -69,6 +71,34 @@ function resolveContextPath(builderPath, contextPath) {
69
71
  return resolvedPath;
70
72
  }
71
73
 
74
+ /**
75
+ * Checks if error indicates Docker is not running or not installed
76
+ * @param {string} errorMessage - Error message to check
77
+ * @returns {boolean} True if Docker is not available
78
+ */
79
+ function isDockerNotAvailableError(errorMessage) {
80
+ return errorMessage.includes('docker: command not found') ||
81
+ errorMessage.includes('Cannot connect to the Docker daemon') ||
82
+ errorMessage.includes('Is the docker daemon running') ||
83
+ errorMessage.includes('Cannot connect to Docker');
84
+ }
85
+
86
+ /**
87
+ * Handles Docker build errors and provides user-friendly messages
88
+ * @param {Error} error - Build error
89
+ * @throws {Error} Formatted error message
90
+ */
91
+ function handleBuildError(error) {
92
+ const errorMessage = error.message || error.stderr || String(error);
93
+
94
+ if (isDockerNotAvailableError(errorMessage)) {
95
+ throw new Error('Docker is not running or not installed. Please start Docker Desktop and try again.');
96
+ }
97
+
98
+ const detailedError = error.stderr || error.stdout || errorMessage;
99
+ throw new Error(`Docker build failed: ${detailedError}`);
100
+ }
101
+
72
102
  /**
73
103
  * Executes Docker build command with proper error handling
74
104
  * @param {string} imageName - Image name to build
@@ -82,26 +112,22 @@ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag) {
82
112
  const dockerCommand = `docker build -t ${imageName}:${tag} -f "${dockerfilePath}" "${contextPath}"`;
83
113
 
84
114
  try {
85
- console.log(chalk.blue('Building image...'));
86
- console.log(chalk.gray(`Command: ${dockerCommand}`));
115
+ logger.log(chalk.blue('Building image...'));
116
+ logger.log(chalk.gray(`Command: ${dockerCommand}`));
87
117
 
88
118
  const { stdout, stderr } = await execAsync(dockerCommand);
89
119
 
90
120
  if (stderr && !stderr.includes('warning')) {
91
- console.log(chalk.yellow(stderr));
121
+ logger.log(chalk.yellow(stderr));
92
122
  }
93
123
 
94
124
  if (stdout) {
95
- console.log(stdout);
125
+ logger.log(stdout);
96
126
  }
97
127
 
98
- console.log(chalk.green(`✓ Image built: ${imageName}:${tag}`));
128
+ logger.log(chalk.green(`✓ Image built: ${imageName}:${tag}`));
99
129
  } catch (error) {
100
- if (error.message.includes('docker: command not found')) {
101
- throw new Error('Docker is not running or not installed. Please start Docker Desktop.');
102
- }
103
-
104
- throw new Error(`Docker build failed: ${error.message}`);
130
+ handleBuildError(error);
105
131
  }
106
132
  }
107
133
 
@@ -146,53 +172,179 @@ function detectLanguage(appPath) {
146
172
  /**
147
173
  * Generates a Dockerfile from template based on detected language
148
174
  * Uses Handlebars templates to create optimized Dockerfiles
175
+ * Dockerfiles are stored in ~/.aifabrix/{appName}/ directory
149
176
  *
150
177
  * @async
151
178
  * @function generateDockerfile
152
- * @param {string} appPath - Path to application directory
179
+ * @param {string} appNameOrPath - Application name or path (backward compatibility)
153
180
  * @param {string} language - Target language ('typescript', 'python')
154
181
  * @param {Object} config - Application configuration from variables.yaml
155
182
  * @returns {Promise<string>} Path to generated Dockerfile
156
183
  * @throws {Error} If template generation fails
157
184
  *
158
185
  * @example
159
- * const dockerfilePath = await generateDockerfile('./myapp', 'typescript', config);
160
- * // Returns: './myapp/.aifabrix/Dockerfile.typescript'
186
+ * const dockerfilePath = await generateDockerfile('myapp', 'typescript', config);
187
+ * // Returns: '~/.aifabrix/myapp/Dockerfile.typescript'
161
188
  */
162
- async function generateDockerfile(appPath, language, config) {
163
- const templatePath = path.join(__dirname, '..', 'templates', language, 'Dockerfile.hbs');
164
-
165
- if (!fsSync.existsSync(templatePath)) {
166
- throw new Error(`Template not found for language: ${language}`);
189
+ async function generateDockerfile(appNameOrPath, language, config, buildConfig = {}) {
190
+ let appName;
191
+ if (appNameOrPath.includes(path.sep) || appNameOrPath.includes('/') || appNameOrPath.includes('\\')) {
192
+ appName = path.basename(appNameOrPath);
193
+ } else {
194
+ appName = appNameOrPath;
167
195
  }
168
196
 
169
- const templateContent = fsSync.readFileSync(templatePath, 'utf8');
170
- const template = handlebars.compile(templateContent);
197
+ const template = dockerfileUtils.loadDockerfileTemplate(language);
198
+ const isAppFlag = buildConfig.context === '../..';
199
+ const appSourcePath = isAppFlag ? `apps/${appName}/` : '.';
171
200
 
172
- // Prepare template variables
173
201
  const templateVars = {
174
202
  port: config.port || 3000,
175
203
  healthCheck: {
176
204
  interval: config.healthCheck?.interval || 30,
177
205
  path: config.healthCheck?.path || '/health'
178
206
  },
179
- startupCommand: config.startupCommand
207
+ startupCommand: config.startupCommand,
208
+ appSourcePath: appSourcePath
180
209
  };
181
210
 
182
- const dockerfileContent = template(templateVars);
211
+ const dockerfileContent = dockerfileUtils.renderDockerfile(template, templateVars, language, isAppFlag, appSourcePath);
183
212
 
184
- // Create .aifabrix directory if it doesn't exist
185
- const aifabrixDir = path.join(appPath, '.aifabrix');
213
+ const aifabrixDir = path.join(os.homedir(), '.aifabrix', appName);
186
214
  if (!fsSync.existsSync(aifabrixDir)) {
187
215
  await fs.mkdir(aifabrixDir, { recursive: true });
188
216
  }
189
217
 
190
218
  const dockerfilePath = path.join(aifabrixDir, `Dockerfile.${language}`);
191
- await fs.writeFile(dockerfilePath, dockerfileContent);
219
+ await fs.writeFile(dockerfilePath, dockerfileContent, 'utf8');
220
+
221
+ return dockerfilePath;
222
+ }
223
+
224
+ /**
225
+ * Determines Dockerfile path, generating from template if needed
226
+ * @async
227
+ * @param {string} appName - Application name
228
+ * @param {Object} options - Dockerfile determination options
229
+ * @param {string} options.language - Application language
230
+ * @param {Object} options.config - Application configuration
231
+ * @param {Object} options.buildConfig - Build configuration
232
+ * @param {string} options.contextPath - Build context path (absolute)
233
+ * @param {boolean} options.forceTemplate - Force template flag
234
+ * @returns {Promise<string>} Path to Dockerfile
235
+ */
236
+ async function determineDockerfile(appName, options) {
237
+ const builderPath = path.join(process.cwd(), 'builder', appName);
238
+
239
+ const templateDockerfile = dockerfileUtils.checkTemplateDockerfile(builderPath, appName, options.forceTemplate);
240
+ if (templateDockerfile) {
241
+ logger.log(chalk.green(`✓ Using existing Dockerfile: builder/${appName}/Dockerfile`));
242
+ return templateDockerfile;
243
+ }
192
244
 
245
+ const customDockerfile = dockerfileUtils.checkProjectDockerfile(builderPath, appName, options.buildConfig, options.contextPath, options.forceTemplate);
246
+ if (customDockerfile) {
247
+ logger.log(chalk.green(`✓ Using custom Dockerfile: ${options.buildConfig.dockerfile}`));
248
+ return customDockerfile;
249
+ }
250
+
251
+ const dockerfilePath = await generateDockerfile(appName, options.language, options.config, options.buildConfig);
252
+ const relativePath = path.relative(process.cwd(), dockerfilePath);
253
+ logger.log(chalk.green(`✓ Generated Dockerfile from template: ${relativePath}`));
193
254
  return dockerfilePath;
194
255
  }
195
256
 
257
+ /**
258
+ * Prepares build context path
259
+ * @param {string} appName - Application name
260
+ * @param {string} contextPath - Relative context path
261
+ * @returns {string} Absolute context path
262
+ */
263
+ function prepareBuildContext(appName, contextPath) {
264
+ // Ensure contextPath is a string
265
+ const context = typeof contextPath === 'string' ? contextPath : (contextPath || '');
266
+ return resolveContextPath(
267
+ path.join(process.cwd(), 'builder', appName),
268
+ context
269
+ );
270
+ }
271
+
272
+ /**
273
+ * Loads and validates configuration for build
274
+ * @async
275
+ * @param {string} appName - Application name
276
+ * @returns {Promise<Object>} Configuration object with config, imageName, and buildConfig
277
+ * @throws {Error} If configuration cannot be loaded or validated
278
+ */
279
+ async function loadAndValidateConfig(appName) {
280
+ const variables = await loadVariablesYaml(appName);
281
+
282
+ // Validate configuration
283
+ const validation = await validator.validateVariables(appName);
284
+ if (!validation.valid) {
285
+ throw new Error(`Configuration validation failed:\n${validation.errors.join('\n')}`);
286
+ }
287
+
288
+ // Extract image name
289
+ let imageName;
290
+ if (typeof variables.image === 'string') {
291
+ imageName = variables.image.split(':')[0];
292
+ } else if (variables.image?.name) {
293
+ imageName = variables.image.name;
294
+ } else if (variables.app?.key) {
295
+ imageName = variables.app.key;
296
+ } else {
297
+ imageName = appName;
298
+ }
299
+
300
+ // Extract build config
301
+ const buildConfig = variables.build || {};
302
+
303
+ return {
304
+ config: variables,
305
+ imageName,
306
+ buildConfig
307
+ };
308
+ }
309
+
310
+ /**
311
+ * Executes Docker build and handles tagging
312
+ * @async
313
+ * @param {string} imageName - Image name
314
+ * @param {string} dockerfilePath - Path to Dockerfile
315
+ * @param {string} contextPath - Build context path
316
+ * @param {string} tag - Image tag
317
+ * @param {Object} options - Build options
318
+ */
319
+ async function executeBuild(imageName, dockerfilePath, contextPath, tag, options) {
320
+ await executeDockerBuild(imageName, dockerfilePath, contextPath, tag);
321
+
322
+ // Tag image if additional tag provided
323
+ if (options.tag && options.tag !== 'latest') {
324
+ await execAsync(`docker tag ${imageName}:${tag} ${imageName}:latest`);
325
+ logger.log(chalk.green(`✓ Tagged image: ${imageName}:latest`));
326
+ }
327
+ }
328
+
329
+ async function postBuildTasks(appName, buildConfig) {
330
+ try {
331
+ const envPath = await secrets.generateEnvFile(appName, buildConfig.secrets);
332
+ logger.log(chalk.green(`✓ Generated .env file: ${envPath}`));
333
+ if (buildConfig.envOutputPath) {
334
+ const builderPath = path.join(process.cwd(), 'builder', appName);
335
+ const outputPath = path.resolve(builderPath, buildConfig.envOutputPath);
336
+ const outputDir = path.dirname(outputPath);
337
+ if (!fsSync.existsSync(outputDir)) {
338
+ await fs.mkdir(outputDir, { recursive: true });
339
+ }
340
+ await fs.copyFile(envPath, outputPath);
341
+ logger.log(chalk.green(`✓ Copied .env to: ${buildConfig.envOutputPath}`));
342
+ }
343
+ } catch (error) {
344
+ logger.log(chalk.yellow(`⚠️ Warning: Could not generate .env file: ${error.message}`));
345
+ }
346
+ }
347
+
196
348
  /**
197
349
  * Builds a container image for the specified application
198
350
  * Auto-detects runtime and generates Dockerfile if needed
@@ -213,89 +365,48 @@ async function generateDockerfile(appPath, language, config) {
213
365
  */
214
366
  async function buildApp(appName, options = {}) {
215
367
  try {
216
- console.log(chalk.blue(`\n🔨 Building application: ${appName}`));
368
+ logger.log(chalk.blue(`\n🔨 Building application: ${appName}`));
217
369
 
218
370
  // 1. Load and validate configuration
219
- const config = await loadVariablesYaml(appName);
220
- console.log(chalk.green(`✓ Loaded configuration from builder/${appName}/variables.yaml`));
221
-
222
- // Validate configuration
223
- const validation = await validator.validateVariables(appName);
224
- if (!validation.valid) {
225
- console.log(chalk.red('❌ Configuration validation failed:'));
226
- validation.errors.forEach(error => console.log(chalk.red(` - ${error}`)));
227
- throw new Error('Configuration validation failed');
228
- }
371
+ const { config, imageName, buildConfig } = await loadAndValidateConfig(appName);
229
372
 
230
- // Extract configuration values
231
- const imageName = config.image?.split(':')[0] || appName;
232
- const buildConfig = config.build || {};
373
+ // 2. Prepare build context
374
+ const contextPath = prepareBuildContext(appName, buildConfig.context);
233
375
 
234
- // 2. Determine language
376
+ // 3. Check if Dockerfile exists in builder/{appName}/ directory
377
+ const builderPath = path.join(process.cwd(), 'builder', appName);
378
+ const appDockerfilePath = path.join(builderPath, 'Dockerfile');
379
+ const hasExistingDockerfile = fsSync.existsSync(appDockerfilePath) && !options.forceTemplate;
380
+
381
+ // 4. Determine language (skip if existing Dockerfile found)
235
382
  let language = options.language || buildConfig.language;
236
- if (!language) {
237
- const builderPath = path.join(process.cwd(), 'builder', appName);
383
+ if (!language && !hasExistingDockerfile) {
238
384
  language = detectLanguage(builderPath);
385
+ } else if (!language) {
386
+ // Default language if existing Dockerfile is found (won't be used, but needed for API)
387
+ language = 'typescript';
239
388
  }
240
- console.log(chalk.green(`✓ Detected language: ${language}`));
241
-
242
- // 3. Determine Dockerfile
243
- let dockerfilePath;
244
- const customDockerfile = buildConfig.dockerfile;
245
-
246
- if (customDockerfile && !options.forceTemplate) {
247
- const customPath = path.join(process.cwd(), 'builder', appName, customDockerfile);
248
- if (fsSync.existsSync(customPath)) {
249
- dockerfilePath = customPath;
250
- console.log(chalk.green(`✓ Using custom Dockerfile: ${customDockerfile}`));
251
- }
389
+ if (!hasExistingDockerfile) {
390
+ logger.log(chalk.green(`✓ Detected language: ${language}`));
252
391
  }
253
392
 
254
- if (!dockerfilePath || options.forceTemplate) {
255
- // Generate Dockerfile from template
256
- const builderPath = path.join(process.cwd(), 'builder', appName);
257
- dockerfilePath = await generateDockerfile(builderPath, language, config);
258
- console.log(chalk.green(`✓ Generated Dockerfile from template: .aifabrix/Dockerfile.${language}`));
259
- }
260
-
261
- // 4. Determine build context
262
- const contextPath = resolveContextPath(
263
- path.join(process.cwd(), 'builder', appName),
264
- buildConfig.context
265
- );
393
+ // 5. Determine Dockerfile (needs context path to generate in correct location)
394
+ const dockerfilePath = await determineDockerfile(appName, {
395
+ language,
396
+ config,
397
+ buildConfig,
398
+ contextPath,
399
+ forceTemplate: options.forceTemplate
400
+ });
266
401
 
267
- // 5. Build Docker image
402
+ // 6. Build Docker image
268
403
  const tag = options.tag || 'latest';
269
- await executeDockerBuild(imageName, dockerfilePath, contextPath, tag);
270
-
271
- // 6. Tag image if additional tag provided
272
- if (options.tag && options.tag !== 'latest') {
273
- await execAsync(`docker tag ${imageName}:${tag} ${imageName}:latest`);
274
- console.log(chalk.green(`✓ Tagged image: ${imageName}:latest`));
275
- }
404
+ await executeBuild(imageName, dockerfilePath, contextPath, tag, options);
276
405
 
277
- // 7. Generate .env file
278
- try {
279
- const envPath = await secrets.generateEnvFile(appName, buildConfig.secrets);
280
- console.log(chalk.green(`✓ Generated .env file: ${envPath}`));
281
-
282
- // Copy to output path if specified
283
- if (buildConfig.envOutputPath) {
284
- const outputPath = path.resolve(path.join(process.cwd(), 'builder', appName), buildConfig.envOutputPath);
285
- const outputDir = path.dirname(outputPath);
286
-
287
- if (!fsSync.existsSync(outputDir)) {
288
- await fs.mkdir(outputDir, { recursive: true });
289
- }
290
-
291
- await fs.copyFile(envPath, outputPath);
292
- console.log(chalk.green(`✓ Copied .env to: ${buildConfig.envOutputPath}`));
293
- }
294
- } catch (error) {
295
- console.log(chalk.yellow(`⚠️ Warning: Could not generate .env file: ${error.message}`));
296
- }
406
+ // 7. Post-build tasks
407
+ await postBuildTasks(appName, buildConfig);
297
408
 
298
- console.log(chalk.green('\n✅ Build completed successfully!'));
409
+ logger.log(chalk.green('\n✅ Build completed successfully!'));
299
410
  return `${imageName}:${tag}`;
300
411
 
301
412
  } catch (error) {
package/lib/cli.js CHANGED
@@ -15,12 +15,33 @@ 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 chalk = require('chalk');
19
+ const logger = require('./utils/logger');
20
+ const { validateCommand, handleCommandError } = require('./utils/cli-utils');
21
+ const { handleLogin } = require('./commands/login');
18
22
 
19
23
  /**
20
24
  * Sets up all CLI commands on the Commander program instance
21
25
  * @param {Command} program - Commander program instance
22
26
  */
23
27
  function setupCommands(program) {
28
+ // Authentication command
29
+ program.command('login')
30
+ .description('Authenticate with Miso Controller')
31
+ .option('-u, --url <url>', 'Controller URL', 'http://localhost:3000')
32
+ .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
+ .action(async(options) => {
37
+ try {
38
+ await handleLogin(options);
39
+ } catch (error) {
40
+ logger.error(chalk.red('\n❌ Login failed:'), error.message);
41
+ process.exit(1);
42
+ }
43
+ });
44
+
24
45
  // Infrastructure commands
25
46
  program.command('up')
26
47
  .description('Start local infrastructure services (Postgres, Redis, pgAdmin, Redis Commander)')
@@ -58,8 +79,10 @@ function setupCommands(program) {
58
79
  .option('-s, --storage', 'Requires file storage')
59
80
  .option('-a, --authentication', 'Requires authentication/RBAC')
60
81
  .option('-l, --language <lang>', 'Runtime language (typescript/python)')
61
- .option('-t, --template <name>', 'Template to use')
82
+ .option('-t, --template <name>', 'Template to use (e.g., miso-controller, keycloak)')
83
+ .option('--app', 'Generate minimal application files (package.json, index.ts or requirements.txt, main.py)')
62
84
  .option('-g, --github', 'Generate GitHub Actions workflows')
85
+ .option('--github-steps <steps>', 'Extra GitHub workflow steps (comma-separated, e.g., npm,test)')
63
86
  .option('--main-branch <branch>', 'Main branch name for workflows', 'main')
64
87
  .action(async(appName, options) => {
65
88
  try {
@@ -78,7 +101,7 @@ function setupCommands(program) {
78
101
  .action(async(appName, options) => {
79
102
  try {
80
103
  const imageTag = await app.buildApp(appName, options);
81
- console.log(`✅ Built image: ${imageTag}`);
104
+ logger.log(`✅ Built image: ${imageTag}`);
82
105
  } catch (error) {
83
106
  handleCommandError(error, 'build');
84
107
  process.exit(1);
@@ -113,8 +136,12 @@ function setupCommands(program) {
113
136
 
114
137
  program.command('deploy <app>')
115
138
  .description('Deploy to Azure via Miso Controller')
116
- .option('-c, --controller <url>', 'Controller URL (required)')
117
- .option('-e, --environment <env>', 'Target environment (dev/tst/pro)')
139
+ .option('-c, --controller <url>', 'Controller URL')
140
+ .option('-e, --environment <env>', 'Environment (dev, tst, pro)', 'dev')
141
+ .option('--client-id <id>', 'Client ID (overrides config)')
142
+ .option('--client-secret <secret>', 'Client Secret (overrides config)')
143
+ .option('--poll', 'Poll for deployment status', true)
144
+ .option('--no-poll', 'Do not poll for status')
118
145
  .action(async(appName, options) => {
119
146
  try {
120
147
  await app.deployApp(appName, options);
@@ -130,32 +157,32 @@ function setupCommands(program) {
130
157
  .action(async() => {
131
158
  try {
132
159
  const result = await validator.checkEnvironment();
133
- console.log('\n🔍 AI Fabrix Environment Check\n');
160
+ logger.log('\n🔍 AI Fabrix Environment Check\n');
134
161
 
135
- console.log(`Docker: ${result.docker === 'ok' ? '✅ Running' : '❌ Not available'}`);
136
- console.log(`Ports: ${result.ports === 'ok' ? '✅ Available' : '⚠️ Some ports in use'}`);
137
- console.log(`Secrets: ${result.secrets === 'ok' ? '✅ Configured' : '❌ Missing'}`);
162
+ logger.log(`Docker: ${result.docker === 'ok' ? '✅ Running' : '❌ Not available'}`);
163
+ logger.log(`Ports: ${result.ports === 'ok' ? '✅ Available' : '⚠️ Some ports in use'}`);
164
+ logger.log(`Secrets: ${result.secrets === 'ok' ? '✅ Configured' : '❌ Missing'}`);
138
165
 
139
166
  if (result.recommendations.length > 0) {
140
- console.log('\n📋 Recommendations:');
141
- result.recommendations.forEach(rec => console.log(` • ${rec}`));
167
+ logger.log('\n📋 Recommendations:');
168
+ result.recommendations.forEach(rec => logger.log(` • ${rec}`));
142
169
  }
143
170
 
144
171
  // Check infrastructure health if Docker is available
145
172
  if (result.docker === 'ok') {
146
173
  try {
147
174
  const health = await infra.checkInfraHealth();
148
- console.log('\n🏥 Infrastructure Health:');
175
+ logger.log('\n🏥 Infrastructure Health:');
149
176
  Object.entries(health).forEach(([service, status]) => {
150
177
  const icon = status === 'healthy' ? '✅' : status === 'unknown' ? '❓' : '❌';
151
- console.log(` ${icon} ${service}: ${status}`);
178
+ logger.log(` ${icon} ${service}: ${status}`);
152
179
  });
153
180
  } catch (error) {
154
- console.log('\n🏥 Infrastructure: Not running');
181
+ logger.log('\n🏥 Infrastructure: Not running');
155
182
  }
156
183
  }
157
184
 
158
- console.log('');
185
+ logger.log('');
159
186
  } catch (error) {
160
187
  handleCommandError(error, 'doctor');
161
188
  process.exit(1);
@@ -167,15 +194,15 @@ function setupCommands(program) {
167
194
  .action(async() => {
168
195
  try {
169
196
  const status = await infra.getInfraStatus();
170
- console.log('\n📊 Infrastructure Status\n');
197
+ logger.log('\n📊 Infrastructure Status\n');
171
198
 
172
199
  Object.entries(status).forEach(([service, info]) => {
173
200
  const icon = info.status === 'running' ? '✅' : '❌';
174
- console.log(`${icon} ${service}:`);
175
- console.log(` Status: ${info.status}`);
176
- console.log(` Port: ${info.port}`);
177
- console.log(` URL: ${info.url}`);
178
- console.log('');
201
+ logger.log(`${icon} ${service}:`);
202
+ logger.log(` Status: ${info.status}`);
203
+ logger.log(` Port: ${info.port}`);
204
+ logger.log(` URL: ${info.url}`);
205
+ logger.log('');
179
206
  });
180
207
  } catch (error) {
181
208
  handleCommandError(error, 'status');
@@ -188,7 +215,7 @@ function setupCommands(program) {
188
215
  .action(async(service) => {
189
216
  try {
190
217
  await infra.restartService(service);
191
- console.log(`✅ ${service} service restarted successfully`);
218
+ logger.log(`✅ ${service} service restarted successfully`);
192
219
  } catch (error) {
193
220
  handleCommandError(error, 'restart');
194
221
  process.exit(1);
@@ -198,10 +225,11 @@ function setupCommands(program) {
198
225
  // Utility commands
199
226
  program.command('resolve <app>')
200
227
  .description('Generate .env file from template')
201
- .action(async(appName) => {
228
+ .option('-f, --force', 'Generate missing secret keys in secrets file')
229
+ .action(async(appName, options) => {
202
230
  try {
203
- const envPath = await secrets.generateEnvFile(appName);
204
- console.log(`✓ Generated .env file: ${envPath}`);
231
+ const envPath = await secrets.generateEnvFile(appName, undefined, 'local', options.force);
232
+ logger.log(`✓ Generated .env file: ${envPath}`);
205
233
  } catch (error) {
206
234
  handleCommandError(error, 'resolve');
207
235
  process.exit(1);
@@ -214,15 +242,17 @@ function setupCommands(program) {
214
242
  try {
215
243
  const result = await generator.generateDeployJsonWithValidation(appName);
216
244
  if (result.success) {
217
- console.log(`✓ Generated deployment JSON: ${result.path}`);
245
+ logger.log(`✓ Generated deployment JSON: ${result.path}`);
218
246
 
219
- if (result.validation.warnings.length > 0) {
220
- console.log('\n⚠️ Warnings:');
221
- result.validation.warnings.forEach(warning => console.log(` • ${warning}`));
247
+ if (result.validation.warnings && result.validation.warnings.length > 0) {
248
+ logger.log('\n⚠️ Warnings:');
249
+ result.validation.warnings.forEach(warning => logger.log(` • ${warning}`));
222
250
  }
223
251
  } else {
224
- console.log('❌ Validation failed:');
225
- result.validation.errors.forEach(error => console.log(` • ${error}`));
252
+ logger.log('❌ Validation failed:');
253
+ if (result.validation.errors && result.validation.errors.length > 0) {
254
+ result.validation.errors.forEach(error => logger.log(` • ${error}`));
255
+ }
226
256
  process.exit(1);
227
257
  }
228
258
  } catch (error) {
@@ -236,68 +266,28 @@ function setupCommands(program) {
236
266
  .action(async(appName) => {
237
267
  try {
238
268
  const key = await keyGenerator.generateDeploymentKey(appName);
239
- console.log(`\nDeployment key for ${appName}:`);
240
- console.log(key);
241
- console.log(`\nGenerated from: builder/${appName}/variables.yaml`);
269
+ logger.log(`\nDeployment key for ${appName}:`);
270
+ logger.log(key);
242
271
  } catch (error) {
243
272
  handleCommandError(error, 'genkey');
244
273
  process.exit(1);
245
274
  }
246
275
  });
247
- }
248
-
249
- /**
250
- * Validates command arguments and provides helpful error messages
251
- * @param {string} command - Command name
252
- * @param {Object} options - Command options
253
- * @returns {boolean} True if valid
254
- */
255
- function validateCommand(_command, _options) {
256
- // TODO: Implement command validation
257
- // TODO: Add helpful error messages for common issues
258
- return true;
259
- }
260
276
 
261
- /**
262
- * Handles command execution errors with user-friendly messages
263
- * @param {Error} error - The error that occurred
264
- * @param {string} command - Command that failed
265
- */
266
- function handleCommandError(error, command) {
267
- console.error(`\n❌ Error in ${command} command:`);
268
-
269
- // Provide specific error messages for common issues
270
- if (error.message.includes('Docker')) {
271
- console.error(' Docker is not running or not installed.');
272
- console.error(' Please start Docker Desktop and try again.');
273
- } else if (error.message.includes('port')) {
274
- console.error(' Port conflict detected.');
275
- console.error(' Run "aifabrix doctor" to check which ports are in use.');
276
- } else if (error.message.includes('permission')) {
277
- console.error(' Permission denied.');
278
- console.error(' Make sure you have the necessary permissions to run Docker commands.');
279
- } else if (error.message.includes('Azure CLI') || error.message.includes('az --version')) {
280
- console.error(' Azure CLI is not installed.');
281
- console.error(' Install from: https://docs.microsoft.com/cli/azure/install-azure-cli');
282
- console.error(' Run: az login');
283
- } else if (error.message.includes('authenticate') || error.message.includes('ACR')) {
284
- console.error(' Azure Container Registry authentication failed.');
285
- console.error(' Run: az acr login --name <registry-name>');
286
- console.error(' Or login to Azure: az login');
287
- } else if (error.message.includes('not found locally') || error.message.includes('not found')) {
288
- console.error(' Docker image not found.');
289
- console.error(' Run: aifabrix build <app> first');
290
- } else if (error.message.includes('Invalid ACR URL') || error.message.includes('Expected format')) {
291
- console.error(' Invalid registry URL format.');
292
- console.error(' Use format: *.azurecr.io (e.g., myacr.azurecr.io)');
293
- } else if (error.message.includes('Registry URL is required')) {
294
- console.error(' Registry URL is required.');
295
- console.error(' Provide via --registry flag or configure in variables.yaml under image.registry');
296
- } else {
297
- console.error(` ${error.message}`);
298
- }
299
-
300
- console.error('\n💡 Run "aifabrix doctor" for environment diagnostics.\n');
277
+ program.command('dockerfile <app>')
278
+ .description('Generate Dockerfile for an application')
279
+ .option('-l, --language <lang>', 'Override language detection')
280
+ .option('-f, --force', 'Overwrite existing Dockerfile')
281
+ .action(async(appName, options) => {
282
+ try {
283
+ const dockerfilePath = await app.generateDockerfileForApp(appName, options);
284
+ logger.log(chalk.green('\n✅ Dockerfile generated successfully!'));
285
+ logger.log(chalk.gray(`Location: ${dockerfilePath}`));
286
+ } catch (error) {
287
+ handleCommandError(error, 'dockerfile');
288
+ process.exit(1);
289
+ }
290
+ });
301
291
  }
302
292
 
303
293
  module.exports = {