@aifabrix/builder 2.0.0 → 2.0.2
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/README.md +6 -2
- package/bin/aifabrix.js +9 -3
- package/jest.config.integration.js +30 -0
- package/lib/app-config.js +157 -0
- package/lib/app-deploy.js +233 -82
- package/lib/app-dockerfile.js +112 -0
- package/lib/app-prompts.js +244 -0
- package/lib/app-push.js +172 -0
- package/lib/app-run.js +334 -133
- package/lib/app.js +208 -274
- package/lib/audit-logger.js +2 -0
- package/lib/build.js +209 -98
- package/lib/cli.js +76 -86
- package/lib/commands/app.js +414 -0
- package/lib/commands/login.js +304 -0
- package/lib/config.js +78 -0
- package/lib/deployer.js +225 -81
- package/lib/env-reader.js +45 -30
- package/lib/generator.js +308 -191
- package/lib/github-generator.js +67 -7
- package/lib/infra.js +156 -61
- package/lib/push.js +105 -10
- package/lib/schema/application-schema.json +30 -2
- package/lib/schema/infrastructure-schema.json +589 -0
- package/lib/secrets.js +229 -24
- package/lib/template-validator.js +205 -0
- package/lib/templates.js +305 -170
- package/lib/utils/api.js +329 -0
- package/lib/utils/cli-utils.js +97 -0
- package/lib/utils/dockerfile-utils.js +131 -0
- package/lib/utils/environment-checker.js +125 -0
- package/lib/utils/error-formatter.js +61 -0
- package/lib/utils/health-check.js +187 -0
- package/lib/utils/logger.js +53 -0
- package/lib/utils/template-helpers.js +223 -0
- package/lib/utils/variable-transformer.js +271 -0
- package/lib/validator.js +27 -112
- package/package.json +13 -10
- package/templates/README.md +75 -3
- package/templates/applications/keycloak/Dockerfile +36 -0
- package/templates/applications/keycloak/env.template +32 -0
- package/templates/applications/keycloak/rbac.yaml +37 -0
- package/templates/applications/keycloak/variables.yaml +56 -0
- package/templates/applications/miso-controller/Dockerfile +125 -0
- package/templates/applications/miso-controller/env.template +129 -0
- package/templates/applications/miso-controller/rbac.yaml +168 -0
- package/templates/applications/miso-controller/variables.yaml +56 -0
- package/templates/github/release.yaml.hbs +5 -26
- package/templates/github/steps/npm.hbs +24 -0
- package/templates/infra/compose.yaml +6 -6
- package/templates/python/docker-compose.hbs +19 -12
- package/templates/python/main.py +80 -0
- package/templates/python/requirements.txt +4 -0
- package/templates/typescript/Dockerfile.hbs +2 -2
- package/templates/typescript/docker-compose.hbs +19 -12
- package/templates/typescript/index.ts +116 -0
- package/templates/typescript/package.json +26 -0
- package/templates/typescript/tsconfig.json +24 -0
package/lib/utils/api.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder API Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for making API calls to the controller
|
|
5
|
+
* Supports both bearer token and ClientId/Secret authentication
|
|
6
|
+
* Supports OAuth2 Device Code Flow (RFC 8628)
|
|
7
|
+
*
|
|
8
|
+
* @fileoverview API calling utilities for AI Fabrix Builder
|
|
9
|
+
* @author AI Fabrix Team
|
|
10
|
+
* @version 2.0.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Make an API call with proper error handling
|
|
15
|
+
* @param {string} url - API endpoint URL
|
|
16
|
+
* @param {Object} options - Fetch options
|
|
17
|
+
* @returns {Promise<Object>} Response object with success flag
|
|
18
|
+
*/
|
|
19
|
+
async function makeApiCall(url, options = {}) {
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(url, options);
|
|
22
|
+
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
const errorText = await response.text();
|
|
25
|
+
let errorMessage;
|
|
26
|
+
try {
|
|
27
|
+
const errorJson = JSON.parse(errorText);
|
|
28
|
+
errorMessage = errorJson.error || errorJson.message || 'Unknown error';
|
|
29
|
+
} catch {
|
|
30
|
+
errorMessage = errorText || `HTTP ${response.status}: ${response.statusText}`;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
success: false,
|
|
34
|
+
error: errorMessage,
|
|
35
|
+
status: response.status
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const contentType = response.headers.get('content-type');
|
|
40
|
+
if (contentType && contentType.includes('application/json')) {
|
|
41
|
+
const data = await response.json();
|
|
42
|
+
return {
|
|
43
|
+
success: true,
|
|
44
|
+
data,
|
|
45
|
+
status: response.status
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const text = await response.text();
|
|
50
|
+
return {
|
|
51
|
+
success: true,
|
|
52
|
+
data: text,
|
|
53
|
+
status: response.status
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
error: error.message,
|
|
60
|
+
network: true
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Make an authenticated API call with bearer token
|
|
67
|
+
* @param {string} url - API endpoint URL
|
|
68
|
+
* @param {Object} options - Fetch options
|
|
69
|
+
* @param {string} token - Bearer token
|
|
70
|
+
* @returns {Promise<Object>} Response object
|
|
71
|
+
*/
|
|
72
|
+
async function authenticatedApiCall(url, options = {}, token) {
|
|
73
|
+
const headers = {
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
...options.headers
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (token) {
|
|
79
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return makeApiCall(url, {
|
|
83
|
+
...options,
|
|
84
|
+
headers
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Parses device code response from API
|
|
90
|
+
* @function parseDeviceCodeResponse
|
|
91
|
+
* @param {Object} response - API response object
|
|
92
|
+
* @returns {Object} Parsed device code response
|
|
93
|
+
* @throws {Error} If response is invalid
|
|
94
|
+
*/
|
|
95
|
+
function parseDeviceCodeResponse(response) {
|
|
96
|
+
const apiResponse = response.data;
|
|
97
|
+
const responseData = apiResponse.data || apiResponse;
|
|
98
|
+
|
|
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;
|
|
103
|
+
const interval = responseData.interval || 5;
|
|
104
|
+
|
|
105
|
+
if (!deviceCode || !userCode || !verificationUri) {
|
|
106
|
+
throw new Error('Invalid device code response: missing required fields');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
device_code: deviceCode,
|
|
111
|
+
user_code: userCode,
|
|
112
|
+
verification_uri: verificationUri,
|
|
113
|
+
expires_in: expiresIn,
|
|
114
|
+
interval: interval
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Initiates OAuth2 Device Code Flow
|
|
120
|
+
* Calls the device code endpoint to get device_code and user_code
|
|
121
|
+
*
|
|
122
|
+
* @async
|
|
123
|
+
* @function initiateDeviceCodeFlow
|
|
124
|
+
* @param {string} controllerUrl - Base URL of the controller
|
|
125
|
+
* @param {string} environment - Environment key (e.g., 'dev', 'tst', 'pro')
|
|
126
|
+
* @returns {Promise<Object>} Device code response with device_code, user_code, verification_uri, expires_in, interval
|
|
127
|
+
* @throws {Error} If initiation fails
|
|
128
|
+
*/
|
|
129
|
+
async function initiateDeviceCodeFlow(controllerUrl, environment) {
|
|
130
|
+
if (!environment || typeof environment !== 'string') {
|
|
131
|
+
throw new Error('Environment key is required');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const url = `${controllerUrl}/api/v1/auth/login?environment=${encodeURIComponent(environment)}`;
|
|
135
|
+
const response = await makeApiCall(url, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: {
|
|
138
|
+
'Content-Type': 'application/json'
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (!response.success) {
|
|
143
|
+
throw new Error(`Device code initiation failed: ${response.error || 'Unknown error'}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return parseDeviceCodeResponse(response);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Checks if token has expired based on elapsed time
|
|
151
|
+
* @function checkTokenExpiration
|
|
152
|
+
* @param {number} startTime - Start time in milliseconds
|
|
153
|
+
* @param {number} expiresIn - Expiration time in seconds
|
|
154
|
+
* @throws {Error} If token has expired
|
|
155
|
+
*/
|
|
156
|
+
function checkTokenExpiration(startTime, expiresIn) {
|
|
157
|
+
const maxWaitTime = (expiresIn + 30) * 1000;
|
|
158
|
+
if (Date.now() - startTime > maxWaitTime) {
|
|
159
|
+
throw new Error('Device code expired: Maximum polling time exceeded');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Parses token response from API
|
|
165
|
+
* @function parseTokenResponse
|
|
166
|
+
* @param {Object} response - API response object
|
|
167
|
+
* @returns {Object|null} Parsed token response or null if pending
|
|
168
|
+
*/
|
|
169
|
+
function parseTokenResponse(response) {
|
|
170
|
+
const apiResponse = response.data;
|
|
171
|
+
const responseData = apiResponse.data || apiResponse;
|
|
172
|
+
|
|
173
|
+
const error = responseData.error || apiResponse.error;
|
|
174
|
+
if (error === 'authorization_pending' || error === 'slow_down') {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
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;
|
|
181
|
+
|
|
182
|
+
if (!accessToken) {
|
|
183
|
+
throw new Error('Invalid token response: missing accessToken');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
access_token: accessToken,
|
|
188
|
+
refresh_token: refreshToken,
|
|
189
|
+
expires_in: expiresIn
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Handles polling errors
|
|
195
|
+
* @function handlePollingErrors
|
|
196
|
+
* @param {string} error - Error code
|
|
197
|
+
* @param {number} status - HTTP status code
|
|
198
|
+
* @throws {Error} For fatal errors
|
|
199
|
+
* @returns {boolean} True if should continue polling
|
|
200
|
+
*/
|
|
201
|
+
function handlePollingErrors(error, status) {
|
|
202
|
+
if (error === 'authorization_pending' || status === 202) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check error field first, then status code
|
|
207
|
+
if (error === 'authorization_declined') {
|
|
208
|
+
throw new Error('Authorization declined: User denied the request');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (error === 'expired_token' || status === 410) {
|
|
212
|
+
throw new Error('Device code expired: Please restart the authentication process');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (error === 'slow_down') {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
throw new Error(`Token polling failed: ${error}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Waits for next polling interval
|
|
224
|
+
* @async
|
|
225
|
+
* @function waitForNextPoll
|
|
226
|
+
* @param {number} interval - Polling interval in seconds
|
|
227
|
+
* @param {boolean} slowDown - Whether to slow down
|
|
228
|
+
*/
|
|
229
|
+
async function waitForNextPoll(interval, slowDown) {
|
|
230
|
+
const waitInterval = slowDown ? interval * 2 : interval;
|
|
231
|
+
await new Promise(resolve => setTimeout(resolve, waitInterval * 1000));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Polls for token during Device Code Flow
|
|
236
|
+
* Continuously polls the token endpoint until user approves or flow expires
|
|
237
|
+
*
|
|
238
|
+
* @async
|
|
239
|
+
* @function pollDeviceCodeToken
|
|
240
|
+
* @param {string} controllerUrl - Base URL of the controller
|
|
241
|
+
* @param {string} deviceCode - Device code from initiation
|
|
242
|
+
* @param {number} interval - Polling interval in seconds
|
|
243
|
+
* @param {number} expiresIn - Expiration time in seconds
|
|
244
|
+
* @param {Function} [onPoll] - Optional callback called on each poll attempt
|
|
245
|
+
* @returns {Promise<Object>} Token response with access_token, refresh_token, expires_in
|
|
246
|
+
* @throws {Error} If polling fails or token is expired/declined
|
|
247
|
+
*/
|
|
248
|
+
async function pollDeviceCodeToken(controllerUrl, deviceCode, interval, expiresIn, onPoll) {
|
|
249
|
+
if (!deviceCode || typeof deviceCode !== 'string') {
|
|
250
|
+
throw new Error('Device code is required');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const url = `${controllerUrl}/api/v1/auth/login/device/token`;
|
|
254
|
+
const startTime = Date.now();
|
|
255
|
+
|
|
256
|
+
// eslint-disable-next-line no-constant-condition
|
|
257
|
+
while (true) {
|
|
258
|
+
checkTokenExpiration(startTime, expiresIn);
|
|
259
|
+
|
|
260
|
+
if (onPoll) {
|
|
261
|
+
onPoll();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const response = await makeApiCall(url, {
|
|
265
|
+
method: 'POST',
|
|
266
|
+
headers: {
|
|
267
|
+
'Content-Type': 'application/json'
|
|
268
|
+
},
|
|
269
|
+
body: JSON.stringify({
|
|
270
|
+
deviceCode: deviceCode
|
|
271
|
+
})
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (response.success) {
|
|
275
|
+
const tokenResponse = parseTokenResponse(response);
|
|
276
|
+
if (tokenResponse) {
|
|
277
|
+
return tokenResponse;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const apiResponse = response.data;
|
|
281
|
+
const responseData = apiResponse.data || apiResponse;
|
|
282
|
+
const error = responseData.error || apiResponse.error;
|
|
283
|
+
const slowDown = error === 'slow_down';
|
|
284
|
+
await waitForNextPoll(interval, slowDown);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const apiResponse = response.data || {};
|
|
289
|
+
const errorData = typeof apiResponse === 'object' ? apiResponse : {};
|
|
290
|
+
const error = errorData.error || response.error || 'Unknown error';
|
|
291
|
+
const shouldContinue = handlePollingErrors(error, response.status);
|
|
292
|
+
|
|
293
|
+
if (shouldContinue) {
|
|
294
|
+
const slowDown = error === 'slow_down';
|
|
295
|
+
await waitForNextPoll(interval, slowDown);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Displays device code information to the user
|
|
303
|
+
* Formats user code and verification URL for easy reading
|
|
304
|
+
*
|
|
305
|
+
* @function displayDeviceCodeInfo
|
|
306
|
+
* @param {string} userCode - User code to display
|
|
307
|
+
* @param {string} verificationUri - Verification URL
|
|
308
|
+
* @param {Object} logger - Logger instance with log method
|
|
309
|
+
* @param {Object} chalk - Chalk instance for colored output
|
|
310
|
+
*/
|
|
311
|
+
function displayDeviceCodeInfo(userCode, verificationUri, logger, chalk) {
|
|
312
|
+
logger.log(chalk.cyan('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
313
|
+
logger.log(chalk.cyan(' Device Code Flow Authentication'));
|
|
314
|
+
logger.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
|
|
315
|
+
logger.log(chalk.yellow('To complete authentication:'));
|
|
316
|
+
logger.log(chalk.gray(' 1. Visit: ') + chalk.blue.underline(verificationUri));
|
|
317
|
+
logger.log(chalk.gray(' 2. Enter code: ') + chalk.bold.cyan(userCode));
|
|
318
|
+
logger.log(chalk.gray(' 3. Approve the request\n'));
|
|
319
|
+
logger.log(chalk.gray('Waiting for approval...'));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
module.exports = {
|
|
323
|
+
makeApiCall,
|
|
324
|
+
authenticatedApiCall,
|
|
325
|
+
initiateDeviceCodeFlow,
|
|
326
|
+
pollDeviceCodeToken,
|
|
327
|
+
displayDeviceCodeInfo
|
|
328
|
+
};
|
|
329
|
+
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Utility Functions
|
|
3
|
+
*
|
|
4
|
+
* Utility functions for CLI command handling and validation.
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview CLI utilities for error handling and validation
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const logger = require('./logger');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validates a command and its options
|
|
15
|
+
* @param {string} _command - Command name
|
|
16
|
+
* @param {Object} _options - Command options
|
|
17
|
+
* @returns {boolean} True if valid
|
|
18
|
+
*/
|
|
19
|
+
function validateCommand(_command, _options) {
|
|
20
|
+
// TODO: Implement command validation
|
|
21
|
+
// TODO: Add helpful error messages for common issues
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Formats error message based on error type
|
|
27
|
+
* @function formatError
|
|
28
|
+
* @param {Error} error - The error that occurred
|
|
29
|
+
* @returns {string[]} Array of error message lines
|
|
30
|
+
*/
|
|
31
|
+
function formatError(error) {
|
|
32
|
+
const messages = [];
|
|
33
|
+
const errorMsg = error.message || '';
|
|
34
|
+
|
|
35
|
+
// Check for specific error patterns first (most specific to least specific)
|
|
36
|
+
if (errorMsg.includes('Configuration not found')) {
|
|
37
|
+
messages.push(` ${errorMsg}`);
|
|
38
|
+
} else if (errorMsg.includes('not found locally') || (errorMsg.includes('Docker image') && errorMsg.includes('not found'))) {
|
|
39
|
+
messages.push(' Docker image not found.');
|
|
40
|
+
messages.push(' Run: aifabrix build <app> first');
|
|
41
|
+
} else if (errorMsg.includes('Docker') && (errorMsg.includes('not running') || errorMsg.includes('not installed') || errorMsg.includes('Cannot connect'))) {
|
|
42
|
+
messages.push(' Docker is not running or not installed.');
|
|
43
|
+
messages.push(' Please start Docker Desktop and try again.');
|
|
44
|
+
} else if (errorMsg.includes('port')) {
|
|
45
|
+
messages.push(' Port conflict detected.');
|
|
46
|
+
messages.push(' Run "aifabrix doctor" to check which ports are in use.');
|
|
47
|
+
} else if (errorMsg.includes('permission')) {
|
|
48
|
+
messages.push(' Permission denied.');
|
|
49
|
+
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.');
|
|
52
|
+
messages.push(' Install from: https://docs.microsoft.com/cli/azure/install-azure-cli');
|
|
53
|
+
messages.push(' Run: az login');
|
|
54
|
+
} else if (errorMsg.includes('authenticate') || errorMsg.includes('ACR')) {
|
|
55
|
+
messages.push(' Azure Container Registry authentication failed.');
|
|
56
|
+
messages.push(' Run: az acr login --name <registry-name>');
|
|
57
|
+
messages.push(' Or login to Azure: az login');
|
|
58
|
+
} else if (errorMsg.includes('Invalid ACR URL') || errorMsg.includes('Expected format')) {
|
|
59
|
+
messages.push(' Invalid registry URL format.');
|
|
60
|
+
messages.push(' Use format: *.azurecr.io (e.g., myacr.azurecr.io)');
|
|
61
|
+
} else if (errorMsg.includes('Registry URL is required')) {
|
|
62
|
+
messages.push(' Registry URL is required.');
|
|
63
|
+
messages.push(' Provide via --registry flag or configure in variables.yaml under image.registry');
|
|
64
|
+
} else {
|
|
65
|
+
messages.push(` ${errorMsg}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return messages;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Logs error messages
|
|
73
|
+
* @function logError
|
|
74
|
+
* @param {string} command - Command that failed
|
|
75
|
+
* @param {string[]} errorMessages - Error message lines
|
|
76
|
+
*/
|
|
77
|
+
function logError(command, errorMessages) {
|
|
78
|
+
logger.error(`\n❌ Error in ${command} command:`);
|
|
79
|
+
errorMessages.forEach(msg => logger.error(msg));
|
|
80
|
+
logger.error('\n💡 Run "aifabrix doctor" for environment diagnostics.\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Handles command errors with user-friendly messages
|
|
85
|
+
* @param {Error} error - The error that occurred
|
|
86
|
+
* @param {string} command - Command that failed
|
|
87
|
+
*/
|
|
88
|
+
function handleCommandError(error, command) {
|
|
89
|
+
const errorMessages = formatError(error);
|
|
90
|
+
logError(command, errorMessages);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
validateCommand,
|
|
95
|
+
handleCommandError
|
|
96
|
+
};
|
|
97
|
+
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dockerfile Utility Functions
|
|
3
|
+
*
|
|
4
|
+
* This module handles Dockerfile template loading, rendering,
|
|
5
|
+
* and path resolution. Separated from build.js to maintain
|
|
6
|
+
* file size limits and improve code organization.
|
|
7
|
+
*
|
|
8
|
+
* @fileoverview Dockerfile utility functions for AI Fabrix Builder
|
|
9
|
+
* @author AI Fabrix Team
|
|
10
|
+
* @version 2.0.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fsSync = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const handlebars = require('handlebars');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Loads Dockerfile template for language
|
|
19
|
+
* @function loadDockerfileTemplate
|
|
20
|
+
* @param {string} language - Language ('typescript' or 'python')
|
|
21
|
+
* @returns {Function} Compiled Handlebars template
|
|
22
|
+
* @throws {Error} If template not found
|
|
23
|
+
*/
|
|
24
|
+
function loadDockerfileTemplate(language) {
|
|
25
|
+
const templatePath = path.join(__dirname, '..', '..', 'templates', language, 'Dockerfile.hbs');
|
|
26
|
+
|
|
27
|
+
if (!fsSync.existsSync(templatePath)) {
|
|
28
|
+
throw new Error(`Template not found for language: ${language}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const templateContent = fsSync.readFileSync(templatePath, 'utf8');
|
|
32
|
+
return handlebars.compile(templateContent);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Renders Dockerfile with template variables
|
|
37
|
+
* @function renderDockerfile
|
|
38
|
+
* @param {Function} template - Compiled Handlebars template
|
|
39
|
+
* @param {Object} templateVars - Template variables
|
|
40
|
+
* @param {string} language - Language ('typescript' or 'python')
|
|
41
|
+
* @param {boolean} isAppFlag - Whether --app flag was used
|
|
42
|
+
* @param {string} appSourcePath - Application source path
|
|
43
|
+
* @returns {string} Rendered Dockerfile content
|
|
44
|
+
*/
|
|
45
|
+
function renderDockerfile(template, templateVars, language, isAppFlag, appSourcePath) {
|
|
46
|
+
let dockerfileContent = template(templateVars);
|
|
47
|
+
|
|
48
|
+
if (!isAppFlag) {
|
|
49
|
+
return dockerfileContent;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
dockerfileContent = dockerfileContent.replace(
|
|
53
|
+
/^COPY \. \./gm,
|
|
54
|
+
`COPY ${appSourcePath} .`
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (language === 'python') {
|
|
58
|
+
// Replace COPY requirements*.txt with app-specific path
|
|
59
|
+
dockerfileContent = dockerfileContent.replace(
|
|
60
|
+
/^COPY requirements\*\.txt \./gm,
|
|
61
|
+
`COPY ${appSourcePath}requirements*.txt ./`
|
|
62
|
+
);
|
|
63
|
+
// Also handle case where it might be COPY requirements.txt
|
|
64
|
+
dockerfileContent = dockerfileContent.replace(
|
|
65
|
+
/^COPY requirements\.txt \./gm,
|
|
66
|
+
`COPY ${appSourcePath}requirements.txt ./`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (language === 'typescript') {
|
|
71
|
+
dockerfileContent = dockerfileContent.replace(
|
|
72
|
+
/^COPY package\*\.json \./gm,
|
|
73
|
+
`COPY ${appSourcePath}package*.json ./`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return dockerfileContent;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Checks for template Dockerfile in builder directory
|
|
82
|
+
* @function checkTemplateDockerfile
|
|
83
|
+
* @param {string} builderPath - Builder directory path
|
|
84
|
+
* @param {string} appName - Application name
|
|
85
|
+
* @param {boolean} forceTemplate - Force template flag
|
|
86
|
+
* @returns {string|null} Dockerfile path or null
|
|
87
|
+
*/
|
|
88
|
+
function checkTemplateDockerfile(builderPath, appName, forceTemplate) {
|
|
89
|
+
const appDockerfilePath = path.join(builderPath, 'Dockerfile');
|
|
90
|
+
if (fsSync.existsSync(appDockerfilePath) && !forceTemplate) {
|
|
91
|
+
return appDockerfilePath;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Checks for custom Dockerfile from variables.yaml
|
|
98
|
+
* @function checkProjectDockerfile
|
|
99
|
+
* @param {string} builderPath - Builder directory path
|
|
100
|
+
* @param {string} appName - Application name
|
|
101
|
+
* @param {Object} buildConfig - Build configuration
|
|
102
|
+
* @param {string} contextPath - Build context path
|
|
103
|
+
* @param {boolean} forceTemplate - Force template flag
|
|
104
|
+
* @returns {string|null} Dockerfile path or null
|
|
105
|
+
*/
|
|
106
|
+
function checkProjectDockerfile(builderPath, appName, buildConfig, contextPath, forceTemplate) {
|
|
107
|
+
const customDockerfile = buildConfig.dockerfile;
|
|
108
|
+
if (!customDockerfile || forceTemplate) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const customPath = path.resolve(contextPath, customDockerfile);
|
|
113
|
+
if (fsSync.existsSync(customPath)) {
|
|
114
|
+
return customPath;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const builderCustomPath = path.join(process.cwd(), 'builder', appName, customDockerfile);
|
|
118
|
+
if (fsSync.existsSync(builderCustomPath)) {
|
|
119
|
+
return builderCustomPath;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
loadDockerfileTemplate,
|
|
127
|
+
renderDockerfile,
|
|
128
|
+
checkTemplateDockerfile,
|
|
129
|
+
checkProjectDockerfile
|
|
130
|
+
};
|
|
131
|
+
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Checking Utilities
|
|
3
|
+
*
|
|
4
|
+
* Checks the development environment for common issues
|
|
5
|
+
* Validates Docker, ports, secrets, and other requirements
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Environment checking utilities for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Checks if Docker is installed and available
|
|
18
|
+
*
|
|
19
|
+
* @async
|
|
20
|
+
* @function checkDocker
|
|
21
|
+
* @returns {Promise<string>} 'ok' if Docker is available, 'error' otherwise
|
|
22
|
+
*/
|
|
23
|
+
async function checkDocker() {
|
|
24
|
+
try {
|
|
25
|
+
const { exec } = require('child_process');
|
|
26
|
+
const { promisify } = require('util');
|
|
27
|
+
const execAsync = promisify(exec);
|
|
28
|
+
|
|
29
|
+
await execAsync('docker --version');
|
|
30
|
+
await execAsync('docker-compose --version');
|
|
31
|
+
return 'ok';
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return 'error';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Checks if required ports are available
|
|
39
|
+
*
|
|
40
|
+
* @async
|
|
41
|
+
* @function checkPorts
|
|
42
|
+
* @returns {Promise<string>} 'ok' if all ports are available, 'warning' otherwise
|
|
43
|
+
*/
|
|
44
|
+
async function checkPorts() {
|
|
45
|
+
const requiredPorts = [5432, 6379, 5050, 8081];
|
|
46
|
+
const netstat = require('net');
|
|
47
|
+
let portIssues = 0;
|
|
48
|
+
|
|
49
|
+
for (const port of requiredPorts) {
|
|
50
|
+
try {
|
|
51
|
+
await new Promise((resolve, reject) => {
|
|
52
|
+
const server = netstat.createServer();
|
|
53
|
+
server.listen(port, () => {
|
|
54
|
+
server.close(resolve);
|
|
55
|
+
});
|
|
56
|
+
server.on('error', reject);
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
portIssues++;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return portIssues === 0 ? 'ok' : 'warning';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Checks if secrets file exists
|
|
68
|
+
*
|
|
69
|
+
* @function checkSecrets
|
|
70
|
+
* @returns {string} 'ok' if secrets file exists, 'missing' otherwise
|
|
71
|
+
*/
|
|
72
|
+
function checkSecrets() {
|
|
73
|
+
const secretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
|
|
74
|
+
return fs.existsSync(secretsPath) ? 'ok' : 'missing';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Checks the development environment for common issues
|
|
79
|
+
* Validates Docker, ports, secrets, and other requirements
|
|
80
|
+
*
|
|
81
|
+
* @async
|
|
82
|
+
* @function checkEnvironment
|
|
83
|
+
* @returns {Promise<Object>} Environment check result
|
|
84
|
+
* @throws {Error} If critical issues are found
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* const result = await checkEnvironment();
|
|
88
|
+
* // Returns: { docker: 'ok', ports: 'ok', secrets: 'missing', recommendations: [...] }
|
|
89
|
+
*/
|
|
90
|
+
async function checkEnvironment() {
|
|
91
|
+
const result = {
|
|
92
|
+
docker: 'unknown',
|
|
93
|
+
ports: 'unknown',
|
|
94
|
+
secrets: 'unknown',
|
|
95
|
+
recommendations: []
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Check Docker
|
|
99
|
+
result.docker = await checkDocker();
|
|
100
|
+
if (result.docker === 'error') {
|
|
101
|
+
result.recommendations.push('Install Docker and Docker Compose');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check ports
|
|
105
|
+
result.ports = await checkPorts();
|
|
106
|
+
if (result.ports === 'warning') {
|
|
107
|
+
result.recommendations.push('Some required ports (5432, 6379, 5050, 8081) are in use');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check secrets
|
|
111
|
+
result.secrets = checkSecrets();
|
|
112
|
+
if (result.secrets === 'missing') {
|
|
113
|
+
result.recommendations.push('Create secrets file: ~/.aifabrix/secrets.yaml');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
checkDocker,
|
|
121
|
+
checkPorts,
|
|
122
|
+
checkSecrets,
|
|
123
|
+
checkEnvironment
|
|
124
|
+
};
|
|
125
|
+
|