@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.
- package/lib/app-deploy.js +73 -29
- package/lib/app-list.js +132 -0
- package/lib/app-readme.js +11 -4
- package/lib/app-register.js +435 -0
- package/lib/app-rotate-secret.js +164 -0
- package/lib/app-run.js +98 -84
- package/lib/app.js +13 -0
- package/lib/audit-logger.js +195 -15
- package/lib/build.js +155 -42
- package/lib/cli.js +104 -8
- package/lib/commands/app.js +8 -391
- package/lib/commands/login.js +130 -36
- package/lib/commands/secure.js +260 -0
- package/lib/config.js +315 -4
- package/lib/deployer.js +221 -183
- package/lib/infra.js +177 -112
- package/lib/push.js +34 -7
- package/lib/secrets.js +89 -23
- package/lib/templates.js +1 -1
- package/lib/utils/api-error-handler.js +465 -0
- package/lib/utils/api.js +165 -16
- package/lib/utils/auth-headers.js +84 -0
- package/lib/utils/build-copy.js +162 -0
- package/lib/utils/cli-utils.js +49 -3
- package/lib/utils/compose-generator.js +57 -16
- package/lib/utils/deployment-errors.js +90 -0
- package/lib/utils/deployment-validation.js +60 -0
- package/lib/utils/dev-config.js +83 -0
- package/lib/utils/docker-build.js +24 -0
- package/lib/utils/env-template.js +30 -10
- package/lib/utils/health-check.js +18 -1
- package/lib/utils/infra-containers.js +101 -0
- package/lib/utils/local-secrets.js +0 -2
- package/lib/utils/secrets-encryption.js +203 -0
- package/lib/utils/secrets-path.js +22 -3
- package/lib/utils/token-manager.js +381 -0
- package/package.json +2 -2
- package/templates/applications/README.md.hbs +155 -23
- package/templates/applications/miso-controller/Dockerfile +7 -119
- package/templates/infra/compose.yaml.hbs +93 -0
- package/templates/python/docker-compose.hbs +25 -17
- package/templates/typescript/docker-compose.hbs +25 -17
- 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
|
|
51
|
+
let errorData;
|
|
26
52
|
try {
|
|
27
|
-
|
|
28
|
-
errorMessage = errorJson.error || errorJson.message || 'Unknown error';
|
|
53
|
+
errorData = JSON.parse(errorText);
|
|
29
54
|
} catch {
|
|
30
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
const
|
|
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
|
-
|
|
179
|
-
const
|
|
180
|
-
const
|
|
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
|
+
|
package/lib/utils/cli-utils.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|