@aifabrix/builder 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/app.js ADDED
@@ -0,0 +1,472 @@
1
+ /**
2
+ * AI Fabrix Builder Application Management
3
+ *
4
+ * This module handles application building, running, and deployment.
5
+ * Includes runtime detection, Dockerfile generation, and container management.
6
+ *
7
+ * @fileoverview Application build and run management for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const fs = require('fs').promises;
13
+ const path = require('path');
14
+ const inquirer = require('inquirer');
15
+ const chalk = require('chalk');
16
+ const yaml = require('js-yaml');
17
+ const { generateVariablesYaml, generateEnvTemplate, generateRbacYaml } = require('./templates');
18
+ const { readExistingEnv, generateEnvTemplate: generateEnvTemplateFromReader } = require('./env-reader');
19
+ const build = require('./build');
20
+ const appRun = require('./app-run');
21
+ const pushUtils = require('./push');
22
+
23
+ /**
24
+ * Validate application name format
25
+ * @param {string} appName - Application name to validate
26
+ * @throws {Error} If app name is invalid
27
+ */
28
+ function validateAppName(appName) {
29
+ if (!appName || typeof appName !== 'string') {
30
+ throw new Error('Application name is required');
31
+ }
32
+
33
+ // App name should be lowercase, alphanumeric with dashes, 3-40 characters
34
+ const nameRegex = /^[a-z0-9-]{3,40}$/;
35
+ if (!nameRegex.test(appName)) {
36
+ throw new Error('Application name must be 3-40 characters, lowercase letters, numbers, and dashes only');
37
+ }
38
+
39
+ // Cannot start or end with dash
40
+ if (appName.startsWith('-') || appName.endsWith('-')) {
41
+ throw new Error('Application name cannot start or end with a dash');
42
+ }
43
+
44
+ // Cannot have consecutive dashes
45
+ if (appName.includes('--')) {
46
+ throw new Error('Application name cannot have consecutive dashes');
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Prompt for missing configuration options
52
+ * @param {string} appName - Application name
53
+ * @param {Object} options - Provided options
54
+ * @returns {Promise<Object>} Complete configuration
55
+ */
56
+ async function promptForOptions(appName, options) {
57
+ const questions = [];
58
+
59
+ // Port validation
60
+ if (!options.port) {
61
+ questions.push({
62
+ type: 'input',
63
+ name: 'port',
64
+ message: 'What port should the application run on?',
65
+ default: '3000',
66
+ validate: (input) => {
67
+ const port = parseInt(input, 10);
68
+ if (isNaN(port) || port < 1 || port > 65535) {
69
+ return 'Port must be a number between 1 and 65535';
70
+ }
71
+ return true;
72
+ }
73
+ });
74
+ }
75
+
76
+ // Language selection
77
+ if (!options.language) {
78
+ questions.push({
79
+ type: 'list',
80
+ name: 'language',
81
+ message: 'What language is your application written in?',
82
+ choices: [
83
+ { name: 'TypeScript/Node.js', value: 'typescript' },
84
+ { name: 'Python', value: 'python' }
85
+ ],
86
+ default: 'typescript'
87
+ });
88
+ }
89
+
90
+ // Service options
91
+ if (!Object.prototype.hasOwnProperty.call(options, 'database')) {
92
+ questions.push({
93
+ type: 'confirm',
94
+ name: 'database',
95
+ message: 'Does your application need a database?',
96
+ default: false
97
+ });
98
+ }
99
+
100
+ if (!Object.prototype.hasOwnProperty.call(options, 'redis')) {
101
+ questions.push({
102
+ type: 'confirm',
103
+ name: 'redis',
104
+ message: 'Does your application need Redis?',
105
+ default: false
106
+ });
107
+ }
108
+
109
+ if (!Object.prototype.hasOwnProperty.call(options, 'storage')) {
110
+ questions.push({
111
+ type: 'confirm',
112
+ name: 'storage',
113
+ message: 'Does your application need file storage?',
114
+ default: false
115
+ });
116
+ }
117
+
118
+ if (!Object.prototype.hasOwnProperty.call(options, 'authentication')) {
119
+ questions.push({
120
+ type: 'confirm',
121
+ name: 'authentication',
122
+ message: 'Does your application need authentication/RBAC?',
123
+ default: false
124
+ });
125
+ }
126
+
127
+ // Prompt for missing options
128
+ const answers = questions.length > 0 ? await inquirer.prompt(questions) : {};
129
+
130
+ // Merge provided options with answers
131
+ return {
132
+ appName,
133
+ port: parseInt(options.port || answers.port || 3000, 10),
134
+ language: options.language || answers.language || 'typescript',
135
+ database: options.database || answers.database || false,
136
+ redis: options.redis || answers.redis || false,
137
+ storage: options.storage || answers.storage || false,
138
+ authentication: options.authentication || answers.authentication || false
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Generate all configuration files for the application
144
+ * @param {string} appPath - Path to application directory
145
+ * @param {string} appName - Application name
146
+ * @param {Object} config - Application configuration
147
+ * @param {Object} existingEnv - Existing environment variables
148
+ */
149
+ async function generateConfigFiles(appPath, appName, config, existingEnv) {
150
+ try {
151
+ // Generate variables.yaml
152
+ const variablesYaml = generateVariablesYaml(appName, config);
153
+ await fs.writeFile(path.join(appPath, 'variables.yaml'), variablesYaml);
154
+
155
+ // Generate env.template
156
+ let envTemplate;
157
+ if (existingEnv) {
158
+ const envResult = await generateEnvTemplateFromReader(config, existingEnv);
159
+ envTemplate = envResult.template;
160
+
161
+ if (envResult.warnings.length > 0) {
162
+ console.log(chalk.yellow('\n⚠️ Environment conversion warnings:'));
163
+ envResult.warnings.forEach(warning => console.log(chalk.yellow(` - ${warning}`)));
164
+ }
165
+ } else {
166
+ envTemplate = generateEnvTemplate(config);
167
+ }
168
+ await fs.writeFile(path.join(appPath, 'env.template'), envTemplate);
169
+
170
+ // Generate rbac.yaml if authentication is enabled
171
+ if (config.authentication) {
172
+ const rbacYaml = generateRbacYaml(appName, config);
173
+ if (rbacYaml) {
174
+ await fs.writeFile(path.join(appPath, 'rbac.yaml'), rbacYaml);
175
+ }
176
+ }
177
+
178
+ // Generate aifabrix-deploy.json template
179
+ const deployJson = {
180
+ apiVersion: 'v1',
181
+ kind: 'ApplicationDeployment',
182
+ metadata: {
183
+ name: appName,
184
+ namespace: 'default'
185
+ },
186
+ spec: {
187
+ application: {
188
+ name: appName,
189
+ version: '1.0.0',
190
+ language: config.language,
191
+ port: config.port
192
+ },
193
+ services: {
194
+ database: config.database,
195
+ redis: config.redis,
196
+ storage: config.storage,
197
+ authentication: config.authentication
198
+ },
199
+ deployment: {
200
+ replicas: 1,
201
+ strategy: 'RollingUpdate'
202
+ }
203
+ }
204
+ };
205
+
206
+ await fs.writeFile(
207
+ path.join(appPath, 'aifabrix-deploy.json'),
208
+ JSON.stringify(deployJson, null, 2)
209
+ );
210
+
211
+ } catch (error) {
212
+ throw new Error(`Failed to generate configuration files: ${error.message}`);
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Creates new application with scaffolded configuration files
218
+ * Prompts for configuration options and generates builder/ folder structure
219
+ *
220
+ * @async
221
+ * @function createApp
222
+ * @param {string} appName - Name of the application to create
223
+ * @param {Object} options - Creation options
224
+ * @param {number} [options.port] - Application port
225
+ * @param {boolean} [options.database] - Requires database
226
+ * @param {boolean} [options.redis] - Requires Redis
227
+ * @param {boolean} [options.storage] - Requires file storage
228
+ * @param {boolean} [options.authentication] - Requires authentication/RBAC
229
+ * @param {string} [options.language] - Runtime language (typescript/python)
230
+ * @param {string} [options.template] - Template to use (platform for Keycloak/Miso)
231
+ * @returns {Promise<void>} Resolves when app is created
232
+ * @throws {Error} If creation fails
233
+ *
234
+ * @example
235
+ * await createApp('myapp', { port: 3000, database: true, language: 'typescript' });
236
+ * // Creates builder/ with variables.yaml, env.template, rbac.yaml
237
+ */
238
+ async function createApp(appName, options = {}) {
239
+ try {
240
+ // Validate app name format
241
+ validateAppName(appName);
242
+
243
+ // Check if directory already exists
244
+ const builderPath = path.join(process.cwd(), 'builder');
245
+ const appPath = path.join(builderPath, appName);
246
+
247
+ try {
248
+ await fs.access(appPath);
249
+ throw new Error(`Application '${appName}' already exists in builder/${appName}/`);
250
+ } catch (error) {
251
+ if (error.code !== 'ENOENT') {
252
+ throw error;
253
+ }
254
+ }
255
+
256
+ // Prompt for missing options
257
+ const config = await promptForOptions(appName, options);
258
+
259
+ // Create directory structure
260
+ await fs.mkdir(appPath, { recursive: true });
261
+
262
+ // Check for existing .env file
263
+ const existingEnv = await readExistingEnv(process.cwd());
264
+ let envConversionMessage = '';
265
+
266
+ if (existingEnv) {
267
+ envConversionMessage = '\n✓ Found existing .env file - sensitive values will be converted to kv:// references';
268
+ }
269
+
270
+ // Generate configuration files
271
+ await generateConfigFiles(appPath, appName, config, existingEnv);
272
+
273
+ // Generate GitHub workflows if requested
274
+ if (options.github) {
275
+ const githubGen = require('./github-generator');
276
+ const workflowFiles = await githubGen.generateGithubWorkflows(
277
+ process.cwd(),
278
+ config,
279
+ {
280
+ mainBranch: options.mainBranch || 'main',
281
+ uploadCoverage: true,
282
+ publishToNpm: options.template === 'platform'
283
+ }
284
+ );
285
+
286
+ console.log(chalk.green('✓ Generated GitHub Actions workflows:'));
287
+ workflowFiles.forEach(file => console.log(chalk.gray(` - ${file}`)));
288
+ }
289
+
290
+ // Display success message
291
+ console.log(chalk.green('\n✓ Application created successfully!'));
292
+ console.log(chalk.blue(`\nApplication: ${appName}`));
293
+ console.log(chalk.blue(`Location: builder/${appName}/`));
294
+ console.log(chalk.blue(`Language: ${config.language}`));
295
+ console.log(chalk.blue(`Port: ${config.port}`));
296
+
297
+ if (config.database) console.log(chalk.yellow(' - Database enabled'));
298
+ if (config.redis) console.log(chalk.yellow(' - Redis enabled'));
299
+ if (config.storage) console.log(chalk.yellow(' - Storage enabled'));
300
+ if (config.authentication) console.log(chalk.yellow(' - Authentication enabled'));
301
+
302
+ console.log(chalk.gray(envConversionMessage));
303
+
304
+ console.log(chalk.green('\nNext steps:'));
305
+ console.log(chalk.white('1. Copy env.template to .env and fill in your values'));
306
+ console.log(chalk.white('2. Run: aifabrix build ' + appName));
307
+ console.log(chalk.white('3. Run: aifabrix run ' + appName));
308
+
309
+ } catch (error) {
310
+ throw new Error(`Failed to create application: ${error.message}`);
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Builds a container image for the specified application
316
+ * Auto-detects runtime and generates Dockerfile if needed
317
+ *
318
+ * @async
319
+ * @function buildApp
320
+ * @param {string} appName - Name of the application to build
321
+ * @param {Object} options - Build options
322
+ * @param {string} [options.language] - Override language detection
323
+ * @param {boolean} [options.forceTemplate] - Force rebuild from template
324
+ * @param {string} [options.tag] - Image tag (default: latest)
325
+ * @returns {Promise<string>} Image tag that was built
326
+ * @throws {Error} If build fails or app configuration is invalid
327
+ *
328
+ * @example
329
+ * const imageTag = await buildApp('myapp', { language: 'typescript' });
330
+ * // Returns: 'myapp:latest'
331
+ */
332
+ async function buildApp(appName, options = {}) {
333
+ return build.buildApp(appName, options);
334
+ }
335
+
336
+ /**
337
+ * Detects the runtime language of an application
338
+ * Analyzes project files to determine TypeScript, Python, etc.
339
+ *
340
+ * @function detectLanguage
341
+ * @param {string} appPath - Path to application directory
342
+ * @returns {string} Detected language ('typescript', 'python', etc.)
343
+ * @throws {Error} If language cannot be detected
344
+ *
345
+ * @example
346
+ * const language = detectLanguage('./myapp');
347
+ * // Returns: 'typescript'
348
+ */
349
+ function detectLanguage(appPath) {
350
+ return build.detectLanguage(appPath);
351
+ }
352
+
353
+ /**
354
+ * Generates a Dockerfile from template based on detected language
355
+ * Uses Handlebars templates to create optimized Dockerfiles
356
+ *
357
+ * @async
358
+ * @function generateDockerfile
359
+ * @param {string} appPath - Path to application directory
360
+ * @param {string} language - Target language ('typescript', 'python')
361
+ * @param {Object} config - Application configuration from variables.yaml
362
+ * @returns {Promise<string>} Path to generated Dockerfile
363
+ * @throws {Error} If template generation fails
364
+ *
365
+ * @example
366
+ * const dockerfilePath = await generateDockerfile('./myapp', 'typescript', config);
367
+ * // Returns: './myapp/.aifabrix/Dockerfile.typescript'
368
+ */
369
+ async function generateDockerfile(appPath, language, config) {
370
+ return build.generateDockerfile(appPath, language, config);
371
+ }
372
+
373
+ /**
374
+ * Runs the application locally using Docker
375
+ * Starts container with proper port mapping and environment
376
+ *
377
+ * @async
378
+ * @function runApp
379
+ * @param {string} appName - Name of the application to run
380
+ * @param {Object} options - Run options
381
+ * @param {number} [options.port] - Override local port
382
+ * @returns {Promise<void>} Resolves when app is running
383
+ * @throws {Error} If run fails or app is not built
384
+ *
385
+ * @example
386
+ * await runApp('myapp', { port: 3001 });
387
+ * // Application is now running on localhost:3001
388
+ */
389
+ async function runApp(appName, options = {}) {
390
+ return appRun.runApp(appName, options);
391
+ }
392
+
393
+ /**
394
+ * Pushes application image to Azure Container Registry
395
+ * @async
396
+ * @function pushApp
397
+ * @param {string} appName - Name of the application
398
+ * @param {Object} options - Push options (registry, tag)
399
+ * @returns {Promise<void>} Resolves when push is complete
400
+ */
401
+ async function pushApp(appName, options = {}) {
402
+ try {
403
+ // Validate app name
404
+ validateAppName(appName);
405
+
406
+ const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
407
+ let config;
408
+ try {
409
+ config = yaml.load(await fs.readFile(configPath, 'utf8'));
410
+ } catch (error) {
411
+ throw new Error(`Failed to load configuration: ${configPath}\nRun 'aifabrix create ${appName}' first`);
412
+ }
413
+
414
+ const registry = options.registry || config.image?.registry;
415
+ if (!registry) {
416
+ throw new Error('Registry URL is required. Provide via --registry flag or configure in variables.yaml under image.registry');
417
+ }
418
+
419
+ if (!pushUtils.validateRegistryURL(registry)) {
420
+ throw new Error(`Invalid registry URL format: ${registry}. Expected format: *.azurecr.io`);
421
+ }
422
+
423
+ const tags = options.tag ? options.tag.split(',').map(t => t.trim()) : ['latest'];
424
+
425
+ if (!await pushUtils.checkLocalImageExists(appName, 'latest')) {
426
+ throw new Error(`Docker image ${appName}:latest not found locally.\nRun 'aifabrix build ${appName}' first`);
427
+ }
428
+
429
+ if (!await pushUtils.checkAzureCLIInstalled()) {
430
+ throw new Error('Azure CLI is not installed. Install from: https://docs.microsoft.com/cli/azure/install-azure-cli');
431
+ }
432
+
433
+ if (await pushUtils.checkACRAuthentication(registry)) {
434
+ console.log(chalk.green(`✓ Already authenticated with ${registry}`));
435
+ } else {
436
+ await pushUtils.authenticateACR(registry);
437
+ }
438
+
439
+ await Promise.all(tags.map(async(tag) => {
440
+ await pushUtils.tagImage(`${appName}:latest`, `${registry}/${appName}:${tag}`);
441
+ await pushUtils.pushImage(`${registry}/${appName}:${tag}`);
442
+ }));
443
+
444
+ console.log(chalk.green(`\n✓ Successfully pushed ${tags.length} tag(s) to ${registry}`));
445
+ console.log(chalk.gray(`Image: ${registry}/${appName}:*`));
446
+ console.log(chalk.gray(`Tags: ${tags.join(', ')}`));
447
+
448
+ } catch (error) {
449
+ throw new Error(`Failed to push application: ${error.message}`);
450
+ }
451
+ }
452
+
453
+ async function deployApp(appName, options = {}) {
454
+ const appDeploy = require('./app-deploy');
455
+ return appDeploy.deployApp(appName, options);
456
+ }
457
+
458
+ module.exports = {
459
+ createApp,
460
+ buildApp,
461
+ runApp,
462
+ detectLanguage,
463
+ generateDockerfile,
464
+ pushApp,
465
+ deployApp,
466
+ checkImageExists: appRun.checkImageExists,
467
+ checkContainerRunning: appRun.checkContainerRunning,
468
+ stopAndRemoveContainer: appRun.stopAndRemoveContainer,
469
+ checkPortAvailable: appRun.checkPortAvailable,
470
+ generateDockerCompose: appRun.generateDockerCompose,
471
+ waitForHealthCheck: appRun.waitForHealthCheck
472
+ };
@@ -0,0 +1,162 @@
1
+ /**
2
+ * AI Fabrix Builder Audit Logger
3
+ *
4
+ * ISO 27001 compliant structured logging for audit trails.
5
+ * Provides secure, audit-ready logging with sensitive data masking.
6
+ *
7
+ * @fileoverview Audit and compliance logging for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ /**
13
+ * Masks sensitive data in strings
14
+ * Prevents secrets, keys, and passwords from appearing in logs
15
+ *
16
+ * @param {string} value - Value to mask
17
+ * @returns {string} Masked value
18
+ */
19
+ function maskSensitiveData(value) {
20
+ if (!value || typeof value !== 'string') {
21
+ return value;
22
+ }
23
+
24
+ // Mask patterns: passwords, secrets, keys, tokens
25
+ const sensitivePatterns = [
26
+ { pattern: /password[=:]\s*([^\s]+)/gi, replacement: 'password=***' },
27
+ { pattern: /secret[=:]\s*([^\s]+)/gi, replacement: 'secret=***' },
28
+ { pattern: /key[=:]\s*([^\s]+)/gi, replacement: 'key=***' },
29
+ { pattern: /token[=:]\s*([^\s]+)/gi, replacement: 'token=***' },
30
+ { pattern: /api[_-]?key[=:]\s*([^\s]+)/gi, replacement: 'api_key=***' }
31
+ ];
32
+
33
+ let masked = value;
34
+ for (const { pattern, replacement } of sensitivePatterns) {
35
+ masked = masked.replace(pattern, replacement);
36
+ }
37
+
38
+ // If value looks like a hash/key (long hex string), mask it
39
+ if (/^[a-f0-9]{32,}$/i.test(masked.trim())) {
40
+ return '***';
41
+ }
42
+
43
+ return masked;
44
+ }
45
+
46
+ /**
47
+ * Creates an audit log entry with ISO 27001 compliance
48
+ *
49
+ * @param {string} level - Log level (INFO, WARN, ERROR, AUDIT)
50
+ * @param {string} message - Log message
51
+ * @param {Object} metadata - Additional metadata
52
+ * @returns {Object} Structured log entry
53
+ */
54
+ function createAuditEntry(level, message, metadata = {}) {
55
+ const entry = {
56
+ timestamp: new Date().toISOString(),
57
+ level: level.toUpperCase(),
58
+ message: maskSensitiveData(message),
59
+ metadata: {}
60
+ };
61
+
62
+ // Mask sensitive metadata
63
+ for (const [key, value] of Object.entries(metadata)) {
64
+ entry.metadata[key] = maskSensitiveData(
65
+ typeof value === 'string' ? value : JSON.stringify(value)
66
+ );
67
+ }
68
+
69
+ return entry;
70
+ }
71
+
72
+ /**
73
+ * Logs audit entry to console (structured JSON format)
74
+ *
75
+ * @param {string} level - Log level
76
+ * @param {string} message - Log message
77
+ * @param {Object} metadata - Additional metadata
78
+ */
79
+ function auditLog(level, message, metadata) {
80
+ const entry = createAuditEntry(level, message, metadata);
81
+ console.log(JSON.stringify(entry));
82
+ }
83
+
84
+ /**
85
+ * Logs deployment attempt with full audit trail
86
+ *
87
+ * @param {string} appName - Application name
88
+ * @param {string} controllerUrl - Controller URL
89
+ * @param {Object} options - Deployment options
90
+ */
91
+ function logDeploymentAttempt(appName, controllerUrl, options = {}) {
92
+ auditLog('AUDIT', 'Deployment initiated', {
93
+ action: 'deploy',
94
+ appName,
95
+ controllerUrl,
96
+ environment: options.environment || 'unknown',
97
+ timestamp: Date.now()
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Logs deployment success
103
+ *
104
+ * @param {string} appName - Application name
105
+ * @param {string} deploymentId - Deployment ID
106
+ * @param {string} controllerUrl - Controller URL
107
+ */
108
+ function logDeploymentSuccess(appName, deploymentId, controllerUrl) {
109
+ auditLog('AUDIT', 'Deployment succeeded', {
110
+ action: 'deploy',
111
+ appName,
112
+ deploymentId,
113
+ controllerUrl,
114
+ status: 'success',
115
+ timestamp: Date.now()
116
+ });
117
+ }
118
+
119
+ /**
120
+ * Logs deployment failure with error details
121
+ *
122
+ * @param {string} appName - Application name
123
+ * @param {string} controllerUrl - Controller URL
124
+ * @param {Error} error - Error that occurred
125
+ */
126
+ function logDeploymentFailure(appName, controllerUrl, error) {
127
+ auditLog('ERROR', 'Deployment failed', {
128
+ action: 'deploy',
129
+ appName,
130
+ controllerUrl,
131
+ status: 'failure',
132
+ errorMessage: error.message,
133
+ errorCode: error.code || 'UNKNOWN',
134
+ timestamp: Date.now()
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Logs security-related events
140
+ *
141
+ * @param {string} event - Security event name
142
+ * @param {Object} details - Event details
143
+ */
144
+ function logSecurityEvent(event, details = {}) {
145
+ auditLog('AUDIT', `Security event: ${event}`, {
146
+ eventType: 'security',
147
+ event,
148
+ ...details,
149
+ timestamp: Date.now()
150
+ });
151
+ }
152
+
153
+ module.exports = {
154
+ auditLog,
155
+ logDeploymentAttempt,
156
+ logDeploymentSuccess,
157
+ logDeploymentFailure,
158
+ logSecurityEvent,
159
+ maskSensitiveData,
160
+ createAuditEntry
161
+ };
162
+