@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/LICENSE +21 -0
- package/README.md +75 -0
- package/bin/aifabrix.js +51 -0
- package/lib/app-deploy.js +209 -0
- package/lib/app-run.js +291 -0
- package/lib/app.js +472 -0
- package/lib/audit-logger.js +162 -0
- package/lib/build.js +313 -0
- package/lib/cli.js +307 -0
- package/lib/deployer.js +256 -0
- package/lib/env-reader.js +250 -0
- package/lib/generator.js +361 -0
- package/lib/github-generator.js +220 -0
- package/lib/infra.js +300 -0
- package/lib/key-generator.js +93 -0
- package/lib/push.js +141 -0
- package/lib/schema/application-schema.json +649 -0
- package/lib/schema/env-config.yaml +15 -0
- package/lib/secrets.js +282 -0
- package/lib/templates.js +301 -0
- package/lib/validator.js +377 -0
- package/package.json +59 -0
- package/templates/README.md +51 -0
- package/templates/github/ci.yaml.hbs +15 -0
- package/templates/github/pr-checks.yaml.hbs +35 -0
- package/templates/github/release.yaml.hbs +79 -0
- package/templates/github/test.hbs +11 -0
- package/templates/github/test.yaml.hbs +11 -0
- package/templates/infra/compose.yaml +93 -0
- package/templates/python/Dockerfile.hbs +49 -0
- package/templates/python/docker-compose.hbs +69 -0
- package/templates/typescript/Dockerfile.hbs +46 -0
- package/templates/typescript/docker-compose.hbs +69 -0
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
|
+
};
|