@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
@@ -22,6 +22,157 @@ function validateCommand(_command, _options) {
22
22
  return true;
23
23
  }
24
24
 
25
+ /**
26
+ * Format already formatted error message
27
+ * @param {string} formatted - Formatted error message
28
+ * @returns {string[]} Array of error message lines
29
+ */
30
+ function formatFormattedError(formatted) {
31
+ const messages = [];
32
+ const lines = formatted.split('\n');
33
+ lines.forEach(line => {
34
+ if (line.trim()) {
35
+ messages.push(` ${line}`);
36
+ }
37
+ });
38
+ return messages;
39
+ }
40
+
41
+ /**
42
+ * Format Docker-related errors
43
+ * @param {string} errorMsg - Error message
44
+ * @returns {string[]|null} Array of error message lines or null if not a Docker error
45
+ */
46
+ function formatDockerError(errorMsg) {
47
+ if (errorMsg.includes('not found locally') || (errorMsg.includes('Docker image') && errorMsg.includes('not found'))) {
48
+ return [
49
+ ' Docker image not found.',
50
+ ' Run: aifabrix build <app> first'
51
+ ];
52
+ }
53
+ if (errorMsg.includes('Docker') && (errorMsg.includes('not running') || errorMsg.includes('not installed') || errorMsg.includes('Cannot connect'))) {
54
+ return [
55
+ ' Docker is not running or not installed.',
56
+ ' Please start Docker Desktop and try again.'
57
+ ];
58
+ }
59
+ if (errorMsg.toLowerCase().includes('port') && (errorMsg.includes('already in use') || errorMsg.includes('in use') || errorMsg.includes('conflict'))) {
60
+ return [
61
+ ' Port conflict detected.',
62
+ ' Run "aifabrix doctor" to check which ports are in use.'
63
+ ];
64
+ }
65
+ if ((errorMsg.includes('permission denied') || errorMsg.includes('EACCES') || errorMsg.includes('Permission denied')) && !errorMsg.includes('permissions/') && !errorMsg.includes('Field "permissions')) {
66
+ return [
67
+ ' Permission denied.',
68
+ ' Make sure you have the necessary permissions to run Docker commands.'
69
+ ];
70
+ }
71
+ return null;
72
+ }
73
+
74
+ /**
75
+ * Format Azure-related errors
76
+ * @param {string} errorMsg - Error message
77
+ * @returns {string[]|null} Array of error message lines or null if not an Azure error
78
+ */
79
+ function formatAzureError(errorMsg) {
80
+ if (errorMsg.includes('Azure CLI is not installed') || errorMsg.includes('az --version failed') || (errorMsg.includes('az') && errorMsg.includes('failed'))) {
81
+ return [
82
+ ' Azure CLI is not installed or not working properly.',
83
+ ' Install from: https://docs.microsoft.com/cli/azure/install-azure-cli',
84
+ ' Run: az login'
85
+ ];
86
+ }
87
+ if (errorMsg.includes('Invalid ACR URL') || errorMsg.includes('Invalid registry URL') || errorMsg.includes('Expected format')) {
88
+ return [
89
+ ' Invalid registry URL format.',
90
+ ' Use format: *.azurecr.io (e.g., myacr.azurecr.io)'
91
+ ];
92
+ }
93
+ if (errorMsg.includes('authenticate') || errorMsg.includes('ACR') || errorMsg.includes('Authentication required')) {
94
+ return [
95
+ ' Azure Container Registry authentication failed.',
96
+ ' Run: az acr login --name <registry-name>',
97
+ ' Or login to Azure: az login'
98
+ ];
99
+ }
100
+ if (errorMsg.includes('Registry URL is required')) {
101
+ return [
102
+ ' Registry URL is required.',
103
+ ' Provide via --registry flag or configure in variables.yaml under image.registry'
104
+ ];
105
+ }
106
+ return null;
107
+ }
108
+
109
+ /**
110
+ * Format secrets-related errors
111
+ * @param {string} errorMsg - Error message
112
+ * @returns {string[]|null} Array of error message lines or null if not a secrets error
113
+ */
114
+ function formatSecretsError(errorMsg) {
115
+ if (!errorMsg.includes('Missing secrets')) {
116
+ return null;
117
+ }
118
+
119
+ const messages = [];
120
+ const missingSecretsMatch = errorMsg.match(/Missing secrets: ([^\n]+)/);
121
+ const fileInfoMatch = errorMsg.match(/Secrets file location: ([^\n]+)/);
122
+ const resolveMatch = errorMsg.match(/Run "aifabrix resolve ([^"]+)"/);
123
+
124
+ if (missingSecretsMatch) {
125
+ messages.push(` Missing secrets: ${missingSecretsMatch[1]}`);
126
+ } else {
127
+ messages.push(' Missing secrets in secrets file.');
128
+ }
129
+
130
+ if (fileInfoMatch) {
131
+ messages.push(` Secrets file location: ${fileInfoMatch[1]}`);
132
+ }
133
+
134
+ if (resolveMatch) {
135
+ messages.push(` Run: aifabrix resolve ${resolveMatch[1]} to generate missing secrets.`);
136
+ } else {
137
+ messages.push(' Run: aifabrix resolve <app-name> to generate missing secrets.');
138
+ }
139
+
140
+ return messages;
141
+ }
142
+
143
+ /**
144
+ * Format deployment-related errors
145
+ * @param {string} errorMsg - Error message
146
+ * @returns {string[]|null} Array of error message lines or null if not a deployment error
147
+ */
148
+ function formatDeploymentError(errorMsg) {
149
+ if (!errorMsg.includes('Deployment failed after')) {
150
+ return null;
151
+ }
152
+
153
+ const match = errorMsg.match(/Deployment failed after \d+ attempts: (.+)/);
154
+ if (match) {
155
+ return [` ${match[1]}`];
156
+ }
157
+ return [` ${errorMsg}`];
158
+ }
159
+
160
+ /**
161
+ * Format validation errors
162
+ * @param {string} errorMsg - Error message
163
+ * @returns {string[]|null} Array of error message lines or null if not a validation error
164
+ */
165
+ function formatValidationError(errorMsg) {
166
+ if (errorMsg.includes('Configuration not found') ||
167
+ errorMsg.includes('does not match schema') ||
168
+ errorMsg.includes('Validation failed') ||
169
+ errorMsg.includes('Field "') ||
170
+ errorMsg.includes('Invalid format')) {
171
+ return [` ${errorMsg}`];
172
+ }
173
+ return null;
174
+ }
175
+
25
176
  /**
26
177
  * Formats error message based on error type
27
178
  * @function formatError
@@ -29,92 +180,42 @@ function validateCommand(_command, _options) {
29
180
  * @returns {string[]} Array of error message lines
30
181
  */
31
182
  function formatError(error) {
32
- const messages = [];
33
-
34
183
  // If error has formatted message (from API error handler), use it directly
35
184
  if (error.formatted) {
36
- // Split formatted message into lines and add proper indentation
37
- const lines = error.formatted.split('\n');
38
- lines.forEach(line => {
39
- if (line.trim()) {
40
- messages.push(` ${line}`);
41
- }
42
- });
43
- return messages;
185
+ return formatFormattedError(error.formatted);
44
186
  }
45
187
 
46
188
  const errorMsg = error.message || '';
189
+ const messages = [];
47
190
 
48
- // Check for specific error patterns first (most specific to least specific)
49
- if (errorMsg.includes('Configuration not found')) {
50
- messages.push(` ${errorMsg}`);
51
- } else if (errorMsg.includes('does not match schema') || errorMsg.includes('Validation failed') || errorMsg.includes('Field "') || errorMsg.includes('Invalid format')) {
52
- // Schema validation errors - show the actual error message
53
- messages.push(` ${errorMsg}`);
54
- } else if (errorMsg.includes('not found locally') || (errorMsg.includes('Docker image') && errorMsg.includes('not found'))) {
55
- messages.push(' Docker image not found.');
56
- messages.push(' Run: aifabrix build <app> first');
57
- } else if (errorMsg.includes('Docker') && (errorMsg.includes('not running') || errorMsg.includes('not installed') || errorMsg.includes('Cannot connect'))) {
58
- messages.push(' Docker is not running or not installed.');
59
- messages.push(' Please start Docker Desktop and try again.');
60
- } else if (errorMsg.toLowerCase().includes('port') && (errorMsg.includes('already in use') || errorMsg.includes('in use') || errorMsg.includes('conflict'))) {
61
- messages.push(' Port conflict detected.');
62
- messages.push(' Run "aifabrix doctor" to check which ports are in use.');
63
- } else if ((errorMsg.includes('permission denied') || errorMsg.includes('EACCES') || errorMsg.includes('Permission denied')) && !errorMsg.includes('permissions/') && !errorMsg.includes('Field "permissions')) {
64
- // Only match actual permission denied errors, not validation errors about permissions fields
65
- messages.push(' Permission denied.');
66
- messages.push(' Make sure you have the necessary permissions to run Docker commands.');
67
- } else if (errorMsg.includes('Azure CLI is not installed') || errorMsg.includes('az --version failed') || (errorMsg.includes('az') && errorMsg.includes('failed'))) {
68
- // Specific error for missing Azure CLI installation or Azure CLI command failures
69
- messages.push(' Azure CLI is not installed or not working properly.');
70
- messages.push(' Install from: https://docs.microsoft.com/cli/azure/install-azure-cli');
71
- messages.push(' Run: az login');
72
- } else if (errorMsg.includes('Invalid ACR URL') || errorMsg.includes('Invalid registry URL') || errorMsg.includes('Expected format')) {
73
- messages.push(' Invalid registry URL format.');
74
- messages.push(' Use format: *.azurecr.io (e.g., myacr.azurecr.io)');
75
- } else if (errorMsg.includes('authenticate') || errorMsg.includes('ACR') || errorMsg.includes('Authentication required')) {
76
- messages.push(' Azure Container Registry authentication failed.');
77
- messages.push(' Run: az acr login --name <registry-name>');
78
- messages.push(' Or login to Azure: az login');
79
- } else if (errorMsg.includes('Registry URL is required')) {
80
- messages.push(' Registry URL is required.');
81
- messages.push(' Provide via --registry flag or configure in variables.yaml under image.registry');
82
- } else if (errorMsg.includes('Missing secrets')) {
83
- // Extract the missing secrets list and file info from the error message
84
- const missingSecretsMatch = errorMsg.match(/Missing secrets: ([^\n]+)/);
85
- const fileInfoMatch = errorMsg.match(/Secrets file location: ([^\n]+)/);
86
- const resolveMatch = errorMsg.match(/Run "aifabrix resolve ([^"]+)"/);
87
-
88
- if (missingSecretsMatch) {
89
- messages.push(` Missing secrets: ${missingSecretsMatch[1]}`);
90
- } else {
91
- messages.push(' Missing secrets in secrets file.');
92
- }
191
+ // Try different error formatters in order of specificity
192
+ const dockerError = formatDockerError(errorMsg);
193
+ if (dockerError) {
194
+ return dockerError;
195
+ }
93
196
 
94
- if (fileInfoMatch) {
95
- messages.push(` Secrets file location: ${fileInfoMatch[1]}`);
96
- }
197
+ const azureError = formatAzureError(errorMsg);
198
+ if (azureError) {
199
+ return azureError;
200
+ }
97
201
 
98
- // Always show resolve command suggestion
99
- if (resolveMatch) {
100
- // Extract app name from error message if available
101
- messages.push(` Run: aifabrix resolve ${resolveMatch[1]} to generate missing secrets.`);
102
- } else {
103
- // Generic suggestion if app name not in error message
104
- messages.push(' Run: aifabrix resolve <app-name> to generate missing secrets.');
105
- }
106
- } else if (errorMsg.includes('Deployment failed after')) {
107
- // Handle deployment retry errors - extract the actual error message
108
- const match = errorMsg.match(/Deployment failed after \d+ attempts: (.+)/);
109
- if (match) {
110
- messages.push(` ${match[1]}`);
111
- } else {
112
- messages.push(` ${errorMsg}`);
113
- }
114
- } else {
115
- messages.push(` ${errorMsg}`);
202
+ const secretsError = formatSecretsError(errorMsg);
203
+ if (secretsError) {
204
+ return secretsError;
205
+ }
206
+
207
+ const deploymentError = formatDeploymentError(errorMsg);
208
+ if (deploymentError) {
209
+ return deploymentError;
210
+ }
211
+
212
+ const validationError = formatValidationError(errorMsg);
213
+ if (validationError) {
214
+ return validationError;
116
215
  }
117
216
 
217
+ // Default: return generic error message
218
+ messages.push(` ${errorMsg}`);
118
219
  return messages;
119
220
  }
120
221
 
@@ -74,9 +74,19 @@ handlebars.registerHelper('pgUserName', (dbName) => {
74
74
  * @throws {Error} If template not found
75
75
  */
76
76
  function loadDockerComposeTemplate(language) {
77
- const templatePath = path.join(__dirname, '..', '..', 'templates', language, 'docker-compose.hbs');
77
+ // Use getProjectRoot to reliably find templates in all environments
78
+ const { getProjectRoot } = require('./paths');
79
+ const projectRoot = getProjectRoot();
80
+ const templatePath = path.join(projectRoot, 'templates', language, 'docker-compose.hbs');
81
+
78
82
  if (!fsSync.existsSync(templatePath)) {
79
- throw new Error(`Docker Compose template not found for language: ${language}`);
83
+ // Provide helpful error message with actual paths checked
84
+ const errorMessage = `Docker Compose template not found for language: ${language}\n` +
85
+ ` Expected path: ${templatePath}\n` +
86
+ ` Project root: ${projectRoot}\n` +
87
+ ` Templates directory: ${path.join(projectRoot, 'templates', language)}\n` +
88
+ ` Global PROJECT_ROOT: ${typeof global !== 'undefined' && global.PROJECT_ROOT ? global.PROJECT_ROOT : 'not set'}`;
89
+ throw new Error(errorMessage);
80
90
  }
81
91
 
82
92
  const templateContent = fsSync.readFileSync(templatePath, 'utf8');
@@ -30,22 +30,23 @@ function normalizeControllerUrl(url) {
30
30
 
31
31
  /**
32
32
  * Create token management functions with config access
33
- * @param {Function} getConfigFn - Function to get config
34
- * @param {Function} saveConfigFn - Function to save config
35
- * @param {Function} getSecretsEncryptionKeyFn - Function to get encryption key
36
- * @param {Function} encryptTokenValueFn - Function to encrypt token
37
- * @param {Function} decryptTokenValueFn - Function to decrypt token
38
- * @param {Function} isTokenEncryptedFn - Function to check if token is encrypted
33
+ * @param {Object} params - Function parameters
34
+ * @param {Function} params.getConfigFn - Function to get config
35
+ * @param {Function} params.saveConfigFn - Function to save config
36
+ * @param {Function} params.getSecretsEncryptionKeyFn - Function to get encryption key
37
+ * @param {Function} params.encryptTokenValueFn - Function to encrypt token
38
+ * @param {Function} params.decryptTokenValueFn - Function to decrypt token
39
+ * @param {Function} params.isTokenEncryptedFn - Function to check if token is encrypted
39
40
  * @returns {Object} Token management functions
40
41
  */
41
- function createTokenManagementFunctions(
42
+ function createTokenManagementFunctions({
42
43
  getConfigFn,
43
44
  saveConfigFn,
44
45
  getSecretsEncryptionKeyFn,
45
46
  encryptTokenValueFn,
46
47
  decryptTokenValueFn,
47
48
  isTokenEncryptedFn
48
- ) {
49
+ }) {
49
50
  /**
50
51
  * Extract device token information with encryption/decryption handling
51
52
  * @param {Object} deviceToken - Device token object from config
@@ -219,11 +220,152 @@ function createTokenManagementFunctions(
219
220
  await saveConfigFn(config);
220
221
  }
221
222
 
223
+ /**
224
+ * Clear device token for specific controller
225
+ * @param {string} controllerUrl - Controller URL
226
+ * @returns {Promise<boolean>} True if token was cleared, false if it didn't exist
227
+ */
228
+ async function clearDeviceToken(controllerUrl) {
229
+ const config = await getConfigFn();
230
+ if (!config.device || !controllerUrl) return false;
231
+
232
+ const normalizedUrl = normalizeControllerUrl(controllerUrl);
233
+ let cleared = false;
234
+
235
+ // Try exact match first
236
+ if (config.device[normalizedUrl]) {
237
+ delete config.device[normalizedUrl];
238
+ cleared = true;
239
+ } else {
240
+ // Try to find matching URL by normalizing all stored URLs
241
+ for (const storedUrl of Object.keys(config.device)) {
242
+ if (normalizeControllerUrl(storedUrl) === normalizedUrl) {
243
+ delete config.device[storedUrl];
244
+ cleared = true;
245
+ break;
246
+ }
247
+ }
248
+ }
249
+
250
+ if (cleared) {
251
+ await saveConfigFn(config);
252
+ }
253
+
254
+ return cleared;
255
+ }
256
+
257
+ /**
258
+ * Clear all device tokens
259
+ * @returns {Promise<number>} Number of tokens cleared
260
+ */
261
+ async function clearAllDeviceTokens() {
262
+ const config = await getConfigFn();
263
+ if (!config.device) return 0;
264
+
265
+ const count = Object.keys(config.device).length;
266
+ if (count > 0) {
267
+ config.device = {};
268
+ await saveConfigFn(config);
269
+ }
270
+
271
+ return count;
272
+ }
273
+
274
+ /**
275
+ * Clear client token for specific environment and app
276
+ * @param {string} environment - Environment key
277
+ * @param {string} appName - Application name
278
+ * @returns {Promise<boolean>} True if token was cleared, false if it didn't exist
279
+ */
280
+ async function clearClientToken(environment, appName) {
281
+ const config = await getConfigFn();
282
+ if (!config.environments || !config.environments[environment]) return false;
283
+ if (!config.environments[environment].clients || !config.environments[environment].clients[appName]) return false;
284
+
285
+ delete config.environments[environment].clients[appName];
286
+
287
+ // Clean up empty clients object if no clients remain
288
+ if (Object.keys(config.environments[environment].clients).length === 0) {
289
+ delete config.environments[environment].clients;
290
+ }
291
+
292
+ // Clean up empty environment object if no clients remain
293
+ if (Object.keys(config.environments[environment]).length === 0) {
294
+ delete config.environments[environment];
295
+ }
296
+
297
+ await saveConfigFn(config);
298
+ return true;
299
+ }
300
+
301
+ /**
302
+ * Clear all client tokens for a specific environment
303
+ * @param {string} environment - Environment key
304
+ * @returns {Promise<number>} Number of tokens cleared
305
+ */
306
+ async function clearClientTokensForEnvironment(environment) {
307
+ const config = await getConfigFn();
308
+ if (!config.environments || !config.environments[environment]) return 0;
309
+ if (!config.environments[environment].clients) return 0;
310
+
311
+ const count = Object.keys(config.environments[environment].clients).length;
312
+ if (count > 0) {
313
+ delete config.environments[environment].clients;
314
+ // Clean up empty environment object if no other properties remain
315
+ if (Object.keys(config.environments[environment]).length === 0) {
316
+ delete config.environments[environment];
317
+ }
318
+ await saveConfigFn(config);
319
+ }
320
+
321
+ return count;
322
+ }
323
+
324
+ /**
325
+ * Clear all client tokens across all environments
326
+ * @returns {Promise<number>} Number of tokens cleared
327
+ */
328
+ async function clearAllClientTokens() {
329
+ const config = await getConfigFn();
330
+ if (!config.environments) return 0;
331
+
332
+ let totalCount = 0;
333
+ const environmentsToRemove = [];
334
+
335
+ for (const env of Object.keys(config.environments)) {
336
+ if (config.environments[env].clients) {
337
+ const count = Object.keys(config.environments[env].clients).length;
338
+ totalCount += count;
339
+ delete config.environments[env].clients;
340
+ // Mark environment for removal if no other properties remain
341
+ if (Object.keys(config.environments[env]).length === 0) {
342
+ environmentsToRemove.push(env);
343
+ }
344
+ }
345
+ }
346
+
347
+ // Remove empty environments
348
+ environmentsToRemove.forEach(env => {
349
+ delete config.environments[env];
350
+ });
351
+
352
+ if (totalCount > 0) {
353
+ await saveConfigFn(config);
354
+ }
355
+
356
+ return totalCount;
357
+ }
358
+
222
359
  return {
223
360
  getDeviceToken,
224
361
  getClientToken,
225
362
  saveDeviceToken,
226
- saveClientToken
363
+ saveClientToken,
364
+ clearDeviceToken,
365
+ clearAllDeviceTokens,
366
+ clearClientToken,
367
+ clearClientTokensForEnvironment,
368
+ clearAllClientTokens
227
369
  };
228
370
  }
229
371