@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
|
@@ -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
|
+
|