@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,304 @@
1
+ /**
2
+ * AI Fabrix Builder - Login Command
3
+ *
4
+ * Handles authentication with Miso Controller
5
+ * Supports device code flow and credentials authentication
6
+ *
7
+ * @fileoverview Login command implementation for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const inquirer = require('inquirer');
13
+ const chalk = require('chalk');
14
+ const ora = require('ora');
15
+ const { saveConfig } = require('../config');
16
+ const { makeApiCall, initiateDeviceCodeFlow, pollDeviceCodeToken, displayDeviceCodeInfo } = require('../utils/api');
17
+ const logger = require('../utils/logger');
18
+
19
+ /**
20
+ * Validate environment key format
21
+ * @param {string} envKey - Environment key to validate
22
+ * @throws {Error} If environment key format is invalid
23
+ */
24
+ function validateEnvironmentKey(envKey) {
25
+ if (!/^[a-z0-9-_]+$/i.test(envKey)) {
26
+ throw new Error('Environment key must contain only letters, numbers, hyphens, and underscores');
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Determine and validate authentication method
32
+ * @async
33
+ * @param {string} [method] - Method provided via options
34
+ * @returns {Promise<string>} Validated method ('device' or 'credentials')
35
+ */
36
+ async function determineAuthMethod(method) {
37
+ if (method) {
38
+ if (method !== 'device' && method !== 'credentials') {
39
+ logger.error(chalk.red(`āŒ Invalid method: ${method}. Must be 'device' or 'credentials'`));
40
+ process.exit(1);
41
+ }
42
+ return method;
43
+ }
44
+
45
+ const authMethod = await inquirer.prompt([{
46
+ type: 'list',
47
+ name: 'method',
48
+ message: 'Choose authentication method:',
49
+ choices: [
50
+ { name: 'ClientId + ClientSecret', value: 'credentials' },
51
+ { name: 'Device Code Flow (environment only)', value: 'device' }
52
+ ]
53
+ }]);
54
+ return authMethod.method;
55
+ }
56
+
57
+ /**
58
+ * Prompt for credentials if not provided
59
+ * @async
60
+ * @param {string} [clientId] - Existing client ID
61
+ * @param {string} [clientSecret] - Existing client secret
62
+ * @returns {Promise<{clientId: string, clientSecret: string}>} Credentials
63
+ */
64
+ async function promptForCredentials(clientId, clientSecret) {
65
+ if (clientId && clientSecret) {
66
+ return { clientId: clientId.trim(), clientSecret: clientSecret.trim() };
67
+ }
68
+
69
+ const credentials = await inquirer.prompt([
70
+ {
71
+ type: 'input',
72
+ name: 'clientId',
73
+ message: 'Client ID:',
74
+ default: clientId || '',
75
+ validate: (input) => {
76
+ const value = input.trim();
77
+ if (!value || value.length === 0) {
78
+ return 'Client ID is required';
79
+ }
80
+ return true;
81
+ }
82
+ },
83
+ {
84
+ type: 'password',
85
+ name: 'clientSecret',
86
+ message: 'Client Secret:',
87
+ default: clientSecret || '',
88
+ mask: '*',
89
+ validate: (input) => {
90
+ const value = input.trim();
91
+ if (!value || value.length === 0) {
92
+ return 'Client Secret is required';
93
+ }
94
+ return true;
95
+ }
96
+ }
97
+ ]);
98
+
99
+ return {
100
+ clientId: credentials.clientId.trim(),
101
+ clientSecret: credentials.clientSecret.trim()
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Get and validate environment key
107
+ * @async
108
+ * @param {string} [environment] - Environment key from options
109
+ * @returns {Promise<string>} Validated environment key
110
+ */
111
+ async function getEnvironmentKey(environment) {
112
+ if (environment) {
113
+ const envKey = environment.trim();
114
+ validateEnvironmentKey(envKey);
115
+ return envKey;
116
+ }
117
+
118
+ const envPrompt = await inquirer.prompt([{
119
+ type: 'input',
120
+ name: 'environment',
121
+ message: 'Environment key (e.g., dev, tst, pro):',
122
+ validate: (input) => {
123
+ if (!input || input.trim().length === 0) {
124
+ return 'Environment key is required';
125
+ }
126
+ validateEnvironmentKey(input.trim());
127
+ return true;
128
+ }
129
+ }]);
130
+
131
+ return envPrompt.environment.trim();
132
+ }
133
+
134
+ /**
135
+ * Save login configuration
136
+ * @async
137
+ * @param {string} controllerUrl - Controller URL
138
+ * @param {string} token - Authentication token
139
+ * @param {string} expiresAt - Token expiration time
140
+ * @param {string} [environment] - Environment key
141
+ */
142
+ async function saveLoginConfig(controllerUrl, token, expiresAt, environment) {
143
+ await saveConfig({
144
+ apiUrl: controllerUrl,
145
+ token: token,
146
+ expiresAt: expiresAt,
147
+ environment: environment || undefined
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Handle credentials-based login
153
+ * @async
154
+ * @param {string} controllerUrl - Controller URL
155
+ * @param {string} [clientId] - Client ID from options
156
+ * @param {string} [clientSecret] - Client Secret from options
157
+ * @returns {Promise<string>} Authentication token
158
+ */
159
+ async function handleCredentialsLogin(controllerUrl, clientId, clientSecret) {
160
+ const credentials = await promptForCredentials(clientId, clientSecret);
161
+
162
+ const response = await makeApiCall(`${controllerUrl}/api/v1/auth/login`, {
163
+ method: 'POST',
164
+ headers: {
165
+ 'Content-Type': 'application/json'
166
+ },
167
+ body: JSON.stringify({
168
+ clientId: credentials.clientId,
169
+ clientSecret: credentials.clientSecret
170
+ })
171
+ });
172
+
173
+ if (!response.success) {
174
+ logger.error(chalk.red(`āŒ Login failed: ${response.error}`));
175
+ process.exit(1);
176
+ }
177
+
178
+ return response.data.token || response.data.accessToken;
179
+ }
180
+
181
+ /**
182
+ * Poll for device code token and save configuration
183
+ * @async
184
+ * @param {string} controllerUrl - Controller URL
185
+ * @param {string} deviceCode - Device code
186
+ * @param {number} interval - Polling interval
187
+ * @param {number} expiresIn - Expiration time
188
+ * @param {string} envKey - Environment key
189
+ * @returns {Promise<{token: string, environment: string}>} Token and environment
190
+ */
191
+ async function pollAndSaveDeviceCodeToken(controllerUrl, deviceCode, interval, expiresIn, envKey) {
192
+ const spinner = ora({
193
+ text: 'Waiting for approval',
194
+ spinner: 'dots'
195
+ }).start();
196
+
197
+ let pollCount = 0;
198
+ const pollCallback = () => {
199
+ pollCount++;
200
+ spinner.text = `Waiting for approval (attempt ${pollCount})...`;
201
+ };
202
+
203
+ try {
204
+ const tokenResponse = await pollDeviceCodeToken(
205
+ controllerUrl,
206
+ deviceCode,
207
+ interval,
208
+ expiresIn,
209
+ pollCallback
210
+ );
211
+
212
+ spinner.succeed('Authentication approved!');
213
+
214
+ const token = tokenResponse.access_token;
215
+ const expiresAt = new Date(Date.now() + (tokenResponse.expires_in * 1000)).toISOString();
216
+
217
+ await saveLoginConfig(controllerUrl, token, expiresAt, envKey);
218
+
219
+ logger.log(chalk.green('\nāœ… Successfully logged in!'));
220
+ logger.log(chalk.gray(`Controller: ${controllerUrl}`));
221
+ logger.log(chalk.gray('Token stored securely in ~/.aifabrix/config.yaml\n'));
222
+
223
+ return { token, environment: envKey };
224
+
225
+ } catch (pollError) {
226
+ spinner.fail('Authentication failed');
227
+ throw pollError;
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Handle device code flow login
233
+ * @async
234
+ * @param {string} controllerUrl - Controller URL
235
+ * @param {string} [environment] - Environment key from options
236
+ * @returns {Promise<{token: string, environment: string}>} Token and environment
237
+ */
238
+ async function handleDeviceCodeLogin(controllerUrl, environment) {
239
+ const envKey = await getEnvironmentKey(environment);
240
+
241
+ logger.log(chalk.blue('\nšŸ“± Initiating device code flow...\n'));
242
+
243
+ try {
244
+ const deviceCodeResponse = await initiateDeviceCodeFlow(controllerUrl, envKey);
245
+
246
+ displayDeviceCodeInfo(deviceCodeResponse.user_code, deviceCodeResponse.verification_uri, logger, chalk);
247
+
248
+ return await pollAndSaveDeviceCodeToken(
249
+ controllerUrl,
250
+ deviceCodeResponse.device_code,
251
+ deviceCodeResponse.interval,
252
+ deviceCodeResponse.expires_in,
253
+ envKey
254
+ );
255
+
256
+ } catch (deviceError) {
257
+ logger.error(chalk.red(`\nāŒ Device code flow failed: ${deviceError.message}`));
258
+ process.exit(1);
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Handle login command action
264
+ * @async
265
+ * @function handleLogin
266
+ * @param {Object} options - Login options
267
+ * @param {string} [options.url] - Controller URL (default: 'http://localhost:3000')
268
+ * @param {string} [options.method] - Authentication method ('device' or 'credentials')
269
+ * @param {string} [options.clientId] - Client ID (for credentials method)
270
+ * @param {string} [options.clientSecret] - Client Secret (for credentials method)
271
+ * @param {string} [options.environment] - Environment key (for device method)
272
+ * @returns {Promise<void>} Resolves when login completes
273
+ * @throws {Error} If login fails
274
+ */
275
+ async function handleLogin(options) {
276
+ logger.log(chalk.blue('\nšŸ” Logging in to Miso Controller...\n'));
277
+
278
+ const controllerUrl = options.url.replace(/\/$/, '');
279
+ logger.log(chalk.gray(`Controller URL: ${controllerUrl}`));
280
+
281
+ const method = await determineAuthMethod(options.method);
282
+ let token;
283
+ let environment = null;
284
+
285
+ if (method === 'credentials') {
286
+ token = await handleCredentialsLogin(controllerUrl, options.clientId, options.clientSecret);
287
+ } else if (method === 'device') {
288
+ const result = await handleDeviceCodeLogin(controllerUrl, options.environment);
289
+ token = result.token;
290
+ environment = result.environment;
291
+ return; // Early return for device flow (already saved config)
292
+ }
293
+
294
+ // Save configuration for credentials method
295
+ const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
296
+ await saveLoginConfig(controllerUrl, token, expiresAt, environment);
297
+
298
+ logger.log(chalk.green('\nāœ… Successfully logged in!'));
299
+ logger.log(chalk.gray(`Controller: ${controllerUrl}`));
300
+ logger.log(chalk.gray('Token stored securely in ~/.aifabrix/config.yaml\n'));
301
+ }
302
+
303
+ module.exports = { handleLogin };
304
+
package/lib/config.js ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * AI Fabrix Builder Configuration Management
3
+ *
4
+ * Manages stored authentication configuration for CLI
5
+ * Stores controller URL and auth tokens securely
6
+ *
7
+ * @fileoverview Configuration storage 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 os = require('os');
15
+ const yaml = require('js-yaml');
16
+
17
+ const CONFIG_DIR = path.join(os.homedir(), '.aifabrix');
18
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.yaml');
19
+
20
+ /**
21
+ * Get stored configuration
22
+ * @returns {Promise<Object>} Configuration object with apiUrl and token
23
+ */
24
+ async function getConfig() {
25
+ try {
26
+ const configContent = await fs.readFile(CONFIG_FILE, 'utf8');
27
+ return yaml.load(configContent);
28
+ } catch (error) {
29
+ if (error.code === 'ENOENT') {
30
+ return { apiUrl: null, token: null };
31
+ }
32
+ throw new Error(`Failed to read config: ${error.message}`);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Save configuration
38
+ * @param {Object} data - Configuration data with apiUrl and token
39
+ * @returns {Promise<void>}
40
+ */
41
+ async function saveConfig(data) {
42
+ try {
43
+ // Create directory if it doesn't exist
44
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
45
+
46
+ // Set secure permissions
47
+ const configContent = yaml.dump(data);
48
+ await fs.writeFile(CONFIG_FILE, configContent, {
49
+ mode: 0o600,
50
+ flag: 'w'
51
+ });
52
+ } catch (error) {
53
+ throw new Error(`Failed to save config: ${error.message}`);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Clear stored configuration
59
+ * @returns {Promise<void>}
60
+ */
61
+ async function clearConfig() {
62
+ try {
63
+ await fs.unlink(CONFIG_FILE);
64
+ } catch (error) {
65
+ if (error.code !== 'ENOENT') {
66
+ throw new Error(`Failed to clear config: ${error.message}`);
67
+ }
68
+ }
69
+ }
70
+
71
+ module.exports = {
72
+ getConfig,
73
+ saveConfig,
74
+ clearConfig,
75
+ CONFIG_DIR,
76
+ CONFIG_FILE
77
+ };
78
+