@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.
Files changed (58) hide show
  1. package/README.md +6 -2
  2. package/bin/aifabrix.js +9 -3
  3. package/jest.config.integration.js +30 -0
  4. package/lib/app-config.js +157 -0
  5. package/lib/app-deploy.js +233 -82
  6. package/lib/app-dockerfile.js +112 -0
  7. package/lib/app-prompts.js +244 -0
  8. package/lib/app-push.js +172 -0
  9. package/lib/app-run.js +334 -133
  10. package/lib/app.js +208 -274
  11. package/lib/audit-logger.js +2 -0
  12. package/lib/build.js +209 -98
  13. package/lib/cli.js +76 -86
  14. package/lib/commands/app.js +414 -0
  15. package/lib/commands/login.js +304 -0
  16. package/lib/config.js +78 -0
  17. package/lib/deployer.js +225 -81
  18. package/lib/env-reader.js +45 -30
  19. package/lib/generator.js +308 -191
  20. package/lib/github-generator.js +67 -7
  21. package/lib/infra.js +156 -61
  22. package/lib/push.js +105 -10
  23. package/lib/schema/application-schema.json +30 -2
  24. package/lib/schema/infrastructure-schema.json +589 -0
  25. package/lib/secrets.js +229 -24
  26. package/lib/template-validator.js +205 -0
  27. package/lib/templates.js +305 -170
  28. package/lib/utils/api.js +329 -0
  29. package/lib/utils/cli-utils.js +97 -0
  30. package/lib/utils/dockerfile-utils.js +131 -0
  31. package/lib/utils/environment-checker.js +125 -0
  32. package/lib/utils/error-formatter.js +61 -0
  33. package/lib/utils/health-check.js +187 -0
  34. package/lib/utils/logger.js +53 -0
  35. package/lib/utils/template-helpers.js +223 -0
  36. package/lib/utils/variable-transformer.js +271 -0
  37. package/lib/validator.js +27 -112
  38. package/package.json +13 -10
  39. package/templates/README.md +75 -3
  40. package/templates/applications/keycloak/Dockerfile +36 -0
  41. package/templates/applications/keycloak/env.template +32 -0
  42. package/templates/applications/keycloak/rbac.yaml +37 -0
  43. package/templates/applications/keycloak/variables.yaml +56 -0
  44. package/templates/applications/miso-controller/Dockerfile +125 -0
  45. package/templates/applications/miso-controller/env.template +129 -0
  46. package/templates/applications/miso-controller/rbac.yaml +168 -0
  47. package/templates/applications/miso-controller/variables.yaml +56 -0
  48. package/templates/github/release.yaml.hbs +5 -26
  49. package/templates/github/steps/npm.hbs +24 -0
  50. package/templates/infra/compose.yaml +6 -6
  51. package/templates/python/docker-compose.hbs +19 -12
  52. package/templates/python/main.py +80 -0
  53. package/templates/python/requirements.txt +4 -0
  54. package/templates/typescript/Dockerfile.hbs +2 -2
  55. package/templates/typescript/docker-compose.hbs +19 -12
  56. package/templates/typescript/index.ts +116 -0
  57. package/templates/typescript/package.json +26 -0
  58. package/templates/typescript/tsconfig.json +24 -0
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Application Dockerfile Generation
3
+ *
4
+ * Handles Dockerfile generation for applications
5
+ *
6
+ * @fileoverview Dockerfile generation for AI Fabrix Builder
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const fs = require('fs').promises;
12
+ const path = require('path');
13
+ const chalk = require('chalk');
14
+ const yaml = require('js-yaml');
15
+ const build = require('./build');
16
+ const { validateAppName } = require('./app-push');
17
+ const logger = require('./utils/logger');
18
+
19
+ /**
20
+ * Checks if Dockerfile exists and validates overwrite permission
21
+ * @async
22
+ * @param {string} dockerfilePath - Path to Dockerfile
23
+ * @param {Object} options - Generation options
24
+ * @throws {Error} If Dockerfile exists and force is not enabled
25
+ */
26
+ async function checkDockerfileExists(dockerfilePath, options) {
27
+ try {
28
+ await fs.access(dockerfilePath);
29
+ if (!options.force) {
30
+ throw new Error(`Dockerfile already exists at ${dockerfilePath}. Use --force to overwrite.`);
31
+ }
32
+ } catch (error) {
33
+ if (error.code === 'ENOENT') {
34
+ // File doesn't exist, that's okay
35
+ return;
36
+ }
37
+ throw error;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Loads application configuration from variables.yaml
43
+ * @async
44
+ * @param {string} configPath - Path to variables.yaml
45
+ * @param {Object} options - Generation options
46
+ * @returns {Promise<Object>} Application configuration
47
+ * @throws {Error} If configuration cannot be loaded
48
+ */
49
+ async function loadAppConfig(configPath, options) {
50
+ try {
51
+ const yamlContent = await fs.readFile(configPath, 'utf8');
52
+ const variables = yaml.load(yamlContent);
53
+ return {
54
+ language: options.language || variables.build?.language || 'typescript',
55
+ port: variables.build?.port || variables.port || 3000,
56
+ ...variables
57
+ };
58
+ } catch {
59
+ throw new Error(`Failed to load configuration from ${configPath}`);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Generates and copies Dockerfile to app directory
65
+ * @async
66
+ * @param {string} appPath - Application directory path
67
+ * @param {string} dockerfilePath - Target Dockerfile path
68
+ * @param {Object} config - Application configuration
69
+ * @returns {Promise<string>} Path to generated Dockerfile
70
+ */
71
+ async function generateAndCopyDockerfile(appPath, dockerfilePath, config) {
72
+ // Extract buildConfig from config to pass to generateDockerfile
73
+ const buildConfig = config.build || {};
74
+ const generatedPath = await build.generateDockerfile(appPath, config.language, config, buildConfig);
75
+ await fs.copyFile(generatedPath, dockerfilePath);
76
+ logger.log(chalk.green('✓ Generated Dockerfile from template'));
77
+ return dockerfilePath;
78
+ }
79
+
80
+ /**
81
+ * Generate Dockerfile for an application
82
+ * @param {string} appName - Application name
83
+ * @param {Object} options - Generation options
84
+ * @returns {Promise<string>} Path to generated Dockerfile
85
+ */
86
+ async function generateDockerfileForApp(appName, options = {}) {
87
+ try {
88
+ // Validate app name
89
+ validateAppName(appName);
90
+
91
+ const appPath = path.join(process.cwd(), 'builder', appName);
92
+ const dockerfilePath = path.join(appPath, 'Dockerfile');
93
+
94
+ // Check if Dockerfile already exists
95
+ await checkDockerfileExists(dockerfilePath, options);
96
+
97
+ // Load configuration
98
+ const configPath = path.join(appPath, 'variables.yaml');
99
+ const config = await loadAppConfig(configPath, options);
100
+
101
+ // Generate and copy Dockerfile
102
+ return await generateAndCopyDockerfile(appPath, dockerfilePath, config);
103
+
104
+ } catch (error) {
105
+ throw new Error(`Failed to generate Dockerfile: ${error.message}`);
106
+ }
107
+ }
108
+
109
+ module.exports = {
110
+ generateDockerfileForApp
111
+ };
112
+
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Application Prompting Utilities
3
+ *
4
+ * Handles interactive prompts for application configuration
5
+ *
6
+ * @fileoverview Prompt utilities for AI Fabrix Builder
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const inquirer = require('inquirer');
12
+
13
+ /**
14
+ * Builds basic questions (port, language)
15
+ * @param {Object} options - Provided options
16
+ * @returns {Array} Array of question objects
17
+ */
18
+ function buildBasicQuestions(options) {
19
+ const questions = [];
20
+
21
+ // Port validation
22
+ if (!options.port) {
23
+ questions.push({
24
+ type: 'input',
25
+ name: 'port',
26
+ message: 'What port should the application run on?',
27
+ default: '3000',
28
+ validate: (input) => {
29
+ const port = parseInt(input, 10);
30
+ if (isNaN(port) || port < 1 || port > 65535) {
31
+ return 'Port must be a number between 1 and 65535';
32
+ }
33
+ return true;
34
+ }
35
+ });
36
+ }
37
+
38
+ // Language selection
39
+ if (!options.language) {
40
+ questions.push({
41
+ type: 'list',
42
+ name: 'language',
43
+ message: 'What language is your application written in?',
44
+ choices: [
45
+ { name: 'TypeScript/Node.js', value: 'typescript' },
46
+ { name: 'Python', value: 'python' }
47
+ ],
48
+ default: 'typescript'
49
+ });
50
+ }
51
+
52
+ return questions;
53
+ }
54
+
55
+ /**
56
+ * Builds service questions (database, redis, storage, authentication)
57
+ * @param {Object} options - Provided options
58
+ * @returns {Array} Array of question objects
59
+ */
60
+ function buildServiceQuestions(options) {
61
+ const questions = [];
62
+
63
+ if (!Object.prototype.hasOwnProperty.call(options, 'database')) {
64
+ questions.push({
65
+ type: 'confirm',
66
+ name: 'database',
67
+ message: 'Does your application need a database?',
68
+ default: false
69
+ });
70
+ }
71
+
72
+ if (!Object.prototype.hasOwnProperty.call(options, 'redis')) {
73
+ questions.push({
74
+ type: 'confirm',
75
+ name: 'redis',
76
+ message: 'Does your application need Redis?',
77
+ default: false
78
+ });
79
+ }
80
+
81
+ if (!Object.prototype.hasOwnProperty.call(options, 'storage')) {
82
+ questions.push({
83
+ type: 'confirm',
84
+ name: 'storage',
85
+ message: 'Does your application need file storage?',
86
+ default: false
87
+ });
88
+ }
89
+
90
+ if (!Object.prototype.hasOwnProperty.call(options, 'authentication')) {
91
+ questions.push({
92
+ type: 'confirm',
93
+ name: 'authentication',
94
+ message: 'Does your application need authentication/RBAC?',
95
+ default: false
96
+ });
97
+ }
98
+
99
+ return questions;
100
+ }
101
+
102
+ /**
103
+ * Builds workflow questions (GitHub, Controller)
104
+ * @param {Object} options - Provided options
105
+ * @returns {Array} Array of question objects
106
+ */
107
+ function buildWorkflowQuestions(options) {
108
+ const questions = [];
109
+
110
+ // GitHub workflows
111
+ if (!Object.prototype.hasOwnProperty.call(options, 'github')) {
112
+ questions.push({
113
+ type: 'confirm',
114
+ name: 'github',
115
+ message: 'Do you need GitHub Actions workflows?',
116
+ default: false
117
+ });
118
+ }
119
+
120
+ // Controller deployment
121
+ if (!Object.prototype.hasOwnProperty.call(options, 'controller') &&
122
+ options.github !== false) {
123
+ questions.push({
124
+ type: 'confirm',
125
+ name: 'controller',
126
+ message: 'Do you need Controller deployment workflow?',
127
+ default: false,
128
+ when: (answers) => answers.github !== false
129
+ });
130
+ }
131
+
132
+ // Controller URL
133
+ if (!options.controllerUrl && options.controller &&
134
+ !Object.prototype.hasOwnProperty.call(options, 'controllerUrl')) {
135
+ const misoHost = process.env.MISO_HOST || 'localhost';
136
+ questions.push({
137
+ type: 'input',
138
+ name: 'controllerUrl',
139
+ message: 'Enter Controller URL:',
140
+ default: `http://${misoHost}:3000`,
141
+ when: (answers) => answers.controller === true
142
+ });
143
+ }
144
+
145
+ return questions;
146
+ }
147
+
148
+ /**
149
+ * Resolves conflicts between options and answers for a specific field
150
+ * @function resolveField
151
+ * @param {*} optionValue - Value from options
152
+ * @param {*} answerValue - Value from answers
153
+ * @param {*} defaultValue - Default value
154
+ * @returns {*} Resolved value
155
+ */
156
+ function resolveField(optionValue, answerValue, defaultValue) {
157
+ if (optionValue !== undefined && optionValue !== null) {
158
+ return optionValue;
159
+ }
160
+ if (answerValue !== undefined && answerValue !== null) {
161
+ return answerValue;
162
+ }
163
+ return defaultValue;
164
+ }
165
+
166
+ /**
167
+ * Resolves conflicts between options and answers for optional boolean fields
168
+ * @function resolveOptionalBoolean
169
+ * @param {*} optionValue - Value from options
170
+ * @param {*} answerValue - Value from answers
171
+ * @param {*} defaultValue - Default value
172
+ * @returns {*} Resolved value
173
+ */
174
+ function resolveOptionalBoolean(optionValue, answerValue, defaultValue) {
175
+ if (optionValue !== undefined) {
176
+ return optionValue;
177
+ }
178
+ return answerValue !== undefined ? answerValue : defaultValue;
179
+ }
180
+
181
+ /**
182
+ * Resolves conflicts between options and answers
183
+ * @function resolveConflicts
184
+ * @param {Object} options - Provided options
185
+ * @param {Object} answers - Prompt answers
186
+ * @returns {Object} Resolved configuration
187
+ */
188
+ function resolveConflicts(options, answers) {
189
+ return {
190
+ port: parseInt(resolveField(options.port, answers.port, 3000), 10),
191
+ language: resolveField(options.language, answers.language, 'typescript'),
192
+ database: resolveField(options.database, answers.database, false),
193
+ redis: resolveField(options.redis, answers.redis, false),
194
+ storage: resolveField(options.storage, answers.storage, false),
195
+ authentication: resolveField(options.authentication, answers.authentication, false),
196
+ github: resolveOptionalBoolean(options.github, answers.github, false),
197
+ controller: resolveOptionalBoolean(options.controller, answers.controller, false),
198
+ controllerUrl: resolveField(options.controllerUrl, answers.controllerUrl, undefined)
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Merges provided options with prompt answers
204
+ * @param {string} appName - Application name
205
+ * @param {Object} options - Provided options
206
+ * @param {Object} answers - Prompt answers
207
+ * @returns {Object} Complete configuration
208
+ */
209
+ function mergePromptAnswers(appName, options, answers) {
210
+ return {
211
+ appName,
212
+ ...resolveConflicts(options, answers)
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Prompt for missing configuration options
218
+ * @param {string} appName - Application name
219
+ * @param {Object} options - Provided options
220
+ * @returns {Promise<Object>} Complete configuration
221
+ */
222
+ async function promptForOptions(appName, options) {
223
+ // Default github to false if not provided (make it truly optional)
224
+ if (!Object.prototype.hasOwnProperty.call(options, 'github')) {
225
+ options.github = false;
226
+ }
227
+
228
+ const questions = [
229
+ ...buildBasicQuestions(options),
230
+ ...buildServiceQuestions(options),
231
+ ...buildWorkflowQuestions(options)
232
+ ];
233
+
234
+ // Prompt for missing options
235
+ const answers = questions.length > 0 ? await inquirer.prompt(questions) : {};
236
+
237
+ // Merge provided options with answers
238
+ return mergePromptAnswers(appName, options, answers);
239
+ }
240
+
241
+ module.exports = {
242
+ promptForOptions
243
+ };
244
+
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Application Push Utilities
3
+ *
4
+ * Handles pushing Docker images to container registries
5
+ *
6
+ * @fileoverview Push functionality for AI Fabrix Builder
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const fs = require('fs').promises;
12
+ const path = require('path');
13
+ const chalk = require('chalk');
14
+ const yaml = require('js-yaml');
15
+ const pushUtils = require('./push');
16
+ const logger = require('./utils/logger');
17
+
18
+ /**
19
+ * Validate application name format
20
+ * @param {string} appName - Application name to validate
21
+ * @throws {Error} If app name is invalid
22
+ */
23
+ function validateAppName(appName) {
24
+ if (!appName || typeof appName !== 'string') {
25
+ throw new Error('Application name is required');
26
+ }
27
+
28
+ // App name should be lowercase, alphanumeric with dashes, 3-40 characters
29
+ const nameRegex = /^[a-z0-9-]{3,40}$/;
30
+ if (!nameRegex.test(appName)) {
31
+ throw new Error('Application name must be 3-40 characters, lowercase letters, numbers, and dashes only');
32
+ }
33
+
34
+ // Cannot start or end with dash
35
+ if (appName.startsWith('-') || appName.endsWith('-')) {
36
+ throw new Error('Application name cannot start or end with a dash');
37
+ }
38
+
39
+ // Cannot have consecutive dashes
40
+ if (appName.includes('--')) {
41
+ throw new Error('Application name cannot have consecutive dashes');
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Loads push configuration from variables.yaml
47
+ * @async
48
+ * @param {string} appName - Application name
49
+ * @param {Object} options - Push options
50
+ * @returns {Promise<Object>} Configuration with registry
51
+ * @throws {Error} If configuration cannot be loaded
52
+ */
53
+ async function loadPushConfig(appName, options) {
54
+ const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
55
+ try {
56
+ const config = yaml.load(await fs.readFile(configPath, 'utf8'));
57
+ const registry = options.registry || config.image?.registry;
58
+ if (!registry) {
59
+ throw new Error('Registry URL is required. Provide via --registry flag or configure in variables.yaml under image.registry');
60
+ }
61
+ return { registry };
62
+ } catch (error) {
63
+ if (error.message.includes('Registry URL')) {
64
+ throw error;
65
+ }
66
+ throw new Error(`Failed to load configuration: ${configPath}\nRun 'aifabrix create ${appName}' first`);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Validates push configuration
72
+ * @param {string} registry - Registry URL
73
+ * @param {string} appName - Application name
74
+ * @throws {Error} If validation fails
75
+ */
76
+ async function validatePushConfig(registry, appName) {
77
+ // Validate ACR URL format specifically (must be *.azurecr.io)
78
+ if (!/^[^.]+\.azurecr\.io$/.test(registry)) {
79
+ throw new Error(`Invalid ACR URL format: ${registry}. Expected format: *.azurecr.io`);
80
+ }
81
+
82
+ if (!pushUtils.validateRegistryURL(registry)) {
83
+ throw new Error(`Invalid registry URL format: ${registry}. Expected format: *.azurecr.io`);
84
+ }
85
+
86
+ if (!await pushUtils.checkLocalImageExists(appName, 'latest')) {
87
+ throw new Error(`Docker image ${appName}:latest not found locally.\nRun 'aifabrix build ${appName}' first`);
88
+ }
89
+
90
+ if (!await pushUtils.checkAzureCLIInstalled()) {
91
+ throw new Error('Azure CLI is not installed. Install from: https://docs.microsoft.com/cli/azure/install-azure-cli');
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Authenticates with Azure Container Registry
97
+ * @async
98
+ * @param {string} registry - Registry URL
99
+ */
100
+ async function authenticateWithRegistry(registry) {
101
+ if (await pushUtils.checkACRAuthentication(registry)) {
102
+ logger.log(chalk.green(`✓ Already authenticated with ${registry}`));
103
+ } else {
104
+ await pushUtils.authenticateACR(registry);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Pushes image tags to registry
110
+ * @async
111
+ * @param {string} appName - Application name
112
+ * @param {string} registry - Registry URL
113
+ * @param {Array<string>} tags - Image tags
114
+ */
115
+ async function pushImageTags(appName, registry, tags) {
116
+ await Promise.all(tags.map(async(tag) => {
117
+ await pushUtils.tagImage(`${appName}:latest`, `${registry}/${appName}:${tag}`);
118
+ await pushUtils.pushImage(`${registry}/${appName}:${tag}`);
119
+ }));
120
+ }
121
+
122
+ /**
123
+ * Displays push results
124
+ * @param {string} registry - Registry URL
125
+ * @param {string} appName - Application name
126
+ * @param {Array<string>} tags - Image tags
127
+ */
128
+ function displayPushResults(registry, appName, tags) {
129
+ logger.log(chalk.green(`\n✓ Successfully pushed ${tags.length} tag(s) to ${registry}`));
130
+ logger.log(chalk.gray(`Image: ${registry}/${appName}:*`));
131
+ logger.log(chalk.gray(`Tags: ${tags.join(', ')}`));
132
+ }
133
+
134
+ /**
135
+ * Pushes application image to Azure Container Registry
136
+ * @async
137
+ * @function pushApp
138
+ * @param {string} appName - Name of the application
139
+ * @param {Object} options - Push options (registry, tag)
140
+ * @returns {Promise<void>} Resolves when push is complete
141
+ */
142
+ async function pushApp(appName, options = {}) {
143
+ try {
144
+ // Validate app name
145
+ validateAppName(appName);
146
+
147
+ // Load configuration
148
+ const { registry } = await loadPushConfig(appName, options);
149
+
150
+ // Validate push configuration
151
+ await validatePushConfig(registry, appName);
152
+
153
+ // Authenticate with registry
154
+ await authenticateWithRegistry(registry);
155
+
156
+ // Push image tags
157
+ const tags = options.tag ? options.tag.split(',').map(t => t.trim()) : ['latest'];
158
+ await pushImageTags(appName, registry, tags);
159
+
160
+ // Display results
161
+ displayPushResults(registry, appName, tags);
162
+
163
+ } catch (error) {
164
+ throw new Error(`Failed to push application: ${error.message}`);
165
+ }
166
+ }
167
+
168
+ module.exports = {
169
+ pushApp,
170
+ validateAppName
171
+ };
172
+