@aifabrix/builder 2.22.2 → 2.31.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 (62) hide show
  1. package/jest.config.coverage.js +37 -0
  2. package/lib/api/pipeline.api.js +10 -9
  3. package/lib/app-deploy.js +36 -14
  4. package/lib/app-list.js +191 -71
  5. package/lib/app-prompts.js +77 -26
  6. package/lib/app-readme.js +123 -5
  7. package/lib/app-rotate-secret.js +101 -57
  8. package/lib/app-run-helpers.js +200 -172
  9. package/lib/app-run.js +137 -68
  10. package/lib/audit-logger.js +8 -7
  11. package/lib/build.js +161 -250
  12. package/lib/cli.js +73 -65
  13. package/lib/commands/login.js +45 -31
  14. package/lib/commands/logout.js +181 -0
  15. package/lib/commands/secure.js +59 -24
  16. package/lib/config.js +79 -45
  17. package/lib/datasource-deploy.js +89 -29
  18. package/lib/deployer.js +164 -129
  19. package/lib/diff.js +63 -21
  20. package/lib/environment-deploy.js +36 -19
  21. package/lib/external-system-deploy.js +134 -66
  22. package/lib/external-system-download.js +244 -171
  23. package/lib/external-system-test.js +199 -164
  24. package/lib/generator-external.js +145 -72
  25. package/lib/generator-helpers.js +49 -17
  26. package/lib/generator-split.js +105 -58
  27. package/lib/infra.js +101 -131
  28. package/lib/schema/application-schema.json +895 -896
  29. package/lib/schema/env-config.yaml +11 -4
  30. package/lib/template-validator.js +13 -4
  31. package/lib/utils/api.js +8 -8
  32. package/lib/utils/app-register-auth.js +36 -18
  33. package/lib/utils/app-run-containers.js +140 -0
  34. package/lib/utils/auth-headers.js +6 -6
  35. package/lib/utils/build-copy.js +60 -2
  36. package/lib/utils/build-helpers.js +94 -0
  37. package/lib/utils/cli-utils.js +177 -76
  38. package/lib/utils/compose-generator.js +12 -2
  39. package/lib/utils/config-tokens.js +151 -9
  40. package/lib/utils/deployment-errors.js +137 -69
  41. package/lib/utils/deployment-validation-helpers.js +103 -0
  42. package/lib/utils/docker-build.js +57 -0
  43. package/lib/utils/dockerfile-utils.js +13 -3
  44. package/lib/utils/env-copy.js +163 -94
  45. package/lib/utils/env-map.js +226 -86
  46. package/lib/utils/error-formatters/network-errors.js +0 -1
  47. package/lib/utils/external-system-display.js +14 -19
  48. package/lib/utils/external-system-env-helpers.js +107 -0
  49. package/lib/utils/external-system-test-helpers.js +144 -0
  50. package/lib/utils/health-check.js +10 -8
  51. package/lib/utils/infra-status.js +123 -0
  52. package/lib/utils/paths.js +228 -49
  53. package/lib/utils/schema-loader.js +125 -57
  54. package/lib/utils/token-manager.js +3 -3
  55. package/lib/utils/yaml-preserve.js +55 -16
  56. package/lib/validate.js +87 -89
  57. package/package.json +4 -4
  58. package/scripts/ci-fix.sh +19 -0
  59. package/scripts/ci-simulate.sh +19 -0
  60. package/templates/applications/miso-controller/test.yaml +1 -0
  61. package/templates/python/Dockerfile.hbs +8 -45
  62. package/templates/typescript/Dockerfile.hbs +8 -42
package/lib/app-readme.js CHANGED
@@ -44,11 +44,21 @@ function formatAppDisplayName(appName) {
44
44
  * Loads and compiles README.md template
45
45
  * @returns {Function} Compiled Handlebars template
46
46
  * @throws {Error} If template not found
47
+ * @private
47
48
  */
48
- function loadReadmeTemplate() {
49
- const templatePath = path.join(__dirname, '..', 'templates', 'applications', 'README.md.hbs');
49
+ function _loadReadmeTemplate() {
50
+ // Use getProjectRoot to reliably find templates in all environments
51
+ const { getProjectRoot } = require('./utils/paths');
52
+ const projectRoot = getProjectRoot();
53
+ const templatePath = path.join(projectRoot, 'templates', 'applications', 'README.md.hbs');
54
+
50
55
  if (!fsSync.existsSync(templatePath)) {
51
- throw new Error(`README template not found at ${templatePath}`);
56
+ // Provide helpful error message with actual paths checked
57
+ const errorMessage = `README template not found at ${templatePath}\n` +
58
+ ` Project root: ${projectRoot}\n` +
59
+ ` Templates directory: ${path.join(projectRoot, 'templates', 'applications')}\n` +
60
+ ` Global PROJECT_ROOT: ${typeof global !== 'undefined' && global.PROJECT_ROOT ? global.PROJECT_ROOT : 'not set'}`;
61
+ throw new Error(errorMessage);
52
62
  }
53
63
 
54
64
  const templateContent = fsSync.readFileSync(templatePath, 'utf8');
@@ -62,7 +72,6 @@ function loadReadmeTemplate() {
62
72
  * @returns {string} README.md content
63
73
  */
64
74
  function generateReadmeMd(appName, config) {
65
- const template = loadReadmeTemplate();
66
75
  const displayName = formatAppDisplayName(appName);
67
76
  const imageName = `aifabrix/${appName}`;
68
77
  const port = config.port || 3000;
@@ -88,7 +97,116 @@ function generateReadmeMd(appName, config) {
88
97
  hasAnyService
89
98
  };
90
99
 
91
- return template(context);
100
+ // Always generate comprehensive README programmatically to ensure consistency
101
+ // regardless of template file content
102
+ return generateComprehensiveReadme(context);
103
+ }
104
+
105
+ /**
106
+ * Generates comprehensive README.md content programmatically
107
+ * @param {Object} context - Template context
108
+ * @returns {string} Comprehensive README.md content
109
+ */
110
+ function generateComprehensiveReadme(context) {
111
+ const { appName, displayName, imageName, port, registry, hasDatabase, hasRedis, hasStorage, hasAuthentication, hasAnyService } = context;
112
+
113
+ let prerequisites = 'Before running this application, ensure the following prerequisites are met:\n';
114
+ prerequisites += '- `@aifabrix/builder` installed globally\n';
115
+ prerequisites += '- Docker Desktop running\n';
116
+
117
+ if (hasAnyService) {
118
+ if (hasDatabase) {
119
+ prerequisites += '- PostgreSQL database\n';
120
+ }
121
+ if (hasRedis) {
122
+ prerequisites += '- Redis\n';
123
+ }
124
+ if (hasStorage) {
125
+ prerequisites += '- File storage configured\n';
126
+ }
127
+ if (hasAuthentication) {
128
+ prerequisites += '- Authentication/RBAC configured\n';
129
+ }
130
+ } else {
131
+ prerequisites += '- Infrastructure running\n';
132
+ }
133
+
134
+ let troubleshooting = '';
135
+ if (hasDatabase) {
136
+ troubleshooting = `### Database Connection Issues
137
+
138
+ If you encounter database connection errors, ensure:
139
+ - PostgreSQL is running and accessible
140
+ - Database credentials are correctly configured in your \`.env\` file
141
+ - The database name matches your configuration
142
+ - Verify infrastructure is running and PostgreSQL is accessible`;
143
+ } else {
144
+ troubleshooting = 'Verify infrastructure is running.';
145
+ }
146
+
147
+ return `# ${displayName} Builder
148
+
149
+ Build, run, and deploy ${displayName}.
150
+
151
+ ## Prerequisites
152
+
153
+ ${prerequisites}
154
+
155
+ ## Quick Start
156
+
157
+ ### 1. Install
158
+
159
+ Install the AI Fabrix Builder CLI if you haven't already.
160
+
161
+ ### 2. Configure
162
+
163
+ Configure your application settings in \`variables.yaml\`.
164
+
165
+ ### 3. Build & Run Locally
166
+
167
+ Build the application:
168
+ \`\`\`bash
169
+ aifabrix build ${appName}
170
+ \`\`\`
171
+
172
+ Run the application:
173
+ \`\`\`bash
174
+ aifabrix run ${appName}
175
+ \`\`\`
176
+
177
+ The application will be available at http://localhost:${port} (default: ${port}).
178
+
179
+ ### 4. Deploy to Azure
180
+
181
+ Push to registry:
182
+ \`\`\`bash
183
+ aifabrix push ${appName} --registry ${registry} --tag "v1.0.0,latest"
184
+ \`\`\`
185
+
186
+ ## Configuration
187
+
188
+ - **Port**: ${port} (default: 3000)
189
+ - **Image**: ${imageName}:latest
190
+ - **Registry**: ${registry}
191
+
192
+ ## Docker Commands
193
+
194
+ View logs:
195
+ \`\`\`bash
196
+ docker logs aifabrix-${appName} -f
197
+ \`\`\`
198
+
199
+ Stop the application:
200
+ \`\`\`bash
201
+ aifabrix down ${appName}
202
+ \`\`\`
203
+
204
+ ## Troubleshooting
205
+
206
+ ${troubleshooting}
207
+
208
+ For more information, see the [AI Fabrix Builder documentation](https://docs.aifabrix.com).
209
+ `;
92
210
  }
93
211
 
94
212
  /**
@@ -20,6 +20,36 @@ const { updateEnvTemplate } = require('./utils/env-template');
20
20
  const { getEnvironmentPrefix } = require('./app-register');
21
21
  const { generateEnvFile } = require('./secrets');
22
22
 
23
+ /**
24
+ * Find device token from config by trying each stored URL
25
+ * @async
26
+ * @param {Object} deviceConfig - Device configuration object
27
+ * @returns {Promise<Object|null>} Token result with token and controllerUrl, or null if not found
28
+ */
29
+ async function findDeviceTokenFromConfig(deviceConfig) {
30
+ const deviceUrls = Object.keys(deviceConfig);
31
+ if (deviceUrls.length === 0) {
32
+ return null;
33
+ }
34
+
35
+ for (const storedUrl of deviceUrls) {
36
+ try {
37
+ const normalizedStoredUrl = normalizeControllerUrl(storedUrl);
38
+ const deviceToken = await getOrRefreshDeviceToken(normalizedStoredUrl);
39
+ if (deviceToken && deviceToken.token) {
40
+ return {
41
+ token: deviceToken.token,
42
+ controllerUrl: deviceToken.controller || normalizedStoredUrl
43
+ };
44
+ }
45
+ } catch (error) {
46
+ // Continue to next URL
47
+ }
48
+ }
49
+
50
+ return null;
51
+ }
52
+
23
53
  /**
24
54
  * Validate environment parameter
25
55
  * @param {string} environment - Environment ID or key
@@ -79,21 +109,14 @@ function displayRotationResults(appKey, environment, credentials, apiUrl, messag
79
109
  }
80
110
 
81
111
  /**
82
- * Rotate secret for an application
112
+ * Get authentication token for rotation
83
113
  * @async
84
- * @param {string} appKey - Application key
85
- * @param {Object} options - Command options
86
- * @param {string} options.environment - Environment ID or key
87
- * @param {string} [options.controller] - Controller URL (optional, uses configured controller if not provided)
88
- * @throws {Error} If rotation fails
114
+ * @param {string} [controllerUrl] - Optional controller URL
115
+ * @param {Object} config - Configuration object
116
+ * @returns {Promise<Object>} Object with token and actualControllerUrl
117
+ * @throws {Error} If authentication fails
89
118
  */
90
- async function rotateSecret(appKey, options) {
91
- logger.log(chalk.yellow('⚠️ This will invalidate the old ClientSecret!\n'));
92
-
93
- const config = await getConfig();
94
-
95
- // Get controller URL with priority: options.controller > device tokens
96
- const controllerUrl = options.controller || null;
119
+ async function getRotationAuthToken(controllerUrl, config) {
97
120
  let token = null;
98
121
  let actualControllerUrl = null;
99
122
 
@@ -107,7 +130,6 @@ async function rotateSecret(appKey, options) {
107
130
  actualControllerUrl = deviceToken.controller || normalizedUrl;
108
131
  }
109
132
  } catch (error) {
110
- // Show which controller URL failed
111
133
  logger.error(chalk.red(`❌ Failed to authenticate with controller: ${controllerUrl}`));
112
134
  logger.error(chalk.gray(`Error: ${error.message}`));
113
135
  process.exit(1);
@@ -116,21 +138,10 @@ async function rotateSecret(appKey, options) {
116
138
 
117
139
  // If no token yet, try to find any device token in config
118
140
  if (!token && config.device) {
119
- const deviceUrls = Object.keys(config.device);
120
- if (deviceUrls.length > 0) {
121
- for (const storedUrl of deviceUrls) {
122
- try {
123
- const normalizedStoredUrl = normalizeControllerUrl(storedUrl);
124
- const deviceToken = await getOrRefreshDeviceToken(normalizedStoredUrl);
125
- if (deviceToken && deviceToken.token) {
126
- token = deviceToken.token;
127
- actualControllerUrl = deviceToken.controller || normalizedStoredUrl;
128
- break;
129
- }
130
- } catch (error) {
131
- // Continue to next URL
132
- }
133
- }
141
+ const tokenResult = await findDeviceTokenFromConfig(config.device);
142
+ if (tokenResult) {
143
+ token = tokenResult.token;
144
+ actualControllerUrl = tokenResult.controllerUrl;
134
145
  }
135
146
  }
136
147
 
@@ -143,6 +154,65 @@ async function rotateSecret(appKey, options) {
143
154
  process.exit(1);
144
155
  }
145
156
 
157
+ return { token, actualControllerUrl };
158
+ }
159
+
160
+ /**
161
+ * Save credentials locally and update env files
162
+ * @async
163
+ * @param {string} appKey - Application key
164
+ * @param {Object} credentials - Credentials object
165
+ * @param {string} actualControllerUrl - Controller URL
166
+ * @throws {Error} If saving fails
167
+ */
168
+ async function saveCredentialsLocally(appKey, credentials, actualControllerUrl) {
169
+ const clientIdKey = `${appKey}-client-idKeyVault`;
170
+ const clientSecretKey = `${appKey}-client-secretKeyVault`;
171
+
172
+ try {
173
+ await saveLocalSecret(clientIdKey, credentials.clientId);
174
+ await saveLocalSecret(clientSecretKey, credentials.clientSecret);
175
+
176
+ // Update env.template if localhost
177
+ if (isLocalhost(actualControllerUrl)) {
178
+ await updateEnvTemplate(appKey, clientIdKey, clientSecretKey, actualControllerUrl);
179
+
180
+ // Regenerate .env file with updated credentials
181
+ try {
182
+ await generateEnvFile(appKey, null, 'local');
183
+ logger.log(chalk.green('✓ .env file updated with new credentials'));
184
+ } catch (error) {
185
+ logger.warn(chalk.yellow(`⚠️ Could not regenerate .env file: ${error.message}`));
186
+ }
187
+
188
+ logger.log(chalk.green('\n✓ Credentials saved to ~/.aifabrix/secrets.local.yaml'));
189
+ logger.log(chalk.green('✓ env.template updated with MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL\n'));
190
+ } else {
191
+ logger.log(chalk.green('\n✓ Credentials saved to ~/.aifabrix/secrets.local.yaml\n'));
192
+ }
193
+ } catch (error) {
194
+ logger.warn(chalk.yellow(`⚠️ Could not save credentials locally: ${error.message}`));
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Rotate secret for an application
200
+ * @async
201
+ * @param {string} appKey - Application key
202
+ * @param {Object} options - Command options
203
+ * @param {string} options.environment - Environment ID or key
204
+ * @param {string} [options.controller] - Controller URL (optional, uses configured controller if not provided)
205
+ * @throws {Error} If rotation fails
206
+ */
207
+ async function rotateSecret(appKey, options) {
208
+ logger.log(chalk.yellow('⚠️ This will invalidate the old ClientSecret!\n'));
209
+
210
+ const config = await getConfig();
211
+
212
+ // Get authentication token
213
+ const controllerUrl = options.controller || null;
214
+ const { token, actualControllerUrl } = await getRotationAuthToken(controllerUrl, config);
215
+
146
216
  // Validate environment
147
217
  validateEnvironment(options.environment);
148
218
 
@@ -164,34 +234,8 @@ async function rotateSecret(appKey, options) {
164
234
  const credentials = response.data.credentials;
165
235
  const message = response.data.message;
166
236
 
167
- // Save credentials to local secrets (always save when rotating)
168
- const clientIdKey = `${appKey}-client-idKeyVault`;
169
- const clientSecretKey = `${appKey}-client-secretKeyVault`;
170
-
171
- try {
172
- await saveLocalSecret(clientIdKey, credentials.clientId);
173
- await saveLocalSecret(clientSecretKey, credentials.clientSecret);
174
-
175
- // Update env.template if localhost
176
- if (isLocalhost(actualControllerUrl)) {
177
- await updateEnvTemplate(appKey, clientIdKey, clientSecretKey, actualControllerUrl);
178
-
179
- // Regenerate .env file with updated credentials
180
- try {
181
- await generateEnvFile(appKey, null, 'local');
182
- logger.log(chalk.green('✓ .env file updated with new credentials'));
183
- } catch (error) {
184
- logger.warn(chalk.yellow(`⚠️ Could not regenerate .env file: ${error.message}`));
185
- }
186
-
187
- logger.log(chalk.green('\n✓ Credentials saved to ~/.aifabrix/secrets.local.yaml'));
188
- logger.log(chalk.green('✓ env.template updated with MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL\n'));
189
- } else {
190
- logger.log(chalk.green('\n✓ Credentials saved to ~/.aifabrix/secrets.local.yaml\n'));
191
- }
192
- } catch (error) {
193
- logger.warn(chalk.yellow(`⚠️ Could not save credentials locally: ${error.message}`));
194
- }
237
+ // Save credentials locally
238
+ await saveCredentialsLocally(appKey, credentials, actualControllerUrl);
195
239
 
196
240
  // Display results
197
241
  displayRotationResults(appKey, options.environment, credentials, actualControllerUrl, message);