@aifabrix/builder 2.0.0 → 2.0.3

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 (61) hide show
  1. package/README.md +5 -3
  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 +235 -144
  10. package/lib/app.js +208 -274
  11. package/lib/audit-logger.js +2 -0
  12. package/lib/build.js +177 -125
  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/env-config.yaml +9 -1
  25. package/lib/schema/infrastructure-schema.json +589 -0
  26. package/lib/secrets.js +229 -24
  27. package/lib/template-validator.js +205 -0
  28. package/lib/templates.js +305 -170
  29. package/lib/utils/api.js +329 -0
  30. package/lib/utils/cli-utils.js +97 -0
  31. package/lib/utils/compose-generator.js +185 -0
  32. package/lib/utils/docker-build.js +173 -0
  33. package/lib/utils/dockerfile-utils.js +131 -0
  34. package/lib/utils/environment-checker.js +125 -0
  35. package/lib/utils/error-formatter.js +61 -0
  36. package/lib/utils/health-check.js +187 -0
  37. package/lib/utils/logger.js +53 -0
  38. package/lib/utils/template-helpers.js +223 -0
  39. package/lib/utils/variable-transformer.js +271 -0
  40. package/lib/validator.js +27 -112
  41. package/package.json +14 -10
  42. package/templates/README.md +75 -3
  43. package/templates/applications/keycloak/Dockerfile +36 -0
  44. package/templates/applications/keycloak/env.template +32 -0
  45. package/templates/applications/keycloak/rbac.yaml +37 -0
  46. package/templates/applications/keycloak/variables.yaml +56 -0
  47. package/templates/applications/miso-controller/Dockerfile +125 -0
  48. package/templates/applications/miso-controller/env.template +129 -0
  49. package/templates/applications/miso-controller/rbac.yaml +214 -0
  50. package/templates/applications/miso-controller/variables.yaml +56 -0
  51. package/templates/github/release.yaml.hbs +5 -26
  52. package/templates/github/steps/npm.hbs +24 -0
  53. package/templates/infra/compose.yaml +6 -6
  54. package/templates/python/docker-compose.hbs +19 -12
  55. package/templates/python/main.py +80 -0
  56. package/templates/python/requirements.txt +4 -0
  57. package/templates/typescript/Dockerfile.hbs +2 -2
  58. package/templates/typescript/docker-compose.hbs +19 -12
  59. package/templates/typescript/index.ts +116 -0
  60. package/templates/typescript/package.json +26 -0
  61. package/templates/typescript/tsconfig.json +24 -0
package/lib/build.js CHANGED
@@ -13,13 +13,16 @@
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');
25
+ const dockerBuild = require('./utils/docker-build');
23
26
 
24
27
  const execAsync = promisify(exec);
25
28
 
@@ -69,42 +72,6 @@ function resolveContextPath(builderPath, contextPath) {
69
72
  return resolvedPath;
70
73
  }
71
74
 
72
- /**
73
- * Executes Docker build command with proper error handling
74
- * @param {string} imageName - Image name to build
75
- * @param {string} dockerfilePath - Path to Dockerfile
76
- * @param {string} contextPath - Build context path
77
- * @param {string} tag - Image tag
78
- * @returns {Promise<void>} Resolves when build completes
79
- * @throws {Error} If build fails
80
- */
81
- async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag) {
82
- const dockerCommand = `docker build -t ${imageName}:${tag} -f "${dockerfilePath}" "${contextPath}"`;
83
-
84
- try {
85
- console.log(chalk.blue('Building image...'));
86
- console.log(chalk.gray(`Command: ${dockerCommand}`));
87
-
88
- const { stdout, stderr } = await execAsync(dockerCommand);
89
-
90
- if (stderr && !stderr.includes('warning')) {
91
- console.log(chalk.yellow(stderr));
92
- }
93
-
94
- if (stdout) {
95
- console.log(stdout);
96
- }
97
-
98
- console.log(chalk.green(`✓ Image built: ${imageName}:${tag}`));
99
- } 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}`);
105
- }
106
- }
107
-
108
75
  /**
109
76
  * Detects the runtime language of an application
110
77
  * Analyzes project files to determine TypeScript, Python, etc.
@@ -146,53 +113,179 @@ function detectLanguage(appPath) {
146
113
  /**
147
114
  * Generates a Dockerfile from template based on detected language
148
115
  * Uses Handlebars templates to create optimized Dockerfiles
116
+ * Dockerfiles are stored in ~/.aifabrix/{appName}/ directory
149
117
  *
150
118
  * @async
151
119
  * @function generateDockerfile
152
- * @param {string} appPath - Path to application directory
120
+ * @param {string} appNameOrPath - Application name or path (backward compatibility)
153
121
  * @param {string} language - Target language ('typescript', 'python')
154
122
  * @param {Object} config - Application configuration from variables.yaml
155
123
  * @returns {Promise<string>} Path to generated Dockerfile
156
124
  * @throws {Error} If template generation fails
157
125
  *
158
126
  * @example
159
- * const dockerfilePath = await generateDockerfile('./myapp', 'typescript', config);
160
- * // Returns: './myapp/.aifabrix/Dockerfile.typescript'
127
+ * const dockerfilePath = await generateDockerfile('myapp', 'typescript', config);
128
+ * // Returns: '~/.aifabrix/myapp/Dockerfile.typescript'
161
129
  */
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}`);
130
+ async function generateDockerfile(appNameOrPath, language, config, buildConfig = {}) {
131
+ let appName;
132
+ if (appNameOrPath.includes(path.sep) || appNameOrPath.includes('/') || appNameOrPath.includes('\\')) {
133
+ appName = path.basename(appNameOrPath);
134
+ } else {
135
+ appName = appNameOrPath;
167
136
  }
168
137
 
169
- const templateContent = fsSync.readFileSync(templatePath, 'utf8');
170
- const template = handlebars.compile(templateContent);
138
+ const template = dockerfileUtils.loadDockerfileTemplate(language);
139
+ const isAppFlag = buildConfig.context === '../..';
140
+ const appSourcePath = isAppFlag ? `apps/${appName}/` : '.';
171
141
 
172
- // Prepare template variables
173
142
  const templateVars = {
174
143
  port: config.port || 3000,
175
144
  healthCheck: {
176
145
  interval: config.healthCheck?.interval || 30,
177
146
  path: config.healthCheck?.path || '/health'
178
147
  },
179
- startupCommand: config.startupCommand
148
+ startupCommand: config.startupCommand,
149
+ appSourcePath: appSourcePath
180
150
  };
181
151
 
182
- const dockerfileContent = template(templateVars);
152
+ const dockerfileContent = dockerfileUtils.renderDockerfile(template, templateVars, language, isAppFlag, appSourcePath);
183
153
 
184
- // Create .aifabrix directory if it doesn't exist
185
- const aifabrixDir = path.join(appPath, '.aifabrix');
154
+ const aifabrixDir = path.join(os.homedir(), '.aifabrix', appName);
186
155
  if (!fsSync.existsSync(aifabrixDir)) {
187
156
  await fs.mkdir(aifabrixDir, { recursive: true });
188
157
  }
189
158
 
190
159
  const dockerfilePath = path.join(aifabrixDir, `Dockerfile.${language}`);
191
- await fs.writeFile(dockerfilePath, dockerfileContent);
160
+ await fs.writeFile(dockerfilePath, dockerfileContent, 'utf8');
192
161
 
193
162
  return dockerfilePath;
194
163
  }
195
164
 
165
+ /**
166
+ * Determines Dockerfile path, generating from template if needed
167
+ * @async
168
+ * @param {string} appName - Application name
169
+ * @param {Object} options - Dockerfile determination options
170
+ * @param {string} options.language - Application language
171
+ * @param {Object} options.config - Application configuration
172
+ * @param {Object} options.buildConfig - Build configuration
173
+ * @param {string} options.contextPath - Build context path (absolute)
174
+ * @param {boolean} options.forceTemplate - Force template flag
175
+ * @returns {Promise<string>} Path to Dockerfile
176
+ */
177
+ async function determineDockerfile(appName, options) {
178
+ const builderPath = path.join(process.cwd(), 'builder', appName);
179
+
180
+ const templateDockerfile = dockerfileUtils.checkTemplateDockerfile(builderPath, appName, options.forceTemplate);
181
+ if (templateDockerfile) {
182
+ logger.log(chalk.green(`✓ Using existing Dockerfile: builder/${appName}/Dockerfile`));
183
+ return templateDockerfile;
184
+ }
185
+
186
+ const customDockerfile = dockerfileUtils.checkProjectDockerfile(builderPath, appName, options.buildConfig, options.contextPath, options.forceTemplate);
187
+ if (customDockerfile) {
188
+ logger.log(chalk.green(`✓ Using custom Dockerfile: ${options.buildConfig.dockerfile}`));
189
+ return customDockerfile;
190
+ }
191
+
192
+ const dockerfilePath = await generateDockerfile(appName, options.language, options.config, options.buildConfig);
193
+ const relativePath = path.relative(process.cwd(), dockerfilePath);
194
+ logger.log(chalk.green(`✓ Generated Dockerfile from template: ${relativePath}`));
195
+ return dockerfilePath;
196
+ }
197
+
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
+ /**
214
+ * Loads and validates configuration for build
215
+ * @async
216
+ * @param {string} appName - Application name
217
+ * @returns {Promise<Object>} Configuration object with config, imageName, and buildConfig
218
+ * @throws {Error} If configuration cannot be loaded or validated
219
+ */
220
+ async function loadAndValidateConfig(appName) {
221
+ const variables = await loadVariablesYaml(appName);
222
+
223
+ // Validate configuration
224
+ const validation = await validator.validateVariables(appName);
225
+ if (!validation.valid) {
226
+ throw new Error(`Configuration validation failed:\n${validation.errors.join('\n')}`);
227
+ }
228
+
229
+ // Extract image name
230
+ let imageName;
231
+ if (typeof variables.image === 'string') {
232
+ imageName = variables.image.split(':')[0];
233
+ } else if (variables.image?.name) {
234
+ imageName = variables.image.name;
235
+ } else if (variables.app?.key) {
236
+ imageName = variables.app.key;
237
+ } else {
238
+ imageName = appName;
239
+ }
240
+
241
+ // Extract build config
242
+ const buildConfig = variables.build || {};
243
+
244
+ return {
245
+ config: variables,
246
+ imageName,
247
+ buildConfig
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Executes Docker build and handles tagging
253
+ * @async
254
+ * @param {string} imageName - Image name
255
+ * @param {string} dockerfilePath - Path to Dockerfile
256
+ * @param {string} contextPath - Build context path
257
+ * @param {string} tag - Image tag
258
+ * @param {Object} options - Build options
259
+ */
260
+ async function executeBuild(imageName, dockerfilePath, contextPath, tag, options) {
261
+ await dockerBuild.executeDockerBuild(imageName, dockerfilePath, contextPath, tag);
262
+
263
+ // Tag image if additional tag provided
264
+ if (options.tag && options.tag !== 'latest') {
265
+ await execAsync(`docker tag ${imageName}:${tag} ${imageName}:latest`);
266
+ logger.log(chalk.green(`✓ Tagged image: ${imageName}:latest`));
267
+ }
268
+ }
269
+
270
+ async function postBuildTasks(appName, buildConfig) {
271
+ try {
272
+ const envPath = await secrets.generateEnvFile(appName, buildConfig.secrets);
273
+ logger.log(chalk.green(`✓ Generated .env file: ${envPath}`));
274
+ if (buildConfig.envOutputPath) {
275
+ const builderPath = path.join(process.cwd(), 'builder', appName);
276
+ const outputPath = path.resolve(builderPath, buildConfig.envOutputPath);
277
+ const outputDir = path.dirname(outputPath);
278
+ if (!fsSync.existsSync(outputDir)) {
279
+ await fs.mkdir(outputDir, { recursive: true });
280
+ }
281
+ await fs.copyFile(envPath, outputPath);
282
+ logger.log(chalk.green(`✓ Copied .env to: ${buildConfig.envOutputPath}`));
283
+ }
284
+ } catch (error) {
285
+ logger.log(chalk.yellow(`⚠️ Warning: Could not generate .env file: ${error.message}`));
286
+ }
287
+ }
288
+
196
289
  /**
197
290
  * Builds a container image for the specified application
198
291
  * Auto-detects runtime and generates Dockerfile if needed
@@ -213,89 +306,48 @@ async function generateDockerfile(appPath, language, config) {
213
306
  */
214
307
  async function buildApp(appName, options = {}) {
215
308
  try {
216
- console.log(chalk.blue(`\n🔨 Building application: ${appName}`));
309
+ logger.log(chalk.blue(`\n🔨 Building application: ${appName}`));
217
310
 
218
311
  // 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
- }
312
+ const { config, imageName, buildConfig } = await loadAndValidateConfig(appName);
313
+
314
+ // 2. Prepare build context
315
+ const contextPath = prepareBuildContext(appName, buildConfig.context);
229
316
 
230
- // Extract configuration values
231
- const imageName = config.image?.split(':')[0] || appName;
232
- const buildConfig = config.build || {};
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');
320
+ const hasExistingDockerfile = fsSync.existsSync(appDockerfilePath) && !options.forceTemplate;
233
321
 
234
- // 2. Determine language
322
+ // 4. Determine language (skip if existing Dockerfile found)
235
323
  let language = options.language || buildConfig.language;
236
- if (!language) {
237
- const builderPath = path.join(process.cwd(), 'builder', appName);
324
+ if (!language && !hasExistingDockerfile) {
238
325
  language = detectLanguage(builderPath);
326
+ } else if (!language) {
327
+ // Default language if existing Dockerfile is found (won't be used, but needed for API)
328
+ language = 'typescript';
239
329
  }
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
- }
252
- }
253
-
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}`));
330
+ if (!hasExistingDockerfile) {
331
+ logger.log(chalk.green(`✓ Detected language: ${language}`));
259
332
  }
260
333
 
261
- // 4. Determine build context
262
- const contextPath = resolveContextPath(
263
- path.join(process.cwd(), 'builder', appName),
264
- buildConfig.context
265
- );
334
+ // 5. Determine Dockerfile (needs context path to generate in correct location)
335
+ const dockerfilePath = await determineDockerfile(appName, {
336
+ language,
337
+ config,
338
+ buildConfig,
339
+ contextPath,
340
+ forceTemplate: options.forceTemplate
341
+ });
266
342
 
267
- // 5. Build Docker image
343
+ // 6. Build Docker image
268
344
  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
- }
345
+ await executeBuild(imageName, dockerfilePath, contextPath, tag, options);
276
346
 
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
- }
347
+ // 7. Post-build tasks
348
+ await postBuildTasks(appName, buildConfig);
297
349
 
298
- console.log(chalk.green('\n✅ Build completed successfully!'));
350
+ logger.log(chalk.green('\n✅ Build completed successfully!'));
299
351
  return `${imageName}:${tag}`;
300
352
 
301
353
  } catch (error) {
@@ -306,7 +358,7 @@ async function buildApp(appName, options = {}) {
306
358
  module.exports = {
307
359
  loadVariablesYaml,
308
360
  resolveContextPath,
309
- executeDockerBuild,
361
+ executeDockerBuild: dockerBuild.executeDockerBuild,
310
362
  detectLanguage,
311
363
  generateDockerfile,
312
364
  buildApp
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 = {