@fenwave/agent 1.1.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/.claude/settings.local.json +11 -0
- package/Dockerfile +12 -0
- package/LICENSE +29 -0
- package/README.md +434 -0
- package/auth.js +276 -0
- package/cli-commands.js +1185 -0
- package/containerManager.js +385 -0
- package/convert-to-esm.sh +62 -0
- package/docker-actions/apps.js +3256 -0
- package/docker-actions/config-transformer.js +380 -0
- package/docker-actions/containers.js +346 -0
- package/docker-actions/general.js +171 -0
- package/docker-actions/images.js +1128 -0
- package/docker-actions/logs.js +188 -0
- package/docker-actions/metrics.js +270 -0
- package/docker-actions/registry.js +1100 -0
- package/docker-actions/terminal.js +247 -0
- package/docker-actions/volumes.js +696 -0
- package/helper-functions.js +193 -0
- package/index.html +60 -0
- package/index.js +988 -0
- package/package.json +49 -0
- package/setup/setupWizard.js +499 -0
- package/store/agentSessionStore.js +51 -0
- package/store/agentStore.js +113 -0
- package/store/configStore.js +174 -0
- package/store/deviceCredentialStore.js +107 -0
- package/store/npmTokenStore.js +65 -0
- package/store/registryStore.js +329 -0
- package/store/setupState.js +147 -0
- package/utils/deviceInfo.js +98 -0
- package/utils/ecrAuth.js +225 -0
- package/utils/encryption.js +112 -0
- package/utils/envSetup.js +54 -0
- package/utils/errorHandler.js +327 -0
- package/utils/prerequisites.js +323 -0
- package/utils/prompts.js +318 -0
- package/websocket-server.js +364 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Error codes for common errors
|
|
5
|
+
*/
|
|
6
|
+
export const ErrorCode = {
|
|
7
|
+
// Network errors
|
|
8
|
+
NETWORK_ERROR: 'NETWORK_ERROR',
|
|
9
|
+
BACKEND_UNREACHABLE: 'BACKEND_UNREACHABLE',
|
|
10
|
+
TIMEOUT: 'TIMEOUT',
|
|
11
|
+
|
|
12
|
+
// Authentication errors
|
|
13
|
+
INVALID_TOKEN: 'INVALID_TOKEN',
|
|
14
|
+
TOKEN_EXPIRED: 'TOKEN_EXPIRED',
|
|
15
|
+
UNAUTHORIZED: 'UNAUTHORIZED',
|
|
16
|
+
INVALID_CREDENTIALS: 'INVALID_CREDENTIALS',
|
|
17
|
+
|
|
18
|
+
// Registration errors
|
|
19
|
+
REGISTRATION_FAILED: 'REGISTRATION_FAILED',
|
|
20
|
+
DEVICE_ALREADY_REGISTERED: 'DEVICE_ALREADY_REGISTERED',
|
|
21
|
+
RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',
|
|
22
|
+
|
|
23
|
+
// Prerequisites errors
|
|
24
|
+
DOCKER_NOT_INSTALLED: 'DOCKER_NOT_INSTALLED',
|
|
25
|
+
DOCKER_NOT_RUNNING: 'DOCKER_NOT_RUNNING',
|
|
26
|
+
NODE_VERSION_MISMATCH: 'NODE_VERSION_MISMATCH',
|
|
27
|
+
AWS_CLI_NOT_INSTALLED: 'AWS_CLI_NOT_INSTALLED',
|
|
28
|
+
|
|
29
|
+
// File system errors
|
|
30
|
+
PERMISSION_DENIED: 'PERMISSION_DENIED',
|
|
31
|
+
FILE_NOT_FOUND: 'FILE_NOT_FOUND',
|
|
32
|
+
DIRECTORY_CREATE_FAILED: 'DIRECTORY_CREATE_FAILED',
|
|
33
|
+
|
|
34
|
+
// Docker errors
|
|
35
|
+
DOCKER_PULL_FAILED: 'DOCKER_PULL_FAILED',
|
|
36
|
+
ECR_AUTH_FAILED: 'ECR_AUTH_FAILED',
|
|
37
|
+
CONTAINER_START_FAILED: 'CONTAINER_START_FAILED',
|
|
38
|
+
|
|
39
|
+
// Generic errors
|
|
40
|
+
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
|
|
41
|
+
OPERATION_CANCELLED: 'OPERATION_CANCELLED',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* User-friendly error messages and suggestions
|
|
46
|
+
*/
|
|
47
|
+
const ERROR_MESSAGES = {
|
|
48
|
+
[ErrorCode.NETWORK_ERROR]: {
|
|
49
|
+
message: 'Network error occurred',
|
|
50
|
+
suggestion: 'Check your internet connection and try again.',
|
|
51
|
+
},
|
|
52
|
+
[ErrorCode.BACKEND_UNREACHABLE]: {
|
|
53
|
+
message: 'Cannot connect to Backstage backend',
|
|
54
|
+
suggestion:
|
|
55
|
+
'Make sure Backstage is running and accessible. Check the backend URL in your configuration.',
|
|
56
|
+
},
|
|
57
|
+
[ErrorCode.TIMEOUT]: {
|
|
58
|
+
message: 'Operation timed out',
|
|
59
|
+
suggestion: 'The operation took too long. Check your network connection and try again.',
|
|
60
|
+
},
|
|
61
|
+
[ErrorCode.INVALID_TOKEN]: {
|
|
62
|
+
message: 'Invalid registration token',
|
|
63
|
+
suggestion:
|
|
64
|
+
'Get a new registration token from Backstage at /agent-installer and try again.',
|
|
65
|
+
},
|
|
66
|
+
[ErrorCode.TOKEN_EXPIRED]: {
|
|
67
|
+
message: 'Registration token has expired',
|
|
68
|
+
suggestion:
|
|
69
|
+
'Tokens are valid for 7 days. Get a new token from Backstage at /agent-installer.',
|
|
70
|
+
},
|
|
71
|
+
[ErrorCode.UNAUTHORIZED]: {
|
|
72
|
+
message: 'Authentication failed',
|
|
73
|
+
suggestion: 'Run "fenwave login" to authenticate with Backstage.',
|
|
74
|
+
},
|
|
75
|
+
[ErrorCode.INVALID_CREDENTIALS]: {
|
|
76
|
+
message: 'Invalid device credentials',
|
|
77
|
+
suggestion:
|
|
78
|
+
'Your device credentials may have been revoked or are invalid. Run "fenwave register" to re-register your device.',
|
|
79
|
+
},
|
|
80
|
+
[ErrorCode.REGISTRATION_FAILED]: {
|
|
81
|
+
message: 'Device registration failed',
|
|
82
|
+
suggestion:
|
|
83
|
+
'Check your registration token and try again. If the problem persists, contact support.',
|
|
84
|
+
},
|
|
85
|
+
[ErrorCode.DEVICE_ALREADY_REGISTERED]: {
|
|
86
|
+
message: 'Device is already registered',
|
|
87
|
+
suggestion:
|
|
88
|
+
'This device is already registered. Use "fenwave status" to check registration details.',
|
|
89
|
+
},
|
|
90
|
+
[ErrorCode.RATE_LIMIT_EXCEEDED]: {
|
|
91
|
+
message: 'Rate limit exceeded',
|
|
92
|
+
suggestion: 'Too many requests. Please wait a few minutes and try again.',
|
|
93
|
+
},
|
|
94
|
+
[ErrorCode.DOCKER_NOT_INSTALLED]: {
|
|
95
|
+
message: 'Docker is not installed',
|
|
96
|
+
suggestion: 'Install Docker Desktop from https://www.docker.com/products/docker-desktop',
|
|
97
|
+
},
|
|
98
|
+
[ErrorCode.DOCKER_NOT_RUNNING]: {
|
|
99
|
+
message: 'Docker is not running',
|
|
100
|
+
suggestion: 'Start Docker Desktop and try again.',
|
|
101
|
+
},
|
|
102
|
+
[ErrorCode.NODE_VERSION_MISMATCH]: {
|
|
103
|
+
message: 'Node.js version is too old',
|
|
104
|
+
suggestion: 'Install Node.js v18 or higher from https://nodejs.org',
|
|
105
|
+
},
|
|
106
|
+
[ErrorCode.AWS_CLI_NOT_INSTALLED]: {
|
|
107
|
+
message: 'AWS CLI is not installed',
|
|
108
|
+
suggestion: 'Install AWS CLI from https://aws.amazon.com/cli/',
|
|
109
|
+
},
|
|
110
|
+
[ErrorCode.PERMISSION_DENIED]: {
|
|
111
|
+
message: 'Permission denied',
|
|
112
|
+
suggestion:
|
|
113
|
+
'You do not have permission to perform this operation. Try running with appropriate permissions.',
|
|
114
|
+
},
|
|
115
|
+
[ErrorCode.FILE_NOT_FOUND]: {
|
|
116
|
+
message: 'File not found',
|
|
117
|
+
suggestion: 'The required file was not found. Try re-installing the agent.',
|
|
118
|
+
},
|
|
119
|
+
[ErrorCode.DIRECTORY_CREATE_FAILED]: {
|
|
120
|
+
message: 'Failed to create directory',
|
|
121
|
+
suggestion: 'Check file system permissions and available disk space.',
|
|
122
|
+
},
|
|
123
|
+
[ErrorCode.DOCKER_PULL_FAILED]: {
|
|
124
|
+
message: 'Failed to pull Docker image',
|
|
125
|
+
suggestion:
|
|
126
|
+
'Check your Docker registry authentication and network connection. Try running "docker login" if using a private registry.',
|
|
127
|
+
},
|
|
128
|
+
[ErrorCode.ECR_AUTH_FAILED]: {
|
|
129
|
+
message: 'AWS ECR authentication failed',
|
|
130
|
+
suggestion:
|
|
131
|
+
'Configure your AWS credentials using "aws configure" or check your AWS access.',
|
|
132
|
+
},
|
|
133
|
+
[ErrorCode.CONTAINER_START_FAILED]: {
|
|
134
|
+
message: 'Failed to start container',
|
|
135
|
+
suggestion: 'Check Docker logs for details: docker logs <container-id>',
|
|
136
|
+
},
|
|
137
|
+
[ErrorCode.UNKNOWN_ERROR]: {
|
|
138
|
+
message: 'An unknown error occurred',
|
|
139
|
+
suggestion: 'Check the error details above. If the problem persists, contact support.',
|
|
140
|
+
},
|
|
141
|
+
[ErrorCode.OPERATION_CANCELLED]: {
|
|
142
|
+
message: 'Operation cancelled by user',
|
|
143
|
+
suggestion: null,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Parse error and determine error code
|
|
149
|
+
*
|
|
150
|
+
* @param {Error} error - Error object
|
|
151
|
+
* @returns {string} Error code
|
|
152
|
+
*/
|
|
153
|
+
function getErrorCode(error) {
|
|
154
|
+
// Check for network errors
|
|
155
|
+
if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
|
|
156
|
+
return ErrorCode.BACKEND_UNREACHABLE;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (error.code === 'ETIMEDOUT' || error.code === 'ESOCKETTIMEDOUT') {
|
|
160
|
+
return ErrorCode.TIMEOUT;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
|
164
|
+
return ErrorCode.PERMISSION_DENIED;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (error.code === 'ENOENT') {
|
|
168
|
+
return ErrorCode.FILE_NOT_FOUND;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Check for HTTP status codes
|
|
172
|
+
if (error.response) {
|
|
173
|
+
const status = error.response.status;
|
|
174
|
+
|
|
175
|
+
if (status === 401) {
|
|
176
|
+
const errorMessage = error.response.data?.error || '';
|
|
177
|
+
if (errorMessage.includes('token')) {
|
|
178
|
+
if (errorMessage.includes('expired')) {
|
|
179
|
+
return ErrorCode.TOKEN_EXPIRED;
|
|
180
|
+
}
|
|
181
|
+
return ErrorCode.INVALID_TOKEN;
|
|
182
|
+
}
|
|
183
|
+
return ErrorCode.UNAUTHORIZED;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (status === 429) {
|
|
187
|
+
return ErrorCode.RATE_LIMIT_EXCEEDED;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (status >= 500) {
|
|
191
|
+
return ErrorCode.BACKEND_UNREACHABLE;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check error message for specific patterns
|
|
196
|
+
const message = error.message?.toLowerCase() || '';
|
|
197
|
+
|
|
198
|
+
if (message.includes('invalid') && message.includes('token')) {
|
|
199
|
+
return ErrorCode.INVALID_TOKEN;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (message.includes('expired') && message.includes('token')) {
|
|
203
|
+
return ErrorCode.TOKEN_EXPIRED;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (message.includes('docker') && message.includes('not installed')) {
|
|
207
|
+
return ErrorCode.DOCKER_NOT_INSTALLED;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (message.includes('docker') && message.includes('not running')) {
|
|
211
|
+
return ErrorCode.DOCKER_NOT_RUNNING;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (message.includes('ecr') && message.includes('auth')) {
|
|
215
|
+
return ErrorCode.ECR_AUTH_FAILED;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return ErrorCode.UNKNOWN_ERROR;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Handle error and display user-friendly message
|
|
223
|
+
*
|
|
224
|
+
* @param {Error} error - Error object
|
|
225
|
+
* @param {string} context - Context where error occurred (optional)
|
|
226
|
+
*/
|
|
227
|
+
export function handleError(error, context) {
|
|
228
|
+
const errorCode = getErrorCode(error);
|
|
229
|
+
const errorInfo = ERROR_MESSAGES[errorCode] || ERROR_MESSAGES[ErrorCode.UNKNOWN_ERROR];
|
|
230
|
+
|
|
231
|
+
console.log('');
|
|
232
|
+
console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
233
|
+
console.log(chalk.red.bold('❌ Error'));
|
|
234
|
+
console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
235
|
+
|
|
236
|
+
if (context) {
|
|
237
|
+
console.log(chalk.gray(`Context: ${context}`));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.log(chalk.red(`\n${errorInfo.message}`));
|
|
241
|
+
|
|
242
|
+
if (errorInfo.suggestion) {
|
|
243
|
+
console.log(chalk.yellow(`\n💡 Suggestion: ${errorInfo.suggestion}`));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Display technical details in verbose mode
|
|
247
|
+
if (process.env.FW_VERBOSE === 'true' || process.env.DEBUG === 'true') {
|
|
248
|
+
console.log(chalk.gray('\n--- Technical Details ---'));
|
|
249
|
+
console.log(chalk.gray(`Error Code: ${errorCode}`));
|
|
250
|
+
console.log(chalk.gray(`Message: ${error.message}`));
|
|
251
|
+
|
|
252
|
+
if (error.stack) {
|
|
253
|
+
console.log(chalk.gray(`Stack Trace:\n${error.stack}`));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (error.response) {
|
|
257
|
+
console.log(chalk.gray(`HTTP Status: ${error.response.status}`));
|
|
258
|
+
console.log(chalk.gray(`Response: ${JSON.stringify(error.response.data, null, 2)}`));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
263
|
+
console.log('');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Create a custom error with error code
|
|
268
|
+
*
|
|
269
|
+
* @param {string} errorCode - Error code
|
|
270
|
+
* @param {string} details - Additional details
|
|
271
|
+
* @returns {Error} Custom error
|
|
272
|
+
*/
|
|
273
|
+
export function createError(errorCode, details) {
|
|
274
|
+
const error = new Error(ERROR_MESSAGES[errorCode]?.message || 'Unknown error');
|
|
275
|
+
error.code = errorCode;
|
|
276
|
+
error.details = details;
|
|
277
|
+
return error;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Display a simple error message without full error handling
|
|
282
|
+
*
|
|
283
|
+
* @param {string} message - Error message
|
|
284
|
+
*/
|
|
285
|
+
export function displaySimpleError(message) {
|
|
286
|
+
console.log(chalk.red('❌ ' + message));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Display warning
|
|
291
|
+
*
|
|
292
|
+
* @param {string} message - Warning message
|
|
293
|
+
*/
|
|
294
|
+
export function displayWarning(message) {
|
|
295
|
+
console.log(chalk.yellow('⚠️ ' + message));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Check if error is a user cancellation
|
|
300
|
+
*
|
|
301
|
+
* @param {Error} error - Error to check
|
|
302
|
+
* @returns {boolean} True if user cancelled
|
|
303
|
+
*/
|
|
304
|
+
export function isUserCancellation(error) {
|
|
305
|
+
return (
|
|
306
|
+
error.code === ErrorCode.OPERATION_CANCELLED ||
|
|
307
|
+
error.message?.toLowerCase().includes('cancelled') ||
|
|
308
|
+
error.message?.toLowerCase().includes('abort')
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Get help command for error code
|
|
314
|
+
*
|
|
315
|
+
* @param {string} errorCode - Error code
|
|
316
|
+
* @returns {string|null} Help command or null
|
|
317
|
+
*/
|
|
318
|
+
export function getHelpCommand(errorCode) {
|
|
319
|
+
const helpCommands = {
|
|
320
|
+
[ErrorCode.INVALID_TOKEN]: 'fenwave help register',
|
|
321
|
+
[ErrorCode.DOCKER_NOT_INSTALLED]: 'fenwave help prerequisites',
|
|
322
|
+
[ErrorCode.DOCKER_NOT_RUNNING]: 'fenwave help prerequisites',
|
|
323
|
+
[ErrorCode.UNAUTHORIZED]: 'fenwave help login',
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return helpCommands[errorCode] || null;
|
|
327
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check if a command exists
|
|
6
|
+
*
|
|
7
|
+
* @param {string} command - Command to check
|
|
8
|
+
* @returns {boolean} True if command exists
|
|
9
|
+
*/
|
|
10
|
+
function commandExists(command) {
|
|
11
|
+
try {
|
|
12
|
+
execSync(`command -v ${command}`, { stdio: 'ignore' });
|
|
13
|
+
return true;
|
|
14
|
+
} catch (error) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get command version
|
|
21
|
+
*
|
|
22
|
+
* @param {string} command - Command to check
|
|
23
|
+
* @param {string} versionFlag - Flag to get version (default: --version)
|
|
24
|
+
* @returns {string|null} Version string or null if not found
|
|
25
|
+
*/
|
|
26
|
+
function getCommandVersion(command, versionFlag = '--version') {
|
|
27
|
+
try {
|
|
28
|
+
const output = execSync(`${command} ${versionFlag}`, { encoding: 'utf8', stdio: 'pipe' });
|
|
29
|
+
return output.trim().split('\n')[0];
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if Docker is installed and running
|
|
37
|
+
*
|
|
38
|
+
* @returns {Object} Docker status
|
|
39
|
+
*/
|
|
40
|
+
function checkDocker() {
|
|
41
|
+
const installed = commandExists('docker');
|
|
42
|
+
|
|
43
|
+
if (!installed) {
|
|
44
|
+
return {
|
|
45
|
+
name: 'Docker',
|
|
46
|
+
installed: false,
|
|
47
|
+
running: false,
|
|
48
|
+
version: null,
|
|
49
|
+
downloadUrl: 'https://www.docker.com/products/docker-desktop',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const version = getCommandVersion('docker', '--version');
|
|
54
|
+
|
|
55
|
+
// Check if Docker daemon is running
|
|
56
|
+
let running = false;
|
|
57
|
+
try {
|
|
58
|
+
execSync('docker info', { stdio: 'ignore' });
|
|
59
|
+
running = true;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
running = false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
name: 'Docker',
|
|
66
|
+
installed: true,
|
|
67
|
+
running,
|
|
68
|
+
version,
|
|
69
|
+
downloadUrl: 'https://www.docker.com/products/docker-desktop',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if Node.js is installed and meets minimum version
|
|
75
|
+
*
|
|
76
|
+
* @returns {Object} Node.js status
|
|
77
|
+
*/
|
|
78
|
+
function checkNode() {
|
|
79
|
+
const installed = commandExists('node');
|
|
80
|
+
|
|
81
|
+
if (!installed) {
|
|
82
|
+
return {
|
|
83
|
+
name: 'Node.js',
|
|
84
|
+
installed: false,
|
|
85
|
+
version: null,
|
|
86
|
+
meetsRequirement: false,
|
|
87
|
+
minVersion: '18.0.0',
|
|
88
|
+
downloadUrl: 'https://nodejs.org',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const version = getCommandVersion('node', '--version');
|
|
93
|
+
|
|
94
|
+
// Extract major version number
|
|
95
|
+
const majorVersion = version ? parseInt(version.replace('v', '').split('.')[0], 10) : 0;
|
|
96
|
+
const meetsRequirement = majorVersion >= 18;
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
name: 'Node.js',
|
|
100
|
+
installed: true,
|
|
101
|
+
version,
|
|
102
|
+
meetsRequirement,
|
|
103
|
+
minVersion: '18.0.0',
|
|
104
|
+
downloadUrl: 'https://nodejs.org',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if AWS CLI is installed
|
|
110
|
+
*
|
|
111
|
+
* @returns {Object} AWS CLI status
|
|
112
|
+
*/
|
|
113
|
+
function checkAwsCli() {
|
|
114
|
+
const installed = commandExists('aws');
|
|
115
|
+
|
|
116
|
+
if (!installed) {
|
|
117
|
+
return {
|
|
118
|
+
name: 'AWS CLI',
|
|
119
|
+
installed: false,
|
|
120
|
+
version: null,
|
|
121
|
+
downloadUrl: 'https://aws.amazon.com/cli/',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const version = getCommandVersion('aws', '--version');
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
name: 'AWS CLI',
|
|
129
|
+
installed: true,
|
|
130
|
+
version,
|
|
131
|
+
downloadUrl: 'https://aws.amazon.com/cli/',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check network connectivity to Backstage backend
|
|
137
|
+
*
|
|
138
|
+
* @param {string} backendUrl - Backend URL to check
|
|
139
|
+
* @returns {Promise<Object>} Connectivity status
|
|
140
|
+
*/
|
|
141
|
+
async function checkBackendConnectivity(backendUrl) {
|
|
142
|
+
try {
|
|
143
|
+
const axios = (await import('axios')).default;
|
|
144
|
+
|
|
145
|
+
// Try to reach the backend health endpoint or root
|
|
146
|
+
const response = await axios.get(`${backendUrl}/api/agent-cli/health`, {
|
|
147
|
+
timeout: 5000,
|
|
148
|
+
validateStatus: () => true, // Accept any status
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
name: 'Backstage Backend',
|
|
153
|
+
reachable: response.status < 500,
|
|
154
|
+
url: backendUrl,
|
|
155
|
+
status: response.status,
|
|
156
|
+
};
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return {
|
|
159
|
+
name: 'Backstage Backend',
|
|
160
|
+
reachable: false,
|
|
161
|
+
url: backendUrl,
|
|
162
|
+
error: error.message,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Run all prerequisite checks
|
|
169
|
+
*
|
|
170
|
+
* @param {Object} options - Check options
|
|
171
|
+
* @param {string} options.backendUrl - Backend URL for connectivity check
|
|
172
|
+
* @param {boolean} options.verbose - Show detailed output
|
|
173
|
+
* @returns {Promise<Object>} Check results
|
|
174
|
+
*/
|
|
175
|
+
export async function checkPrerequisites(options = {}) {
|
|
176
|
+
const { backendUrl, verbose = false } = options;
|
|
177
|
+
|
|
178
|
+
const results = {
|
|
179
|
+
docker: checkDocker(),
|
|
180
|
+
node: checkNode(),
|
|
181
|
+
awsCli: checkAwsCli(),
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Check backend connectivity if URL provided
|
|
185
|
+
if (backendUrl) {
|
|
186
|
+
results.backend = await checkBackendConnectivity(backendUrl);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Calculate overall status
|
|
190
|
+
const allPassed =
|
|
191
|
+
results.docker.installed &&
|
|
192
|
+
results.docker.running &&
|
|
193
|
+
results.node.installed &&
|
|
194
|
+
results.node.meetsRequirement &&
|
|
195
|
+
results.awsCli.installed &&
|
|
196
|
+
(!backendUrl || results.backend?.reachable);
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
results,
|
|
200
|
+
allPassed,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Display prerequisite check results
|
|
206
|
+
*
|
|
207
|
+
* @param {Object} checkResults - Results from checkPrerequisites()
|
|
208
|
+
*/
|
|
209
|
+
export function displayPrerequisites(checkResults) {
|
|
210
|
+
const { results, allPassed } = checkResults;
|
|
211
|
+
|
|
212
|
+
console.log('\n' + chalk.bold('📋 Prerequisites Check:\n'));
|
|
213
|
+
|
|
214
|
+
// Docker
|
|
215
|
+
if (results.docker.installed && results.docker.running) {
|
|
216
|
+
console.log(chalk.green(' ✅ Docker') + ` (${results.docker.version})`);
|
|
217
|
+
} else if (results.docker.installed && !results.docker.running) {
|
|
218
|
+
console.log(chalk.yellow(' ⚠️ Docker') + ` (installed but not running)`);
|
|
219
|
+
console.log(chalk.yellow(' Please start Docker Desktop'));
|
|
220
|
+
} else {
|
|
221
|
+
console.log(chalk.red(' ❌ Docker') + ' (not installed)');
|
|
222
|
+
console.log(chalk.gray(` Download: ${results.docker.downloadUrl}`));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Node.js
|
|
226
|
+
if (results.node.installed && results.node.meetsRequirement) {
|
|
227
|
+
console.log(chalk.green(' ✅ Node.js') + ` (${results.node.version})`);
|
|
228
|
+
} else if (results.node.installed && !results.node.meetsRequirement) {
|
|
229
|
+
console.log(chalk.yellow(' ⚠️ Node.js') + ` (${results.node.version}, requires ${results.node.minVersion}+)`);
|
|
230
|
+
console.log(chalk.gray(` Download: ${results.node.downloadUrl}`));
|
|
231
|
+
} else {
|
|
232
|
+
console.log(chalk.red(' ❌ Node.js') + ' (not installed)');
|
|
233
|
+
console.log(chalk.gray(` Download: ${results.node.downloadUrl}`));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// AWS CLI
|
|
237
|
+
if (results.awsCli.installed) {
|
|
238
|
+
console.log(chalk.green(' ✅ AWS CLI') + ` (${results.awsCli.version})`);
|
|
239
|
+
} else {
|
|
240
|
+
console.log(chalk.red(' ❌ AWS CLI') + ' (not installed)');
|
|
241
|
+
console.log(chalk.gray(` Download: ${results.awsCli.downloadUrl}`));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Backend connectivity
|
|
245
|
+
if (results.backend) {
|
|
246
|
+
if (results.backend.reachable) {
|
|
247
|
+
console.log(chalk.green(' ✅ Backstage Backend') + ` (${results.backend.url})`);
|
|
248
|
+
} else {
|
|
249
|
+
console.log(chalk.red(' ❌ Backstage Backend') + ' (not reachable)');
|
|
250
|
+
console.log(chalk.gray(` URL: ${results.backend.url}`));
|
|
251
|
+
if (results.backend.error) {
|
|
252
|
+
console.log(chalk.gray(` Error: ${results.backend.error}`));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
console.log('');
|
|
258
|
+
|
|
259
|
+
if (allPassed) {
|
|
260
|
+
console.log(chalk.green('✅ All prerequisites met!\n'));
|
|
261
|
+
} else {
|
|
262
|
+
console.log(chalk.yellow('⚠️ Some prerequisites are missing or not running.\n'));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return allPassed;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get missing prerequisites as a list
|
|
270
|
+
*
|
|
271
|
+
* @param {Object} checkResults - Results from checkPrerequisites()
|
|
272
|
+
* @returns {Array} List of missing prerequisites
|
|
273
|
+
*/
|
|
274
|
+
export function getMissingPrerequisites(checkResults) {
|
|
275
|
+
const { results } = checkResults;
|
|
276
|
+
const missing = [];
|
|
277
|
+
|
|
278
|
+
if (!results.docker.installed) {
|
|
279
|
+
missing.push({
|
|
280
|
+
name: 'Docker',
|
|
281
|
+
reason: 'Not installed',
|
|
282
|
+
action: `Install from ${results.docker.downloadUrl}`,
|
|
283
|
+
});
|
|
284
|
+
} else if (!results.docker.running) {
|
|
285
|
+
missing.push({
|
|
286
|
+
name: 'Docker',
|
|
287
|
+
reason: 'Not running',
|
|
288
|
+
action: 'Start Docker Desktop',
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!results.node.installed) {
|
|
293
|
+
missing.push({
|
|
294
|
+
name: 'Node.js',
|
|
295
|
+
reason: 'Not installed',
|
|
296
|
+
action: `Install from ${results.node.downloadUrl}`,
|
|
297
|
+
});
|
|
298
|
+
} else if (!results.node.meetsRequirement) {
|
|
299
|
+
missing.push({
|
|
300
|
+
name: 'Node.js',
|
|
301
|
+
reason: `Version ${results.node.version} is too old (requires ${results.node.minVersion}+)`,
|
|
302
|
+
action: `Update from ${results.node.downloadUrl}`,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!results.awsCli.installed) {
|
|
307
|
+
missing.push({
|
|
308
|
+
name: 'AWS CLI',
|
|
309
|
+
reason: 'Not installed',
|
|
310
|
+
action: `Install from ${results.awsCli.downloadUrl}`,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (results.backend && !results.backend.reachable) {
|
|
315
|
+
missing.push({
|
|
316
|
+
name: 'Backstage Backend',
|
|
317
|
+
reason: 'Not reachable',
|
|
318
|
+
action: `Check network connection and URL: ${results.backend.url}`,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return missing;
|
|
323
|
+
}
|