@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.
- package/README.md +5 -3
- package/bin/aifabrix.js +9 -3
- package/jest.config.integration.js +30 -0
- package/lib/app-config.js +157 -0
- package/lib/app-deploy.js +233 -82
- package/lib/app-dockerfile.js +112 -0
- package/lib/app-prompts.js +244 -0
- package/lib/app-push.js +172 -0
- package/lib/app-run.js +235 -144
- package/lib/app.js +208 -274
- package/lib/audit-logger.js +2 -0
- package/lib/build.js +177 -125
- package/lib/cli.js +76 -86
- package/lib/commands/app.js +414 -0
- package/lib/commands/login.js +304 -0
- package/lib/config.js +78 -0
- package/lib/deployer.js +225 -81
- package/lib/env-reader.js +45 -30
- package/lib/generator.js +308 -191
- package/lib/github-generator.js +67 -7
- package/lib/infra.js +156 -61
- package/lib/push.js +105 -10
- package/lib/schema/application-schema.json +30 -2
- package/lib/schema/env-config.yaml +9 -1
- package/lib/schema/infrastructure-schema.json +589 -0
- package/lib/secrets.js +229 -24
- package/lib/template-validator.js +205 -0
- package/lib/templates.js +305 -170
- package/lib/utils/api.js +329 -0
- package/lib/utils/cli-utils.js +97 -0
- package/lib/utils/compose-generator.js +185 -0
- package/lib/utils/docker-build.js +173 -0
- package/lib/utils/dockerfile-utils.js +131 -0
- package/lib/utils/environment-checker.js +125 -0
- package/lib/utils/error-formatter.js +61 -0
- package/lib/utils/health-check.js +187 -0
- package/lib/utils/logger.js +53 -0
- package/lib/utils/template-helpers.js +223 -0
- package/lib/utils/variable-transformer.js +271 -0
- package/lib/validator.js +27 -112
- package/package.json +14 -10
- package/templates/README.md +75 -3
- package/templates/applications/keycloak/Dockerfile +36 -0
- package/templates/applications/keycloak/env.template +32 -0
- package/templates/applications/keycloak/rbac.yaml +37 -0
- package/templates/applications/keycloak/variables.yaml +56 -0
- package/templates/applications/miso-controller/Dockerfile +125 -0
- package/templates/applications/miso-controller/env.template +129 -0
- package/templates/applications/miso-controller/rbac.yaml +214 -0
- package/templates/applications/miso-controller/variables.yaml +56 -0
- package/templates/github/release.yaml.hbs +5 -26
- package/templates/github/steps/npm.hbs +24 -0
- package/templates/infra/compose.yaml +6 -6
- package/templates/python/docker-compose.hbs +19 -12
- package/templates/python/main.py +80 -0
- package/templates/python/requirements.txt +4 -0
- package/templates/typescript/Dockerfile.hbs +2 -2
- package/templates/typescript/docker-compose.hbs +19 -12
- package/templates/typescript/index.ts +116 -0
- package/templates/typescript/package.json +26 -0
- 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}
|
|
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('
|
|
160
|
-
* // Returns: '
|
|
127
|
+
* const dockerfilePath = await generateDockerfile('myapp', 'typescript', config);
|
|
128
|
+
* // Returns: '~/.aifabrix/myapp/Dockerfile.typescript'
|
|
161
129
|
*/
|
|
162
|
-
async function generateDockerfile(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
170
|
-
const
|
|
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
|
|
152
|
+
const dockerfileContent = dockerfileUtils.renderDockerfile(template, templateVars, language, isAppFlag, appSourcePath);
|
|
183
153
|
|
|
184
|
-
|
|
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
|
-
|
|
309
|
+
logger.log(chalk.blue(`\n🔨 Building application: ${appName}`));
|
|
217
310
|
|
|
218
311
|
// 1. Load and validate configuration
|
|
219
|
-
const config = await
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
//
|
|
231
|
-
const
|
|
232
|
-
const
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
//
|
|
343
|
+
// 6. Build Docker image
|
|
268
344
|
const tag = options.tag || 'latest';
|
|
269
|
-
await
|
|
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.
|
|
278
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
117
|
-
.option('-e, --environment <env>', '
|
|
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
|
-
|
|
160
|
+
logger.log('\n🔍 AI Fabrix Environment Check\n');
|
|
134
161
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
141
|
-
result.recommendations.forEach(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
|
-
|
|
175
|
+
logger.log('\n🏥 Infrastructure Health:');
|
|
149
176
|
Object.entries(health).forEach(([service, status]) => {
|
|
150
177
|
const icon = status === 'healthy' ? '✅' : status === 'unknown' ? '❓' : '❌';
|
|
151
|
-
|
|
178
|
+
logger.log(` ${icon} ${service}: ${status}`);
|
|
152
179
|
});
|
|
153
180
|
} catch (error) {
|
|
154
|
-
|
|
181
|
+
logger.log('\n🏥 Infrastructure: Not running');
|
|
155
182
|
}
|
|
156
183
|
}
|
|
157
184
|
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
245
|
+
logger.log(`✓ Generated deployment JSON: ${result.path}`);
|
|
218
246
|
|
|
219
|
-
if (result.validation.warnings.length > 0) {
|
|
220
|
-
|
|
221
|
-
result.validation.warnings.forEach(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
|
-
|
|
225
|
-
result.validation.errors
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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 = {
|