@aifabrix/builder 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- 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 +334 -133
- package/lib/app.js +208 -274
- package/lib/audit-logger.js +2 -0
- package/lib/build.js +209 -98
- 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/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/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 +13 -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 +168 -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,15 @@
|
|
|
13
13
|
const fs = require('fs').promises;
|
|
14
14
|
const fsSync = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
|
+
const os = require('os');
|
|
16
17
|
const { exec } = require('child_process');
|
|
17
18
|
const { promisify } = require('util');
|
|
18
19
|
const chalk = require('chalk');
|
|
19
20
|
const yaml = require('js-yaml');
|
|
20
|
-
const handlebars = require('handlebars');
|
|
21
|
-
const validator = require('./validator');
|
|
22
21
|
const secrets = require('./secrets');
|
|
22
|
+
const logger = require('./utils/logger');
|
|
23
|
+
const validator = require('./validator');
|
|
24
|
+
const dockerfileUtils = require('./utils/dockerfile-utils');
|
|
23
25
|
|
|
24
26
|
const execAsync = promisify(exec);
|
|
25
27
|
|
|
@@ -69,6 +71,34 @@ function resolveContextPath(builderPath, contextPath) {
|
|
|
69
71
|
return resolvedPath;
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Checks if error indicates Docker is not running or not installed
|
|
76
|
+
* @param {string} errorMessage - Error message to check
|
|
77
|
+
* @returns {boolean} True if Docker is not available
|
|
78
|
+
*/
|
|
79
|
+
function isDockerNotAvailableError(errorMessage) {
|
|
80
|
+
return errorMessage.includes('docker: command not found') ||
|
|
81
|
+
errorMessage.includes('Cannot connect to the Docker daemon') ||
|
|
82
|
+
errorMessage.includes('Is the docker daemon running') ||
|
|
83
|
+
errorMessage.includes('Cannot connect to Docker');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Handles Docker build errors and provides user-friendly messages
|
|
88
|
+
* @param {Error} error - Build error
|
|
89
|
+
* @throws {Error} Formatted error message
|
|
90
|
+
*/
|
|
91
|
+
function handleBuildError(error) {
|
|
92
|
+
const errorMessage = error.message || error.stderr || String(error);
|
|
93
|
+
|
|
94
|
+
if (isDockerNotAvailableError(errorMessage)) {
|
|
95
|
+
throw new Error('Docker is not running or not installed. Please start Docker Desktop and try again.');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const detailedError = error.stderr || error.stdout || errorMessage;
|
|
99
|
+
throw new Error(`Docker build failed: ${detailedError}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
72
102
|
/**
|
|
73
103
|
* Executes Docker build command with proper error handling
|
|
74
104
|
* @param {string} imageName - Image name to build
|
|
@@ -82,26 +112,22 @@ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag) {
|
|
|
82
112
|
const dockerCommand = `docker build -t ${imageName}:${tag} -f "${dockerfilePath}" "${contextPath}"`;
|
|
83
113
|
|
|
84
114
|
try {
|
|
85
|
-
|
|
86
|
-
|
|
115
|
+
logger.log(chalk.blue('Building image...'));
|
|
116
|
+
logger.log(chalk.gray(`Command: ${dockerCommand}`));
|
|
87
117
|
|
|
88
118
|
const { stdout, stderr } = await execAsync(dockerCommand);
|
|
89
119
|
|
|
90
120
|
if (stderr && !stderr.includes('warning')) {
|
|
91
|
-
|
|
121
|
+
logger.log(chalk.yellow(stderr));
|
|
92
122
|
}
|
|
93
123
|
|
|
94
124
|
if (stdout) {
|
|
95
|
-
|
|
125
|
+
logger.log(stdout);
|
|
96
126
|
}
|
|
97
127
|
|
|
98
|
-
|
|
128
|
+
logger.log(chalk.green(`✓ Image built: ${imageName}:${tag}`));
|
|
99
129
|
} catch (error) {
|
|
100
|
-
|
|
101
|
-
throw new Error('Docker is not running or not installed. Please start Docker Desktop.');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
throw new Error(`Docker build failed: ${error.message}`);
|
|
130
|
+
handleBuildError(error);
|
|
105
131
|
}
|
|
106
132
|
}
|
|
107
133
|
|
|
@@ -146,53 +172,179 @@ function detectLanguage(appPath) {
|
|
|
146
172
|
/**
|
|
147
173
|
* Generates a Dockerfile from template based on detected language
|
|
148
174
|
* Uses Handlebars templates to create optimized Dockerfiles
|
|
175
|
+
* Dockerfiles are stored in ~/.aifabrix/{appName}/ directory
|
|
149
176
|
*
|
|
150
177
|
* @async
|
|
151
178
|
* @function generateDockerfile
|
|
152
|
-
* @param {string}
|
|
179
|
+
* @param {string} appNameOrPath - Application name or path (backward compatibility)
|
|
153
180
|
* @param {string} language - Target language ('typescript', 'python')
|
|
154
181
|
* @param {Object} config - Application configuration from variables.yaml
|
|
155
182
|
* @returns {Promise<string>} Path to generated Dockerfile
|
|
156
183
|
* @throws {Error} If template generation fails
|
|
157
184
|
*
|
|
158
185
|
* @example
|
|
159
|
-
* const dockerfilePath = await generateDockerfile('
|
|
160
|
-
* // Returns: '
|
|
186
|
+
* const dockerfilePath = await generateDockerfile('myapp', 'typescript', config);
|
|
187
|
+
* // Returns: '~/.aifabrix/myapp/Dockerfile.typescript'
|
|
161
188
|
*/
|
|
162
|
-
async function generateDockerfile(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
189
|
+
async function generateDockerfile(appNameOrPath, language, config, buildConfig = {}) {
|
|
190
|
+
let appName;
|
|
191
|
+
if (appNameOrPath.includes(path.sep) || appNameOrPath.includes('/') || appNameOrPath.includes('\\')) {
|
|
192
|
+
appName = path.basename(appNameOrPath);
|
|
193
|
+
} else {
|
|
194
|
+
appName = appNameOrPath;
|
|
167
195
|
}
|
|
168
196
|
|
|
169
|
-
const
|
|
170
|
-
const
|
|
197
|
+
const template = dockerfileUtils.loadDockerfileTemplate(language);
|
|
198
|
+
const isAppFlag = buildConfig.context === '../..';
|
|
199
|
+
const appSourcePath = isAppFlag ? `apps/${appName}/` : '.';
|
|
171
200
|
|
|
172
|
-
// Prepare template variables
|
|
173
201
|
const templateVars = {
|
|
174
202
|
port: config.port || 3000,
|
|
175
203
|
healthCheck: {
|
|
176
204
|
interval: config.healthCheck?.interval || 30,
|
|
177
205
|
path: config.healthCheck?.path || '/health'
|
|
178
206
|
},
|
|
179
|
-
startupCommand: config.startupCommand
|
|
207
|
+
startupCommand: config.startupCommand,
|
|
208
|
+
appSourcePath: appSourcePath
|
|
180
209
|
};
|
|
181
210
|
|
|
182
|
-
const dockerfileContent = template
|
|
211
|
+
const dockerfileContent = dockerfileUtils.renderDockerfile(template, templateVars, language, isAppFlag, appSourcePath);
|
|
183
212
|
|
|
184
|
-
|
|
185
|
-
const aifabrixDir = path.join(appPath, '.aifabrix');
|
|
213
|
+
const aifabrixDir = path.join(os.homedir(), '.aifabrix', appName);
|
|
186
214
|
if (!fsSync.existsSync(aifabrixDir)) {
|
|
187
215
|
await fs.mkdir(aifabrixDir, { recursive: true });
|
|
188
216
|
}
|
|
189
217
|
|
|
190
218
|
const dockerfilePath = path.join(aifabrixDir, `Dockerfile.${language}`);
|
|
191
|
-
await fs.writeFile(dockerfilePath, dockerfileContent);
|
|
219
|
+
await fs.writeFile(dockerfilePath, dockerfileContent, 'utf8');
|
|
220
|
+
|
|
221
|
+
return dockerfilePath;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Determines Dockerfile path, generating from template if needed
|
|
226
|
+
* @async
|
|
227
|
+
* @param {string} appName - Application name
|
|
228
|
+
* @param {Object} options - Dockerfile determination options
|
|
229
|
+
* @param {string} options.language - Application language
|
|
230
|
+
* @param {Object} options.config - Application configuration
|
|
231
|
+
* @param {Object} options.buildConfig - Build configuration
|
|
232
|
+
* @param {string} options.contextPath - Build context path (absolute)
|
|
233
|
+
* @param {boolean} options.forceTemplate - Force template flag
|
|
234
|
+
* @returns {Promise<string>} Path to Dockerfile
|
|
235
|
+
*/
|
|
236
|
+
async function determineDockerfile(appName, options) {
|
|
237
|
+
const builderPath = path.join(process.cwd(), 'builder', appName);
|
|
238
|
+
|
|
239
|
+
const templateDockerfile = dockerfileUtils.checkTemplateDockerfile(builderPath, appName, options.forceTemplate);
|
|
240
|
+
if (templateDockerfile) {
|
|
241
|
+
logger.log(chalk.green(`✓ Using existing Dockerfile: builder/${appName}/Dockerfile`));
|
|
242
|
+
return templateDockerfile;
|
|
243
|
+
}
|
|
192
244
|
|
|
245
|
+
const customDockerfile = dockerfileUtils.checkProjectDockerfile(builderPath, appName, options.buildConfig, options.contextPath, options.forceTemplate);
|
|
246
|
+
if (customDockerfile) {
|
|
247
|
+
logger.log(chalk.green(`✓ Using custom Dockerfile: ${options.buildConfig.dockerfile}`));
|
|
248
|
+
return customDockerfile;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const dockerfilePath = await generateDockerfile(appName, options.language, options.config, options.buildConfig);
|
|
252
|
+
const relativePath = path.relative(process.cwd(), dockerfilePath);
|
|
253
|
+
logger.log(chalk.green(`✓ Generated Dockerfile from template: ${relativePath}`));
|
|
193
254
|
return dockerfilePath;
|
|
194
255
|
}
|
|
195
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Prepares build context path
|
|
259
|
+
* @param {string} appName - Application name
|
|
260
|
+
* @param {string} contextPath - Relative context path
|
|
261
|
+
* @returns {string} Absolute context path
|
|
262
|
+
*/
|
|
263
|
+
function prepareBuildContext(appName, contextPath) {
|
|
264
|
+
// Ensure contextPath is a string
|
|
265
|
+
const context = typeof contextPath === 'string' ? contextPath : (contextPath || '');
|
|
266
|
+
return resolveContextPath(
|
|
267
|
+
path.join(process.cwd(), 'builder', appName),
|
|
268
|
+
context
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Loads and validates configuration for build
|
|
274
|
+
* @async
|
|
275
|
+
* @param {string} appName - Application name
|
|
276
|
+
* @returns {Promise<Object>} Configuration object with config, imageName, and buildConfig
|
|
277
|
+
* @throws {Error} If configuration cannot be loaded or validated
|
|
278
|
+
*/
|
|
279
|
+
async function loadAndValidateConfig(appName) {
|
|
280
|
+
const variables = await loadVariablesYaml(appName);
|
|
281
|
+
|
|
282
|
+
// Validate configuration
|
|
283
|
+
const validation = await validator.validateVariables(appName);
|
|
284
|
+
if (!validation.valid) {
|
|
285
|
+
throw new Error(`Configuration validation failed:\n${validation.errors.join('\n')}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Extract image name
|
|
289
|
+
let imageName;
|
|
290
|
+
if (typeof variables.image === 'string') {
|
|
291
|
+
imageName = variables.image.split(':')[0];
|
|
292
|
+
} else if (variables.image?.name) {
|
|
293
|
+
imageName = variables.image.name;
|
|
294
|
+
} else if (variables.app?.key) {
|
|
295
|
+
imageName = variables.app.key;
|
|
296
|
+
} else {
|
|
297
|
+
imageName = appName;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Extract build config
|
|
301
|
+
const buildConfig = variables.build || {};
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
config: variables,
|
|
305
|
+
imageName,
|
|
306
|
+
buildConfig
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Executes Docker build and handles tagging
|
|
312
|
+
* @async
|
|
313
|
+
* @param {string} imageName - Image name
|
|
314
|
+
* @param {string} dockerfilePath - Path to Dockerfile
|
|
315
|
+
* @param {string} contextPath - Build context path
|
|
316
|
+
* @param {string} tag - Image tag
|
|
317
|
+
* @param {Object} options - Build options
|
|
318
|
+
*/
|
|
319
|
+
async function executeBuild(imageName, dockerfilePath, contextPath, tag, options) {
|
|
320
|
+
await executeDockerBuild(imageName, dockerfilePath, contextPath, tag);
|
|
321
|
+
|
|
322
|
+
// Tag image if additional tag provided
|
|
323
|
+
if (options.tag && options.tag !== 'latest') {
|
|
324
|
+
await execAsync(`docker tag ${imageName}:${tag} ${imageName}:latest`);
|
|
325
|
+
logger.log(chalk.green(`✓ Tagged image: ${imageName}:latest`));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function postBuildTasks(appName, buildConfig) {
|
|
330
|
+
try {
|
|
331
|
+
const envPath = await secrets.generateEnvFile(appName, buildConfig.secrets);
|
|
332
|
+
logger.log(chalk.green(`✓ Generated .env file: ${envPath}`));
|
|
333
|
+
if (buildConfig.envOutputPath) {
|
|
334
|
+
const builderPath = path.join(process.cwd(), 'builder', appName);
|
|
335
|
+
const outputPath = path.resolve(builderPath, buildConfig.envOutputPath);
|
|
336
|
+
const outputDir = path.dirname(outputPath);
|
|
337
|
+
if (!fsSync.existsSync(outputDir)) {
|
|
338
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
339
|
+
}
|
|
340
|
+
await fs.copyFile(envPath, outputPath);
|
|
341
|
+
logger.log(chalk.green(`✓ Copied .env to: ${buildConfig.envOutputPath}`));
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
logger.log(chalk.yellow(`⚠️ Warning: Could not generate .env file: ${error.message}`));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
196
348
|
/**
|
|
197
349
|
* Builds a container image for the specified application
|
|
198
350
|
* Auto-detects runtime and generates Dockerfile if needed
|
|
@@ -213,89 +365,48 @@ async function generateDockerfile(appPath, language, config) {
|
|
|
213
365
|
*/
|
|
214
366
|
async function buildApp(appName, options = {}) {
|
|
215
367
|
try {
|
|
216
|
-
|
|
368
|
+
logger.log(chalk.blue(`\n🔨 Building application: ${appName}`));
|
|
217
369
|
|
|
218
370
|
// 1. Load and validate configuration
|
|
219
|
-
const config = await
|
|
220
|
-
console.log(chalk.green(`✓ Loaded configuration from builder/${appName}/variables.yaml`));
|
|
221
|
-
|
|
222
|
-
// Validate configuration
|
|
223
|
-
const validation = await validator.validateVariables(appName);
|
|
224
|
-
if (!validation.valid) {
|
|
225
|
-
console.log(chalk.red('❌ Configuration validation failed:'));
|
|
226
|
-
validation.errors.forEach(error => console.log(chalk.red(` - ${error}`)));
|
|
227
|
-
throw new Error('Configuration validation failed');
|
|
228
|
-
}
|
|
371
|
+
const { config, imageName, buildConfig } = await loadAndValidateConfig(appName);
|
|
229
372
|
|
|
230
|
-
//
|
|
231
|
-
const
|
|
232
|
-
const buildConfig = config.build || {};
|
|
373
|
+
// 2. Prepare build context
|
|
374
|
+
const contextPath = prepareBuildContext(appName, buildConfig.context);
|
|
233
375
|
|
|
234
|
-
//
|
|
376
|
+
// 3. Check if Dockerfile exists in builder/{appName}/ directory
|
|
377
|
+
const builderPath = path.join(process.cwd(), 'builder', appName);
|
|
378
|
+
const appDockerfilePath = path.join(builderPath, 'Dockerfile');
|
|
379
|
+
const hasExistingDockerfile = fsSync.existsSync(appDockerfilePath) && !options.forceTemplate;
|
|
380
|
+
|
|
381
|
+
// 4. Determine language (skip if existing Dockerfile found)
|
|
235
382
|
let language = options.language || buildConfig.language;
|
|
236
|
-
if (!language) {
|
|
237
|
-
const builderPath = path.join(process.cwd(), 'builder', appName);
|
|
383
|
+
if (!language && !hasExistingDockerfile) {
|
|
238
384
|
language = detectLanguage(builderPath);
|
|
385
|
+
} else if (!language) {
|
|
386
|
+
// Default language if existing Dockerfile is found (won't be used, but needed for API)
|
|
387
|
+
language = 'typescript';
|
|
239
388
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
// 3. Determine Dockerfile
|
|
243
|
-
let dockerfilePath;
|
|
244
|
-
const customDockerfile = buildConfig.dockerfile;
|
|
245
|
-
|
|
246
|
-
if (customDockerfile && !options.forceTemplate) {
|
|
247
|
-
const customPath = path.join(process.cwd(), 'builder', appName, customDockerfile);
|
|
248
|
-
if (fsSync.existsSync(customPath)) {
|
|
249
|
-
dockerfilePath = customPath;
|
|
250
|
-
console.log(chalk.green(`✓ Using custom Dockerfile: ${customDockerfile}`));
|
|
251
|
-
}
|
|
389
|
+
if (!hasExistingDockerfile) {
|
|
390
|
+
logger.log(chalk.green(`✓ Detected language: ${language}`));
|
|
252
391
|
}
|
|
253
392
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const contextPath = resolveContextPath(
|
|
263
|
-
path.join(process.cwd(), 'builder', appName),
|
|
264
|
-
buildConfig.context
|
|
265
|
-
);
|
|
393
|
+
// 5. Determine Dockerfile (needs context path to generate in correct location)
|
|
394
|
+
const dockerfilePath = await determineDockerfile(appName, {
|
|
395
|
+
language,
|
|
396
|
+
config,
|
|
397
|
+
buildConfig,
|
|
398
|
+
contextPath,
|
|
399
|
+
forceTemplate: options.forceTemplate
|
|
400
|
+
});
|
|
266
401
|
|
|
267
|
-
//
|
|
402
|
+
// 6. Build Docker image
|
|
268
403
|
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
|
-
}
|
|
404
|
+
await executeBuild(imageName, dockerfilePath, contextPath, tag, options);
|
|
276
405
|
|
|
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
|
-
}
|
|
406
|
+
// 7. Post-build tasks
|
|
407
|
+
await postBuildTasks(appName, buildConfig);
|
|
297
408
|
|
|
298
|
-
|
|
409
|
+
logger.log(chalk.green('\n✅ Build completed successfully!'));
|
|
299
410
|
return `${imageName}:${tag}`;
|
|
300
411
|
|
|
301
412
|
} catch (error) {
|
package/lib/cli.js
CHANGED
|
@@ -15,12 +15,33 @@ const secrets = require('./secrets');
|
|
|
15
15
|
const generator = require('./generator');
|
|
16
16
|
const validator = require('./validator');
|
|
17
17
|
const keyGenerator = require('./key-generator');
|
|
18
|
+
const chalk = require('chalk');
|
|
19
|
+
const logger = require('./utils/logger');
|
|
20
|
+
const { validateCommand, handleCommandError } = require('./utils/cli-utils');
|
|
21
|
+
const { handleLogin } = require('./commands/login');
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
24
|
* Sets up all CLI commands on the Commander program instance
|
|
21
25
|
* @param {Command} program - Commander program instance
|
|
22
26
|
*/
|
|
23
27
|
function setupCommands(program) {
|
|
28
|
+
// Authentication command
|
|
29
|
+
program.command('login')
|
|
30
|
+
.description('Authenticate with Miso Controller')
|
|
31
|
+
.option('-u, --url <url>', 'Controller URL', 'http://localhost:3000')
|
|
32
|
+
.option('-m, --method <method>', 'Authentication method (device|credentials)')
|
|
33
|
+
.option('--client-id <id>', 'Client ID (for credentials method)')
|
|
34
|
+
.option('--client-secret <secret>', 'Client Secret (for credentials method)')
|
|
35
|
+
.option('-e, --environment <env>', 'Environment key (for device method, e.g., dev, tst, pro)')
|
|
36
|
+
.action(async(options) => {
|
|
37
|
+
try {
|
|
38
|
+
await handleLogin(options);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
logger.error(chalk.red('\n❌ Login failed:'), error.message);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
24
45
|
// Infrastructure commands
|
|
25
46
|
program.command('up')
|
|
26
47
|
.description('Start local infrastructure services (Postgres, Redis, pgAdmin, Redis Commander)')
|
|
@@ -58,8 +79,10 @@ function setupCommands(program) {
|
|
|
58
79
|
.option('-s, --storage', 'Requires file storage')
|
|
59
80
|
.option('-a, --authentication', 'Requires authentication/RBAC')
|
|
60
81
|
.option('-l, --language <lang>', 'Runtime language (typescript/python)')
|
|
61
|
-
.option('-t, --template <name>', 'Template to use')
|
|
82
|
+
.option('-t, --template <name>', 'Template to use (e.g., miso-controller, keycloak)')
|
|
83
|
+
.option('--app', 'Generate minimal application files (package.json, index.ts or requirements.txt, main.py)')
|
|
62
84
|
.option('-g, --github', 'Generate GitHub Actions workflows')
|
|
85
|
+
.option('--github-steps <steps>', 'Extra GitHub workflow steps (comma-separated, e.g., npm,test)')
|
|
63
86
|
.option('--main-branch <branch>', 'Main branch name for workflows', 'main')
|
|
64
87
|
.action(async(appName, options) => {
|
|
65
88
|
try {
|
|
@@ -78,7 +101,7 @@ function setupCommands(program) {
|
|
|
78
101
|
.action(async(appName, options) => {
|
|
79
102
|
try {
|
|
80
103
|
const imageTag = await app.buildApp(appName, options);
|
|
81
|
-
|
|
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 = {
|