@aifabrix/builder 2.1.7 → 2.3.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.
Files changed (43) hide show
  1. package/lib/app-deploy.js +73 -29
  2. package/lib/app-list.js +132 -0
  3. package/lib/app-readme.js +11 -4
  4. package/lib/app-register.js +435 -0
  5. package/lib/app-rotate-secret.js +164 -0
  6. package/lib/app-run.js +98 -84
  7. package/lib/app.js +13 -0
  8. package/lib/audit-logger.js +195 -15
  9. package/lib/build.js +155 -42
  10. package/lib/cli.js +104 -8
  11. package/lib/commands/app.js +8 -391
  12. package/lib/commands/login.js +130 -36
  13. package/lib/commands/secure.js +260 -0
  14. package/lib/config.js +315 -4
  15. package/lib/deployer.js +221 -183
  16. package/lib/infra.js +177 -112
  17. package/lib/push.js +34 -7
  18. package/lib/secrets.js +89 -23
  19. package/lib/templates.js +1 -1
  20. package/lib/utils/api-error-handler.js +465 -0
  21. package/lib/utils/api.js +165 -16
  22. package/lib/utils/auth-headers.js +84 -0
  23. package/lib/utils/build-copy.js +162 -0
  24. package/lib/utils/cli-utils.js +49 -3
  25. package/lib/utils/compose-generator.js +57 -16
  26. package/lib/utils/deployment-errors.js +90 -0
  27. package/lib/utils/deployment-validation.js +60 -0
  28. package/lib/utils/dev-config.js +83 -0
  29. package/lib/utils/docker-build.js +24 -0
  30. package/lib/utils/env-template.js +30 -10
  31. package/lib/utils/health-check.js +18 -1
  32. package/lib/utils/infra-containers.js +101 -0
  33. package/lib/utils/local-secrets.js +0 -2
  34. package/lib/utils/secrets-encryption.js +203 -0
  35. package/lib/utils/secrets-path.js +22 -3
  36. package/lib/utils/token-manager.js +381 -0
  37. package/package.json +2 -2
  38. package/templates/applications/README.md.hbs +155 -23
  39. package/templates/applications/miso-controller/Dockerfile +7 -119
  40. package/templates/infra/compose.yaml.hbs +93 -0
  41. package/templates/python/docker-compose.hbs +25 -17
  42. package/templates/typescript/docker-compose.hbs +25 -17
  43. package/test-output.txt +0 -5431
package/lib/app-deploy.js CHANGED
@@ -15,6 +15,8 @@ const yaml = require('js-yaml');
15
15
  const chalk = require('chalk');
16
16
  const pushUtils = require('./push');
17
17
  const logger = require('./utils/logger');
18
+ const config = require('./config');
19
+ const { getDeploymentAuth } = require('./utils/token-manager');
18
20
 
19
21
  /**
20
22
  * Validate application name format
@@ -22,8 +24,8 @@ const logger = require('./utils/logger');
22
24
  * @throws {Error} If app name is invalid
23
25
  */
24
26
  function validateAppName(appName) {
25
- if (!appName || typeof appName !== 'string') {
26
- throw new Error('Application name is required');
27
+ if (!appName || typeof appName !== 'string' || appName.trim().length === 0) {
28
+ throw new Error('App name is required');
27
29
  }
28
30
 
29
31
  // App name should be lowercase, alphanumeric with dashes, 3-40 characters
@@ -183,9 +185,7 @@ async function loadVariablesFile(variablesPath) {
183
185
  function extractDeploymentConfig(options, variables) {
184
186
  return {
185
187
  controllerUrl: options.controller || variables.deployment?.controllerUrl,
186
- envKey: options.environment || variables.deployment?.environment || 'dev',
187
- clientId: options.clientId || variables.deployment?.clientId,
188
- clientSecret: options.clientSecret || variables.deployment?.clientSecret,
188
+ envKey: options.environment || variables.deployment?.environment,
189
189
  poll: options.poll !== false,
190
190
  pollInterval: options.pollInterval || 5000,
191
191
  pollMaxAttempts: options.pollMaxAttempts || 60
@@ -194,24 +194,24 @@ function extractDeploymentConfig(options, variables) {
194
194
 
195
195
  /**
196
196
  * Validates required deployment configuration
197
- * @param {Object} config - Deployment configuration
197
+ * @param {Object} deploymentConfig - Deployment configuration
198
198
  * @throws {Error} If configuration is invalid
199
199
  */
200
- function validateDeploymentConfig(config) {
201
- if (!config.controllerUrl) {
200
+ function validateDeploymentConfig(deploymentConfig) {
201
+ if (!deploymentConfig.controllerUrl) {
202
202
  throw new Error('Controller URL is required. Set it in variables.yaml or use --controller flag');
203
203
  }
204
- if (!config.clientId || !config.clientSecret) {
205
- throw new Error('Client ID and Client Secret are required. Set them in variables.yaml or use --client-id and --client-secret flags');
204
+ if (!deploymentConfig.auth) {
205
+ throw new Error('Authentication is required. Run "aifabrix login" first or ensure credentials are in secrets.local.yaml');
206
206
  }
207
207
  }
208
208
 
209
209
  /**
210
- * Loads deployment configuration from variables.yaml
210
+ * Loads deployment configuration from variables.yaml and gets/refreshes token
211
211
  * @async
212
212
  * @param {string} appName - Application name
213
213
  * @param {Object} options - CLI options
214
- * @returns {Promise<Object>} Deployment configuration
214
+ * @returns {Promise<Object>} Deployment configuration with token
215
215
  * @throws {Error} If configuration is invalid
216
216
  */
217
217
  async function loadDeploymentConfig(appName, options) {
@@ -221,10 +221,44 @@ async function loadDeploymentConfig(appName, options) {
221
221
  const variablesPath = path.join(builderPath, 'variables.yaml');
222
222
  const variables = await loadVariablesFile(variablesPath);
223
223
 
224
- const config = extractDeploymentConfig(options, variables);
225
- validateDeploymentConfig(config);
224
+ const deploymentConfig = extractDeploymentConfig(options, variables);
226
225
 
227
- return config;
226
+ // Update root-level environment if provided
227
+ if (options.environment) {
228
+ await config.setCurrentEnvironment(options.environment);
229
+ }
230
+
231
+ // Get current environment from root-level config
232
+ const currentEnvironment = await config.getCurrentEnvironment();
233
+ deploymentConfig.envKey = deploymentConfig.envKey || currentEnvironment;
234
+
235
+ // Get controller URL
236
+ if (!deploymentConfig.controllerUrl) {
237
+ throw new Error('Controller URL is required. Set it in variables.yaml or use --controller flag');
238
+ }
239
+
240
+ // Get deployment authentication (device token → client token → credentials)
241
+ try {
242
+ const authConfig = await getDeploymentAuth(
243
+ deploymentConfig.controllerUrl,
244
+ deploymentConfig.envKey,
245
+ appName
246
+ );
247
+ if (!authConfig || !authConfig.controller) {
248
+ throw new Error('Invalid authentication configuration: missing controller URL');
249
+ }
250
+ if (!authConfig.token) {
251
+ throw new Error('Authentication is required');
252
+ }
253
+ deploymentConfig.auth = authConfig;
254
+ deploymentConfig.controllerUrl = authConfig.controller;
255
+ } catch (error) {
256
+ throw new Error(`Failed to get authentication: ${error.message}`);
257
+ }
258
+
259
+ validateDeploymentConfig(deploymentConfig);
260
+
261
+ return deploymentConfig;
228
262
  }
229
263
 
230
264
  /**
@@ -264,22 +298,21 @@ function displayDeploymentInfo(manifest, manifestPath) {
264
298
  * Executes deployment to controller
265
299
  * @async
266
300
  * @param {Object} manifest - Deployment manifest
267
- * @param {Object} config - Deployment configuration
301
+ * @param {Object} deploymentConfig - Deployment configuration
268
302
  * @returns {Promise<Object>} Deployment result
269
303
  */
270
- async function executeDeployment(manifest, config) {
271
- logger.log(chalk.blue(`\nšŸš€ Deploying to ${config.controllerUrl} (environment: ${config.envKey})...`));
304
+ async function executeDeployment(manifest, deploymentConfig) {
305
+ logger.log(chalk.blue(`\nšŸš€ Deploying to ${deploymentConfig.controllerUrl} (environment: ${deploymentConfig.envKey})...`));
272
306
  const deployer = require('./deployer');
273
307
  return await deployer.deployToController(
274
308
  manifest,
275
- config.controllerUrl,
276
- config.envKey,
277
- config.clientId,
278
- config.clientSecret,
309
+ deploymentConfig.controllerUrl,
310
+ deploymentConfig.envKey,
311
+ deploymentConfig.auth,
279
312
  {
280
- poll: config.poll,
281
- pollInterval: config.pollInterval,
282
- pollMaxAttempts: config.pollMaxAttempts
313
+ poll: deploymentConfig.poll,
314
+ pollInterval: deploymentConfig.pollInterval,
315
+ pollMaxAttempts: deploymentConfig.pollMaxAttempts
283
316
  }
284
317
  );
285
318
  }
@@ -312,7 +345,7 @@ function displayDeploymentResults(result) {
312
345
  * @param {string} appName - Name of the application to deploy
313
346
  * @param {Object} options - Deployment options
314
347
  * @param {string} options.controller - Controller URL (required)
315
- * @param {string} [options.environment] - Target environment (dev/tst/pro)
348
+ * @param {string} [options.environment] - Target environment (miso/dev/tst/pro)
316
349
  * @param {boolean} [options.poll] - Poll for deployment status
317
350
  * @param {number} [options.pollInterval] - Polling interval in milliseconds
318
351
  * @returns {Promise<Object>} Deployment result
@@ -322,16 +355,20 @@ function displayDeploymentResults(result) {
322
355
  * await deployApp('myapp', { controller: 'https://controller.aifabrix.ai', environment: 'dev' });
323
356
  */
324
357
  async function deployApp(appName, options = {}) {
358
+ let controllerUrl = null;
359
+ let config = null;
360
+
325
361
  try {
326
362
  // 1. Input validation
327
- if (!appName || typeof appName !== 'string') {
363
+ if (!appName || typeof appName !== 'string' || appName.trim().length === 0) {
328
364
  throw new Error('App name is required');
329
365
  }
330
366
 
331
367
  validateAppName(appName);
332
368
 
333
369
  // 2. Load deployment configuration
334
- const config = await loadDeploymentConfig(appName, options);
370
+ config = await loadDeploymentConfig(appName, options);
371
+ controllerUrl = config.controllerUrl || options.controller || 'unknown';
335
372
 
336
373
  // 3. Generate and validate manifest
337
374
  const { manifest, manifestPath } = await generateAndValidateManifest(appName);
@@ -348,7 +385,14 @@ async function deployApp(appName, options = {}) {
348
385
  return result;
349
386
 
350
387
  } catch (error) {
351
- throw new Error(`Failed to deploy application: ${error.message}`);
388
+ // Use unified error handler from deployer
389
+ // Check if error was already logged (from deployer.js)
390
+ const alreadyLogged = error._logged === true;
391
+ const url = controllerUrl || options.controller || 'unknown';
392
+
393
+ const deployer = require('./deployer');
394
+ // handleDeploymentErrors will log, format, and throw the error
395
+ await deployer.handleDeploymentErrors(error, appName, url, alreadyLogged);
352
396
  }
353
397
  }
354
398
 
@@ -0,0 +1,132 @@
1
+ /**
2
+ * AI Fabrix Builder - App List Command
3
+ *
4
+ * Handles listing applications in an environment
5
+ *
6
+ * @fileoverview App list command implementation for AI Fabrix Builder
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const chalk = require('chalk');
12
+ const { getConfig } = require('./config');
13
+ const { getOrRefreshDeviceToken } = require('./utils/token-manager');
14
+ const { authenticatedApiCall } = require('./utils/api');
15
+ const { formatApiError } = require('./utils/api-error-handler');
16
+ const logger = require('./utils/logger');
17
+
18
+ /**
19
+ * Extract applications array from API response
20
+ * Handles multiple response formats:
21
+ * 1. Wrapped format: { success: true, data: { success: true, data: [...] } }
22
+ * 2. Direct array: { success: true, data: [...] }
23
+ * 3. Paginated format: { success: true, data: { items: [...] } }
24
+ * 4. Wrapped paginated: { success: true, data: { success: true, data: { items: [...] } } }
25
+ * @param {Object} response - API response from authenticatedApiCall
26
+ * @returns {Array} Array of applications
27
+ * @throws {Error} If response format is invalid
28
+ */
29
+ function extractApplications(response) {
30
+ const apiResponse = response.data;
31
+ let applications;
32
+
33
+ // Check if apiResponse.data is an array (wrapped format)
34
+ if (apiResponse && apiResponse.data && Array.isArray(apiResponse.data)) {
35
+ applications = apiResponse.data;
36
+ } else if (Array.isArray(apiResponse)) {
37
+ // Check if apiResponse is directly an array
38
+ applications = apiResponse;
39
+ } else if (apiResponse && Array.isArray(apiResponse.items)) {
40
+ // Check if apiResponse.items is an array (paginated format)
41
+ applications = apiResponse.items;
42
+ } else if (apiResponse && apiResponse.data && apiResponse.data.items && Array.isArray(apiResponse.data.items)) {
43
+ // Check if apiResponse.data.items is an array (wrapped paginated format)
44
+ applications = apiResponse.data.items;
45
+ } else {
46
+ logger.error(chalk.red('āŒ Invalid response: expected data array or items array'));
47
+ logger.error(chalk.gray('\nAPI response type:'), typeof apiResponse);
48
+ logger.error(chalk.gray('API response:'), JSON.stringify(apiResponse, null, 2));
49
+ logger.error(chalk.gray('\nFull response for debugging:'));
50
+ logger.error(chalk.gray(JSON.stringify(response, null, 2)));
51
+ throw new Error('Invalid response format');
52
+ }
53
+
54
+ return applications;
55
+ }
56
+
57
+ /**
58
+ * Display applications list
59
+ * @param {Array} applications - Array of application objects
60
+ * @param {string} environment - Environment name or key
61
+ */
62
+ function displayApplications(applications, environment) {
63
+ const environmentName = environment || 'miso';
64
+ const header = `Applications in ${environmentName} environment`;
65
+
66
+ if (applications.length === 0) {
67
+ logger.log(chalk.bold(`\nšŸ“± ${header}:\n`));
68
+ logger.log(chalk.gray(' No applications found in this environment.\n'));
69
+ return;
70
+ }
71
+
72
+ logger.log(chalk.bold(`\nšŸ“± ${header}:\n`));
73
+ applications.forEach((app) => {
74
+ const hasPipeline = app.configuration?.pipeline?.isActive ? 'āœ“' : 'āœ—';
75
+ logger.log(`${hasPipeline} ${chalk.cyan(app.key)} - ${app.displayName} (${app.status || 'unknown'})`);
76
+ });
77
+ logger.log('');
78
+ }
79
+
80
+ /**
81
+ * List applications in an environment
82
+ * @async
83
+ * @param {Object} options - Command options
84
+ * @param {string} options.environment - Environment ID or key
85
+ * @throws {Error} If listing fails
86
+ */
87
+ async function listApplications(options) {
88
+ const config = await getConfig();
89
+
90
+ // Try to get device token
91
+ let controllerUrl = null;
92
+ let token = null;
93
+
94
+ if (config.device) {
95
+ const deviceUrls = Object.keys(config.device);
96
+ if (deviceUrls.length > 0) {
97
+ controllerUrl = deviceUrls[0];
98
+ const deviceToken = await getOrRefreshDeviceToken(controllerUrl);
99
+ if (deviceToken && deviceToken.token) {
100
+ token = deviceToken.token;
101
+ controllerUrl = deviceToken.controller;
102
+ }
103
+ }
104
+ }
105
+
106
+ if (!token || !controllerUrl) {
107
+ logger.error(chalk.red('āŒ Not logged in. Run: aifabrix login'));
108
+ logger.error(chalk.gray(' Use device code flow: aifabrix login --method device --controller <url>'));
109
+ process.exit(1);
110
+ }
111
+
112
+ const response = await authenticatedApiCall(
113
+ `${controllerUrl}/api/v1/environments/${options.environment}/applications`,
114
+ {},
115
+ token
116
+ );
117
+
118
+ if (!response.success || !response.data) {
119
+ const formattedError = response.formattedError || formatApiError(response);
120
+ logger.error(formattedError);
121
+ // Log full response for debugging
122
+ logger.error(chalk.gray('\nFull response for debugging:'));
123
+ logger.error(chalk.gray(JSON.stringify(response, null, 2)));
124
+ process.exit(1);
125
+ }
126
+
127
+ const applications = extractApplications(response);
128
+ displayApplications(applications, options.environment);
129
+ }
130
+
131
+ module.exports = { listApplications };
132
+
package/lib/app-readme.js CHANGED
@@ -69,16 +69,23 @@ function generateReadmeMd(appName, config) {
69
69
  // Extract registry from nested structure (config.image.registry) or flattened (config.registry)
70
70
  const registry = config.image?.registry || config.registry || 'myacr.azurecr.io';
71
71
 
72
+ const hasDatabase = config.database || config.requires?.database || false;
73
+ const hasRedis = config.redis || config.requires?.redis || false;
74
+ const hasStorage = config.storage || config.requires?.storage || false;
75
+ const hasAuthentication = config.authentication || false;
76
+ const hasAnyService = hasDatabase || hasRedis || hasStorage || hasAuthentication;
77
+
72
78
  const context = {
73
79
  appName,
74
80
  displayName,
75
81
  imageName,
76
82
  port,
77
83
  registry,
78
- hasDatabase: config.database || config.requires?.database || false,
79
- hasRedis: config.redis || config.requires?.redis || false,
80
- hasStorage: config.storage || config.requires?.storage || false,
81
- hasAuthentication: config.authentication || false
84
+ hasDatabase,
85
+ hasRedis,
86
+ hasStorage,
87
+ hasAuthentication,
88
+ hasAnyService
82
89
  };
83
90
 
84
91
  return template(context);