@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.
@@ -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
+ }