@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/utils/api.js CHANGED
@@ -10,6 +10,29 @@
10
10
  * @version 2.0.0
11
11
  */
12
12
 
13
+ const { parseErrorResponse } = require('./api-error-handler');
14
+ const auditLogger = require('../audit-logger');
15
+
16
+ /**
17
+ * Logs API request performance metrics and errors to audit log
18
+ * @param {string} url - API endpoint URL
19
+ * @param {Object} options - Fetch options
20
+ * @param {number} statusCode - HTTP status code
21
+ * @param {number} duration - Request duration in milliseconds
22
+ * @param {boolean} success - Whether the request was successful
23
+ * @param {Object} errorInfo - Error information (if failed)
24
+ */
25
+ async function logApiPerformance(url, options, statusCode, duration, success, errorInfo = {}) {
26
+ // Log all API calls (both success and failure) to audit log for troubleshooting
27
+ // This helps track what API calls were made when errors occur
28
+ try {
29
+ await auditLogger.logApiCall(url, options, statusCode, duration, success, errorInfo);
30
+ } catch (logError) {
31
+ // Don't fail the API call if audit logging fails
32
+ // Silently continue - audit logging should never break functionality
33
+ }
34
+ }
35
+
13
36
  /**
14
37
  * Make an API call with proper error handling
15
38
  * @param {string} url - API endpoint URL
@@ -17,25 +40,45 @@
17
40
  * @returns {Promise<Object>} Response object with success flag
18
41
  */
19
42
  async function makeApiCall(url, options = {}) {
43
+ const startTime = Date.now();
44
+
20
45
  try {
21
46
  const response = await fetch(url, options);
47
+ const duration = Date.now() - startTime;
22
48
 
23
49
  if (!response.ok) {
24
50
  const errorText = await response.text();
25
- let errorMessage;
51
+ let errorData;
26
52
  try {
27
- const errorJson = JSON.parse(errorText);
28
- errorMessage = errorJson.error || errorJson.message || 'Unknown error';
53
+ errorData = JSON.parse(errorText);
29
54
  } catch {
30
- errorMessage = errorText || `HTTP ${response.status}: ${response.statusText}`;
55
+ errorData = errorText || `HTTP ${response.status}: ${response.statusText}`;
31
56
  }
57
+
58
+ // Parse error using error handler
59
+ const parsedError = parseErrorResponse(errorData, response.status, false);
60
+
61
+ // Log error to audit log
62
+ await logApiPerformance(url, options, response.status, duration, false, {
63
+ errorType: parsedError.type,
64
+ errorMessage: parsedError.message,
65
+ errorData: parsedError.data,
66
+ correlationId: parsedError.data?.correlationId
67
+ });
68
+
32
69
  return {
33
70
  success: false,
34
- error: errorMessage,
71
+ error: parsedError.message,
72
+ errorData: parsedError.data,
73
+ errorType: parsedError.type,
74
+ formattedError: parsedError.formatted,
35
75
  status: response.status
36
76
  };
37
77
  }
38
78
 
79
+ // Log successful API call to audit log
80
+ await logApiPerformance(url, options, response.status, duration, true);
81
+
39
82
  const contentType = response.headers.get('content-type');
40
83
  if (contentType && contentType.includes('application/json')) {
41
84
  const data = await response.json();
@@ -54,16 +97,48 @@ async function makeApiCall(url, options = {}) {
54
97
  };
55
98
 
56
99
  } catch (error) {
100
+ const duration = Date.now() - startTime;
101
+
102
+ // Parse network error using error handler
103
+ const parsedError = parseErrorResponse(error.message, 0, true);
104
+
105
+ // Log network error to audit log
106
+ await logApiPerformance(url, options, 0, duration, false, {
107
+ errorType: parsedError.type,
108
+ errorMessage: parsedError.message,
109
+ network: true
110
+ });
111
+
57
112
  return {
58
113
  success: false,
59
- error: error.message,
114
+ error: parsedError.message,
115
+ errorData: parsedError.data,
116
+ errorType: parsedError.type,
117
+ formattedError: parsedError.formatted,
60
118
  network: true
61
119
  };
62
120
  }
63
121
  }
64
122
 
123
+ /**
124
+ * Extract controller URL from API endpoint URL
125
+ * @param {string} url - Full API endpoint URL
126
+ * @returns {string} Controller base URL
127
+ */
128
+ function extractControllerUrl(url) {
129
+ try {
130
+ const urlObj = new URL(url);
131
+ return `${urlObj.protocol}//${urlObj.host}`;
132
+ } catch {
133
+ // If URL parsing fails, try to extract manually
134
+ const match = url.match(/^(https?:\/\/[^/]+)/);
135
+ return match ? match[1] : url;
136
+ }
137
+ }
138
+
65
139
  /**
66
140
  * Make an authenticated API call with bearer token
141
+ * Automatically refreshes device token on 401 errors if refresh token is available
67
142
  * @param {string} url - API endpoint URL
68
143
  * @param {Object} options - Fetch options
69
144
  * @param {string} token - Bearer token
@@ -79,33 +154,63 @@ async function authenticatedApiCall(url, options = {}, token) {
79
154
  headers['Authorization'] = `Bearer ${token}`;
80
155
  }
81
156
 
82
- return makeApiCall(url, {
157
+ const response = await makeApiCall(url, {
83
158
  ...options,
84
159
  headers
85
160
  });
161
+
162
+ // Handle 401 errors with automatic token refresh for device tokens
163
+ if (!response.success && response.status === 401) {
164
+ try {
165
+ // Extract controller URL from request URL
166
+ const controllerUrl = extractControllerUrl(url);
167
+
168
+ // Try to get and refresh device token
169
+ const { getOrRefreshDeviceToken } = require('./token-manager');
170
+ const refreshedToken = await getOrRefreshDeviceToken(controllerUrl);
171
+
172
+ if (refreshedToken && refreshedToken.token) {
173
+ // Retry request with new token
174
+ headers['Authorization'] = `Bearer ${refreshedToken.token}`;
175
+ return makeApiCall(url, {
176
+ ...options,
177
+ headers
178
+ });
179
+ }
180
+ } catch (refreshError) {
181
+ // Refresh failed, return original 401 error
182
+ // This allows the caller to handle the authentication error
183
+ }
184
+ }
185
+
186
+ return response;
86
187
  }
87
188
 
88
189
  /**
89
190
  * Parses device code response from API
191
+ * Matches OpenAPI DeviceCodeResponse schema (camelCase)
90
192
  * @function parseDeviceCodeResponse
91
193
  * @param {Object} response - API response object
92
194
  * @returns {Object} Parsed device code response
93
195
  * @throws {Error} If response is invalid
94
196
  */
95
197
  function parseDeviceCodeResponse(response) {
198
+ // OpenAPI spec: { success: boolean, data: DeviceCodeResponse, timestamp: string }
96
199
  const apiResponse = response.data;
97
200
  const responseData = apiResponse.data || apiResponse;
98
201
 
99
- const deviceCode = responseData.deviceCode || responseData.device_code;
100
- const userCode = responseData.userCode || responseData.user_code;
101
- const verificationUri = responseData.verificationUri || responseData.verification_uri;
102
- const expiresIn = responseData.expiresIn || responseData.expires_in || 600;
202
+ // OpenAPI spec uses camelCase: deviceCode, userCode, verificationUri, expiresIn, interval
203
+ const deviceCode = responseData.deviceCode;
204
+ const userCode = responseData.userCode;
205
+ const verificationUri = responseData.verificationUri;
206
+ const expiresIn = responseData.expiresIn || 600;
103
207
  const interval = responseData.interval || 5;
104
208
 
105
209
  if (!deviceCode || !userCode || !verificationUri) {
106
210
  throw new Error('Invalid device code response: missing required fields');
107
211
  }
108
212
 
213
+ // Return in snake_case for internal consistency (used by existing code)
109
214
  return {
110
215
  device_code: deviceCode,
111
216
  user_code: userCode,
@@ -122,7 +227,7 @@ function parseDeviceCodeResponse(response) {
122
227
  * @async
123
228
  * @function initiateDeviceCodeFlow
124
229
  * @param {string} controllerUrl - Base URL of the controller
125
- * @param {string} environment - Environment key (e.g., 'dev', 'tst', 'pro')
230
+ * @param {string} environment - Environment key (e.g., 'miso', 'dev', 'tst', 'pro')
126
231
  * @returns {Promise<Object>} Device code response with device_code, user_code, verification_uri, expires_in, interval
127
232
  * @throws {Error} If initiation fails
128
233
  */
@@ -162,11 +267,13 @@ function checkTokenExpiration(startTime, expiresIn) {
162
267
 
163
268
  /**
164
269
  * Parses token response from API
270
+ * Matches OpenAPI DeviceCodeTokenResponse schema (camelCase)
165
271
  * @function parseTokenResponse
166
272
  * @param {Object} response - API response object
167
273
  * @returns {Object|null} Parsed token response or null if pending
168
274
  */
169
275
  function parseTokenResponse(response) {
276
+ // OpenAPI spec: { success: boolean, data: DeviceCodeTokenResponse, timestamp: string }
170
277
  const apiResponse = response.data;
171
278
  const responseData = apiResponse.data || apiResponse;
172
279
 
@@ -175,14 +282,16 @@ function parseTokenResponse(response) {
175
282
  return null;
176
283
  }
177
284
 
178
- const accessToken = responseData.accessToken || responseData.access_token;
179
- const refreshToken = responseData.refreshToken || responseData.refresh_token;
180
- const expiresIn = responseData.expiresIn || responseData.expires_in || 3600;
285
+ // OpenAPI spec uses camelCase: accessToken, refreshToken, expiresIn
286
+ const accessToken = responseData.accessToken;
287
+ const refreshToken = responseData.refreshToken;
288
+ const expiresIn = responseData.expiresIn || 3600;
181
289
 
182
290
  if (!accessToken) {
183
291
  throw new Error('Invalid token response: missing accessToken');
184
292
  }
185
293
 
294
+ // Return in snake_case for internal consistency (used by existing code)
186
295
  return {
187
296
  access_token: accessToken,
188
297
  refresh_token: refreshToken,
@@ -319,11 +428,51 @@ function displayDeviceCodeInfo(userCode, verificationUri, logger, chalk) {
319
428
  logger.log(chalk.gray('Waiting for approval...'));
320
429
  }
321
430
 
431
+ /**
432
+ * Refresh device code access token using refresh token
433
+ * Uses OpenAPI /api/v1/auth/login/device/refresh endpoint
434
+ *
435
+ * @async
436
+ * @function refreshDeviceToken
437
+ * @param {string} controllerUrl - Base URL of the controller
438
+ * @param {string} refreshToken - Refresh token from previous authentication
439
+ * @returns {Promise<Object>} Token response with access_token, refresh_token, expires_in
440
+ * @throws {Error} If refresh fails or refresh token is invalid/expired
441
+ */
442
+ async function refreshDeviceToken(controllerUrl, refreshToken) {
443
+ if (!refreshToken || typeof refreshToken !== 'string') {
444
+ throw new Error('Refresh token is required');
445
+ }
446
+
447
+ const url = `${controllerUrl}/api/v1/auth/login/device/refresh`;
448
+ const response = await makeApiCall(url, {
449
+ method: 'POST',
450
+ headers: {
451
+ 'Content-Type': 'application/json'
452
+ },
453
+ body: JSON.stringify({ refreshToken })
454
+ });
455
+
456
+ if (!response.success) {
457
+ const errorMsg = response.error || 'Unknown error';
458
+ throw new Error(`Failed to refresh token: ${errorMsg}`);
459
+ }
460
+
461
+ // Parse response using existing parseTokenResponse function
462
+ const tokenResponse = parseTokenResponse(response);
463
+ if (!tokenResponse) {
464
+ throw new Error('Invalid refresh token response');
465
+ }
466
+
467
+ return tokenResponse;
468
+ }
469
+
322
470
  module.exports = {
323
471
  makeApiCall,
324
472
  authenticatedApiCall,
325
473
  initiateDeviceCodeFlow,
326
474
  pollDeviceCodeToken,
327
- displayDeviceCodeInfo
475
+ displayDeviceCodeInfo,
476
+ refreshDeviceToken
328
477
  };
329
478
 
@@ -0,0 +1,84 @@
1
+ /**
2
+ * AI Fabrix Builder Authentication Headers Utilities
3
+ *
4
+ * Creates authentication headers for API requests
5
+ *
6
+ * @fileoverview Authentication header utilities for AI Fabrix Builder
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ /**
12
+ * Creates authentication headers using Bearer token
13
+ *
14
+ * @param {string} token - Authentication token
15
+ * @returns {Object} Headers object with authentication
16
+ * @throws {Error} If token is missing
17
+ */
18
+ function createBearerTokenHeaders(token) {
19
+ if (!token) {
20
+ throw new Error('Authentication token is required');
21
+ }
22
+ return {
23
+ 'Authorization': `Bearer ${token}`
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Creates authentication headers for Client Credentials flow (legacy support)
29
+ *
30
+ * @param {string} clientId - Application client ID
31
+ * @param {string} clientSecret - Application client secret
32
+ * @returns {Object} Headers object with authentication
33
+ * @throws {Error} If credentials are missing
34
+ */
35
+ function createClientCredentialsHeaders(clientId, clientSecret) {
36
+ if (!clientId || !clientSecret) {
37
+ throw new Error('Client ID and Client Secret are required for authentication');
38
+ }
39
+ return {
40
+ 'x-client-id': clientId,
41
+ 'x-client-secret': clientSecret
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Creates authentication headers based on auth configuration
47
+ * Supports both Bearer token and client credentials authentication
48
+ *
49
+ * @param {Object} authConfig - Authentication configuration
50
+ * @param {string} authConfig.type - Auth type: 'bearer' or 'credentials'
51
+ * @param {string} [authConfig.token] - Bearer token (for type 'bearer')
52
+ * @param {string} [authConfig.clientId] - Client ID (for type 'credentials')
53
+ * @param {string} [authConfig.clientSecret] - Client secret (for type 'credentials')
54
+ * @returns {Object} Headers object with authentication
55
+ * @throws {Error} If auth config is invalid
56
+ */
57
+ function createAuthHeaders(authConfig) {
58
+ if (!authConfig || !authConfig.type) {
59
+ throw new Error('Authentication configuration is required');
60
+ }
61
+
62
+ if (authConfig.type === 'bearer') {
63
+ if (!authConfig.token) {
64
+ throw new Error('Bearer token is required for bearer authentication');
65
+ }
66
+ return createBearerTokenHeaders(authConfig.token);
67
+ }
68
+
69
+ if (authConfig.type === 'credentials') {
70
+ if (!authConfig.clientId || !authConfig.clientSecret) {
71
+ throw new Error('Client ID and Client Secret are required for credentials authentication');
72
+ }
73
+ return createClientCredentialsHeaders(authConfig.clientId, authConfig.clientSecret);
74
+ }
75
+
76
+ throw new Error(`Invalid authentication type: ${authConfig.type}. Must be 'bearer' or 'credentials'`);
77
+ }
78
+
79
+ module.exports = {
80
+ createBearerTokenHeaders,
81
+ createClientCredentialsHeaders,
82
+ createAuthHeaders
83
+ };
84
+
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Build Copy Utilities
3
+ *
4
+ * This module handles copying builder files to developer-specific
5
+ * directories for isolated builds. Ensures each developer has their
6
+ * own build directory to prevent conflicts.
7
+ *
8
+ * @fileoverview Build copy utilities for developer isolation
9
+ * @author AI Fabrix Team
10
+ * @version 2.0.0
11
+ */
12
+
13
+ const fs = require('fs').promises;
14
+ const fsSync = require('fs');
15
+ const path = require('path');
16
+ const os = require('os');
17
+
18
+ /**
19
+ * Copies all files from builder directory to developer-specific directory
20
+ * Preserves directory structure and skips hidden files
21
+ *
22
+ * @async
23
+ * @function copyBuilderToDevDirectory
24
+ * @param {string} appName - Application name
25
+ * @param {number} developerId - Developer ID
26
+ * @returns {Promise<string>} Path to developer-specific directory
27
+ * @throws {Error} If copying fails
28
+ *
29
+ * @example
30
+ * const devPath = await copyBuilderToDevDirectory('myapp', 1);
31
+ * // Returns: '~/.aifabrix/applications-dev-1/myapp-dev-1'
32
+ */
33
+ async function copyBuilderToDevDirectory(appName, developerId) {
34
+ const builderPath = path.join(process.cwd(), 'builder', appName);
35
+
36
+ // Ensure builder directory exists
37
+ if (!fsSync.existsSync(builderPath)) {
38
+ throw new Error(`Builder directory not found: ${builderPath}\nRun 'aifabrix create ${appName}' first`);
39
+ }
40
+
41
+ // Get base directory (applications or applications-dev-{id})
42
+ const baseDir = developerId === 0
43
+ ? path.join(os.homedir(), '.aifabrix', 'applications')
44
+ : path.join(os.homedir(), '.aifabrix', `applications-dev-${developerId}`);
45
+
46
+ // Clear base directory before copying (delete all files)
47
+ if (fsSync.existsSync(baseDir)) {
48
+ const entries = await fs.readdir(baseDir);
49
+ for (const entry of entries) {
50
+ const entryPath = path.join(baseDir, entry);
51
+ const stats = await fs.stat(entryPath);
52
+ if (stats.isDirectory()) {
53
+ await fs.rm(entryPath, { recursive: true, force: true });
54
+ } else {
55
+ await fs.unlink(entryPath);
56
+ }
57
+ }
58
+ }
59
+
60
+ // Get target directory using getDevDirectory()
61
+ const devDir = getDevDirectory(appName, developerId);
62
+
63
+ // Create target directory
64
+ await fs.mkdir(devDir, { recursive: true });
65
+
66
+ // Copy files based on developer ID
67
+ if (developerId === 0) {
68
+ // Dev 0: Copy contents from builder/{appName}/ directly to applications/
69
+ await copyDirectory(builderPath, devDir);
70
+ } else {
71
+ // Dev > 0: Copy builder/{appName}/ to applications-dev-{id}/{appName}-dev-{id}/
72
+ await copyDirectory(builderPath, devDir);
73
+ }
74
+
75
+ return devDir;
76
+ }
77
+
78
+ /**
79
+ * Recursively copies directory contents
80
+ * @async
81
+ * @param {string} sourceDir - Source directory
82
+ * @param {string} targetDir - Target directory
83
+ * @throws {Error} If copying fails
84
+ */
85
+ async function copyDirectory(sourceDir, targetDir) {
86
+ // Ensure target directory exists
87
+ await fs.mkdir(targetDir, { recursive: true });
88
+
89
+ const entries = await fs.readdir(sourceDir);
90
+
91
+ for (const entry of entries) {
92
+ // Skip hidden files and directories (but allow .env, .gitignore, etc.)
93
+ if (entry.startsWith('.') && entry !== '.env' && entry !== '.gitignore') {
94
+ continue;
95
+ }
96
+
97
+ const sourcePath = path.join(sourceDir, entry);
98
+ const targetPath = path.join(targetDir, entry);
99
+
100
+ const stats = await fs.stat(sourcePath);
101
+
102
+ if (stats.isDirectory()) {
103
+ // Recursively copy subdirectories
104
+ await copyDirectory(sourcePath, targetPath);
105
+ } else if (stats.isFile()) {
106
+ // Copy file
107
+ await fs.copyFile(sourcePath, targetPath);
108
+ }
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Gets developer-specific directory path for an application
114
+ * @param {string} appName - Application name
115
+ * @param {number} developerId - Developer ID
116
+ * @returns {string} Path to developer-specific directory
117
+ */
118
+ function getDevDirectory(appName, developerId) {
119
+ if (developerId === 0) {
120
+ // Dev 0: all apps go directly to applications/ (no subdirectory)
121
+ return path.join(os.homedir(), '.aifabrix', 'applications');
122
+ }
123
+ // Dev > 0: apps go to applications-dev-{id}/{appName}-dev-{id}/
124
+ return path.join(os.homedir(), '.aifabrix', `applications-dev-${developerId}`, `${appName}-dev-${developerId}`);
125
+
126
+ }
127
+
128
+ /**
129
+ * Copies app source files from apps directory to dev directory
130
+ * Used when old context format is detected to ensure source files are available
131
+ * @async
132
+ * @param {string} appsSourcePath - Path to apps/{appName} directory
133
+ * @param {string} devDir - Target dev directory
134
+ * @throws {Error} If copying fails
135
+ */
136
+ async function copyAppSourceFiles(appsSourcePath, devDir) {
137
+ if (!fsSync.existsSync(appsSourcePath)) {
138
+ return; // Nothing to copy
139
+ }
140
+
141
+ // Copy all files from apps directory to dev directory
142
+ await copyDirectory(appsSourcePath, devDir);
143
+ }
144
+
145
+ /**
146
+ * Checks if developer-specific directory exists
147
+ * @param {string} appName - Application name
148
+ * @param {number} developerId - Developer ID
149
+ * @returns {boolean} True if directory exists
150
+ */
151
+ function devDirectoryExists(appName, developerId) {
152
+ const devDir = getDevDirectory(appName, developerId);
153
+ return fsSync.existsSync(devDir);
154
+ }
155
+
156
+ module.exports = {
157
+ copyBuilderToDevDirectory,
158
+ copyAppSourceFiles,
159
+ getDevDirectory,
160
+ devDirectoryExists
161
+ };
162
+
@@ -30,6 +30,19 @@ function validateCommand(_command, _options) {
30
30
  */
31
31
  function formatError(error) {
32
32
  const messages = [];
33
+
34
+ // If error has formatted message (from API error handler), use it directly
35
+ 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;
44
+ }
45
+
33
46
  const errorMsg = error.message || '';
34
47
 
35
48
  // Check for specific error patterns first (most specific to least specific)
@@ -47,11 +60,12 @@ function formatError(error) {
47
60
  } else if (errorMsg.includes('permission')) {
48
61
  messages.push(' Permission denied.');
49
62
  messages.push(' Make sure you have the necessary permissions to run Docker commands.');
50
- } else if (errorMsg.includes('Azure CLI') || errorMsg.includes('az --version')) {
51
- messages.push(' Azure CLI is not installed.');
63
+ } else if (errorMsg.includes('Azure CLI is not installed') || errorMsg.includes('az --version failed') || (errorMsg.includes('az') && errorMsg.includes('failed'))) {
64
+ // Specific error for missing Azure CLI installation or Azure CLI command failures
65
+ messages.push(' Azure CLI is not installed or not working properly.');
52
66
  messages.push(' Install from: https://docs.microsoft.com/cli/azure/install-azure-cli');
53
67
  messages.push(' Run: az login');
54
- } else if (errorMsg.includes('authenticate') || errorMsg.includes('ACR')) {
68
+ } else if (errorMsg.includes('authenticate') || errorMsg.includes('ACR') || errorMsg.includes('Authentication required')) {
55
69
  messages.push(' Azure Container Registry authentication failed.');
56
70
  messages.push(' Run: az acr login --name <registry-name>');
57
71
  messages.push(' Or login to Azure: az login');
@@ -61,6 +75,38 @@ function formatError(error) {
61
75
  } else if (errorMsg.includes('Registry URL is required')) {
62
76
  messages.push(' Registry URL is required.');
63
77
  messages.push(' Provide via --registry flag or configure in variables.yaml under image.registry');
78
+ } else if (errorMsg.includes('Missing secrets')) {
79
+ // Extract the missing secrets list and file info from the error message
80
+ const missingSecretsMatch = errorMsg.match(/Missing secrets: ([^\n]+)/);
81
+ const fileInfoMatch = errorMsg.match(/Secrets file location: ([^\n]+)/);
82
+ const resolveMatch = errorMsg.match(/Run "aifabrix resolve ([^"]+)"/);
83
+
84
+ if (missingSecretsMatch) {
85
+ messages.push(` Missing secrets: ${missingSecretsMatch[1]}`);
86
+ } else {
87
+ messages.push(' Missing secrets in secrets file.');
88
+ }
89
+
90
+ if (fileInfoMatch) {
91
+ messages.push(` Secrets file location: ${fileInfoMatch[1]}`);
92
+ }
93
+
94
+ // Always show resolve command suggestion
95
+ if (resolveMatch) {
96
+ // Extract app name from error message if available
97
+ messages.push(` Run: aifabrix resolve ${resolveMatch[1]} to generate missing secrets.`);
98
+ } else {
99
+ // Generic suggestion if app name not in error message
100
+ messages.push(' Run: aifabrix resolve <app-name> to generate missing secrets.');
101
+ }
102
+ } else if (errorMsg.includes('Deployment failed after')) {
103
+ // Handle deployment retry errors - extract the actual error message
104
+ const match = errorMsg.match(/Deployment failed after \d+ attempts: (.+)/);
105
+ if (match) {
106
+ messages.push(` ${match[1]}`);
107
+ } else {
108
+ messages.push(` ${errorMsg}`);
109
+ }
64
110
  } else {
65
111
  messages.push(` ${errorMsg}`);
66
112
  }