@aifabrix/builder 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/build.js ADDED
@@ -0,0 +1,313 @@
1
+ /**
2
+ * AI Fabrix Builder Build Functions
3
+ *
4
+ * This module handles application building, Docker image creation,
5
+ * and Dockerfile generation. Separated from app.js to maintain
6
+ * file size limits and improve code organization.
7
+ *
8
+ * @fileoverview Build functions for AI Fabrix Builder
9
+ * @author AI Fabrix Team
10
+ * @version 2.0.0
11
+ */
12
+
13
+ const fs = require('fs').promises;
14
+ const fsSync = require('fs');
15
+ const path = require('path');
16
+ const { exec } = require('child_process');
17
+ const { promisify } = require('util');
18
+ const chalk = require('chalk');
19
+ const yaml = require('js-yaml');
20
+ const handlebars = require('handlebars');
21
+ const validator = require('./validator');
22
+ const secrets = require('./secrets');
23
+
24
+ const execAsync = promisify(exec);
25
+
26
+ /**
27
+ * Loads variables.yaml configuration for an application
28
+ * @param {string} appName - Application name
29
+ * @returns {Promise<Object>} Configuration object
30
+ * @throws {Error} If file cannot be loaded or parsed
31
+ */
32
+ async function loadVariablesYaml(appName) {
33
+ const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
34
+
35
+ if (!fsSync.existsSync(variablesPath)) {
36
+ throw new Error(`Configuration not found. Run 'aifabrix create ${appName}' first.`);
37
+ }
38
+
39
+ const content = fsSync.readFileSync(variablesPath, 'utf8');
40
+ let variables;
41
+
42
+ try {
43
+ variables = yaml.load(content);
44
+ } catch (error) {
45
+ throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
46
+ }
47
+
48
+ return variables;
49
+ }
50
+
51
+ /**
52
+ * Resolves build context path relative to builder directory
53
+ * @param {string} builderPath - Path to builder directory
54
+ * @param {string} contextPath - Relative context path
55
+ * @returns {string} Absolute context path
56
+ * @throws {Error} If context path doesn't exist
57
+ */
58
+ function resolveContextPath(builderPath, contextPath) {
59
+ if (!contextPath) {
60
+ return process.cwd();
61
+ }
62
+
63
+ const resolvedPath = path.resolve(builderPath, contextPath);
64
+
65
+ if (!fsSync.existsSync(resolvedPath)) {
66
+ throw new Error(`Build context not found: ${resolvedPath}`);
67
+ }
68
+
69
+ return resolvedPath;
70
+ }
71
+
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
+ /**
109
+ * Detects the runtime language of an application
110
+ * Analyzes project files to determine TypeScript, Python, etc.
111
+ *
112
+ * @function detectLanguage
113
+ * @param {string} appPath - Path to application directory
114
+ * @returns {string} Detected language ('typescript', 'python', etc.)
115
+ * @throws {Error} If language cannot be detected
116
+ *
117
+ * @example
118
+ * const language = detectLanguage('./myapp');
119
+ * // Returns: 'typescript'
120
+ */
121
+ function detectLanguage(appPath) {
122
+ const packageJsonPath = path.join(appPath, 'package.json');
123
+ const requirementsPath = path.join(appPath, 'requirements.txt');
124
+ const pyprojectPath = path.join(appPath, 'pyproject.toml');
125
+ const dockerfilePath = path.join(appPath, 'Dockerfile');
126
+
127
+ // Check for package.json (TypeScript/Node.js)
128
+ if (fsSync.existsSync(packageJsonPath)) {
129
+ return 'typescript';
130
+ }
131
+
132
+ // Check for requirements.txt or pyproject.toml (Python)
133
+ if (fsSync.existsSync(requirementsPath) || fsSync.existsSync(pyprojectPath)) {
134
+ return 'python';
135
+ }
136
+
137
+ // Check for custom Dockerfile
138
+ if (fsSync.existsSync(dockerfilePath)) {
139
+ throw new Error('Custom Dockerfile found. Use --force-template to regenerate from template.');
140
+ }
141
+
142
+ // Default to typescript if no indicators found
143
+ return 'typescript';
144
+ }
145
+
146
+ /**
147
+ * Generates a Dockerfile from template based on detected language
148
+ * Uses Handlebars templates to create optimized Dockerfiles
149
+ *
150
+ * @async
151
+ * @function generateDockerfile
152
+ * @param {string} appPath - Path to application directory
153
+ * @param {string} language - Target language ('typescript', 'python')
154
+ * @param {Object} config - Application configuration from variables.yaml
155
+ * @returns {Promise<string>} Path to generated Dockerfile
156
+ * @throws {Error} If template generation fails
157
+ *
158
+ * @example
159
+ * const dockerfilePath = await generateDockerfile('./myapp', 'typescript', config);
160
+ * // Returns: './myapp/.aifabrix/Dockerfile.typescript'
161
+ */
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}`);
167
+ }
168
+
169
+ const templateContent = fsSync.readFileSync(templatePath, 'utf8');
170
+ const template = handlebars.compile(templateContent);
171
+
172
+ // Prepare template variables
173
+ const templateVars = {
174
+ port: config.port || 3000,
175
+ healthCheck: {
176
+ interval: config.healthCheck?.interval || 30,
177
+ path: config.healthCheck?.path || '/health'
178
+ },
179
+ startupCommand: config.startupCommand
180
+ };
181
+
182
+ const dockerfileContent = template(templateVars);
183
+
184
+ // Create .aifabrix directory if it doesn't exist
185
+ const aifabrixDir = path.join(appPath, '.aifabrix');
186
+ if (!fsSync.existsSync(aifabrixDir)) {
187
+ await fs.mkdir(aifabrixDir, { recursive: true });
188
+ }
189
+
190
+ const dockerfilePath = path.join(aifabrixDir, `Dockerfile.${language}`);
191
+ await fs.writeFile(dockerfilePath, dockerfileContent);
192
+
193
+ return dockerfilePath;
194
+ }
195
+
196
+ /**
197
+ * Builds a container image for the specified application
198
+ * Auto-detects runtime and generates Dockerfile if needed
199
+ *
200
+ * @async
201
+ * @function buildApp
202
+ * @param {string} appName - Name of the application to build
203
+ * @param {Object} options - Build options
204
+ * @param {string} [options.language] - Override language detection
205
+ * @param {boolean} [options.forceTemplate] - Force rebuild from template
206
+ * @param {string} [options.tag] - Image tag (default: latest)
207
+ * @returns {Promise<string>} Image tag that was built
208
+ * @throws {Error} If build fails or app configuration is invalid
209
+ *
210
+ * @example
211
+ * const imageTag = await buildApp('myapp', { language: 'typescript' });
212
+ * // Returns: 'myapp:latest'
213
+ */
214
+ async function buildApp(appName, options = {}) {
215
+ try {
216
+ console.log(chalk.blue(`\nšŸ”Ø Building application: ${appName}`));
217
+
218
+ // 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
+ }
229
+
230
+ // Extract configuration values
231
+ const imageName = config.image?.split(':')[0] || appName;
232
+ const buildConfig = config.build || {};
233
+
234
+ // 2. Determine language
235
+ let language = options.language || buildConfig.language;
236
+ if (!language) {
237
+ const builderPath = path.join(process.cwd(), 'builder', appName);
238
+ language = detectLanguage(builderPath);
239
+ }
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}`));
259
+ }
260
+
261
+ // 4. Determine build context
262
+ const contextPath = resolveContextPath(
263
+ path.join(process.cwd(), 'builder', appName),
264
+ buildConfig.context
265
+ );
266
+
267
+ // 5. Build Docker image
268
+ 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
+ }
276
+
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
+ }
297
+
298
+ console.log(chalk.green('\nāœ… Build completed successfully!'));
299
+ return `${imageName}:${tag}`;
300
+
301
+ } catch (error) {
302
+ throw new Error(`Build failed: ${error.message}`);
303
+ }
304
+ }
305
+
306
+ module.exports = {
307
+ loadVariablesYaml,
308
+ resolveContextPath,
309
+ executeDockerBuild,
310
+ detectLanguage,
311
+ generateDockerfile,
312
+ buildApp
313
+ };
package/lib/cli.js ADDED
@@ -0,0 +1,307 @@
1
+ /**
2
+ * AI Fabrix Builder CLI Command Definitions
3
+ *
4
+ * This module defines all CLI commands using Commander.js.
5
+ * Commands: up, down, build, run, push, deploy, resolve, json, genkey, doctor
6
+ *
7
+ * @fileoverview Command definitions for AI Fabrix Builder CLI
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const infra = require('./infra');
13
+ const app = require('./app');
14
+ const secrets = require('./secrets');
15
+ const generator = require('./generator');
16
+ const validator = require('./validator');
17
+ const keyGenerator = require('./key-generator');
18
+
19
+ /**
20
+ * Sets up all CLI commands on the Commander program instance
21
+ * @param {Command} program - Commander program instance
22
+ */
23
+ function setupCommands(program) {
24
+ // Infrastructure commands
25
+ program.command('up')
26
+ .description('Start local infrastructure services (Postgres, Redis, pgAdmin, Redis Commander)')
27
+ .action(async() => {
28
+ try {
29
+ await infra.startInfra();
30
+ } catch (error) {
31
+ handleCommandError(error, 'up');
32
+ process.exit(1);
33
+ }
34
+ });
35
+
36
+ program.command('down')
37
+ .description('Stop and remove local infrastructure services')
38
+ .option('-v, --volumes', 'Remove volumes (deletes all data)')
39
+ .action(async(options) => {
40
+ try {
41
+ if (options.volumes) {
42
+ await infra.stopInfraWithVolumes();
43
+ } else {
44
+ await infra.stopInfra();
45
+ }
46
+ } catch (error) {
47
+ handleCommandError(error, 'down');
48
+ process.exit(1);
49
+ }
50
+ });
51
+
52
+ // Application commands
53
+ program.command('create <app>')
54
+ .description('Create new application with configuration files')
55
+ .option('-p, --port <port>', 'Application port', '3000')
56
+ .option('-d, --database', 'Requires database')
57
+ .option('-r, --redis', 'Requires Redis')
58
+ .option('-s, --storage', 'Requires file storage')
59
+ .option('-a, --authentication', 'Requires authentication/RBAC')
60
+ .option('-l, --language <lang>', 'Runtime language (typescript/python)')
61
+ .option('-t, --template <name>', 'Template to use')
62
+ .option('-g, --github', 'Generate GitHub Actions workflows')
63
+ .option('--main-branch <branch>', 'Main branch name for workflows', 'main')
64
+ .action(async(appName, options) => {
65
+ try {
66
+ await app.createApp(appName, options);
67
+ } catch (error) {
68
+ handleCommandError(error, 'create');
69
+ process.exit(1);
70
+ }
71
+ });
72
+
73
+ program.command('build <app>')
74
+ .description('Build container image (auto-detects runtime)')
75
+ .option('-l, --language <lang>', 'Override language detection')
76
+ .option('-f, --force-template', 'Force rebuild from template')
77
+ .option('-t, --tag <tag>', 'Image tag (default: latest)')
78
+ .action(async(appName, options) => {
79
+ try {
80
+ const imageTag = await app.buildApp(appName, options);
81
+ console.log(`āœ… Built image: ${imageTag}`);
82
+ } catch (error) {
83
+ handleCommandError(error, 'build');
84
+ process.exit(1);
85
+ }
86
+ });
87
+
88
+ program.command('run <app>')
89
+ .description('Run application locally')
90
+ .option('-p, --port <port>', 'Override local port')
91
+ .action(async(appName, options) => {
92
+ try {
93
+ await app.runApp(appName, options);
94
+ } catch (error) {
95
+ handleCommandError(error, 'run');
96
+ process.exit(1);
97
+ }
98
+ });
99
+
100
+ // Deployment commands
101
+ program.command('push <app>')
102
+ .description('Push image to Azure Container Registry')
103
+ .option('-r, --registry <registry>', 'ACR registry URL (overrides variables.yaml)')
104
+ .option('-t, --tag <tag>', 'Image tag(s) - comma-separated for multiple (default: latest)')
105
+ .action(async(appName, options) => {
106
+ try {
107
+ await app.pushApp(appName, options);
108
+ } catch (error) {
109
+ handleCommandError(error, 'push');
110
+ process.exit(1);
111
+ }
112
+ });
113
+
114
+ program.command('deploy <app>')
115
+ .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)')
118
+ .action(async(appName, options) => {
119
+ try {
120
+ await app.deployApp(appName, options);
121
+ } catch (error) {
122
+ handleCommandError(error, 'deploy');
123
+ process.exit(1);
124
+ }
125
+ });
126
+
127
+ // Infrastructure status and management
128
+ program.command('doctor')
129
+ .description('Check environment and configuration')
130
+ .action(async() => {
131
+ try {
132
+ const result = await validator.checkEnvironment();
133
+ console.log('\nšŸ” AI Fabrix Environment Check\n');
134
+
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'}`);
138
+
139
+ if (result.recommendations.length > 0) {
140
+ console.log('\nšŸ“‹ Recommendations:');
141
+ result.recommendations.forEach(rec => console.log(` • ${rec}`));
142
+ }
143
+
144
+ // Check infrastructure health if Docker is available
145
+ if (result.docker === 'ok') {
146
+ try {
147
+ const health = await infra.checkInfraHealth();
148
+ console.log('\nšŸ„ Infrastructure Health:');
149
+ Object.entries(health).forEach(([service, status]) => {
150
+ const icon = status === 'healthy' ? 'āœ…' : status === 'unknown' ? 'ā“' : 'āŒ';
151
+ console.log(` ${icon} ${service}: ${status}`);
152
+ });
153
+ } catch (error) {
154
+ console.log('\nšŸ„ Infrastructure: Not running');
155
+ }
156
+ }
157
+
158
+ console.log('');
159
+ } catch (error) {
160
+ handleCommandError(error, 'doctor');
161
+ process.exit(1);
162
+ }
163
+ });
164
+
165
+ program.command('status')
166
+ .description('Show detailed infrastructure service status')
167
+ .action(async() => {
168
+ try {
169
+ const status = await infra.getInfraStatus();
170
+ console.log('\nšŸ“Š Infrastructure Status\n');
171
+
172
+ Object.entries(status).forEach(([service, info]) => {
173
+ 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('');
179
+ });
180
+ } catch (error) {
181
+ handleCommandError(error, 'status');
182
+ process.exit(1);
183
+ }
184
+ });
185
+
186
+ program.command('restart <service>')
187
+ .description('Restart a specific infrastructure service')
188
+ .action(async(service) => {
189
+ try {
190
+ await infra.restartService(service);
191
+ console.log(`āœ… ${service} service restarted successfully`);
192
+ } catch (error) {
193
+ handleCommandError(error, 'restart');
194
+ process.exit(1);
195
+ }
196
+ });
197
+
198
+ // Utility commands
199
+ program.command('resolve <app>')
200
+ .description('Generate .env file from template')
201
+ .action(async(appName) => {
202
+ try {
203
+ const envPath = await secrets.generateEnvFile(appName);
204
+ console.log(`āœ“ Generated .env file: ${envPath}`);
205
+ } catch (error) {
206
+ handleCommandError(error, 'resolve');
207
+ process.exit(1);
208
+ }
209
+ });
210
+
211
+ program.command('json <app>')
212
+ .description('Generate deployment JSON')
213
+ .action(async(appName) => {
214
+ try {
215
+ const result = await generator.generateDeployJsonWithValidation(appName);
216
+ if (result.success) {
217
+ console.log(`āœ“ Generated deployment JSON: ${result.path}`);
218
+
219
+ if (result.validation.warnings.length > 0) {
220
+ console.log('\nāš ļø Warnings:');
221
+ result.validation.warnings.forEach(warning => console.log(` • ${warning}`));
222
+ }
223
+ } else {
224
+ console.log('āŒ Validation failed:');
225
+ result.validation.errors.forEach(error => console.log(` • ${error}`));
226
+ process.exit(1);
227
+ }
228
+ } catch (error) {
229
+ handleCommandError(error, 'json');
230
+ process.exit(1);
231
+ }
232
+ });
233
+
234
+ program.command('genkey <app>')
235
+ .description('Generate deployment key')
236
+ .action(async(appName) => {
237
+ try {
238
+ 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`);
242
+ } catch (error) {
243
+ handleCommandError(error, 'genkey');
244
+ process.exit(1);
245
+ }
246
+ });
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
+
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');
301
+ }
302
+
303
+ module.exports = {
304
+ setupCommands,
305
+ validateCommand,
306
+ handleCommandError
307
+ };