@aifabrix/builder 2.0.0 → 2.0.3

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 (61) hide show
  1. package/README.md +5 -3
  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 +235 -144
  10. package/lib/app.js +208 -274
  11. package/lib/audit-logger.js +2 -0
  12. package/lib/build.js +177 -125
  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/env-config.yaml +9 -1
  25. package/lib/schema/infrastructure-schema.json +589 -0
  26. package/lib/secrets.js +229 -24
  27. package/lib/template-validator.js +205 -0
  28. package/lib/templates.js +305 -170
  29. package/lib/utils/api.js +329 -0
  30. package/lib/utils/cli-utils.js +97 -0
  31. package/lib/utils/compose-generator.js +185 -0
  32. package/lib/utils/docker-build.js +173 -0
  33. package/lib/utils/dockerfile-utils.js +131 -0
  34. package/lib/utils/environment-checker.js +125 -0
  35. package/lib/utils/error-formatter.js +61 -0
  36. package/lib/utils/health-check.js +187 -0
  37. package/lib/utils/logger.js +53 -0
  38. package/lib/utils/template-helpers.js +223 -0
  39. package/lib/utils/variable-transformer.js +271 -0
  40. package/lib/validator.js +27 -112
  41. package/package.json +14 -10
  42. package/templates/README.md +75 -3
  43. package/templates/applications/keycloak/Dockerfile +36 -0
  44. package/templates/applications/keycloak/env.template +32 -0
  45. package/templates/applications/keycloak/rbac.yaml +37 -0
  46. package/templates/applications/keycloak/variables.yaml +56 -0
  47. package/templates/applications/miso-controller/Dockerfile +125 -0
  48. package/templates/applications/miso-controller/env.template +129 -0
  49. package/templates/applications/miso-controller/rbac.yaml +214 -0
  50. package/templates/applications/miso-controller/variables.yaml +56 -0
  51. package/templates/github/release.yaml.hbs +5 -26
  52. package/templates/github/steps/npm.hbs +24 -0
  53. package/templates/infra/compose.yaml +6 -6
  54. package/templates/python/docker-compose.hbs +19 -12
  55. package/templates/python/main.py +80 -0
  56. package/templates/python/requirements.txt +4 -0
  57. package/templates/typescript/Dockerfile.hbs +2 -2
  58. package/templates/typescript/docker-compose.hbs +19 -12
  59. package/templates/typescript/index.ts +116 -0
  60. package/templates/typescript/package.json +26 -0
  61. package/templates/typescript/tsconfig.json +24 -0
@@ -0,0 +1,414 @@
1
+ /**
2
+ * AI Fabrix Builder - Application Registration Commands
3
+ *
4
+ * Handles application registration, listing, and credential rotation
5
+ * Commands: app register, app list, app rotate-secret
6
+ *
7
+ * @fileoverview Application management commands 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 chalk = require('chalk');
15
+ const yaml = require('js-yaml');
16
+ const { getConfig } = require('../config');
17
+ const { authenticatedApiCall } = require('../utils/api');
18
+ const logger = require('../utils/logger');
19
+
20
+ // Import createApp to auto-generate config if missing
21
+ let createApp;
22
+ try {
23
+ createApp = require('../app').createApp;
24
+ } catch {
25
+ createApp = null;
26
+ }
27
+
28
+ /**
29
+ * Validation schema for application registration
30
+ */
31
+ const registerApplicationSchema = {
32
+ environmentId: (val) => {
33
+ if (!val || val.length < 1) {
34
+ throw new Error('Invalid environment ID format');
35
+ }
36
+ return val;
37
+ },
38
+ key: (val) => {
39
+ if (!val || val.length < 1) {
40
+ throw new Error('Application key is required');
41
+ }
42
+ if (val.length > 50) {
43
+ throw new Error('Application key must be at most 50 characters');
44
+ }
45
+ if (!/^[a-z0-9-]+$/.test(val)) {
46
+ throw new Error('Application key must contain only lowercase letters, numbers, and hyphens');
47
+ }
48
+ return val;
49
+ },
50
+ displayName: (val) => {
51
+ if (!val || val.length < 1) {
52
+ throw new Error('Display name is required');
53
+ }
54
+ if (val.length > 100) {
55
+ throw new Error('Display name must be at most 100 characters');
56
+ }
57
+ return val;
58
+ },
59
+ description: (val) => val || undefined,
60
+ configuration: (val) => {
61
+ const validTypes = ['webapp', 'api', 'service', 'functionapp'];
62
+ const validRegistryModes = ['acr', 'external', 'public'];
63
+
64
+ if (!val || !val.type || !validTypes.includes(val.type)) {
65
+ throw new Error('Configuration type must be one of: webapp, api, service, functionapp');
66
+ }
67
+ if (!val.registryMode || !validRegistryModes.includes(val.registryMode)) {
68
+ throw new Error('Registry mode must be one of: acr, external, public');
69
+ }
70
+ if (val.port !== undefined) {
71
+ if (!Number.isInteger(val.port) || val.port < 1 || val.port > 65535) {
72
+ throw new Error('Port must be an integer between 1 and 65535');
73
+ }
74
+ }
75
+ return val;
76
+ }
77
+ };
78
+
79
+ /**
80
+ * Load variables.yaml file for an application
81
+ * @async
82
+ * @param {string} appKey - Application key
83
+ * @returns {Promise<{variables: Object, created: boolean}>} Variables and creation flag
84
+ */
85
+ async function loadVariablesYaml(appKey) {
86
+ const variablesPath = path.join(process.cwd(), 'builder', appKey, 'variables.yaml');
87
+
88
+ try {
89
+ const variablesContent = await fs.readFile(variablesPath, 'utf-8');
90
+ return { variables: yaml.load(variablesContent), created: false };
91
+ } catch (error) {
92
+ if (error.code === 'ENOENT') {
93
+ logger.log(chalk.yellow(`āš ļø variables.yaml not found for ${appKey}`));
94
+ logger.log(chalk.yellow('šŸ“ Creating minimal configuration...\n'));
95
+ return { variables: null, created: true };
96
+ }
97
+ throw new Error(`Failed to read variables.yaml: ${error.message}`);
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Create minimal application configuration if needed
103
+ * @async
104
+ * @param {string} appKey - Application key
105
+ * @param {Object} options - Registration options
106
+ * @returns {Promise<Object>} Variables after creation
107
+ */
108
+ async function createMinimalAppIfNeeded(appKey, options) {
109
+ if (!createApp) {
110
+ throw new Error('Cannot auto-create application: createApp function not available');
111
+ }
112
+
113
+ await createApp(appKey, {
114
+ port: options.port,
115
+ language: 'typescript',
116
+ database: false,
117
+ redis: false,
118
+ storage: false,
119
+ authentication: false
120
+ });
121
+
122
+ const variablesPath = path.join(process.cwd(), 'builder', appKey, 'variables.yaml');
123
+ const variablesContent = await fs.readFile(variablesPath, 'utf-8');
124
+ return yaml.load(variablesContent);
125
+ }
126
+
127
+ /**
128
+ * Extract application configuration from variables.yaml
129
+ * @param {Object} variables - Variables from YAML file
130
+ * @param {string} appKey - Application key
131
+ * @param {Object} options - Registration options
132
+ * @returns {Object} Extracted configuration
133
+ */
134
+ function extractAppConfiguration(variables, appKey, options) {
135
+ const appKeyFromFile = variables.app?.key || appKey;
136
+ const displayName = variables.app?.name || options.name || appKey;
137
+ const description = variables.app?.description || '';
138
+ const appType = variables.build?.language === 'typescript' ? 'webapp' : 'service';
139
+ const registryMode = 'external';
140
+ const port = variables.build?.port || options.port || 3000;
141
+ const language = variables.build?.language || 'typescript';
142
+
143
+ return {
144
+ appKey: appKeyFromFile,
145
+ displayName,
146
+ description,
147
+ appType,
148
+ registryMode,
149
+ port,
150
+ language
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Validate application registration data
156
+ * @param {Object} config - Application configuration
157
+ * @param {string} originalAppKey - Original app key for error messages
158
+ * @throws {Error} If validation fails
159
+ */
160
+ function validateAppRegistrationData(config, originalAppKey) {
161
+ const missingFields = [];
162
+ if (!config.appKey) missingFields.push('app.key');
163
+ if (!config.displayName) missingFields.push('app.name');
164
+
165
+ if (missingFields.length > 0) {
166
+ logger.error(chalk.red('āŒ Missing required fields in variables.yaml:'));
167
+ missingFields.forEach(field => logger.error(chalk.red(` - ${field}`)));
168
+ logger.error(chalk.red(`\n Please update builder/${originalAppKey}/variables.yaml and try again.`));
169
+ process.exit(1);
170
+ }
171
+
172
+ try {
173
+ registerApplicationSchema.key(config.appKey);
174
+ registerApplicationSchema.displayName(config.displayName);
175
+ registerApplicationSchema.configuration({
176
+ type: config.appType,
177
+ registryMode: config.registryMode,
178
+ port: config.port
179
+ });
180
+ } catch (error) {
181
+ logger.error(chalk.red(`āŒ Invalid configuration: ${error.message}`));
182
+ process.exit(1);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Check if user is authenticated
188
+ * @async
189
+ * @returns {Promise<Object>} Configuration with API URL and token
190
+ */
191
+ async function checkAuthentication() {
192
+ const config = await getConfig();
193
+ if (!config.apiUrl || !config.token) {
194
+ logger.error(chalk.red('āŒ Not logged in. Run: aifabrix login'));
195
+ process.exit(1);
196
+ }
197
+ return config;
198
+ }
199
+
200
+ /**
201
+ * Call registration API
202
+ * @async
203
+ * @param {string} apiUrl - API URL
204
+ * @param {string} token - Authentication token
205
+ * @param {string} environment - Environment ID
206
+ * @param {Object} registrationData - Registration data
207
+ * @returns {Promise<Object>} API response
208
+ */
209
+ async function registerApplication(apiUrl, token, environment, registrationData) {
210
+ const response = await authenticatedApiCall(
211
+ `${apiUrl}/api/v1/environments/${encodeURIComponent(environment)}/applications/register`,
212
+ {
213
+ method: 'POST',
214
+ body: JSON.stringify(registrationData)
215
+ },
216
+ token
217
+ );
218
+
219
+ if (!response.success) {
220
+ logger.error(chalk.red(`āŒ Registration failed: ${response.error}`));
221
+ process.exit(1);
222
+ }
223
+
224
+ return response.data;
225
+ }
226
+
227
+ /**
228
+ * Display registration success and credentials
229
+ * @param {Object} data - Registration response data
230
+ * @param {string} apiUrl - API URL
231
+ */
232
+ function displayRegistrationResults(data, apiUrl) {
233
+ logger.log(chalk.green('āœ… Application registered successfully!\n'));
234
+ logger.log(chalk.bold('šŸ“‹ Application Details:'));
235
+ logger.log(` ID: ${data.application.id}`);
236
+ logger.log(` Key: ${data.application.key}`);
237
+ logger.log(` Display Name: ${data.application.displayName}\n`);
238
+
239
+ logger.log(chalk.bold.yellow('šŸ”‘ CREDENTIALS (save these immediately):'));
240
+ logger.log(chalk.yellow(` Client ID: ${data.credentials.clientId}`));
241
+ logger.log(chalk.yellow(` Client Secret: ${data.credentials.clientSecret}\n`));
242
+
243
+ logger.log(chalk.red('āš ļø IMPORTANT: Client Secret will not be shown again!\n'));
244
+
245
+ logger.log(chalk.bold('šŸ“ Add to GitHub Secrets:'));
246
+ logger.log(chalk.cyan(` AIFABRIX_CLIENT_ID = ${data.credentials.clientId}`));
247
+ logger.log(chalk.cyan(` AIFABRIX_CLIENT_SECRET = ${data.credentials.clientSecret}`));
248
+ logger.log(chalk.cyan(` AIFABRIX_API_URL = ${apiUrl}\n`));
249
+ }
250
+
251
+ /**
252
+ * Setup application management commands
253
+ * @param {Command} program - Commander program instance
254
+ */
255
+ function setupAppCommands(program) {
256
+ const app = program
257
+ .command('app')
258
+ .description('Manage applications');
259
+
260
+ // Register command
261
+ app
262
+ .command('register <appKey>')
263
+ .description('Register application and get pipeline credentials')
264
+ .requiredOption('-e, --environment <env>', 'Environment ID or key')
265
+ .option('-p, --port <port>', 'Application port (default: from variables.yaml)')
266
+ .option('-n, --name <name>', 'Override display name')
267
+ .option('-d, --description <desc>', 'Override description')
268
+ .action(async(appKey, options) => {
269
+ try {
270
+ logger.log(chalk.blue('šŸ“‹ Registering application...\n'));
271
+
272
+ // Load variables.yaml
273
+ const { variables, created } = await loadVariablesYaml(appKey);
274
+ let finalVariables = variables;
275
+
276
+ // Create minimal app if needed
277
+ if (created) {
278
+ finalVariables = await createMinimalAppIfNeeded(appKey, options);
279
+ }
280
+
281
+ // Extract configuration
282
+ const appConfig = extractAppConfiguration(finalVariables, appKey, options);
283
+
284
+ // Validate configuration (pass original appKey for error messages)
285
+ validateAppRegistrationData(appConfig, appKey);
286
+
287
+ // Check authentication
288
+ const config = await checkAuthentication();
289
+
290
+ // Validate environment
291
+ const environment = registerApplicationSchema.environmentId(options.environment);
292
+
293
+ // Prepare registration data
294
+ const registrationData = {
295
+ environmentId: environment,
296
+ key: appConfig.appKey,
297
+ displayName: appConfig.displayName,
298
+ description: appConfig.description || options.description,
299
+ configuration: {
300
+ type: appConfig.appType,
301
+ registryMode: appConfig.registryMode,
302
+ port: appConfig.port,
303
+ language: appConfig.language
304
+ }
305
+ };
306
+
307
+ // Register application
308
+ const responseData = await registerApplication(
309
+ config.apiUrl,
310
+ config.token,
311
+ environment,
312
+ registrationData
313
+ );
314
+
315
+ // Display results
316
+ displayRegistrationResults(responseData, config.apiUrl);
317
+
318
+ } catch (error) {
319
+ logger.error(chalk.red('āŒ Registration failed:'), error.message);
320
+ process.exit(1);
321
+ }
322
+ });
323
+
324
+ // List command
325
+ app
326
+ .command('list')
327
+ .description('List applications')
328
+ .requiredOption('-e, --environment <env>', 'Environment ID or key')
329
+ .action(async(options) => {
330
+ try {
331
+ const config = await getConfig();
332
+ if (!config.apiUrl || !config.token) {
333
+ logger.error(chalk.red('āŒ Not logged in. Run: aifabrix login'));
334
+ process.exit(1);
335
+ }
336
+
337
+ const response = await authenticatedApiCall(
338
+ `${config.apiUrl}/api/v1/applications?environmentId=${options.environment}`,
339
+ {},
340
+ config.token
341
+ );
342
+
343
+ if (!response.success || !response.data) {
344
+ logger.error(chalk.red('āŒ Failed to fetch applications'));
345
+ process.exit(1);
346
+ }
347
+
348
+ logger.log(chalk.bold('\nšŸ“± Applications:\n'));
349
+ response.data.forEach((app) => {
350
+ const hasPipeline = app.configuration?.pipeline?.isActive ? 'āœ“' : 'āœ—';
351
+ logger.log(`${hasPipeline} ${chalk.cyan(app.key)} - ${app.displayName} (${app.status})`);
352
+ });
353
+ logger.log('');
354
+
355
+ } catch (error) {
356
+ logger.error(chalk.red('āŒ Failed to list applications:'), error.message);
357
+ process.exit(1);
358
+ }
359
+ });
360
+
361
+ // Rotate secret command
362
+ app
363
+ .command('rotate-secret')
364
+ .description('Rotate pipeline ClientSecret for an application')
365
+ .requiredOption('-a, --app <appKey>', 'Application key')
366
+ .requiredOption('-e, --environment <env>', 'Environment ID or key')
367
+ .action(async(options) => {
368
+ try {
369
+ logger.log(chalk.yellow('āš ļø This will invalidate the old ClientSecret!\n'));
370
+
371
+ const config = await getConfig();
372
+ if (!config.apiUrl || !config.token) {
373
+ logger.error(chalk.red('āŒ Not logged in. Run: aifabrix login'));
374
+ process.exit(1);
375
+ }
376
+
377
+ // Validate environment
378
+ if (!options.environment || options.environment.length < 1) {
379
+ logger.error(chalk.red('āŒ Environment is required'));
380
+ process.exit(1);
381
+ }
382
+
383
+ const response = await authenticatedApiCall(
384
+ `${config.apiUrl}/api/v1/applications/${options.app}/rotate-secret?environmentId=${options.environment}`,
385
+ {
386
+ method: 'POST'
387
+ },
388
+ config.token
389
+ );
390
+
391
+ if (!response.success) {
392
+ logger.error(chalk.red(`āŒ Rotation failed: ${response.error}`));
393
+ process.exit(1);
394
+ }
395
+
396
+ logger.log(chalk.green('āœ… Secret rotated successfully!\n'));
397
+ logger.log(chalk.bold('šŸ“‹ Application Details:'));
398
+ logger.log(` Key: ${response.data.application?.key || options.app}`);
399
+ logger.log(` Environment: ${options.environment}\n`);
400
+
401
+ logger.log(chalk.bold.yellow('šŸ”‘ NEW CREDENTIALS:'));
402
+ logger.log(chalk.yellow(` Client ID: ${response.data.credentials.clientId}`));
403
+ logger.log(chalk.yellow(` Client Secret: ${response.data.credentials.clientSecret}\n`));
404
+ logger.log(chalk.red('āš ļø Old secret is now invalid. Update GitHub Secrets!\n'));
405
+
406
+ } catch (error) {
407
+ logger.error(chalk.red('āŒ Rotation failed:'), error.message);
408
+ process.exit(1);
409
+ }
410
+ });
411
+ }
412
+
413
+ module.exports = { setupAppCommands };
414
+