@edgible-team/cli 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/LICENSE +136 -0
  2. package/README.md +450 -0
  3. package/dist/client/api-client.js +1057 -0
  4. package/dist/client/index.js +21 -0
  5. package/dist/commands/agent.js +1280 -0
  6. package/dist/commands/ai.js +608 -0
  7. package/dist/commands/application.js +885 -0
  8. package/dist/commands/auth.js +570 -0
  9. package/dist/commands/base/BaseCommand.js +93 -0
  10. package/dist/commands/base/CommandHandler.js +7 -0
  11. package/dist/commands/base/command-wrapper.js +58 -0
  12. package/dist/commands/base/middleware.js +77 -0
  13. package/dist/commands/config.js +116 -0
  14. package/dist/commands/connectivity.js +59 -0
  15. package/dist/commands/debug.js +98 -0
  16. package/dist/commands/discover.js +144 -0
  17. package/dist/commands/examples/migrated-command-example.js +180 -0
  18. package/dist/commands/gateway.js +494 -0
  19. package/dist/commands/managedGateway.js +787 -0
  20. package/dist/commands/utils/config-validator.js +76 -0
  21. package/dist/commands/utils/gateway-prompt.js +79 -0
  22. package/dist/commands/utils/input-parser.js +120 -0
  23. package/dist/commands/utils/output-formatter.js +109 -0
  24. package/dist/config/app-config.js +99 -0
  25. package/dist/detection/SystemCapabilityDetector.js +1244 -0
  26. package/dist/detection/ToolDetector.js +305 -0
  27. package/dist/detection/WorkloadDetector.js +314 -0
  28. package/dist/di/bindings.js +99 -0
  29. package/dist/di/container.js +88 -0
  30. package/dist/di/types.js +32 -0
  31. package/dist/index.js +52 -0
  32. package/dist/interfaces/IDaemonManager.js +3 -0
  33. package/dist/repositories/config-repository.js +62 -0
  34. package/dist/repositories/gateway-repository.js +35 -0
  35. package/dist/scripts/postinstall.js +101 -0
  36. package/dist/services/AgentStatusManager.js +299 -0
  37. package/dist/services/ConnectivityTester.js +271 -0
  38. package/dist/services/DependencyInstaller.js +475 -0
  39. package/dist/services/LocalAgentManager.js +2216 -0
  40. package/dist/services/application/ApplicationService.js +299 -0
  41. package/dist/services/auth/AuthService.js +214 -0
  42. package/dist/services/aws.js +644 -0
  43. package/dist/services/daemon/DaemonManagerFactory.js +65 -0
  44. package/dist/services/daemon/DockerDaemonManager.js +395 -0
  45. package/dist/services/daemon/LaunchdDaemonManager.js +257 -0
  46. package/dist/services/daemon/PodmanDaemonManager.js +369 -0
  47. package/dist/services/daemon/SystemdDaemonManager.js +221 -0
  48. package/dist/services/daemon/WindowsServiceDaemonManager.js +210 -0
  49. package/dist/services/daemon/index.js +16 -0
  50. package/dist/services/edgible.js +3060 -0
  51. package/dist/services/gateway/GatewayService.js +334 -0
  52. package/dist/state/config.js +146 -0
  53. package/dist/types/AgentConfig.js +5 -0
  54. package/dist/types/AgentStatus.js +5 -0
  55. package/dist/types/ApiClient.js +5 -0
  56. package/dist/types/ApiRequests.js +5 -0
  57. package/dist/types/ApiResponses.js +5 -0
  58. package/dist/types/Application.js +5 -0
  59. package/dist/types/CaddyJson.js +5 -0
  60. package/dist/types/UnifiedAgentStatus.js +56 -0
  61. package/dist/types/WireGuard.js +5 -0
  62. package/dist/types/Workload.js +5 -0
  63. package/dist/types/agent.js +5 -0
  64. package/dist/types/command-options.js +5 -0
  65. package/dist/types/connectivity.js +5 -0
  66. package/dist/types/errors.js +250 -0
  67. package/dist/types/gateway-types.js +5 -0
  68. package/dist/types/index.js +48 -0
  69. package/dist/types/models/ApplicationData.js +5 -0
  70. package/dist/types/models/CertificateData.js +5 -0
  71. package/dist/types/models/DeviceData.js +5 -0
  72. package/dist/types/models/DevicePoolData.js +5 -0
  73. package/dist/types/models/OrganizationData.js +5 -0
  74. package/dist/types/models/OrganizationInviteData.js +5 -0
  75. package/dist/types/models/ProviderConfiguration.js +5 -0
  76. package/dist/types/models/ResourceData.js +5 -0
  77. package/dist/types/models/ServiceResourceData.js +5 -0
  78. package/dist/types/models/UserData.js +5 -0
  79. package/dist/types/route.js +5 -0
  80. package/dist/types/validation/schemas.js +218 -0
  81. package/dist/types/validation.js +5 -0
  82. package/dist/utils/FileIntegrityManager.js +256 -0
  83. package/dist/utils/PathMigration.js +219 -0
  84. package/dist/utils/PathResolver.js +235 -0
  85. package/dist/utils/PlatformDetector.js +277 -0
  86. package/dist/utils/console-logger.js +130 -0
  87. package/dist/utils/docker-compose-parser.js +179 -0
  88. package/dist/utils/errors.js +130 -0
  89. package/dist/utils/health-checker.js +155 -0
  90. package/dist/utils/json-logger.js +72 -0
  91. package/dist/utils/log-formatter.js +293 -0
  92. package/dist/utils/logger.js +59 -0
  93. package/dist/utils/network-utils.js +217 -0
  94. package/dist/utils/output.js +182 -0
  95. package/dist/utils/passwordValidation.js +91 -0
  96. package/dist/utils/progress.js +167 -0
  97. package/dist/utils/sudo-checker.js +22 -0
  98. package/dist/utils/urls.js +32 -0
  99. package/dist/utils/validation.js +31 -0
  100. package/dist/validation/schemas.js +175 -0
  101. package/dist/validation/validator.js +67 -0
  102. package/package.json +83 -0
@@ -0,0 +1,3060 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.EdgibleService = void 0;
40
+ const client_1 = require("../client");
41
+ const passwordValidation_1 = require("../utils/passwordValidation");
42
+ const config_1 = require("../state/config");
43
+ const aws_1 = require("./aws");
44
+ const ConnectivityTester_1 = require("./ConnectivityTester");
45
+ const LocalAgentManager_1 = require("./LocalAgentManager");
46
+ const urls_1 = require("../utils/urls");
47
+ const chalk_1 = __importDefault(require("chalk"));
48
+ const path = __importStar(require("path"));
49
+ const fs = __importStar(require("fs"));
50
+ const PathResolver_1 = require("../utils/PathResolver");
51
+ /**
52
+ * Edgible service interface
53
+ * Uses the real API client for authentication and device management
54
+ */
55
+ class EdgibleService {
56
+ constructor(baseUrl) {
57
+ this.applications = [];
58
+ this.baseUrl = baseUrl || (0, urls_1.getApiBaseUrl)();
59
+ this.apiClient = (0, client_1.createApiClient)(this.baseUrl);
60
+ this.configManager = new config_1.ConfigManager();
61
+ // Restore tokens from config if available (synchronous)
62
+ this.restoreTokensFromConfigSync();
63
+ }
64
+ restoreTokensFromConfigSync() {
65
+ const config = this.configManager.getConfig();
66
+ if (config.accessToken) {
67
+ this.apiClient.setAccessToken(config.accessToken);
68
+ }
69
+ if (config.idToken) {
70
+ this.apiClient.setIdToken(config.idToken);
71
+ }
72
+ if (config.refreshToken) {
73
+ this.apiClient.setRefreshToken(config.refreshToken);
74
+ }
75
+ // Log token restoration status for debugging
76
+ console.log('[EdgibleService] Token restoration:', {
77
+ hasAccessToken: !!config.accessToken,
78
+ hasIdToken: !!config.idToken,
79
+ hasRefreshToken: !!config.refreshToken,
80
+ organizationId: config.organizationId,
81
+ deviceId: config.deviceId
82
+ });
83
+ }
84
+ async attemptAutoRelogin() {
85
+ const config = this.configManager.getConfig();
86
+ // If no tokens but we have device credentials, attempt automatic re-login
87
+ if (!config.idToken && !config.accessToken && config.deviceId && config.devicePassword) {
88
+ console.log('[EdgibleService] No tokens available, attempting automatic device re-login...');
89
+ try {
90
+ await this.loginDevice(config.deviceId, config.devicePassword);
91
+ console.log('[EdgibleService] Automatic re-login successful');
92
+ return true;
93
+ }
94
+ catch (error) {
95
+ console.log('[EdgibleService] Automatic re-login failed:', error instanceof Error ? error.message : String(error));
96
+ return false;
97
+ }
98
+ }
99
+ return false;
100
+ }
101
+ saveTokensToConfig() {
102
+ const accessToken = this.apiClient.getAccessToken();
103
+ const idToken = this.apiClient.getIdToken();
104
+ const refreshToken = this.apiClient.getRefreshToken();
105
+ console.log('[EdgibleService] Saving tokens:', {
106
+ hasAccessToken: !!accessToken,
107
+ hasIdToken: !!idToken,
108
+ hasRefreshToken: !!refreshToken
109
+ });
110
+ this.configManager.updateConfig({
111
+ accessToken,
112
+ idToken,
113
+ refreshToken
114
+ });
115
+ }
116
+ /**
117
+ * Authenticate user with email and password
118
+ * @param email - Email address
119
+ * @param password - Password
120
+ * @returns Promise<LoginResponse> - Login response with tokens
121
+ */
122
+ async loginUser(email, password) {
123
+ try {
124
+ const loginRequest = { email, password };
125
+ const response = await this.apiClient.login(loginRequest);
126
+ // The API client already sets the tokens internally, just save them to config
127
+ this.saveTokensToConfig();
128
+ return response;
129
+ }
130
+ catch (error) {
131
+ console.error('Error logging in user:', error);
132
+ throw error;
133
+ }
134
+ }
135
+ /**
136
+ * Create a device with orphaned organization (device login without user account)
137
+ * @param deviceName - Name of the device
138
+ * @param password - Password for the device (must meet Cognito requirements)
139
+ * @param userEmail - Optional user email for invite
140
+ * @returns Promise<CreateDeviceWithOrphanedOrganizationResponse> - Device creation response
141
+ */
142
+ async createDeviceWithOrganization(deviceName, password, userEmail) {
143
+ try {
144
+ // Validate password meets Cognito requirements
145
+ const passwordValidation = (0, passwordValidation_1.validateCognitoPassword)(password);
146
+ if (!passwordValidation.isValid) {
147
+ throw new Error(`Password validation failed: ${passwordValidation.errors.join(', ')}`);
148
+ }
149
+ const request = {
150
+ name: deviceName,
151
+ description: `CLI device: ${deviceName}`,
152
+ type: 'laptop', // Default type for CLI devices
153
+ organizationName: `${deviceName} Organization`,
154
+ organizationDescription: `Organization created for device ${deviceName}`,
155
+ userEmail: userEmail,
156
+ password: password
157
+ };
158
+ const response = await this.apiClient.createDeviceWithOrphanedOrganization(request);
159
+ // Note: The createDeviceWithOrphanedOrganization response doesn't include tokens
160
+ // Tokens would be obtained through a separate login call with the device credentials
161
+ return response;
162
+ }
163
+ catch (error) {
164
+ console.error('Error creating device with organization:', error);
165
+ throw error;
166
+ }
167
+ }
168
+ /**
169
+ * Check if an account exists for the given email
170
+ * @param email - Email address to check
171
+ * @returns Promise<boolean> - True if account exists
172
+ */
173
+ async checkAccount(email) {
174
+ try {
175
+ // Clear any existing tokens before checking account to ensure a clean login attempt
176
+ // This prevents interference from previous sessions
177
+ this.apiClient.clearTokens();
178
+ // Try to authenticate with a dummy password to check if account exists
179
+ // This is a simple way to check account existence without exposing real authentication
180
+ try {
181
+ await this.apiClient.login({ email, password: 'dummy' });
182
+ // Clear tokens again after check since we used a dummy password
183
+ this.apiClient.clearTokens();
184
+ return true;
185
+ }
186
+ catch (error) {
187
+ // Clear tokens in case any were set during the failed attempt
188
+ this.apiClient.clearTokens();
189
+ // If error is "UserNotFoundException", account doesn't exist
190
+ if (error.message?.includes('UserNotFoundException') || error.message?.includes('NotAuthorizedException')) {
191
+ return false;
192
+ }
193
+ // For other errors, assume account exists but password is wrong
194
+ return true;
195
+ }
196
+ }
197
+ catch (error) {
198
+ // Ensure tokens are cleared even on unexpected errors
199
+ this.apiClient.clearTokens();
200
+ console.error('Error checking account:', error);
201
+ return false;
202
+ }
203
+ }
204
+ /**
205
+ * Authenticate device with device ID and password
206
+ * This method is used when the CLI needs to authenticate as a device.
207
+ * NOTE: This will overwrite user tokens. Use verifyDeviceCredentials() for verification only.
208
+ * @param deviceId - Device ID
209
+ * @param password - Device password
210
+ * @returns Promise<LoginResponse> - Login response with tokens
211
+ */
212
+ async loginDevice(deviceId, password) {
213
+ try {
214
+ const loginRequest = { email: deviceId, password };
215
+ const response = await this.apiClient.login(loginRequest);
216
+ // The API client already sets the tokens internally, just save them to config
217
+ // NOTE: This overwrites user tokens - use this only when CLI needs to act as device
218
+ this.saveTokensToConfig();
219
+ return response;
220
+ }
221
+ catch (error) {
222
+ console.error('Error logging in device:', error);
223
+ throw error;
224
+ }
225
+ }
226
+ /**
227
+ * Verify device credentials without overwriting user tokens
228
+ * This creates a temporary API client instance for verification
229
+ * @param deviceId - Device ID
230
+ * @param password - Device password
231
+ * @returns Promise<boolean> - True if credentials are valid
232
+ */
233
+ async verifyDeviceCredentials(deviceId, password) {
234
+ try {
235
+ // Save current user tokens to restore later
236
+ const currentAccessToken = this.apiClient.getAccessToken();
237
+ const currentIdToken = this.apiClient.getIdToken();
238
+ const currentRefreshToken = this.apiClient.getRefreshToken();
239
+ // Create a temporary API client for verification
240
+ const tempApiClient = (0, client_1.createApiClient)(this.baseUrl);
241
+ const loginRequest = { email: deviceId, password };
242
+ try {
243
+ await tempApiClient.login(loginRequest);
244
+ // Verification successful
245
+ // Restore user tokens
246
+ if (currentAccessToken) {
247
+ this.apiClient.setAccessToken(currentAccessToken);
248
+ }
249
+ if (currentIdToken) {
250
+ this.apiClient.setIdToken(currentIdToken);
251
+ }
252
+ if (currentRefreshToken) {
253
+ this.apiClient.setRefreshToken(currentRefreshToken);
254
+ }
255
+ return true;
256
+ }
257
+ catch (error) {
258
+ // Verification failed, but restore user tokens anyway
259
+ if (currentAccessToken) {
260
+ this.apiClient.setAccessToken(currentAccessToken);
261
+ }
262
+ if (currentIdToken) {
263
+ this.apiClient.setIdToken(currentIdToken);
264
+ }
265
+ if (currentRefreshToken) {
266
+ this.apiClient.setRefreshToken(currentRefreshToken);
267
+ }
268
+ return false;
269
+ }
270
+ }
271
+ catch (error) {
272
+ console.error('Error verifying device credentials:', error);
273
+ return false;
274
+ }
275
+ }
276
+ /**
277
+ * Refresh authentication tokens
278
+ * @param refreshToken - Refresh token
279
+ * @returns Promise<RefreshTokenResponse> - New tokens
280
+ */
281
+ async refreshToken(refreshToken) {
282
+ try {
283
+ const response = await this.apiClient.refreshToken({ refreshToken });
284
+ // Save new tokens to config
285
+ this.saveTokensToConfig();
286
+ return response;
287
+ }
288
+ catch (error) {
289
+ console.error('Error refreshing token:', error);
290
+ throw error;
291
+ }
292
+ }
293
+ /**
294
+ * Get access token
295
+ * @returns string | undefined - Access token
296
+ */
297
+ getAccessToken() {
298
+ return this.apiClient.getAccessToken();
299
+ }
300
+ /**
301
+ * Get ID token
302
+ * @returns string | undefined - ID token
303
+ */
304
+ getIdToken() {
305
+ return this.apiClient.getIdToken();
306
+ }
307
+ /**
308
+ * Get refresh token
309
+ * @returns string | undefined - Refresh token
310
+ */
311
+ getRefreshToken() {
312
+ return this.apiClient.getRefreshToken();
313
+ }
314
+ /**
315
+ * Create a new account
316
+ * @param email - Email address
317
+ * @param name - Full name
318
+ * @returns Promise<boolean> - True if account created successfully
319
+ */
320
+ async createAccount(email, name) {
321
+ try {
322
+ // This would typically redirect to web signup or use a different endpoint
323
+ console.log(`Creating account for: ${email} (${name})`);
324
+ console.log('Account creation should be done through the web interface');
325
+ return true;
326
+ }
327
+ catch (error) {
328
+ console.error('Error creating account:', error);
329
+ return false;
330
+ }
331
+ }
332
+ /**
333
+ * Get account status
334
+ * @param email - Email address
335
+ * @returns Promise<object> - Account status information
336
+ */
337
+ async getAccountStatus(email) {
338
+ try {
339
+ // In a real implementation, this would make an API call
340
+ console.log(`Getting account status for: ${email}`);
341
+ // Simulate API call delay
342
+ await new Promise(resolve => setTimeout(resolve, 1000));
343
+ // For demo purposes, return mock data
344
+ return {
345
+ email,
346
+ status: 'active',
347
+ createdAt: new Date().toISOString(),
348
+ lastLogin: new Date().toISOString()
349
+ };
350
+ }
351
+ catch (error) {
352
+ console.error('Error getting account status:', error);
353
+ throw error;
354
+ }
355
+ }
356
+ /**
357
+ * Set up an application for a workload
358
+ * @param workload - The workload to set up an application for
359
+ * @param port - The port to expose
360
+ * @param protocol - The protocol to use
361
+ * @param description - Optional description
362
+ * @returns Promise<Application> - The created application
363
+ */
364
+ async setupApplication(workload, port, protocol = 'http', description, gatewayId) {
365
+ try {
366
+ // Attempt auto re-login if tokens are missing but credentials are available
367
+ await this.attemptAutoRelogin();
368
+ console.log(`Setting up application for workload: ${workload.name}`);
369
+ // Get organization ID from config
370
+ const configManager = new config_1.ConfigManager();
371
+ const config = configManager.getConfig();
372
+ if (!config.organizationId) {
373
+ throw new Error('No organization ID found. Please login first.');
374
+ }
375
+ // Generate a serving IP (in real implementation, this would come from the API)
376
+ const servingIp = this.generateServingIp();
377
+ const url = `${protocol}://${servingIp}:${port}`;
378
+ // Create application via API
379
+ const deviceIds = [config.deviceId || 'unknown'];
380
+ const createRequest = {
381
+ name: `${workload.name}-app`,
382
+ description: description || `Application for ${workload.name}`,
383
+ organizationId: config.organizationId,
384
+ configuration: {
385
+ port: port,
386
+ protocol: protocol,
387
+ url
388
+ },
389
+ deviceIds: deviceIds,
390
+ gatewayIds: gatewayId ? [gatewayId] : [],
391
+ subtype: 'local-preexisting'
392
+ };
393
+ const response = await this.apiClient.createApplication(createRequest);
394
+ // Validate API response
395
+ if (!response || !response.application) {
396
+ throw new Error('Invalid API response: missing application data');
397
+ }
398
+ if (!response.application.id) {
399
+ throw new Error('Invalid API response: missing application ID');
400
+ }
401
+ // Map API response to CLI Application format
402
+ const application = {
403
+ id: response.application.id,
404
+ name: `${workload.name}-app`,
405
+ workloadId: workload.id,
406
+ servingIp: servingIp,
407
+ port: port,
408
+ protocol: protocol,
409
+ status: 'active',
410
+ createdAt: new Date().toISOString(),
411
+ description: description || `Application for ${workload.name}`
412
+ };
413
+ return application;
414
+ }
415
+ catch (error) {
416
+ console.error('Error setting up application:', error);
417
+ throw error;
418
+ }
419
+ }
420
+ /**
421
+ * Get all applications
422
+ * @returns Promise<Application[]> - List of applications
423
+ */
424
+ async getApplications() {
425
+ try {
426
+ // Attempt auto re-login if tokens are missing but credentials are available
427
+ await this.attemptAutoRelogin();
428
+ console.log('Getting applications...');
429
+ const configManager = new config_1.ConfigManager();
430
+ const config = configManager.getConfig();
431
+ let organizationId;
432
+ // Check if we have an organization ID in the config (device login)
433
+ if (config.organizationId) {
434
+ organizationId = config.organizationId;
435
+ }
436
+ else if (config.email) {
437
+ // User login - get their organizations first
438
+ console.log('Getting user organizations...');
439
+ const userOrgs = await this.apiClient.getUserOrganizations(config.email);
440
+ // Filter for standard (non-billing) organizations only
441
+ const standardOrgs = userOrgs.organizations.filter((org) => org.organizationType !== 'billing');
442
+ if (standardOrgs.length === 0) {
443
+ console.log('No standard organizations found for this user.');
444
+ return [];
445
+ }
446
+ // Use the first standard organization
447
+ organizationId = standardOrgs[0].organizationId;
448
+ console.log(`Using organization: ${standardOrgs[0].name}`);
449
+ }
450
+ else {
451
+ console.log('No organization ID or email found. Please login first.');
452
+ return [];
453
+ }
454
+ // Call the real API to get organization applications
455
+ const response = await this.apiClient.getOrganizationApplications(organizationId);
456
+ // Map API response to CLI Application format
457
+ const applications = response.applications.map(app => ({
458
+ id: app.id,
459
+ name: app.name,
460
+ workloadId: app.deviceIds[0] || 'unknown', // Use first device ID as workload ID
461
+ servingIp: app.url ? new URL(app.url).hostname : 'unknown',
462
+ port: app.url ? parseInt(new URL(app.url).port) || 80 : 80,
463
+ protocol: app.url && app.url.startsWith('https') ? 'https' : 'http',
464
+ status: app.status === 'running' ? 'active' : 'inactive',
465
+ createdAt: app.createdAt,
466
+ description: app.description
467
+ }));
468
+ return applications;
469
+ }
470
+ catch (error) {
471
+ console.error('Error getting applications:', error);
472
+ throw error;
473
+ }
474
+ }
475
+ /**
476
+ * Get application by ID
477
+ * @param id - Application ID
478
+ * @returns Promise<Application | null> - The application or null if not found
479
+ */
480
+ async getApplication(id) {
481
+ try {
482
+ // Try to get from API first
483
+ try {
484
+ const response = await this.apiClient.getApplication(id);
485
+ const appData = response.application;
486
+ // Extract port and protocol from configuration if needed
487
+ const port = appData.configuration?.port || 3000;
488
+ const protocol = (appData.configuration?.protocol || 'http');
489
+ const workloadId = appData.configuration?.workloadId || '';
490
+ return {
491
+ id: appData.id,
492
+ name: appData.name,
493
+ description: appData.description || '',
494
+ servingIp: this.generateServingIp(),
495
+ port: typeof port === 'number' ? port : 3000,
496
+ protocol: protocol,
497
+ status: appData.status === 'running' ? 'active' : 'inactive',
498
+ workloadId: workloadId || undefined,
499
+ createdAt: appData.createdAt || new Date().toISOString(),
500
+ url: appData.url
501
+ };
502
+ }
503
+ catch (apiError) {
504
+ // Fall back to local storage if API fails
505
+ const application = this.applications.find(app => app.id === id);
506
+ if (application) {
507
+ return application;
508
+ }
509
+ throw new Error(`Application not found: ${id}`);
510
+ }
511
+ }
512
+ catch (error) {
513
+ console.error('Error getting application:', error);
514
+ throw error;
515
+ }
516
+ }
517
+ /**
518
+ * Delete an application
519
+ * @param id - Application ID
520
+ * @returns Promise<boolean> - True if deleted successfully
521
+ */
522
+ async deleteApplication(id) {
523
+ try {
524
+ console.log(`Deleting application: ${id}`);
525
+ // Simulate API call delay
526
+ await new Promise(resolve => setTimeout(resolve, 1000));
527
+ const index = this.applications.findIndex(app => app.id === id);
528
+ if (index !== -1) {
529
+ this.applications.splice(index, 1);
530
+ return true;
531
+ }
532
+ return false;
533
+ }
534
+ catch (error) {
535
+ console.error('Error deleting application:', error);
536
+ throw error;
537
+ }
538
+ }
539
+ /**
540
+ * Generate a mock serving IP address
541
+ * In a real implementation, this would be provided by the Edgible service
542
+ */
543
+ generateServingIp() {
544
+ // Generate a mock IP in the 10.x.x.x range
545
+ const octet1 = 10;
546
+ const octet2 = Math.floor(Math.random() * 255);
547
+ const octet3 = Math.floor(Math.random() * 255);
548
+ const octet4 = Math.floor(Math.random() * 254) + 1; // Avoid .0
549
+ return `${octet1}.${octet2}.${octet3}.${octet4}`;
550
+ }
551
+ /**
552
+ * Get user organizations
553
+ * @param email - Email address
554
+ * @returns Promise<GetUserOrganizationsResponse> - User organizations
555
+ */
556
+ async getUserOrganizations(email) {
557
+ try {
558
+ return await this.apiClient.getUserOrganizations(email);
559
+ }
560
+ catch (error) {
561
+ console.error('Error getting user organizations:', error);
562
+ throw error;
563
+ }
564
+ }
565
+ // Gateway Management Methods
566
+ /**
567
+ * Create a gateway device and EC2 instance
568
+ */
569
+ async createGateway(config) {
570
+ try {
571
+ // Attempt auto re-login if tokens are missing but credentials are available
572
+ await this.attemptAutoRelogin();
573
+ console.log(chalk_1.default.blue('Creating gateway...'));
574
+ // Get organization ID
575
+ const configManager = new config_1.ConfigManager();
576
+ const userConfig = configManager.getConfig();
577
+ if (!userConfig.organizationId) {
578
+ throw new Error('No organization ID found. Please login first.');
579
+ }
580
+ // Initialize AWS service
581
+ const awsService = new aws_1.AWSService(config.awsProfile, config.region);
582
+ // Check AWS CLI availability
583
+ const awsCheck = await awsService.checkAWSCLI();
584
+ if (!awsCheck.available) {
585
+ throw new Error('AWS CLI is not installed or not available');
586
+ }
587
+ // Validate AWS credentials
588
+ const credentialsValid = await awsService.validateCredentials(config.awsProfile);
589
+ if (!credentialsValid) {
590
+ throw new Error('AWS credentials are invalid or not configured');
591
+ }
592
+ // Generate unique key pair name
593
+ const keyPairName = `edgible-gateway-${Date.now()}`;
594
+ // Create key pair
595
+ console.log(chalk_1.default.gray('Creating SSH key pair...'));
596
+ const keyPair = await awsService.createKeyPair(keyPairName);
597
+ // Save SSH key locally
598
+ const keyPath = awsService.saveSSHKey(keyPair, keyPairName);
599
+ console.log(chalk_1.default.green(`✓ SSH key saved to: ${keyPath}`));
600
+ // Create EC2 instance
601
+ console.log(chalk_1.default.gray('Creating EC2 instance...'));
602
+ const ec2Instance = await awsService.createEC2Instance({
603
+ name: config.name,
604
+ instanceType: config.instanceType || 't3.micro',
605
+ keyPairName: keyPairName,
606
+ userData: this.generateAgentUserData(userConfig.deviceId || 'unknown')
607
+ });
608
+ console.log(chalk_1.default.green(`✓ EC2 instance created: ${ec2Instance.instanceId}`));
609
+ // Create gateway device via API (regular device with gateway type)
610
+ console.log(chalk_1.default.gray('Creating gateway device...'));
611
+ const deviceRequest = {
612
+ name: config.name,
613
+ description: config.description || `Gateway: ${config.name}`,
614
+ organizationId: userConfig.organizationId,
615
+ type: 'gateway',
616
+ userEmail: userConfig.email || '',
617
+ configuration: {
618
+ awsProfile: config.awsProfile,
619
+ region: config.region || 'us-east-1',
620
+ instanceType: config.instanceType || 't3.micro',
621
+ keyPairName: keyPairName,
622
+ ec2InstanceId: ec2Instance.instanceId,
623
+ publicIp: ec2Instance.publicIp,
624
+ privateIp: ec2Instance.privateIp,
625
+ state: ec2Instance.state
626
+ }
627
+ };
628
+ const response = await this.apiClient.createDevice(deviceRequest);
629
+ // Store gateway info in config
630
+ configManager.addGateway(response.device.id, {
631
+ name: config.name,
632
+ deviceId: response.device.id,
633
+ ec2InstanceId: ec2Instance.instanceId,
634
+ publicIp: ec2Instance.publicIp,
635
+ privateIp: ec2Instance.privateIp,
636
+ keyPairName: keyPairName,
637
+ keyPath: keyPath,
638
+ region: config.region || 'us-east-1'
639
+ });
640
+ // Store AWS profile and region
641
+ if (config.awsProfile) {
642
+ configManager.setAWSProfile(config.awsProfile);
643
+ }
644
+ if (config.region) {
645
+ configManager.setAWSRegion(config.region);
646
+ }
647
+ console.log(chalk_1.default.green('✓ Gateway created successfully!'));
648
+ return {
649
+ message: 'Gateway created successfully',
650
+ gateway: {
651
+ id: response.device.id,
652
+ name: response.device.name,
653
+ description: response.device.description,
654
+ organizationId: response.device.organizationId,
655
+ createdAt: response.device.createdAt
656
+ },
657
+ ec2Instance: {
658
+ instanceId: ec2Instance.instanceId,
659
+ publicIp: ec2Instance.publicIp,
660
+ privateIp: ec2Instance.privateIp,
661
+ state: ec2Instance.state,
662
+ keyPairName: keyPairName,
663
+ region: config.region || 'us-east-1'
664
+ },
665
+ sshKey: {
666
+ privateKey: keyPair.privateKey,
667
+ publicKey: keyPair.publicKey,
668
+ keyName: keyPairName
669
+ }
670
+ };
671
+ }
672
+ catch (error) {
673
+ console.error(chalk_1.default.red('Error creating gateway:'), error);
674
+ throw error;
675
+ }
676
+ }
677
+ /**
678
+ * Create a serving device in the organization
679
+ */
680
+ async createServingDevice(config) {
681
+ try {
682
+ // Attempt auto re-login if tokens are missing but credentials are available
683
+ await this.attemptAutoRelogin();
684
+ const userConfig = this.configManager.getConfig();
685
+ if (!userConfig.organizationId) {
686
+ throw new Error('No organization ID found. Please login first.');
687
+ }
688
+ // Create device via API - password will be generated by the API
689
+ const deviceRequest = {
690
+ name: config.name,
691
+ description: config.description || `Serving device: ${config.name}`,
692
+ organizationId: userConfig.organizationId,
693
+ type: 'server',
694
+ userEmail: userConfig.email || ''
695
+ };
696
+ const response = await this.apiClient.createDevice(deviceRequest);
697
+ return {
698
+ device: {
699
+ id: response.device.id,
700
+ name: response.device.name,
701
+ description: response.device.description,
702
+ type: response.device.type,
703
+ organizationId: response.device.organizationId,
704
+ createdAt: response.device.createdAt,
705
+ password: response.device.password // Include password from API response
706
+ }
707
+ };
708
+ }
709
+ catch (error) {
710
+ console.error(chalk_1.default.red('Error creating serving device:'), error);
711
+ throw error;
712
+ }
713
+ }
714
+ /**
715
+ * List all serving devices
716
+ */
717
+ async listServingDevices() {
718
+ try {
719
+ // Attempt auto re-login if tokens are missing but credentials are available
720
+ await this.attemptAutoRelogin();
721
+ const configManager = new config_1.ConfigManager();
722
+ const userConfig = configManager.getConfig();
723
+ if (!userConfig.organizationId) {
724
+ throw new Error('No organization ID found. Please login first.');
725
+ }
726
+ // Get all devices from the organization
727
+ const devicesResponse = await this.apiClient.getOrganizationDevices(userConfig.organizationId);
728
+ // Filter for serving devices (devices with type 'server')
729
+ const servingDevices = devicesResponse.devices.filter((device) => device.type === 'server');
730
+ // Map serving devices
731
+ const devices = servingDevices.map((device) => ({
732
+ id: device.id,
733
+ name: device.name,
734
+ description: device.description,
735
+ type: device.type,
736
+ organizationId: device.organizationId,
737
+ createdAt: device.createdAt
738
+ }));
739
+ return {
740
+ devices
741
+ };
742
+ }
743
+ catch (error) {
744
+ console.error(chalk_1.default.red('Error listing serving devices:'), error);
745
+ throw error;
746
+ }
747
+ }
748
+ /**
749
+ * Get a specific device by ID
750
+ */
751
+ async getDevice(deviceId) {
752
+ try {
753
+ // Attempt auto re-login if tokens are missing but credentials are available
754
+ await this.attemptAutoRelogin();
755
+ // Call the API to get device details
756
+ const deviceResponse = await this.apiClient.getDevice(deviceId);
757
+ return deviceResponse;
758
+ }
759
+ catch (error) {
760
+ console.error(chalk_1.default.red('Error fetching device:'), error);
761
+ throw error;
762
+ }
763
+ }
764
+ /**
765
+ * List all gateways
766
+ */
767
+ async listGateways() {
768
+ try {
769
+ // Attempt auto re-login if tokens are missing but credentials are available
770
+ await this.attemptAutoRelogin();
771
+ const configManager = new config_1.ConfigManager();
772
+ const userConfig = configManager.getConfig();
773
+ if (!userConfig.organizationId) {
774
+ throw new Error('No organization ID found. Please login first.');
775
+ }
776
+ // Get all devices from the organization
777
+ const devicesResponse = await this.apiClient.getOrganizationDevices(userConfig.organizationId);
778
+ // Filter for gateway devices
779
+ const gatewayDevices = devicesResponse.devices.filter((device) => device.type === 'gateway');
780
+ // Enrich with local gateway info
781
+ const gateways = gatewayDevices.map((device) => {
782
+ const localInfo = configManager.getGateway(device.id);
783
+ return {
784
+ device: {
785
+ id: device.id,
786
+ name: device.name,
787
+ description: device.description,
788
+ type: device.type,
789
+ organizationId: device.organizationId,
790
+ createdAt: device.createdAt
791
+ },
792
+ ec2Instance: localInfo ? {
793
+ instanceId: localInfo.ec2InstanceId,
794
+ publicIp: localInfo.publicIp,
795
+ privateIp: localInfo.privateIp,
796
+ state: 'running', // Assume running if in config
797
+ region: localInfo.region
798
+ } : undefined,
799
+ applicationsCount: 0 // TODO: Calculate from local applications
800
+ };
801
+ });
802
+ return {
803
+ gateways
804
+ };
805
+ }
806
+ catch (error) {
807
+ console.error(chalk_1.default.red('Error listing gateways:'), error);
808
+ throw error;
809
+ }
810
+ }
811
+ /**
812
+ * Delete a gateway
813
+ */
814
+ async deleteGateway(gatewayId, force = false) {
815
+ try {
816
+ // Attempt auto re-login if tokens are missing but credentials are available
817
+ await this.attemptAutoRelogin();
818
+ const configManager = new config_1.ConfigManager();
819
+ const gatewayInfo = configManager.getGateway(gatewayId);
820
+ if (!gatewayInfo) {
821
+ throw new Error('Gateway not found in local configuration');
822
+ }
823
+ // Check if gateway has active applications (unless forced)
824
+ if (!force) {
825
+ const applications = await this.getGatewayApplications(gatewayId);
826
+ if (applications.length > 0) {
827
+ throw new Error(`Gateway has ${applications.length} active applications. Use --force to delete anyway.`);
828
+ }
829
+ }
830
+ // Terminate EC2 instance
831
+ const awsService = new aws_1.AWSService(configManager.getAWSProfile(), gatewayInfo.region);
832
+ await awsService.terminateInstance(gatewayInfo.ec2InstanceId);
833
+ console.log(chalk_1.default.green(`✓ EC2 instance terminated: ${gatewayInfo.ec2InstanceId}`));
834
+ // Delete key pair
835
+ await awsService.deleteKeyPair(gatewayInfo.keyPairName);
836
+ console.log(chalk_1.default.green(`✓ Key pair deleted: ${gatewayInfo.keyPairName}`));
837
+ // Delete gateway device via API
838
+ const response = await this.apiClient.deleteDevice(gatewayId);
839
+ // Remove from local config
840
+ configManager.removeGateway(gatewayId);
841
+ console.log(chalk_1.default.green('✓ Gateway deleted successfully!'));
842
+ return {
843
+ success: true,
844
+ message: 'Gateway deleted successfully'
845
+ };
846
+ }
847
+ catch (error) {
848
+ console.error(chalk_1.default.red('Error deleting gateway:'), error);
849
+ throw error;
850
+ }
851
+ }
852
+ /**
853
+ * Resync agent on gateway
854
+ */
855
+ // Managed Gateway Methods (admin/debug only)
856
+ /**
857
+ * Create a managed gateway (admin only)
858
+ */
859
+ async createManagedGateway(config) {
860
+ try {
861
+ await this.attemptAutoRelogin();
862
+ console.log(chalk_1.default.blue('Creating managed gateway...'));
863
+ const response = await this.apiClient.createManagedGateway({
864
+ region: config.region || 'us-east-1',
865
+ instanceType: config.instanceType || 't3.micro',
866
+ name: config.name
867
+ });
868
+ console.log(chalk_1.default.green('✓ Managed gateway created successfully!'));
869
+ return response;
870
+ }
871
+ catch (error) {
872
+ console.error(chalk_1.default.red('Error creating managed gateway:'), error);
873
+ throw error;
874
+ }
875
+ }
876
+ /**
877
+ * List all managed gateways (admin only)
878
+ */
879
+ async listManagedGateways() {
880
+ try {
881
+ await this.attemptAutoRelogin();
882
+ const response = await this.apiClient.listManagedGateways();
883
+ return response;
884
+ }
885
+ catch (error) {
886
+ console.error(chalk_1.default.red('Error listing managed gateways:'), error);
887
+ throw error;
888
+ }
889
+ }
890
+ /**
891
+ * Get managed gateway details (admin only)
892
+ */
893
+ async getManagedGateway(gatewayId) {
894
+ try {
895
+ await this.attemptAutoRelogin();
896
+ const response = await this.apiClient.getManagedGateway({ gatewayId });
897
+ return response;
898
+ }
899
+ catch (error) {
900
+ console.error(chalk_1.default.red('Error getting managed gateway:'), error);
901
+ throw error;
902
+ }
903
+ }
904
+ /**
905
+ * Delete a managed gateway (admin only)
906
+ */
907
+ async deleteManagedGateway(gatewayId, force = false) {
908
+ try {
909
+ await this.attemptAutoRelogin();
910
+ await this.apiClient.deleteManagedGateway({
911
+ gatewayId,
912
+ force
913
+ });
914
+ console.log(chalk_1.default.green('✓ Managed gateway deleted successfully!'));
915
+ return { success: true };
916
+ }
917
+ catch (error) {
918
+ console.error(chalk_1.default.red('Error deleting managed gateway:'), error);
919
+ throw error;
920
+ }
921
+ }
922
+ /**
923
+ * Resync agent on a managed gateway (admin only)
924
+ */
925
+ async resyncManagedGatewayAgent(gatewayId, agentVersion, installFromLocal, reboot, wipeLogs, debug) {
926
+ try {
927
+ await this.attemptAutoRelogin();
928
+ // Get managed gateway details to get SSH info
929
+ const gatewayDetails = await this.getManagedGateway(gatewayId);
930
+ if (!gatewayDetails.success || !gatewayDetails.gateway) {
931
+ throw new Error('Managed gateway not found');
932
+ }
933
+ const gateway = gatewayDetails.gateway;
934
+ const ipAddress = gateway.ipAddress;
935
+ if (!ipAddress) {
936
+ throw new Error('Gateway IP address not available');
937
+ }
938
+ // Get SSH key from backend
939
+ console.log(chalk_1.default.gray('Fetching SSH key from backend...'));
940
+ const sshKeyResponse = await this.apiClient.getManagedGatewaySSHKey({ gatewayId });
941
+ if (!sshKeyResponse.success || !sshKeyResponse.sshPrivateKey) {
942
+ throw new Error('Failed to retrieve SSH key for managed gateway');
943
+ }
944
+ const sshPrivateKey = sshKeyResponse.sshPrivateKey;
945
+ // Use AWSService to handle SSH operations
946
+ const { AWSService } = await Promise.resolve().then(() => __importStar(require('./aws')));
947
+ const awsService = new AWSService();
948
+ // Create temporary SSH key file
949
+ const os = require('os');
950
+ const path = require('path');
951
+ const fs = require('fs');
952
+ const tempKeyPath = path.join(os.tmpdir(), `edgible-mgw-${gatewayId}-${Date.now()}.pem`);
953
+ try {
954
+ // Write SSH key to temp file
955
+ fs.writeFileSync(tempKeyPath, sshPrivateKey, { mode: 0o600 });
956
+ // Create SSH connection
957
+ const connection = {
958
+ host: ipAddress,
959
+ port: 22,
960
+ username: 'ec2-user',
961
+ privateKey: sshPrivateKey
962
+ };
963
+ // Wait for instance to be ready for SSH
964
+ console.log(chalk_1.default.gray('Waiting for instance to be ready for SSH...'));
965
+ // Note: For managed gateways, we may not have the EC2 instance ID readily available
966
+ // We'll try to connect directly
967
+ await new Promise(resolve => setTimeout(resolve, 2000)); // Brief wait
968
+ // Test SSH connection
969
+ console.log(chalk_1.default.gray('Testing SSH connection...'));
970
+ try {
971
+ await awsService.executeSSHCommand(connection, 'echo "SSH connection successful"');
972
+ console.log(chalk_1.default.green('✓ SSH connection established'));
973
+ }
974
+ catch (sshError) {
975
+ console.error(chalk_1.default.red('✗ SSH connection failed:'), sshError);
976
+ throw new Error(`SSH connection failed: ${sshError instanceof Error ? sshError.message : 'Unknown error'}`);
977
+ }
978
+ // Wipe logs if requested (before stopping service)
979
+ let logsWiped = false;
980
+ if (wipeLogs) {
981
+ console.log(chalk_1.default.gray('Wiping agent logs...'));
982
+ try {
983
+ const wipeResult = await this.wipeManagedGatewayLogs(gatewayId);
984
+ if (wipeResult.success) {
985
+ console.log(chalk_1.default.green('✓ Logs wiped successfully'));
986
+ logsWiped = true;
987
+ }
988
+ else {
989
+ console.log(chalk_1.default.yellow('⚠ Failed to wipe logs, continuing with resync'));
990
+ }
991
+ }
992
+ catch (wipeError) {
993
+ console.log(chalk_1.default.yellow(`⚠ Error wiping logs: ${wipeError instanceof Error ? wipeError.message : String(wipeError)}, continuing with resync`));
994
+ }
995
+ }
996
+ // Use the same resync logic as regular gateways
997
+ // Stop agent service
998
+ console.log(chalk_1.default.gray('Stopping agent service...'));
999
+ await awsService.executeSSHCommand(connection, 'sudo systemctl stop edgible-agent');
1000
+ // Ensure the agent directory exists
1001
+ console.log(chalk_1.default.gray('Creating agent directory...'));
1002
+ await awsService.executeSSHCommand(connection, 'sudo mkdir -p /opt/edgible-agent');
1003
+ await awsService.executeSSHCommand(connection, 'sudo chown root:root /opt/edgible-agent');
1004
+ // Check Node.js installation and path
1005
+ console.log(chalk_1.default.gray('Checking Node.js installation...'));
1006
+ const nodeCheck = await awsService.executeSSHCommand(connection, 'which node || echo "node not found"');
1007
+ const nodePath = nodeCheck.stdout.trim();
1008
+ console.log(chalk_1.default.gray(`Node.js path: ${nodePath}`));
1009
+ if (nodePath === 'node not found') {
1010
+ console.log(chalk_1.default.gray('Installing Node.js...'));
1011
+ await awsService.executeSSHCommand(connection, 'curl -fsSL https://rpm.nodesource.com/setup_18.x | sudo bash -');
1012
+ await awsService.executeSSHCommand(connection, 'sudo yum install -y nodejs');
1013
+ }
1014
+ // Check and install required system dependencies
1015
+ console.log(chalk_1.default.gray('Checking system dependencies...'));
1016
+ // Check iptables
1017
+ const iptablesCheck = await awsService.executeSSHCommand(connection, 'which iptables || echo "iptables not found"');
1018
+ if (iptablesCheck.stdout.trim() === 'iptables not found') {
1019
+ console.log(chalk_1.default.gray('Installing iptables...'));
1020
+ await awsService.executeSSHCommand(connection, 'sudo yum install -y iptables');
1021
+ }
1022
+ else {
1023
+ console.log(chalk_1.default.gray('✓ iptables is installed'));
1024
+ }
1025
+ // Check WireGuard
1026
+ const wireguardCheck = await awsService.executeSSHCommand(connection, 'which wg || echo "wireguard not found"');
1027
+ if (wireguardCheck.stdout.trim() === 'wireguard not found') {
1028
+ console.log(chalk_1.default.gray('Installing WireGuard...'));
1029
+ await awsService.executeSSHCommand(connection, 'sudo amazon-linux-extras install epel -y || sudo yum install -y epel-release');
1030
+ await awsService.executeSSHCommand(connection, 'sudo yum install -y wireguard-tools');
1031
+ await awsService.executeSSHCommand(connection, 'sudo modprobe wireguard || true');
1032
+ }
1033
+ else {
1034
+ console.log(chalk_1.default.gray('✓ WireGuard is installed'));
1035
+ }
1036
+ // Check HAProxy (gateway devices use HAProxy instead of Caddy)
1037
+ const haproxyCheck = await awsService.executeSSHCommand(connection, 'which haproxy 2>/dev/null || test -f /usr/sbin/haproxy && echo "haproxy found" || echo "haproxy not found"');
1038
+ if (haproxyCheck.stdout.trim() === 'haproxy not found' || (haproxyCheck.stdout.trim() === '' && !haproxyCheck.stdout.includes('haproxy found'))) {
1039
+ console.log(chalk_1.default.gray('Installing HAProxy...'));
1040
+ // Install HAProxy via package manager
1041
+ await awsService.executeSSHCommand(connection, 'sudo yum install -y haproxy');
1042
+ // Create HAProxy config directory
1043
+ await awsService.executeSSHCommand(connection, 'sudo mkdir -p /etc/haproxy');
1044
+ // Create HAProxy runtime socket directory
1045
+ await awsService.executeSSHCommand(connection, 'sudo mkdir -p /var/run/haproxy');
1046
+ // Set capabilities for HAProxy to bind to privileged ports
1047
+ await awsService.executeSSHCommand(connection, 'sudo setcap cap_net_bind_service=+ep /usr/sbin/haproxy || true');
1048
+ // Create HAProxy chroot directory
1049
+ await awsService.executeSSHCommand(connection, 'sudo mkdir -p /var/lib/haproxy');
1050
+ await awsService.executeSSHCommand(connection, 'sudo chown haproxy:haproxy /var/lib/haproxy || sudo chown root:root /var/lib/haproxy || true');
1051
+ // Verify installation
1052
+ const verifyResult = await awsService.executeSSHCommand(connection, '/usr/sbin/haproxy -v 2>&1');
1053
+ if (!verifyResult.stdout.includes('HAProxy') && !verifyResult.stdout.includes('version')) {
1054
+ throw new Error(`HAProxy installation verification failed. Output: ${verifyResult.stdout}`);
1055
+ }
1056
+ console.log(chalk_1.default.green('✓ HAProxy installed successfully'));
1057
+ }
1058
+ else {
1059
+ console.log(chalk_1.default.gray('✓ HAProxy is installed'));
1060
+ // Verify it's accessible
1061
+ const verifyResult = await awsService.executeSSHCommand(connection, 'haproxy -v 2>&1 || /usr/sbin/haproxy -v 2>&1');
1062
+ if (verifyResult.stdout.includes('command not found') && !verifyResult.stdout.includes('HAProxy') && !verifyResult.stdout.includes('version')) {
1063
+ console.log(chalk_1.default.yellow('⚠ HAProxy found but not accessible, verifying...'));
1064
+ // Re-verify
1065
+ const reVerifyResult = await awsService.executeSSHCommand(connection, 'haproxy -v 2>&1 || /usr/sbin/haproxy -v 2>&1');
1066
+ if (!reVerifyResult.stdout.includes('HAProxy') && !reVerifyResult.stdout.includes('version')) {
1067
+ console.log(chalk_1.default.yellow('⚠ HAProxy verification still failing, but continuing...'));
1068
+ }
1069
+ }
1070
+ }
1071
+ // Get device password from API
1072
+ console.log(chalk_1.default.gray('Retrieving device credentials...'));
1073
+ const deviceResponse = await this.apiClient.getDevice(gatewayId);
1074
+ const devicePassword = deviceResponse.device?.password || '';
1075
+ if (!devicePassword) {
1076
+ throw new Error('Could not retrieve device password from API');
1077
+ }
1078
+ // Create agent configuration file
1079
+ console.log(chalk_1.default.gray('Creating agent configuration...'));
1080
+ const configManager = new config_1.ConfigManager();
1081
+ const userConfig = configManager.getConfig();
1082
+ const agentConfig = {
1083
+ deviceId: gatewayId,
1084
+ devicePassword: devicePassword,
1085
+ deviceType: 'gateway',
1086
+ apiBaseUrl: (0, urls_1.getApiBaseUrl)(),
1087
+ organizationId: gateway.organizationId || '',
1088
+ firewallEnabled: true,
1089
+ pollingInterval: 60000,
1090
+ healthCheckTimeout: 5000,
1091
+ maxRetries: 3,
1092
+ logLevel: debug ? 'debug' : 'info',
1093
+ updateEnabled: true,
1094
+ updateCheckInterval: 3600000,
1095
+ wireguardMode: userConfig.wireguardMode || 'kernel',
1096
+ wireguardGoBinary: userConfig.wireguardGoBinary || 'wireguard-go'
1097
+ };
1098
+ const configFile = `/tmp/agent-config-${Date.now()}.json`;
1099
+ fs.writeFileSync(configFile, JSON.stringify(agentConfig, null, 2));
1100
+ try {
1101
+ console.log(chalk_1.default.gray('Uploading agent configuration...'));
1102
+ // Upload to user's home directory first (where we have write permissions)
1103
+ // Then move to /opt/edgible-agent with sudo
1104
+ const homeDir = connection.username === 'ec2-user' ? '/home/ec2-user' : `/home/${connection.username}`;
1105
+ const remoteHomeConfigPath = `${homeDir}/agent-config-${Date.now()}.json`;
1106
+ const remoteConfigPath = '/opt/edgible-agent/agent.config.json';
1107
+ console.log(chalk_1.default.gray(` Local file: ${configFile}`));
1108
+ console.log(chalk_1.default.gray(` Uploading to: ${remoteHomeConfigPath}`));
1109
+ try {
1110
+ await awsService.uploadFile(connection, configFile, remoteHomeConfigPath);
1111
+ console.log(chalk_1.default.green(' ✓ Config file uploaded successfully'));
1112
+ // Move to final location with sudo and set ownership
1113
+ console.log(chalk_1.default.gray(` Moving to: ${remoteConfigPath}`));
1114
+ const moveResult = await awsService.executeSSHCommand(connection, `sudo mv ${remoteHomeConfigPath} ${remoteConfigPath} && sudo chown root:root ${remoteConfigPath} && echo "OK" || echo "FAIL"`);
1115
+ if (moveResult.stdout.trim() === 'OK') {
1116
+ console.log(chalk_1.default.green(' ✓ Config file moved and ownership set'));
1117
+ }
1118
+ else {
1119
+ throw new Error(`Failed to move config file: ${moveResult.stderr || moveResult.stdout}`);
1120
+ }
1121
+ }
1122
+ catch (configError) {
1123
+ console.error(chalk_1.default.red(` ✗ Config upload failed: ${configError instanceof Error ? configError.message : String(configError)}`));
1124
+ throw new Error(`Failed to upload agent configuration: ${configError instanceof Error ? configError.message : String(configError)}`);
1125
+ }
1126
+ }
1127
+ finally {
1128
+ if (fs.existsSync(configFile)) {
1129
+ fs.unlinkSync(configFile);
1130
+ }
1131
+ }
1132
+ // Create systemd service file
1133
+ console.log(chalk_1.default.gray('Creating systemd service...'));
1134
+ const finalNodePath = nodePath === 'node not found' ? '/usr/bin/node' : nodePath;
1135
+ const systemAgentPath = PathResolver_1.PathResolver.getSystemDataPath() + '/agent';
1136
+ const serviceContent = `[Unit]
1137
+ Description=Edgible Agent
1138
+ After=network.target
1139
+
1140
+ [Service]
1141
+ Type=simple
1142
+ User=root
1143
+ WorkingDirectory=/opt/edgible-agent
1144
+ ExecStart=${finalNodePath} /opt/edgible-agent/index.js start -c /opt/edgible-agent/agent.config.json
1145
+ Restart=always
1146
+ RestartSec=10
1147
+ Environment=NODE_ENV=production
1148
+ Environment=EDGIBLE_CONFIG_PATH=${systemAgentPath}
1149
+ Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
1150
+
1151
+ [Install]
1152
+ WantedBy=multi-user.target`;
1153
+ const serviceFile = `/tmp/edgible-agent-${Date.now()}.service`;
1154
+ fs.writeFileSync(serviceFile, serviceContent);
1155
+ try {
1156
+ await awsService.uploadFile(connection, serviceFile, '/tmp/edgible-agent.service');
1157
+ await awsService.executeSSHCommand(connection, 'sudo mv /tmp/edgible-agent.service /etc/systemd/system/edgible-agent.service');
1158
+ await awsService.executeSSHCommand(connection, 'sudo systemctl daemon-reload');
1159
+ await awsService.executeSSHCommand(connection, 'sudo systemctl enable edgible-agent.service');
1160
+ }
1161
+ finally {
1162
+ if (fs.existsSync(serviceFile)) {
1163
+ fs.unlinkSync(serviceFile);
1164
+ }
1165
+ }
1166
+ // Upload or download agent code
1167
+ if (installFromLocal) {
1168
+ // Use existing local upload behavior
1169
+ // Gateway resync always uses gateway build
1170
+ console.log(chalk_1.default.gray('Uploading agent code from local directory...'));
1171
+ const agentV2Path = path.resolve(__dirname, '../../../agent-v2');
1172
+ const agentDistPath = path.join(agentV2Path, 'dist');
1173
+ const agentPackageJsonPath = path.join(agentV2Path, 'package.json');
1174
+ // Ensure gateway build exists (gateway resync always needs gateway code)
1175
+ const { execSync } = require('child_process');
1176
+ const gatewayCodePath = path.join(agentDistPath, 'gateway');
1177
+ const hasGatewayCode = fs.existsSync(gatewayCodePath);
1178
+ if (!fs.existsSync(agentDistPath) || !hasGatewayCode) {
1179
+ console.log(chalk_1.default.yellow('⚠ Building gateway agent (required for gateway resync)...'));
1180
+ try {
1181
+ execSync('npm run build:gateway', {
1182
+ cwd: agentV2Path,
1183
+ stdio: 'inherit'
1184
+ });
1185
+ console.log(chalk_1.default.green('✓ Gateway build completed'));
1186
+ }
1187
+ catch (buildError) {
1188
+ throw new Error(`Failed to build gateway agent: ${buildError instanceof Error ? buildError.message : String(buildError)}\n\n` +
1189
+ `Please build manually:\n` +
1190
+ ` cd agent-v2\n` +
1191
+ ` npm run build:gateway`);
1192
+ }
1193
+ }
1194
+ if (!fs.existsSync(agentDistPath)) {
1195
+ throw new Error('Agent distribution not found. This feature requires development environment setup. Please use S3 download instead.');
1196
+ }
1197
+ if (!fs.existsSync(agentPackageJsonPath)) {
1198
+ throw new Error('Agent package.json not found.');
1199
+ }
1200
+ // Create a tarball of the dist directory and package.json
1201
+ const tarPath = `/tmp/agent-dist-${Date.now()}.tar.gz`;
1202
+ try {
1203
+ console.log(chalk_1.default.gray('Creating agent distribution tarball...'));
1204
+ console.log(chalk_1.default.gray(` Source dist: ${agentDistPath}`));
1205
+ console.log(chalk_1.default.gray(` Source package.json: ${agentPackageJsonPath}`));
1206
+ // Create tarball with dist contents at root level and package.json
1207
+ const tempDir = `/tmp/agent-upload-${Date.now()}`;
1208
+ console.log(chalk_1.default.gray(` Creating temp directory: ${tempDir}`));
1209
+ fs.mkdirSync(tempDir, { recursive: true });
1210
+ fs.copyFileSync(agentPackageJsonPath, path.join(tempDir, 'package.json'));
1211
+ // Copy contents of dist/ directly to tempDir root (so index.js ends up at /opt/edgible-agent/index.js)
1212
+ // Copy all files from dist including hidden files, but exclude . and ..
1213
+ execSync(`sh -c 'cd "${agentDistPath}" && find . -mindepth 1 -maxdepth 1 -exec cp -r {} "${tempDir}/" \\;'`, { stdio: 'inherit' });
1214
+ console.log(chalk_1.default.gray(` Creating tarball: ${tarPath}`));
1215
+ execSync(`cd "${tempDir}" && tar -czf "${tarPath}" .`, { stdio: 'inherit' });
1216
+ // Verify tarball was created
1217
+ if (!fs.existsSync(tarPath)) {
1218
+ throw new Error(`Failed to create tarball at ${tarPath}`);
1219
+ }
1220
+ const tarStats = fs.statSync(tarPath);
1221
+ const tarSizeMB = (tarStats.size / (1024 * 1024)).toFixed(2);
1222
+ console.log(chalk_1.default.gray(` Tarball created: ${tarSizeMB} MB`));
1223
+ // Clean up temp directory
1224
+ console.log(chalk_1.default.gray(' Cleaning up temp directory...'));
1225
+ fs.rmSync(tempDir, { recursive: true, force: true });
1226
+ // Upload tarball to user's home directory first (where we have write permissions)
1227
+ // Then move to /tmp with sudo if needed
1228
+ // Use explicit home directory path since SFTP might not expand ~
1229
+ const homeDir = connection.username === 'ec2-user' ? '/home/ec2-user' : `/home/${connection.username}`;
1230
+ const remoteHomePath = `${homeDir}/agent-dist-${Date.now()}.tar.gz`;
1231
+ const remoteTmpPath = `/tmp/agent-dist.tar.gz`;
1232
+ console.log(chalk_1.default.gray('Checking remote directory permissions...'));
1233
+ try {
1234
+ const homeDirCheck = await awsService.executeSSHCommand(connection, `test -d ${homeDir} && test -w ${homeDir} && echo "OK" || echo "FAIL"`);
1235
+ if (homeDirCheck.stdout.trim() !== 'OK') {
1236
+ console.log(chalk_1.default.yellow(` Warning: Home directory ${homeDir} may not be writable`));
1237
+ console.log(chalk_1.default.gray(` Attempting to verify with ls -ld: ${homeDir}`));
1238
+ const lsResult = await awsService.executeSSHCommand(connection, `ls -ld ${homeDir} 2>&1 || echo "directory_not_found"`);
1239
+ console.log(chalk_1.default.gray(` Directory info: ${lsResult.stdout.trim()}`));
1240
+ }
1241
+ else {
1242
+ console.log(chalk_1.default.gray(` ✓ Home directory ${homeDir} is writable`));
1243
+ }
1244
+ }
1245
+ catch (checkError) {
1246
+ console.log(chalk_1.default.yellow(` Warning: Could not verify directory permissions: ${checkError instanceof Error ? checkError.message : String(checkError)}`));
1247
+ }
1248
+ console.log(chalk_1.default.blue('Uploading tarball to remote server...'));
1249
+ console.log(chalk_1.default.gray(` Local file: ${tarPath}`));
1250
+ console.log(chalk_1.default.gray(` Remote path: ${remoteHomePath}`));
1251
+ console.log(chalk_1.default.gray(` File size: ${tarSizeMB} MB`));
1252
+ try {
1253
+ await awsService.uploadFile(connection, tarPath, remoteHomePath);
1254
+ console.log(chalk_1.default.green(' ✓ Upload completed successfully'));
1255
+ // Verify file was uploaded
1256
+ console.log(chalk_1.default.gray(' Verifying uploaded file...'));
1257
+ const verifyResult = await awsService.executeSSHCommand(connection, `test -f ${remoteHomePath} && ls -lh ${remoteHomePath} || echo "file_not_found"`);
1258
+ if (verifyResult.stdout.includes('file_not_found')) {
1259
+ throw new Error(`Upload verification failed: file not found at ${remoteHomePath}`);
1260
+ }
1261
+ console.log(chalk_1.default.gray(` File verified: ${verifyResult.stdout.trim()}`));
1262
+ }
1263
+ catch (uploadError) {
1264
+ console.error(chalk_1.default.red(` ✗ Upload failed: ${uploadError instanceof Error ? uploadError.message : String(uploadError)}`));
1265
+ throw new Error(`Failed to upload file to ${remoteHomePath}: ${uploadError instanceof Error ? uploadError.message : String(uploadError)}`);
1266
+ }
1267
+ // Move to /tmp using sudo (in case /tmp has permission restrictions)
1268
+ console.log(chalk_1.default.gray(`Moving file from ${remoteHomePath} to ${remoteTmpPath}...`));
1269
+ try {
1270
+ const moveResult = await awsService.executeSSHCommand(connection, `sudo mv ${remoteHomePath} ${remoteTmpPath} && sudo chmod 644 ${remoteTmpPath} && echo "OK" || echo "FAIL"`);
1271
+ if (moveResult.stdout.trim() === 'OK') {
1272
+ console.log(chalk_1.default.green(' ✓ File moved successfully'));
1273
+ }
1274
+ else {
1275
+ throw new Error(`Failed to move file: ${moveResult.stderr || moveResult.stdout}`);
1276
+ }
1277
+ }
1278
+ catch (moveError) {
1279
+ console.error(chalk_1.default.red(` ✗ Move failed: ${moveError instanceof Error ? moveError.message : String(moveError)}`));
1280
+ throw new Error(`Failed to move file from ${remoteHomePath} to ${remoteTmpPath}: ${moveError instanceof Error ? moveError.message : String(moveError)}`);
1281
+ }
1282
+ // Extract on remote server
1283
+ console.log(chalk_1.default.gray('Extracting agent code...'));
1284
+ try {
1285
+ const extractResult = await awsService.executeSSHCommand(connection, 'cd /opt/edgible-agent && sudo tar -xzf /tmp/agent-dist.tar.gz && sudo rm /tmp/agent-dist.tar.gz && echo "OK" || echo "FAIL"');
1286
+ if (extractResult.stdout.trim() === 'OK') {
1287
+ console.log(chalk_1.default.green(' ✓ Extraction completed successfully'));
1288
+ }
1289
+ else {
1290
+ console.log(chalk_1.default.yellow(` Warning: Extraction may have failed: ${extractResult.stderr || extractResult.stdout}`));
1291
+ }
1292
+ }
1293
+ catch (extractError) {
1294
+ console.error(chalk_1.default.red(` ✗ Extraction failed: ${extractError instanceof Error ? extractError.message : String(extractError)}`));
1295
+ throw new Error(`Failed to extract agent code: ${extractError instanceof Error ? extractError.message : String(extractError)}`);
1296
+ }
1297
+ // Install production dependencies (run as root since directory is owned by root)
1298
+ console.log(chalk_1.default.gray('Installing agent dependencies...'));
1299
+ try {
1300
+ const installResult = await awsService.executeSSHCommand(connection, 'cd /opt/edgible-agent && sudo npm install --production --no-save');
1301
+ if (installResult.exitCode === 0) {
1302
+ console.log(chalk_1.default.green(' ✓ Dependencies installed successfully'));
1303
+ }
1304
+ else {
1305
+ console.log(chalk_1.default.yellow(` Warning: npm install exited with code ${installResult.exitCode}`));
1306
+ console.log(chalk_1.default.gray(` stdout: ${installResult.stdout}`));
1307
+ if (installResult.stderr) {
1308
+ console.log(chalk_1.default.gray(` stderr: ${installResult.stderr}`));
1309
+ }
1310
+ }
1311
+ }
1312
+ catch (installError) {
1313
+ console.error(chalk_1.default.red(` ✗ Dependency installation failed: ${installError instanceof Error ? installError.message : String(installError)}`));
1314
+ throw new Error(`Failed to install dependencies: ${installError instanceof Error ? installError.message : String(installError)}`);
1315
+ }
1316
+ // Ensure index.js is executable
1317
+ console.log(chalk_1.default.gray('Setting file permissions...'));
1318
+ try {
1319
+ await awsService.executeSSHCommand(connection, 'sudo chmod +x /opt/edgible-agent/index.js || true');
1320
+ // Set proper ownership (root will own everything)
1321
+ await awsService.executeSSHCommand(connection, 'sudo chown -R root:root /opt/edgible-agent');
1322
+ // Create logs directory for root user
1323
+ const systemAgentPath = PathResolver_1.PathResolver.getSystemDataPath() + '/agent';
1324
+ await awsService.executeSSHCommand(connection, `sudo mkdir -p ${systemAgentPath}/logs`);
1325
+ await awsService.executeSSHCommand(connection, `sudo chown -R root:root ${PathResolver_1.PathResolver.getSystemDataPath()}`);
1326
+ await awsService.executeSSHCommand(connection, `sudo chmod -R 755 ${PathResolver_1.PathResolver.getSystemDataPath()}`);
1327
+ console.log(chalk_1.default.green(' ✓ Permissions and ownership set correctly'));
1328
+ }
1329
+ catch (permError) {
1330
+ console.error(chalk_1.default.red(` ✗ Failed to set permissions: ${permError instanceof Error ? permError.message : String(permError)}`));
1331
+ throw new Error(`Failed to set file permissions: ${permError instanceof Error ? permError.message : String(permError)}`);
1332
+ }
1333
+ }
1334
+ finally {
1335
+ if (fs.existsSync(tarPath)) {
1336
+ fs.unlinkSync(tarPath);
1337
+ }
1338
+ }
1339
+ }
1340
+ else {
1341
+ // Download from CloudFront on remote server
1342
+ console.log(chalk_1.default.gray('Downloading agent from CloudFront...'));
1343
+ const distributionUrl = (0, urls_1.getDistributionUrl)();
1344
+ const version = agentVersion || 'latest';
1345
+ // Gateway resync always uses gateway build
1346
+ // Build CloudFront URL with gateway device type prefix
1347
+ // Format: {distributionUrl}/gateway/{version}.zip
1348
+ const downloadUrl = `${distributionUrl}/gateway/${version}.zip`;
1349
+ console.log(chalk_1.default.gray(` Distribution: ${distributionUrl}`));
1350
+ console.log(chalk_1.default.gray(` Device Type: gateway`));
1351
+ console.log(chalk_1.default.gray(` Version: ${version}`));
1352
+ console.log(chalk_1.default.gray(` URL: ${downloadUrl}`));
1353
+ // Check if unzip is installed, install if needed
1354
+ console.log(chalk_1.default.gray('Checking for unzip utility...'));
1355
+ const unzipCheck = await awsService.executeSSHCommand(connection, 'which unzip || echo "unzip not found"');
1356
+ if (unzipCheck.stdout.trim() === 'unzip not found') {
1357
+ console.log(chalk_1.default.gray('Installing unzip...'));
1358
+ await awsService.executeSSHCommand(connection, 'sudo yum install -y unzip || sudo apt-get install -y unzip');
1359
+ }
1360
+ else {
1361
+ console.log(chalk_1.default.gray('✓ unzip is installed'));
1362
+ }
1363
+ // Download and extract on remote server with verbose output
1364
+ const downloadScript = `
1365
+ set -e
1366
+ echo "Step 1: Cleaning old files..."
1367
+ cd /opt/edgible-agent && sudo rm -rf dist package.json node_modules index.js *.map && echo "✓ Cleaned"
1368
+ echo "Step 2: Downloading agent..."
1369
+ sudo curl -L -f --max-time 300 --connect-timeout 30 -w "Downloaded %{size_download} bytes in %{time_total}s\\n" -o /tmp/agent.zip "${downloadUrl}" && echo "✓ Downloaded"
1370
+ echo "Step 3: Extracting agent..."
1371
+ sudo unzip -o -q /tmp/agent.zip -d /opt/edgible-agent && echo "✓ Extracted"
1372
+ echo "Step 4: Cleaning up..."
1373
+ sudo rm /tmp/agent.zip && echo "✓ Cleanup complete"
1374
+ echo "OK"
1375
+ `;
1376
+ console.log(chalk_1.default.gray(' Downloading agent (this may take a moment)...'));
1377
+ try {
1378
+ const downloadResult = await awsService.executeSSHCommand(connection, downloadScript);
1379
+ // Log the output for debugging
1380
+ if (downloadResult.stdout) {
1381
+ console.log(chalk_1.default.gray(' Output:'), downloadResult.stdout.trim());
1382
+ }
1383
+ if (downloadResult.stderr) {
1384
+ console.log(chalk_1.default.yellow(' Stderr:'), downloadResult.stderr.trim());
1385
+ }
1386
+ if (downloadResult.stdout.includes('OK')) {
1387
+ console.log(chalk_1.default.green(' ✓ Download and extraction completed successfully'));
1388
+ }
1389
+ else {
1390
+ throw new Error(`Download failed: ${downloadResult.stderr || downloadResult.stdout}`);
1391
+ }
1392
+ }
1393
+ catch (downloadError) {
1394
+ console.error(chalk_1.default.red(` ✗ Download failed: ${downloadError instanceof Error ? downloadError.message : String(downloadError)}`));
1395
+ throw downloadError;
1396
+ }
1397
+ // Verify dependencies are present (already included in zip)
1398
+ console.log(chalk_1.default.gray('Verifying agent dependencies...'));
1399
+ try {
1400
+ const verifyResult = await awsService.executeSSHCommand(connection, 'test -d /opt/edgible-agent/node_modules/commander && echo "OK" || echo "MISSING"');
1401
+ if (verifyResult.stdout.trim() === 'OK') {
1402
+ console.log(chalk_1.default.green(' ✓ Dependencies verified (included in package)'));
1403
+ }
1404
+ else {
1405
+ console.log(chalk_1.default.yellow(' ⚠ Dependencies missing, installing...'));
1406
+ const installResult = await awsService.executeSSHCommand(connection, 'cd /opt/edgible-agent && sudo npm install --production --no-save --no-audit --no-fund --loglevel=error');
1407
+ if (installResult.exitCode === 0) {
1408
+ console.log(chalk_1.default.green(' ✓ Dependencies installed successfully'));
1409
+ }
1410
+ else {
1411
+ console.log(chalk_1.default.yellow(` Warning: npm install exited with code ${installResult.exitCode}`));
1412
+ }
1413
+ }
1414
+ }
1415
+ catch (verifyError) {
1416
+ console.log(chalk_1.default.yellow(` ⚠ Could not verify dependencies: ${verifyError instanceof Error ? verifyError.message : String(verifyError)}`));
1417
+ }
1418
+ // Set file permissions
1419
+ console.log(chalk_1.default.gray('Setting file permissions...'));
1420
+ try {
1421
+ await awsService.executeSSHCommand(connection, 'sudo chmod +x /opt/edgible-agent/index.js || true');
1422
+ await awsService.executeSSHCommand(connection, 'sudo chown -R root:root /opt/edgible-agent');
1423
+ const systemAgentPath = PathResolver_1.PathResolver.getSystemDataPath() + '/agent';
1424
+ await awsService.executeSSHCommand(connection, `sudo mkdir -p ${systemAgentPath}/logs`);
1425
+ await awsService.executeSSHCommand(connection, `sudo chown -R root:root ${PathResolver_1.PathResolver.getSystemDataPath()}`);
1426
+ await awsService.executeSSHCommand(connection, `sudo chmod -R 755 ${PathResolver_1.PathResolver.getSystemDataPath()}`);
1427
+ console.log(chalk_1.default.green(' ✓ Permissions and ownership set correctly'));
1428
+ }
1429
+ catch (permError) {
1430
+ console.error(chalk_1.default.red(` ✗ Failed to set permissions: ${permError instanceof Error ? permError.message : String(permError)}`));
1431
+ throw new Error(`Failed to set file permissions: ${permError instanceof Error ? permError.message : String(permError)}`);
1432
+ }
1433
+ }
1434
+ // Start agent service
1435
+ console.log(chalk_1.default.gray('Starting agent service...'));
1436
+ try {
1437
+ await awsService.executeSSHCommand(connection, 'sudo systemctl restart edgible-agent');
1438
+ }
1439
+ catch (startError) {
1440
+ console.error(chalk_1.default.red('Failed to start service:'), startError);
1441
+ // Check service status and logs
1442
+ const statusResult = await awsService.executeSSHCommand(connection, 'sudo systemctl status edgible-agent');
1443
+ console.log(chalk_1.default.red('Service status:'), statusResult.stdout);
1444
+ console.log(chalk_1.default.red('Service stderr:'), statusResult.stderr);
1445
+ throw new Error('Agent service failed to start');
1446
+ }
1447
+ // Verify agent is running
1448
+ const statusResult = await awsService.executeSSHCommand(connection, 'sudo systemctl is-active edgible-agent');
1449
+ if (statusResult.stdout.trim() !== 'active') {
1450
+ // Get more details about why it failed
1451
+ const statusDetails = await awsService.executeSSHCommand(connection, 'sudo systemctl status edgible-agent');
1452
+ console.log(chalk_1.default.red('Service status details:'), statusDetails.stdout);
1453
+ throw new Error('Agent service failed to start');
1454
+ }
1455
+ console.log(chalk_1.default.green('✓ Agent resynced successfully!'));
1456
+ // Reboot if requested
1457
+ let rebooted = false;
1458
+ if (reboot) {
1459
+ console.log(chalk_1.default.blue('Rebooting gateway...'));
1460
+ try {
1461
+ const rebootResult = await this.executeManagedGatewayCommand(gatewayId, 'sudo reboot');
1462
+ // Note: The command may not return successfully because the gateway reboots
1463
+ // and disconnects the SSH session. We check if we got an error that suggests
1464
+ // the connection was lost (which is expected during reboot)
1465
+ if (rebootResult.success || (rebootResult.error && rebootResult.error.includes('ECONNRESET'))) {
1466
+ console.log(chalk_1.default.green('✓ Reboot command sent successfully'));
1467
+ console.log(chalk_1.default.yellow('⚠ Gateway is rebooting. SSH connection will be lost.'));
1468
+ console.log(chalk_1.default.gray(' The gateway should be back online in a few minutes.'));
1469
+ rebooted = true;
1470
+ }
1471
+ else {
1472
+ console.log(chalk_1.default.yellow('⚠ Reboot command may have failed, but continuing...'));
1473
+ if (rebootResult.error) {
1474
+ console.log(chalk_1.default.gray(` Error: ${rebootResult.error}`));
1475
+ }
1476
+ }
1477
+ }
1478
+ catch (rebootError) {
1479
+ // If the error is a connection reset, that's expected during reboot
1480
+ if (rebootError instanceof Error && (rebootError.message.includes('ECONNRESET') || rebootError.message.includes('connection'))) {
1481
+ console.log(chalk_1.default.green('✓ Reboot command sent (connection lost as expected)'));
1482
+ console.log(chalk_1.default.yellow('⚠ Gateway is rebooting.'));
1483
+ rebooted = true;
1484
+ }
1485
+ else {
1486
+ console.log(chalk_1.default.yellow(`⚠ Error sending reboot command: ${rebootError instanceof Error ? rebootError.message : String(rebootError)}`));
1487
+ }
1488
+ }
1489
+ }
1490
+ return {
1491
+ message: 'Agent resynced successfully',
1492
+ success: true,
1493
+ agentVersion: agentVersion || 'latest',
1494
+ syncTimestamp: new Date().toISOString(),
1495
+ logsWiped: logsWiped,
1496
+ rebooted: rebooted
1497
+ };
1498
+ }
1499
+ finally {
1500
+ // Clean up temp key file
1501
+ try {
1502
+ if (fs.existsSync(tempKeyPath)) {
1503
+ fs.unlinkSync(tempKeyPath);
1504
+ }
1505
+ }
1506
+ catch (cleanupError) {
1507
+ // Ignore cleanup errors
1508
+ }
1509
+ }
1510
+ }
1511
+ catch (error) {
1512
+ console.error(chalk_1.default.red('Error resyncing managed gateway agent:'), error);
1513
+ throw error;
1514
+ }
1515
+ }
1516
+ async resyncGatewayAgent(gatewayId, agentVersion, installFromLocal) {
1517
+ try {
1518
+ // Attempt auto re-login if tokens are missing but credentials are available
1519
+ await this.attemptAutoRelogin();
1520
+ const configManager = new config_1.ConfigManager();
1521
+ const gatewayInfo = configManager.getGateway(gatewayId);
1522
+ if (!gatewayInfo) {
1523
+ throw new Error('Gateway not found in local configuration');
1524
+ }
1525
+ // Load SSH key
1526
+ const awsService = new aws_1.AWSService(configManager.getAWSProfile(), gatewayInfo.region);
1527
+ const privateKey = awsService.loadSSHKey(gatewayInfo.keyPath);
1528
+ // Wait for instance to be ready for SSH
1529
+ const isReady = await awsService.waitForSSHReady(gatewayInfo.ec2InstanceId);
1530
+ if (!isReady) {
1531
+ throw new Error('Instance is not ready for SSH connections');
1532
+ }
1533
+ // Create SSH connection
1534
+ const connection = {
1535
+ host: gatewayInfo.publicIp,
1536
+ port: 22,
1537
+ username: 'ec2-user',
1538
+ privateKey: privateKey
1539
+ };
1540
+ console.log(chalk_1.default.blue('Connecting to gateway...'));
1541
+ console.log(chalk_1.default.gray(`SSH Host: ${connection.host}:${connection.port}`));
1542
+ console.log(chalk_1.default.gray(`SSH User: ${connection.username}`));
1543
+ console.log(chalk_1.default.gray(`SSH Key: ${gatewayInfo.keyPath}`));
1544
+ // Test SSH connection first
1545
+ console.log(chalk_1.default.gray('Testing SSH connection...'));
1546
+ try {
1547
+ await awsService.executeSSHCommand(connection, 'echo "SSH connection successful"');
1548
+ console.log(chalk_1.default.green('✓ SSH connection established'));
1549
+ }
1550
+ catch (sshError) {
1551
+ console.error(chalk_1.default.red('✗ SSH connection failed:'), sshError);
1552
+ throw new Error(`SSH connection failed: ${sshError instanceof Error ? sshError.message : 'Unknown error'}`);
1553
+ }
1554
+ // Stop agent service
1555
+ console.log(chalk_1.default.gray('Stopping agent service...'));
1556
+ await awsService.executeSSHCommand(connection, 'sudo systemctl stop edgible-agent');
1557
+ // Ensure the agent directory exists
1558
+ console.log(chalk_1.default.gray('Creating agent directory...'));
1559
+ await awsService.executeSSHCommand(connection, 'sudo mkdir -p /opt/edgible-agent');
1560
+ await awsService.executeSSHCommand(connection, 'sudo chown root:root /opt/edgible-agent');
1561
+ // Check Node.js installation and path
1562
+ console.log(chalk_1.default.gray('Checking Node.js installation...'));
1563
+ const nodeCheck = await awsService.executeSSHCommand(connection, 'which node || echo "node not found"');
1564
+ const nodePath = nodeCheck.stdout.trim();
1565
+ console.log(chalk_1.default.gray(`Node.js path: ${nodePath}`));
1566
+ if (nodePath === 'node not found') {
1567
+ console.log(chalk_1.default.gray('Installing Node.js...'));
1568
+ await awsService.executeSSHCommand(connection, 'curl -fsSL https://rpm.nodesource.com/setup_18.x | sudo bash -');
1569
+ await awsService.executeSSHCommand(connection, 'sudo yum install -y nodejs');
1570
+ }
1571
+ // Check and install required system dependencies
1572
+ console.log(chalk_1.default.gray('Checking system dependencies...'));
1573
+ // Check iptables
1574
+ const iptablesCheck = await awsService.executeSSHCommand(connection, 'which iptables || echo "iptables not found"');
1575
+ if (iptablesCheck.stdout.trim() === 'iptables not found') {
1576
+ console.log(chalk_1.default.gray('Installing iptables...'));
1577
+ await awsService.executeSSHCommand(connection, 'sudo yum install -y iptables');
1578
+ }
1579
+ else {
1580
+ console.log(chalk_1.default.gray('✓ iptables is installed'));
1581
+ }
1582
+ // Check WireGuard
1583
+ const wireguardCheck = await awsService.executeSSHCommand(connection, 'which wg || echo "wireguard not found"');
1584
+ if (wireguardCheck.stdout.trim() === 'wireguard not found') {
1585
+ console.log(chalk_1.default.gray('Installing WireGuard...'));
1586
+ // For Amazon Linux 2, we need to install WireGuard from EPEL
1587
+ await awsService.executeSSHCommand(connection, 'sudo amazon-linux-extras install epel -y || sudo yum install -y epel-release');
1588
+ await awsService.executeSSHCommand(connection, 'sudo yum install -y wireguard-tools');
1589
+ // Load the kernel module (if not already loaded)
1590
+ await awsService.executeSSHCommand(connection, 'sudo modprobe wireguard || true');
1591
+ }
1592
+ else {
1593
+ console.log(chalk_1.default.gray('✓ WireGuard is installed'));
1594
+ }
1595
+ // Check HAProxy (gateway devices use HAProxy instead of Caddy)
1596
+ const haproxyCheck = await awsService.executeSSHCommand(connection, 'which haproxy 2>/dev/null || test -f /usr/sbin/haproxy && echo "haproxy found" || echo "haproxy not found"');
1597
+ if (haproxyCheck.stdout.trim() === 'haproxy not found' || (haproxyCheck.stdout.trim() === '' && !haproxyCheck.stdout.includes('haproxy found'))) {
1598
+ console.log(chalk_1.default.gray('Installing HAProxy...'));
1599
+ // Install HAProxy via package manager
1600
+ await awsService.executeSSHCommand(connection, 'sudo yum install -y haproxy');
1601
+ // Create HAProxy config directory
1602
+ await awsService.executeSSHCommand(connection, 'sudo mkdir -p /etc/haproxy');
1603
+ // Create HAProxy runtime socket directory (required for admin socket)
1604
+ await awsService.executeSSHCommand(connection, 'sudo mkdir -p /var/run/haproxy');
1605
+ await awsService.executeSSHCommand(connection, 'sudo chmod 755 /var/run/haproxy');
1606
+ // Set capabilities for HAProxy to bind to privileged ports
1607
+ await awsService.executeSSHCommand(connection, 'sudo setcap cap_net_bind_service=+ep /usr/sbin/haproxy || true');
1608
+ // Create HAProxy chroot directory
1609
+ await awsService.executeSSHCommand(connection, 'sudo mkdir -p /var/lib/haproxy');
1610
+ await awsService.executeSSHCommand(connection, 'sudo chown haproxy:haproxy /var/lib/haproxy || sudo chown root:root /var/lib/haproxy || true');
1611
+ // Verify installation
1612
+ const verifyResult = await awsService.executeSSHCommand(connection, '/usr/sbin/haproxy -v 2>&1');
1613
+ if (!verifyResult.stdout.includes('HAProxy') && !verifyResult.stdout.includes('version')) {
1614
+ throw new Error(`HAProxy installation verification failed. Output: ${verifyResult.stdout}`);
1615
+ }
1616
+ console.log(chalk_1.default.green('✓ HAProxy installed successfully'));
1617
+ }
1618
+ else {
1619
+ console.log(chalk_1.default.gray('✓ HAProxy is installed'));
1620
+ // Verify it's accessible
1621
+ const verifyResult = await awsService.executeSSHCommand(connection, 'haproxy -v 2>&1 || /usr/sbin/haproxy -v 2>&1');
1622
+ if (verifyResult.stdout.includes('command not found') && !verifyResult.stdout.includes('HAProxy') && !verifyResult.stdout.includes('version')) {
1623
+ console.log(chalk_1.default.yellow('⚠ HAProxy found but not accessible, verifying...'));
1624
+ // Re-verify
1625
+ const reVerifyResult = await awsService.executeSSHCommand(connection, 'haproxy -v 2>&1 || /usr/sbin/haproxy -v 2>&1');
1626
+ if (!reVerifyResult.stdout.includes('HAProxy') && !reVerifyResult.stdout.includes('version')) {
1627
+ console.log(chalk_1.default.yellow('⚠ HAProxy verification still failing, but continuing...'));
1628
+ }
1629
+ }
1630
+ }
1631
+ // Get device password from API
1632
+ console.log(chalk_1.default.gray('Retrieving device credentials...'));
1633
+ const deviceResponse = await this.apiClient.getDevice(gatewayInfo.deviceId);
1634
+ const devicePassword = deviceResponse.device?.password || '';
1635
+ if (!devicePassword) {
1636
+ throw new Error('Could not retrieve device password from API');
1637
+ }
1638
+ // Create agent configuration file
1639
+ console.log(chalk_1.default.gray('Creating agent configuration...'));
1640
+ const userConfig = configManager.getConfig();
1641
+ const agentConfig = {
1642
+ deviceId: gatewayInfo.deviceId,
1643
+ devicePassword: devicePassword,
1644
+ deviceType: 'gateway',
1645
+ apiBaseUrl: (0, urls_1.getApiBaseUrl)(),
1646
+ organizationId: userConfig.organizationId || '',
1647
+ firewallEnabled: true,
1648
+ pollingInterval: 60000,
1649
+ healthCheckTimeout: 5000,
1650
+ maxRetries: 3,
1651
+ logLevel: 'info',
1652
+ updateEnabled: true,
1653
+ updateCheckInterval: 3600000,
1654
+ wireguardMode: userConfig.wireguardMode || 'kernel',
1655
+ wireguardGoBinary: userConfig.wireguardGoBinary || 'wireguard-go'
1656
+ };
1657
+ const configFile = `/tmp/agent-config-${Date.now()}.json`;
1658
+ fs.writeFileSync(configFile, JSON.stringify(agentConfig, null, 2));
1659
+ try {
1660
+ console.log(chalk_1.default.gray('Uploading agent configuration...'));
1661
+ // Upload to user's home directory first (where we have write permissions)
1662
+ // Then move to /opt/edgible-agent with sudo
1663
+ const homeDir = connection.username === 'ec2-user' ? '/home/ec2-user' : `/home/${connection.username}`;
1664
+ const remoteHomeConfigPath = `${homeDir}/agent-config-${Date.now()}.json`;
1665
+ const remoteConfigPath = '/opt/edgible-agent/agent.config.json';
1666
+ console.log(chalk_1.default.gray(` Local file: ${configFile}`));
1667
+ console.log(chalk_1.default.gray(` Uploading to: ${remoteHomeConfigPath}`));
1668
+ try {
1669
+ await awsService.uploadFile(connection, configFile, remoteHomeConfigPath);
1670
+ console.log(chalk_1.default.green(' ✓ Config file uploaded successfully'));
1671
+ // Move to final location with sudo and set ownership
1672
+ console.log(chalk_1.default.gray(` Moving to: ${remoteConfigPath}`));
1673
+ const moveResult = await awsService.executeSSHCommand(connection, `sudo mv ${remoteHomeConfigPath} ${remoteConfigPath} && sudo chown root:root ${remoteConfigPath} && echo "OK" || echo "FAIL"`);
1674
+ if (moveResult.stdout.trim() === 'OK') {
1675
+ console.log(chalk_1.default.green(' ✓ Config file moved and ownership set'));
1676
+ }
1677
+ else {
1678
+ throw new Error(`Failed to move config file: ${moveResult.stderr || moveResult.stdout}`);
1679
+ }
1680
+ }
1681
+ catch (configError) {
1682
+ console.error(chalk_1.default.red(` ✗ Config upload failed: ${configError instanceof Error ? configError.message : String(configError)}`));
1683
+ throw new Error(`Failed to upload agent configuration: ${configError instanceof Error ? configError.message : String(configError)}`);
1684
+ }
1685
+ }
1686
+ finally {
1687
+ if (fs.existsSync(configFile)) {
1688
+ fs.unlinkSync(configFile);
1689
+ }
1690
+ }
1691
+ // Create systemd service file
1692
+ console.log(chalk_1.default.gray('Creating systemd service...'));
1693
+ const finalNodePath = nodePath === 'node not found' ? '/usr/bin/node' : nodePath;
1694
+ const systemAgentPath = PathResolver_1.PathResolver.getSystemDataPath() + '/agent';
1695
+ const serviceContent = `[Unit]
1696
+ Description=Edgible Agent
1697
+ After=network.target
1698
+
1699
+ [Service]
1700
+ Type=simple
1701
+ User=root
1702
+ WorkingDirectory=/opt/edgible-agent
1703
+ ExecStart=${finalNodePath} /opt/edgible-agent/index.js start -c /opt/edgible-agent/agent.config.json
1704
+ Restart=always
1705
+ RestartSec=10
1706
+ Environment=NODE_ENV=production
1707
+ Environment=EDGIBLE_CONFIG_PATH=${systemAgentPath}
1708
+ Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
1709
+
1710
+ [Install]
1711
+ WantedBy=multi-user.target`;
1712
+ const serviceFile = `/tmp/edgible-agent-${Date.now()}.service`;
1713
+ fs.writeFileSync(serviceFile, serviceContent);
1714
+ try {
1715
+ await awsService.uploadFile(connection, serviceFile, '/tmp/edgible-agent.service');
1716
+ await awsService.executeSSHCommand(connection, 'sudo mv /tmp/edgible-agent.service /etc/systemd/system/edgible-agent.service');
1717
+ await awsService.executeSSHCommand(connection, 'sudo systemctl daemon-reload');
1718
+ await awsService.executeSSHCommand(connection, 'sudo systemctl enable edgible-agent.service');
1719
+ }
1720
+ finally {
1721
+ if (fs.existsSync(serviceFile)) {
1722
+ fs.unlinkSync(serviceFile);
1723
+ }
1724
+ }
1725
+ // Upload or download agent code
1726
+ if (installFromLocal) {
1727
+ // Use existing local upload behavior
1728
+ console.log(chalk_1.default.gray('Uploading agent code from local directory...'));
1729
+ const agentV2Path = path.resolve(__dirname, '../../../agent-v2');
1730
+ const agentDistPath = path.join(agentV2Path, 'dist');
1731
+ const agentPackageJsonPath = path.join(agentV2Path, 'package.json');
1732
+ if (!fs.existsSync(agentDistPath)) {
1733
+ throw new Error('Agent distribution not found. This feature requires development environment setup. Please use S3 download instead.');
1734
+ }
1735
+ if (!fs.existsSync(agentPackageJsonPath)) {
1736
+ throw new Error('Agent package.json not found.');
1737
+ }
1738
+ // Create a tarball of the dist directory and package.json
1739
+ const { execSync } = require('child_process');
1740
+ const tarPath = `/tmp/agent-dist-${Date.now()}.tar.gz`;
1741
+ try {
1742
+ console.log(chalk_1.default.gray('Creating agent distribution tarball...'));
1743
+ console.log(chalk_1.default.gray(` Source dist: ${agentDistPath}`));
1744
+ console.log(chalk_1.default.gray(` Source package.json: ${agentPackageJsonPath}`));
1745
+ // Create tarball with both dist and package.json
1746
+ const tempDir = `/tmp/agent-upload-${Date.now()}`;
1747
+ console.log(chalk_1.default.gray(` Creating temp directory: ${tempDir}`));
1748
+ fs.mkdirSync(tempDir, { recursive: true });
1749
+ fs.copyFileSync(agentPackageJsonPath, path.join(tempDir, 'package.json'));
1750
+ execSync(`cp -r "${agentDistPath}" "${tempDir}/dist"`, { stdio: 'inherit' });
1751
+ console.log(chalk_1.default.gray(` Creating tarball: ${tarPath}`));
1752
+ execSync(`cd "${tempDir}" && tar -czf "${tarPath}" .`, { stdio: 'inherit' });
1753
+ // Verify tarball was created
1754
+ if (!fs.existsSync(tarPath)) {
1755
+ throw new Error(`Failed to create tarball at ${tarPath}`);
1756
+ }
1757
+ const tarStats = fs.statSync(tarPath);
1758
+ const tarSizeMB = (tarStats.size / (1024 * 1024)).toFixed(2);
1759
+ console.log(chalk_1.default.gray(` Tarball created: ${tarSizeMB} MB`));
1760
+ // Clean up temp directory
1761
+ console.log(chalk_1.default.gray(' Cleaning up temp directory...'));
1762
+ fs.rmSync(tempDir, { recursive: true, force: true });
1763
+ // Upload tarball to user's home directory first (where we have write permissions)
1764
+ // Then move to /tmp with sudo if needed
1765
+ // Use explicit home directory path since SFTP might not expand ~
1766
+ const homeDir = connection.username === 'ec2-user' ? '/home/ec2-user' : `/home/${connection.username}`;
1767
+ const remoteHomePath = `${homeDir}/agent-dist-${Date.now()}.tar.gz`;
1768
+ const remoteTmpPath = `/tmp/agent-dist.tar.gz`;
1769
+ console.log(chalk_1.default.gray('Checking remote directory permissions...'));
1770
+ try {
1771
+ const homeDirCheck = await awsService.executeSSHCommand(connection, `test -d ${homeDir} && test -w ${homeDir} && echo "OK" || echo "FAIL"`);
1772
+ if (homeDirCheck.stdout.trim() !== 'OK') {
1773
+ console.log(chalk_1.default.yellow(` Warning: Home directory ${homeDir} may not be writable`));
1774
+ console.log(chalk_1.default.gray(` Attempting to verify with ls -ld: ${homeDir}`));
1775
+ const lsResult = await awsService.executeSSHCommand(connection, `ls -ld ${homeDir} 2>&1 || echo "directory_not_found"`);
1776
+ console.log(chalk_1.default.gray(` Directory info: ${lsResult.stdout.trim()}`));
1777
+ }
1778
+ else {
1779
+ console.log(chalk_1.default.gray(` ✓ Home directory ${homeDir} is writable`));
1780
+ }
1781
+ }
1782
+ catch (checkError) {
1783
+ console.log(chalk_1.default.yellow(` Warning: Could not verify directory permissions: ${checkError instanceof Error ? checkError.message : String(checkError)}`));
1784
+ }
1785
+ console.log(chalk_1.default.blue('Uploading tarball to remote server...'));
1786
+ console.log(chalk_1.default.gray(` Local file: ${tarPath}`));
1787
+ console.log(chalk_1.default.gray(` Remote path: ${remoteHomePath}`));
1788
+ console.log(chalk_1.default.gray(` File size: ${tarSizeMB} MB`));
1789
+ try {
1790
+ await awsService.uploadFile(connection, tarPath, remoteHomePath);
1791
+ console.log(chalk_1.default.green(' ✓ Upload completed successfully'));
1792
+ // Verify file was uploaded
1793
+ console.log(chalk_1.default.gray(' Verifying uploaded file...'));
1794
+ const verifyResult = await awsService.executeSSHCommand(connection, `test -f ${remoteHomePath} && ls -lh ${remoteHomePath} || echo "file_not_found"`);
1795
+ if (verifyResult.stdout.includes('file_not_found')) {
1796
+ throw new Error(`Upload verification failed: file not found at ${remoteHomePath}`);
1797
+ }
1798
+ console.log(chalk_1.default.gray(` File verified: ${verifyResult.stdout.trim()}`));
1799
+ }
1800
+ catch (uploadError) {
1801
+ console.error(chalk_1.default.red(` ✗ Upload failed: ${uploadError instanceof Error ? uploadError.message : String(uploadError)}`));
1802
+ throw new Error(`Failed to upload file to ${remoteHomePath}: ${uploadError instanceof Error ? uploadError.message : String(uploadError)}`);
1803
+ }
1804
+ // Move to /tmp using sudo (in case /tmp has permission restrictions)
1805
+ console.log(chalk_1.default.gray(`Moving file from ${remoteHomePath} to ${remoteTmpPath}...`));
1806
+ try {
1807
+ const moveResult = await awsService.executeSSHCommand(connection, `sudo mv ${remoteHomePath} ${remoteTmpPath} && sudo chmod 644 ${remoteTmpPath} && echo "OK" || echo "FAIL"`);
1808
+ if (moveResult.stdout.trim() === 'OK') {
1809
+ console.log(chalk_1.default.green(' ✓ File moved successfully'));
1810
+ }
1811
+ else {
1812
+ throw new Error(`Failed to move file: ${moveResult.stderr || moveResult.stdout}`);
1813
+ }
1814
+ }
1815
+ catch (moveError) {
1816
+ console.error(chalk_1.default.red(` ✗ Move failed: ${moveError instanceof Error ? moveError.message : String(moveError)}`));
1817
+ throw new Error(`Failed to move file from ${remoteHomePath} to ${remoteTmpPath}: ${moveError instanceof Error ? moveError.message : String(moveError)}`);
1818
+ }
1819
+ // Extract on remote server
1820
+ console.log(chalk_1.default.gray('Extracting agent code...'));
1821
+ try {
1822
+ const extractResult = await awsService.executeSSHCommand(connection, 'cd /opt/edgible-agent && sudo tar -xzf /tmp/agent-dist.tar.gz && sudo rm /tmp/agent-dist.tar.gz && echo "OK" || echo "FAIL"');
1823
+ if (extractResult.stdout.trim() === 'OK') {
1824
+ console.log(chalk_1.default.green(' ✓ Extraction completed successfully'));
1825
+ }
1826
+ else {
1827
+ console.log(chalk_1.default.yellow(` Warning: Extraction may have failed: ${extractResult.stderr || extractResult.stdout}`));
1828
+ }
1829
+ }
1830
+ catch (extractError) {
1831
+ console.error(chalk_1.default.red(` ✗ Extraction failed: ${extractError instanceof Error ? extractError.message : String(extractError)}`));
1832
+ throw new Error(`Failed to extract agent code: ${extractError instanceof Error ? extractError.message : String(extractError)}`);
1833
+ }
1834
+ // Install production dependencies (run as root since directory is owned by root)
1835
+ console.log(chalk_1.default.gray('Installing agent dependencies...'));
1836
+ try {
1837
+ const installResult = await awsService.executeSSHCommand(connection, 'cd /opt/edgible-agent && sudo npm install --production --no-save');
1838
+ if (installResult.exitCode === 0) {
1839
+ console.log(chalk_1.default.green(' ✓ Dependencies installed successfully'));
1840
+ }
1841
+ else {
1842
+ console.log(chalk_1.default.yellow(` Warning: npm install exited with code ${installResult.exitCode}`));
1843
+ console.log(chalk_1.default.gray(` stdout: ${installResult.stdout}`));
1844
+ if (installResult.stderr) {
1845
+ console.log(chalk_1.default.gray(` stderr: ${installResult.stderr}`));
1846
+ }
1847
+ }
1848
+ }
1849
+ catch (installError) {
1850
+ console.error(chalk_1.default.red(` ✗ Dependency installation failed: ${installError instanceof Error ? installError.message : String(installError)}`));
1851
+ throw new Error(`Failed to install dependencies: ${installError instanceof Error ? installError.message : String(installError)}`);
1852
+ }
1853
+ // Ensure index.js is executable
1854
+ console.log(chalk_1.default.gray('Setting file permissions...'));
1855
+ try {
1856
+ await awsService.executeSSHCommand(connection, 'sudo chmod +x /opt/edgible-agent/dist/index.js || true');
1857
+ // Set proper ownership (root will own everything)
1858
+ await awsService.executeSSHCommand(connection, 'sudo chown -R root:root /opt/edgible-agent');
1859
+ // Create logs directory for root user
1860
+ await awsService.executeSSHCommand(connection, 'sudo mkdir -p /opt/edgible-agent/.edgible/agent/logs');
1861
+ await awsService.executeSSHCommand(connection, 'sudo chown -R root:root /opt/edgible-agent/.edgible');
1862
+ await awsService.executeSSHCommand(connection, 'sudo chmod -R 755 /opt/edgible-agent/.edgible');
1863
+ console.log(chalk_1.default.green(' ✓ Permissions and ownership set correctly'));
1864
+ }
1865
+ catch (permError) {
1866
+ console.error(chalk_1.default.red(` ✗ Failed to set permissions: ${permError instanceof Error ? permError.message : String(permError)}`));
1867
+ throw new Error(`Failed to set file permissions: ${permError instanceof Error ? permError.message : String(permError)}`);
1868
+ }
1869
+ }
1870
+ finally {
1871
+ // Clean up tarball if it exists
1872
+ if (fs.existsSync(tarPath)) {
1873
+ fs.unlinkSync(tarPath);
1874
+ }
1875
+ }
1876
+ }
1877
+ else {
1878
+ // Download from CloudFront on remote server
1879
+ console.log(chalk_1.default.gray('Downloading agent from CloudFront...'));
1880
+ const distributionUrl = (0, urls_1.getDistributionUrl)();
1881
+ const version = agentVersion || 'latest';
1882
+ // Build CloudFront URL directly - no bucket or S3 path needed
1883
+ // Stage determines both the distribution URL and the bucket (backend detail)
1884
+ const downloadUrl = `${distributionUrl}/${version}.zip`;
1885
+ console.log(chalk_1.default.gray(` Distribution: ${distributionUrl}`));
1886
+ console.log(chalk_1.default.gray(` Version: ${version}`));
1887
+ console.log(chalk_1.default.gray(` URL: ${downloadUrl}`));
1888
+ // Check if unzip is installed, install if needed
1889
+ console.log(chalk_1.default.gray('Checking for unzip utility...'));
1890
+ const unzipCheck = await awsService.executeSSHCommand(connection, 'which unzip || echo "unzip not found"');
1891
+ if (unzipCheck.stdout.trim() === 'unzip not found') {
1892
+ console.log(chalk_1.default.gray('Installing unzip...'));
1893
+ await awsService.executeSSHCommand(connection, 'sudo yum install -y unzip || sudo apt-get install -y unzip');
1894
+ }
1895
+ else {
1896
+ console.log(chalk_1.default.gray('✓ unzip is installed'));
1897
+ }
1898
+ // Download and extract on remote server with verbose output
1899
+ const downloadScript = `
1900
+ set -e
1901
+ echo "Step 1: Cleaning old files..."
1902
+ cd /opt/edgible-agent && sudo rm -rf dist package.json node_modules index.js *.map && echo "✓ Cleaned"
1903
+ echo "Step 2: Downloading agent..."
1904
+ sudo curl -L -f --max-time 300 --connect-timeout 30 -w "Downloaded %{size_download} bytes in %{time_total}s\\n" -o /tmp/agent.zip "${downloadUrl}" && echo "✓ Downloaded"
1905
+ echo "Step 3: Extracting agent..."
1906
+ sudo unzip -o -q /tmp/agent.zip -d /opt/edgible-agent && echo "✓ Extracted"
1907
+ echo "Step 4: Cleaning up..."
1908
+ sudo rm /tmp/agent.zip && echo "✓ Cleanup complete"
1909
+ echo "OK"
1910
+ `;
1911
+ console.log(chalk_1.default.gray(' Downloading agent (this may take a moment)...'));
1912
+ try {
1913
+ const downloadResult = await awsService.executeSSHCommand(connection, downloadScript);
1914
+ // Log the output for debugging
1915
+ if (downloadResult.stdout) {
1916
+ console.log(chalk_1.default.gray(' Output:'), downloadResult.stdout.trim());
1917
+ }
1918
+ if (downloadResult.stderr) {
1919
+ console.log(chalk_1.default.yellow(' Stderr:'), downloadResult.stderr.trim());
1920
+ }
1921
+ if (downloadResult.stdout.includes('OK')) {
1922
+ console.log(chalk_1.default.green(' ✓ Download and extraction completed successfully'));
1923
+ }
1924
+ else {
1925
+ throw new Error(`Download failed: ${downloadResult.stderr || downloadResult.stdout}`);
1926
+ }
1927
+ }
1928
+ catch (downloadError) {
1929
+ console.error(chalk_1.default.red(` ✗ Download failed: ${downloadError instanceof Error ? downloadError.message : String(downloadError)}`));
1930
+ throw downloadError;
1931
+ }
1932
+ // Verify dependencies are present (already included in zip)
1933
+ console.log(chalk_1.default.gray('Verifying agent dependencies...'));
1934
+ try {
1935
+ const verifyResult = await awsService.executeSSHCommand(connection, 'test -d /opt/edgible-agent/node_modules/commander && echo "OK" || echo "MISSING"');
1936
+ if (verifyResult.stdout.trim() === 'OK') {
1937
+ console.log(chalk_1.default.green(' ✓ Dependencies verified (included in package)'));
1938
+ }
1939
+ else {
1940
+ console.log(chalk_1.default.yellow(' ⚠ Dependencies missing, installing...'));
1941
+ const installResult = await awsService.executeSSHCommand(connection, 'cd /opt/edgible-agent && sudo npm install --production --no-save --no-audit --no-fund --loglevel=error');
1942
+ if (installResult.exitCode === 0) {
1943
+ console.log(chalk_1.default.green(' ✓ Dependencies installed successfully'));
1944
+ }
1945
+ else {
1946
+ console.log(chalk_1.default.yellow(` Warning: npm install exited with code ${installResult.exitCode}`));
1947
+ }
1948
+ }
1949
+ }
1950
+ catch (verifyError) {
1951
+ console.log(chalk_1.default.yellow(` ⚠ Could not verify dependencies: ${verifyError instanceof Error ? verifyError.message : String(verifyError)}`));
1952
+ }
1953
+ // Set file permissions
1954
+ console.log(chalk_1.default.gray('Setting file permissions...'));
1955
+ try {
1956
+ await awsService.executeSSHCommand(connection, 'sudo chmod +x /opt/edgible-agent/index.js || true');
1957
+ await awsService.executeSSHCommand(connection, 'sudo chown -R root:root /opt/edgible-agent');
1958
+ await awsService.executeSSHCommand(connection, 'sudo mkdir -p /opt/edgible-agent/.edgible/agent/logs');
1959
+ await awsService.executeSSHCommand(connection, 'sudo chown -R root:root /opt/edgible-agent/.edgible');
1960
+ await awsService.executeSSHCommand(connection, 'sudo chmod -R 755 /opt/edgible-agent/.edgible');
1961
+ console.log(chalk_1.default.green(' ✓ Permissions and ownership set correctly'));
1962
+ }
1963
+ catch (permError) {
1964
+ console.error(chalk_1.default.red(` ✗ Failed to set permissions: ${permError instanceof Error ? permError.message : String(permError)}`));
1965
+ throw new Error(`Failed to set file permissions: ${permError instanceof Error ? permError.message : String(permError)}`);
1966
+ }
1967
+ }
1968
+ // Start agent service
1969
+ console.log(chalk_1.default.gray('Starting agent service...'));
1970
+ try {
1971
+ await awsService.executeSSHCommand(connection, 'sudo systemctl restart edgible-agent');
1972
+ }
1973
+ catch (startError) {
1974
+ console.error(chalk_1.default.red('Failed to start service:'), startError);
1975
+ // Check service status and logs
1976
+ const statusResult = await awsService.executeSSHCommand(connection, 'sudo systemctl status edgible-agent');
1977
+ console.log(chalk_1.default.red('Service status:'), statusResult.stdout);
1978
+ console.log(chalk_1.default.red('Service stderr:'), statusResult.stderr);
1979
+ throw new Error('Agent service failed to start');
1980
+ }
1981
+ // Verify agent is running
1982
+ const statusResult = await awsService.executeSSHCommand(connection, 'sudo systemctl is-active edgible-agent');
1983
+ if (statusResult.stdout.trim() !== 'active') {
1984
+ // Get more details about why it failed
1985
+ const statusDetails = await awsService.executeSSHCommand(connection, 'sudo systemctl status edgible-agent');
1986
+ console.log(chalk_1.default.red('Service status details:'), statusDetails.stdout);
1987
+ throw new Error('Agent service failed to start');
1988
+ }
1989
+ console.log(chalk_1.default.green('✓ Agent resynced successfully!'));
1990
+ return {
1991
+ message: 'Agent resynced successfully',
1992
+ success: true,
1993
+ agentVersion: agentVersion || 'latest',
1994
+ syncTimestamp: new Date().toISOString()
1995
+ };
1996
+ }
1997
+ catch (error) {
1998
+ console.error(chalk_1.default.red('Error resyncing agent:'), error);
1999
+ throw error;
2000
+ }
2001
+ }
2002
+ /**
2003
+ * Get comprehensive gateway diagnostics including status, logs, and system info
2004
+ */
2005
+ async getGatewayDiagnostics(gatewayId) {
2006
+ try {
2007
+ // Attempt auto re-login if tokens are missing but credentials are available
2008
+ await this.attemptAutoRelogin();
2009
+ const configManager = new config_1.ConfigManager();
2010
+ const gatewayInfo = configManager.getGateway(gatewayId);
2011
+ if (!gatewayInfo) {
2012
+ throw new Error('Gateway not found in local configuration');
2013
+ }
2014
+ // Load SSH key
2015
+ const awsService = new aws_1.AWSService(configManager.getAWSProfile(), gatewayInfo.region);
2016
+ const privateKey = awsService.loadSSHKey(gatewayInfo.keyPath);
2017
+ // Wait for instance to be ready for SSH
2018
+ const isReady = await awsService.waitForSSHReady(gatewayInfo.ec2InstanceId);
2019
+ if (!isReady) {
2020
+ throw new Error('Instance is not ready for SSH connections');
2021
+ }
2022
+ // Create SSH connection
2023
+ const connection = {
2024
+ host: gatewayInfo.publicIp,
2025
+ port: 22,
2026
+ username: 'ec2-user',
2027
+ privateKey: privateKey
2028
+ };
2029
+ // Execute multiple commands in parallel where possible
2030
+ // For logs, try multiple locations and fall back to journalctl
2031
+ const commands = {
2032
+ status: 'cat ~/.edgible/agent/status.json /opt/edgible-agent/.edgible/agent/status.json 2>/dev/null | head -1 || echo "Status file not found"',
2033
+ agentLogs: '(test -f ~/.edgible/agent/logs/agent.log && tail -n 100 ~/.edgible/agent/logs/agent.log) || (test -f /opt/edgible-agent/.edgible/agent/logs/agent.log && tail -n 100 /opt/edgible-agent/.edgible/agent/logs/agent.log) || (sudo journalctl -u edgible-agent.service -n 100 --no-pager 2>/dev/null) || echo "No logs available (checking journalctl...)"',
2034
+ systemInfo: 'echo "=== System Resources ===" && free -h && echo "" && echo "=== Disk Usage ===" && df -h / && echo "" && echo "=== Uptime ===" && uptime && echo "" && echo "=== Load Average ===" && cat /proc/loadavg',
2035
+ wireguardStatus: 'sudo wg show 2>/dev/null || echo "WireGuard not running or no interfaces"',
2036
+ caddyStatus: 'curl -s http://localhost:2019/config/ 2>/dev/null | head -50 || echo "Caddy API not accessible"',
2037
+ serviceStatus: 'sudo systemctl status edgible-agent.service --no-pager -l 2>/dev/null || echo "Service status unavailable"',
2038
+ networkInfo: 'echo "=== Network Interfaces ===" && ip addr show && echo "" && echo "=== WireGuard Interfaces ===" && ip link show type wireguard 2>/dev/null || echo "No WireGuard interfaces"',
2039
+ logPaths: 'echo "=== Checking log paths ===" && (test -d ~/.edgible/agent/logs && echo "~/.edgible/agent/logs exists" && ls -la ~/.edgible/agent/logs/ 2>/dev/null) || echo "~/.edgible/agent/logs not found" && (test -d /opt/edgible-agent/.edgible/agent/logs && echo "/opt/edgible-agent/.edgible/agent/logs exists" && ls -la /opt/edgible-agent/.edgible/agent/logs/ 2>/dev/null) || echo "/opt/edgible-agent/.edgible/agent/logs not found"'
2040
+ };
2041
+ const results = {};
2042
+ // Execute all commands
2043
+ for (const [key, command] of Object.entries(commands)) {
2044
+ try {
2045
+ const result = await awsService.executeSSHCommand(connection, command);
2046
+ results[key] = result.stdout || result.stderr || '';
2047
+ }
2048
+ catch (error) {
2049
+ results[key] = `Error executing command: ${error instanceof Error ? error.message : String(error)}`;
2050
+ }
2051
+ }
2052
+ return {
2053
+ status: results.status,
2054
+ agentLogs: results.agentLogs,
2055
+ systemInfo: results.systemInfo,
2056
+ wireguardStatus: results.wireguardStatus,
2057
+ caddyStatus: results.caddyStatus,
2058
+ serviceStatus: results.serviceStatus,
2059
+ networkInfo: results.networkInfo,
2060
+ logPaths: results.logPaths || '',
2061
+ success: true
2062
+ };
2063
+ }
2064
+ catch (error) {
2065
+ console.error(chalk_1.default.red('Error retrieving gateway diagnostics:'), error);
2066
+ throw error;
2067
+ }
2068
+ }
2069
+ /**
2070
+ * Get agent logs from a gateway
2071
+ */
2072
+ /**
2073
+ * Get logs from a managed gateway (admin only)
2074
+ */
2075
+ async getManagedGatewayLogs(gatewayId, options) {
2076
+ try {
2077
+ await this.attemptAutoRelogin();
2078
+ // Get managed gateway details
2079
+ const gatewayDetails = await this.getManagedGateway(gatewayId);
2080
+ if (!gatewayDetails.success || !gatewayDetails.gateway) {
2081
+ throw new Error('Managed gateway not found');
2082
+ }
2083
+ const gateway = gatewayDetails.gateway;
2084
+ const ipAddress = gateway.ipAddress;
2085
+ if (!ipAddress) {
2086
+ throw new Error('Gateway IP address not available');
2087
+ }
2088
+ // Get SSH key from backend
2089
+ const sshKeyResponse = await this.apiClient.getManagedGatewaySSHKey({ gatewayId });
2090
+ if (!sshKeyResponse.success || !sshKeyResponse.sshPrivateKey) {
2091
+ throw new Error('Failed to retrieve SSH key for managed gateway');
2092
+ }
2093
+ const sshPrivateKey = sshKeyResponse.sshPrivateKey;
2094
+ // Create SSH connection
2095
+ const connection = {
2096
+ host: ipAddress,
2097
+ port: 22,
2098
+ username: 'ec2-user',
2099
+ privateKey: sshPrivateKey
2100
+ };
2101
+ // Use the same logic as getGatewayLogs but with custom connection
2102
+ return await this.executeGatewayLogs(connection, options);
2103
+ }
2104
+ catch (error) {
2105
+ console.error(chalk_1.default.red('Error getting managed gateway logs:'), error);
2106
+ throw error;
2107
+ }
2108
+ }
2109
+ /**
2110
+ * Execute a command on a managed gateway
2111
+ */
2112
+ async executeManagedGatewayCommand(gatewayId, command) {
2113
+ try {
2114
+ await this.attemptAutoRelogin();
2115
+ // Get managed gateway details
2116
+ const gatewayDetails = await this.getManagedGateway(gatewayId);
2117
+ if (!gatewayDetails.success || !gatewayDetails.gateway) {
2118
+ throw new Error('Managed gateway not found');
2119
+ }
2120
+ const gateway = gatewayDetails.gateway;
2121
+ const ipAddress = gateway.ipAddress;
2122
+ if (!ipAddress) {
2123
+ throw new Error('Gateway IP address not available');
2124
+ }
2125
+ // Get SSH key from backend
2126
+ const sshKeyResponse = await this.apiClient.getManagedGatewaySSHKey({ gatewayId });
2127
+ if (!sshKeyResponse.success || !sshKeyResponse.sshPrivateKey) {
2128
+ throw new Error('Failed to retrieve SSH key for managed gateway');
2129
+ }
2130
+ const sshPrivateKey = sshKeyResponse.sshPrivateKey;
2131
+ // Create SSH connection
2132
+ const connection = {
2133
+ host: ipAddress,
2134
+ port: 22,
2135
+ username: 'ec2-user',
2136
+ privateKey: sshPrivateKey
2137
+ };
2138
+ const { AWSService } = await Promise.resolve().then(() => __importStar(require('./aws')));
2139
+ const awsService = new AWSService();
2140
+ // Test SSH connection
2141
+ try {
2142
+ await awsService.executeSSHCommand(connection, 'echo "SSH connection successful"');
2143
+ }
2144
+ catch (sshError) {
2145
+ throw new Error(`SSH connection failed: ${sshError instanceof Error ? sshError.message : 'Unknown error'}`);
2146
+ }
2147
+ // Execute the command
2148
+ const result = await awsService.executeSSHCommand(connection, command);
2149
+ return {
2150
+ success: result.exitCode === 0,
2151
+ output: result.stdout,
2152
+ error: result.stderr || undefined
2153
+ };
2154
+ }
2155
+ catch (error) {
2156
+ return {
2157
+ success: false,
2158
+ output: '',
2159
+ error: error instanceof Error ? error.message : String(error)
2160
+ };
2161
+ }
2162
+ }
2163
+ /**
2164
+ * Execute gateway logs retrieval (internal method)
2165
+ */
2166
+ async executeGatewayLogs(connection, options) {
2167
+ const { AWSService } = await Promise.resolve().then(() => __importStar(require('./aws')));
2168
+ const awsService = new AWSService();
2169
+ // Test SSH connection
2170
+ try {
2171
+ await awsService.executeSSHCommand(connection, 'echo "SSH connection successful"');
2172
+ }
2173
+ catch (sshError) {
2174
+ throw new Error(`SSH connection failed: ${sshError instanceof Error ? sshError.message : 'Unknown error'}`);
2175
+ }
2176
+ const lines = options.lines || 50;
2177
+ const follow = options.follow || false;
2178
+ const level = options.level || 'all';
2179
+ const comprehensive = options.comprehensive || false;
2180
+ if (comprehensive) {
2181
+ // Comprehensive diagnostics
2182
+ const commands = [
2183
+ 'systemctl status edgible-agent --no-pager || echo "Service not found"',
2184
+ 'hostname && uname -a',
2185
+ 'wg show || echo "WireGuard not configured"',
2186
+ 'haproxy -v 2>&1 || echo "HAProxy not found"',
2187
+ 'iptables -L -n -v | head -20 || echo "iptables not available"',
2188
+ 'df -h',
2189
+ 'free -h',
2190
+ 'uptime'
2191
+ ];
2192
+ let output = '';
2193
+ for (const cmd of commands) {
2194
+ try {
2195
+ const result = await awsService.executeSSHCommand(connection, cmd);
2196
+ output += `\n=== ${cmd} ===\n${result.stdout}\n`;
2197
+ }
2198
+ catch (error) {
2199
+ output += `\n=== ${cmd} ===\nError: ${error instanceof Error ? error.message : String(error)}\n`;
2200
+ }
2201
+ }
2202
+ return { success: true, logs: output };
2203
+ }
2204
+ // Regular logs
2205
+ const journalCommand = 'journalctl -u edgible-agent --no-pager';
2206
+ // Map log levels to journalctl priority levels
2207
+ // journalctl -p uses syslog priorities: emerg(0), alert(1), crit(2), err(3), warning(4), notice(5), info(6), debug(7)
2208
+ // Using -p shows that priority and above, so -p debug shows all logs including debug
2209
+ let levelFilter = '';
2210
+ if (level !== 'all') {
2211
+ // Map our level names to journalctl priority names
2212
+ const priorityMap = {
2213
+ 'error': 'err',
2214
+ 'warn': 'warning',
2215
+ 'info': 'info',
2216
+ 'debug': 'debug'
2217
+ };
2218
+ const journalPriority = priorityMap[level] || level;
2219
+ levelFilter = ` -p ${journalPriority}`;
2220
+ }
2221
+ else {
2222
+ // When level is 'all', explicitly include all priorities including debug
2223
+ // Some systems may filter debug logs by default, so we explicitly include them
2224
+ levelFilter = ' --priority=0..7';
2225
+ }
2226
+ const linesArg = follow ? '' : ` -n ${lines}`;
2227
+ if (follow) {
2228
+ // Follow logs in real-time
2229
+ const result = await awsService.executeSSHCommand(connection, journalCommand + levelFilter + ' -f');
2230
+ return { success: true, logs: result.stdout };
2231
+ }
2232
+ else {
2233
+ const result = await awsService.executeSSHCommand(connection, journalCommand + levelFilter + linesArg);
2234
+ return { success: true, logs: result.stdout };
2235
+ }
2236
+ }
2237
+ async getGatewayLogs(gatewayId, options = {}) {
2238
+ try {
2239
+ // Attempt auto re-login if tokens are missing but credentials are available
2240
+ await this.attemptAutoRelogin();
2241
+ const configManager = new config_1.ConfigManager();
2242
+ const gatewayInfo = configManager.getGateway(gatewayId);
2243
+ if (!gatewayInfo) {
2244
+ throw new Error('Gateway not found in local configuration');
2245
+ }
2246
+ // Load SSH key
2247
+ const awsService = new aws_1.AWSService(configManager.getAWSProfile(), gatewayInfo.region);
2248
+ const privateKey = awsService.loadSSHKey(gatewayInfo.keyPath);
2249
+ // Wait for instance to be ready for SSH
2250
+ const isReady = await awsService.waitForSSHReady(gatewayInfo.ec2InstanceId);
2251
+ if (!isReady) {
2252
+ throw new Error('Instance is not ready for SSH connections');
2253
+ }
2254
+ // If comprehensive mode, return diagnostics (this handles SSH connection internally)
2255
+ if (options.comprehensive) {
2256
+ const diagnostics = await this.getGatewayDiagnostics(gatewayId);
2257
+ let output = chalk_1.default.blue.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
2258
+ output += chalk_1.default.blue.bold(' GATEWAY DIAGNOSTICS\n');
2259
+ output += chalk_1.default.blue.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n');
2260
+ // Agent Status
2261
+ output += chalk_1.default.yellow.bold('📊 AGENT STATUS\n');
2262
+ output += chalk_1.default.gray('─'.repeat(80) + '\n');
2263
+ try {
2264
+ const status = JSON.parse(diagnostics.status);
2265
+ output += chalk_1.default.white(`Running: ${status.running ? chalk_1.default.green('✓ Yes') : chalk_1.default.red('✗ No')}\n`);
2266
+ output += chalk_1.default.white(`Health: ${status.health || 'unknown'}\n`);
2267
+ output += chalk_1.default.white(`Device ID: ${status.deviceId || 'N/A'}\n`);
2268
+ output += chalk_1.default.white(`Device Type: ${status.deviceType || 'N/A'}\n`);
2269
+ output += chalk_1.default.white(`Uptime: ${status.uptime ? Math.round(status.uptime / 1000 / 60) + ' minutes' : 'N/A'}\n`);
2270
+ output += chalk_1.default.white(`Last Poll: ${status.lastPoll || 'N/A'}\n`);
2271
+ output += chalk_1.default.white(`API Connected: ${status.apiConnected ? chalk_1.default.green('✓ Yes') : chalk_1.default.red('✗ No')}\n`);
2272
+ output += chalk_1.default.white(`Applications: ${status.applications?.length || 0}\n`);
2273
+ if (status.lastError) {
2274
+ output += chalk_1.default.red(`Last Error: ${status.lastError}\n`);
2275
+ }
2276
+ }
2277
+ catch {
2278
+ output += chalk_1.default.gray(diagnostics.status + '\n');
2279
+ }
2280
+ output += '\n';
2281
+ // System Information
2282
+ output += chalk_1.default.yellow.bold('🖥️ SYSTEM INFORMATION\n');
2283
+ output += chalk_1.default.gray('─'.repeat(80) + '\n');
2284
+ output += chalk_1.default.white(diagnostics.systemInfo + '\n');
2285
+ output += '\n';
2286
+ // Service Status
2287
+ output += chalk_1.default.yellow.bold('⚙️ SERVICE STATUS\n');
2288
+ output += chalk_1.default.gray('─'.repeat(80) + '\n');
2289
+ output += chalk_1.default.white(diagnostics.serviceStatus + '\n');
2290
+ output += '\n';
2291
+ // WireGuard Status
2292
+ output += chalk_1.default.yellow.bold('🔐 WIREGUARD STATUS\n');
2293
+ output += chalk_1.default.gray('─'.repeat(80) + '\n');
2294
+ output += chalk_1.default.white(diagnostics.wireguardStatus + '\n');
2295
+ output += '\n';
2296
+ // Network Information
2297
+ output += chalk_1.default.yellow.bold('🌐 NETWORK INFORMATION\n');
2298
+ output += chalk_1.default.gray('─'.repeat(80) + '\n');
2299
+ output += chalk_1.default.white(diagnostics.networkInfo + '\n');
2300
+ output += '\n';
2301
+ // Caddy Status
2302
+ output += chalk_1.default.yellow.bold('🚀 CADDY STATUS\n');
2303
+ output += chalk_1.default.gray('─'.repeat(80) + '\n');
2304
+ output += chalk_1.default.white(diagnostics.caddyStatus + '\n');
2305
+ output += '\n';
2306
+ // Log Paths Info (if available)
2307
+ if (diagnostics.logPaths && diagnostics.logPaths.trim() && !diagnostics.logPaths.includes('Checking log paths')) {
2308
+ output += chalk_1.default.yellow.bold('📁 LOG FILE PATHS\n');
2309
+ output += chalk_1.default.gray('─'.repeat(80) + '\n');
2310
+ output += chalk_1.default.white(diagnostics.logPaths + '\n');
2311
+ output += '\n';
2312
+ }
2313
+ // Agent Logs
2314
+ output += chalk_1.default.yellow.bold('📋 AGENT LOGS (Last 100 lines)\n');
2315
+ output += chalk_1.default.gray('─'.repeat(80) + '\n');
2316
+ if (diagnostics.agentLogs && diagnostics.agentLogs.trim() &&
2317
+ !diagnostics.agentLogs.includes('No logs available') &&
2318
+ !diagnostics.agentLogs.includes('Log file not found')) {
2319
+ output += chalk_1.default.white(diagnostics.agentLogs + '\n');
2320
+ }
2321
+ else {
2322
+ // Logs not found in file, try journalctl
2323
+ output += chalk_1.default.yellow('⚠ Log file not found. Using journalctl (systemd logs):\n');
2324
+ output += chalk_1.default.gray(' (Logs may only be available via systemd journal)\n\n');
2325
+ // The agentLogs command already tries journalctl as fallback, so it should have the logs
2326
+ // But if it still failed, show what we got
2327
+ if (diagnostics.agentLogs && diagnostics.agentLogs.trim()) {
2328
+ output += chalk_1.default.white(diagnostics.agentLogs + '\n');
2329
+ }
2330
+ else {
2331
+ output += chalk_1.default.red('✗ Could not retrieve logs from any source\n');
2332
+ }
2333
+ }
2334
+ output += '\n';
2335
+ output += chalk_1.default.blue.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
2336
+ return {
2337
+ logs: output,
2338
+ success: true
2339
+ };
2340
+ }
2341
+ // Create SSH connection for regular log retrieval
2342
+ const connection = {
2343
+ host: gatewayInfo.publicIp,
2344
+ port: 22,
2345
+ username: 'ec2-user',
2346
+ privateKey: privateKey
2347
+ };
2348
+ console.log(chalk_1.default.blue('Connecting to gateway...'));
2349
+ console.log(chalk_1.default.gray(`SSH Host: ${connection.host}:${connection.port}`));
2350
+ // Test SSH connection first
2351
+ console.log(chalk_1.default.gray('Testing SSH connection...'));
2352
+ try {
2353
+ await awsService.executeSSHCommand(connection, 'echo "SSH connection successful"');
2354
+ console.log(chalk_1.default.green('✓ SSH connection established'));
2355
+ }
2356
+ catch (sshError) {
2357
+ console.error(chalk_1.default.red('✗ SSH connection failed:'), sshError);
2358
+ throw new Error(`SSH connection failed: ${sshError instanceof Error ? sshError.message : 'Unknown error'}`);
2359
+ }
2360
+ // Build journalctl command
2361
+ let journalCommand = 'sudo journalctl -u edgible-agent.service --no-pager';
2362
+ if (options.lines) {
2363
+ journalCommand += ` -n ${options.lines}`;
2364
+ }
2365
+ if (options.level && options.level !== 'all') {
2366
+ journalCommand += ` --priority=${options.level}`;
2367
+ }
2368
+ console.log(chalk_1.default.gray('Retrieving agent logs...'));
2369
+ if (options.follow) {
2370
+ console.log(chalk_1.default.yellow('Following logs (press Ctrl+C to stop)...'));
2371
+ // For follow mode, we'll stream the logs
2372
+ const result = await awsService.executeSSHCommand(connection, journalCommand + ' -f');
2373
+ return {
2374
+ logs: result.stdout,
2375
+ success: true
2376
+ };
2377
+ }
2378
+ else {
2379
+ // For regular mode, get the logs and return
2380
+ const result = await awsService.executeSSHCommand(connection, journalCommand);
2381
+ return {
2382
+ logs: result.stdout,
2383
+ success: true
2384
+ };
2385
+ }
2386
+ }
2387
+ catch (error) {
2388
+ console.error(chalk_1.default.red('Error retrieving gateway logs:'), error);
2389
+ throw error;
2390
+ }
2391
+ }
2392
+ /**
2393
+ * Wipe agent log file on a gateway
2394
+ */
2395
+ async wipeGatewayLogs(gatewayId) {
2396
+ try {
2397
+ await this.attemptAutoRelogin();
2398
+ const configManager = new config_1.ConfigManager();
2399
+ const gatewayInfo = configManager.getGateway(gatewayId);
2400
+ if (!gatewayInfo) {
2401
+ throw new Error('Gateway not found in local configuration');
2402
+ }
2403
+ // Load SSH key
2404
+ const awsService = new aws_1.AWSService(configManager.getAWSProfile(), gatewayInfo.region);
2405
+ const privateKey = awsService.loadSSHKey(gatewayInfo.keyPath);
2406
+ // Wait for instance to be ready for SSH
2407
+ const isReady = await awsService.waitForSSHReady(gatewayInfo.ec2InstanceId);
2408
+ if (!isReady) {
2409
+ throw new Error('Instance is not ready for SSH connections');
2410
+ }
2411
+ // Create SSH connection
2412
+ const connection = {
2413
+ host: gatewayInfo.publicIp,
2414
+ port: 22,
2415
+ username: 'ec2-user',
2416
+ privateKey: privateKey
2417
+ };
2418
+ // Test SSH connection
2419
+ await awsService.executeSSHCommand(connection, 'echo "SSH connection successful"');
2420
+ // Log file locations (try both locations)
2421
+ const logPaths = [
2422
+ '/opt/edgible-agent/.edgible/agent/logs/agent.log',
2423
+ '~/.edgible/agent/logs/agent.log'
2424
+ ];
2425
+ let wipedFiles = [];
2426
+ let errors = [];
2427
+ for (const logPath of logPaths) {
2428
+ try {
2429
+ // Check if file exists and wipe it
2430
+ const checkResult = await awsService.executeSSHCommand(connection, `test -f ${logPath} && echo "EXISTS" || echo "NOT_FOUND"`);
2431
+ if (checkResult.stdout.trim() === 'EXISTS') {
2432
+ // Wipe the log file (truncate to 0 bytes)
2433
+ await awsService.executeSSHCommand(connection, `sudo truncate -s 0 ${logPath} || sudo > ${logPath}`);
2434
+ wipedFiles.push(logPath);
2435
+ console.log(chalk_1.default.green(` ✓ Cleared ${logPath}`));
2436
+ }
2437
+ }
2438
+ catch (error) {
2439
+ errors.push(`Failed to clear ${logPath}: ${error instanceof Error ? error.message : String(error)}`);
2440
+ }
2441
+ }
2442
+ // Also clear systemd journal logs if requested (optional)
2443
+ try {
2444
+ await awsService.executeSSHCommand(connection, 'sudo journalctl --vacuum-time=1s -u edgible-agent.service || true');
2445
+ console.log(chalk_1.default.gray(' ✓ Cleared systemd journal logs'));
2446
+ }
2447
+ catch (error) {
2448
+ // Ignore journalctl errors
2449
+ }
2450
+ if (wipedFiles.length === 0 && errors.length > 0) {
2451
+ return {
2452
+ success: false,
2453
+ message: `No log files found or cleared. Errors: ${errors.join('; ')}`
2454
+ };
2455
+ }
2456
+ return {
2457
+ success: true,
2458
+ logFile: wipedFiles.length > 0 ? wipedFiles[0] : undefined,
2459
+ message: wipedFiles.length > 0 ? `Cleared ${wipedFiles.length} log file(s)` : 'No log files found to clear'
2460
+ };
2461
+ }
2462
+ catch (error) {
2463
+ console.error(chalk_1.default.red('Error wiping gateway logs:'), error);
2464
+ throw error;
2465
+ }
2466
+ }
2467
+ /**
2468
+ * Wipe agent log file on a managed gateway
2469
+ */
2470
+ async wipeManagedGatewayLogs(gatewayId) {
2471
+ try {
2472
+ await this.attemptAutoRelogin();
2473
+ // Get managed gateway details
2474
+ const gatewayDetails = await this.getManagedGateway(gatewayId);
2475
+ if (!gatewayDetails.success || !gatewayDetails.gateway) {
2476
+ throw new Error('Managed gateway not found');
2477
+ }
2478
+ const gateway = gatewayDetails.gateway;
2479
+ const ipAddress = gateway.ipAddress;
2480
+ if (!ipAddress) {
2481
+ throw new Error('Gateway IP address not available');
2482
+ }
2483
+ // Get SSH key from backend
2484
+ const sshKeyResponse = await this.apiClient.getManagedGatewaySSHKey({ gatewayId });
2485
+ if (!sshKeyResponse.success || !sshKeyResponse.sshPrivateKey) {
2486
+ throw new Error('Failed to retrieve SSH key for managed gateway');
2487
+ }
2488
+ const sshPrivateKey = sshKeyResponse.sshPrivateKey;
2489
+ // Create SSH connection
2490
+ const connection = {
2491
+ host: ipAddress,
2492
+ port: 22,
2493
+ username: 'ec2-user',
2494
+ privateKey: sshPrivateKey
2495
+ };
2496
+ // Use AWSService to handle SSH operations
2497
+ const { AWSService } = await Promise.resolve().then(() => __importStar(require('./aws')));
2498
+ const awsService = new AWSService();
2499
+ // Test SSH connection
2500
+ await awsService.executeSSHCommand(connection, 'echo "SSH connection successful"');
2501
+ // Log file locations (try both locations)
2502
+ const logPaths = [
2503
+ '/opt/edgible-agent/.edgible/agent/logs/agent.log',
2504
+ '~/.edgible/agent/logs/agent.log'
2505
+ ];
2506
+ let wipedFiles = [];
2507
+ let errors = [];
2508
+ for (const logPath of logPaths) {
2509
+ try {
2510
+ // Check if file exists and wipe it
2511
+ const checkResult = await awsService.executeSSHCommand(connection, `test -f ${logPath} && echo "EXISTS" || echo "NOT_FOUND"`);
2512
+ if (checkResult.stdout.trim() === 'EXISTS') {
2513
+ // Wipe the log file (truncate to 0 bytes)
2514
+ await awsService.executeSSHCommand(connection, `sudo truncate -s 0 ${logPath} || sudo > ${logPath}`);
2515
+ wipedFiles.push(logPath);
2516
+ console.log(chalk_1.default.green(` ✓ Cleared ${logPath}`));
2517
+ }
2518
+ }
2519
+ catch (error) {
2520
+ errors.push(`Failed to clear ${logPath}: ${error instanceof Error ? error.message : String(error)}`);
2521
+ }
2522
+ }
2523
+ // Also clear systemd journal logs if requested (optional)
2524
+ try {
2525
+ await awsService.executeSSHCommand(connection, 'sudo journalctl --vacuum-time=1s -u edgible-agent.service || true');
2526
+ console.log(chalk_1.default.gray(' ✓ Cleared systemd journal logs'));
2527
+ }
2528
+ catch (error) {
2529
+ // Ignore journalctl errors
2530
+ }
2531
+ if (wipedFiles.length === 0 && errors.length > 0) {
2532
+ return {
2533
+ success: false,
2534
+ message: `No log files found or cleared. Errors: ${errors.join('; ')}`
2535
+ };
2536
+ }
2537
+ return {
2538
+ success: true,
2539
+ logFile: wipedFiles.length > 0 ? wipedFiles[0] : undefined,
2540
+ message: wipedFiles.length > 0 ? `Cleared ${wipedFiles.length} log file(s)` : 'No log files found to clear'
2541
+ };
2542
+ }
2543
+ catch (error) {
2544
+ console.error(chalk_1.default.red('Error wiping managed gateway logs:'), error);
2545
+ throw error;
2546
+ }
2547
+ }
2548
+ /**
2549
+ * Get applications for a specific gateway
2550
+ */
2551
+ async getGatewayApplications(gatewayId) {
2552
+ try {
2553
+ // Attempt auto re-login if tokens are missing but credentials are available
2554
+ await this.attemptAutoRelogin();
2555
+ const configManager = new config_1.ConfigManager();
2556
+ const userConfig = configManager.getConfig();
2557
+ if (!userConfig.organizationId) {
2558
+ throw new Error('No organization ID found. Please login first.');
2559
+ }
2560
+ // Get all applications from the organization
2561
+ const applicationsResponse = await this.apiClient.getOrganizationApplications(userConfig.organizationId);
2562
+ // Filter for applications that use this gateway
2563
+ const gatewayApplications = applicationsResponse.applications.filter((app) => app.deviceIds && app.deviceIds.includes(gatewayId));
2564
+ // Map API response to CLI Application format
2565
+ const applications = gatewayApplications.map((app) => ({
2566
+ id: app.id,
2567
+ name: app.name,
2568
+ workloadId: app.deviceIds[0] || 'unknown',
2569
+ servingIp: app.url ? new URL(app.url).hostname : 'unknown',
2570
+ port: app.url ? parseInt(new URL(app.url).port) || 80 : 80,
2571
+ protocol: app.url && app.url.startsWith('https') ? 'https' : 'http',
2572
+ status: app.status === 'running' ? 'active' : 'inactive',
2573
+ createdAt: app.createdAt,
2574
+ description: app.description
2575
+ }));
2576
+ return applications;
2577
+ }
2578
+ catch (error) {
2579
+ console.error(chalk_1.default.red('Error getting gateway applications:'), error);
2580
+ throw error;
2581
+ }
2582
+ }
2583
+ /**
2584
+ * Generate user data script for agent
2585
+ */
2586
+ generateAgentUserData(deviceId) {
2587
+ const systemAgentPath = PathResolver_1.PathResolver.getSystemDataPath() + '/agent';
2588
+ return `#!/bin/bash
2589
+ # Update system
2590
+ yum update -y
2591
+
2592
+ # Install required system dependencies
2593
+ # Install iptables (usually pre-installed, but ensure it's there)
2594
+ yum install -y iptables
2595
+
2596
+ # Install WireGuard
2597
+ amazon-linux-extras install epel -y || yum install -y epel-release
2598
+ yum install -y wireguard-tools
2599
+ modprobe wireguard || true
2600
+
2601
+ # Install Caddy
2602
+ yum install -y tar curl grep
2603
+ CADDY_VERSION=$(curl -s "https://api.github.com/repos/caddyserver/caddy/releases/latest" | grep -Po '"tag_name": "\\K[^"]*')
2604
+ curl -1sLf "https://github.com/caddyserver/caddy/releases/download/\${CADDY_VERSION}/caddy_\${CADDY_VERSION}_linux_amd64.tar.gz" | tar -C /usr/bin -xzf - caddy
2605
+ chmod +x /usr/bin/caddy
2606
+ setcap cap_net_bind_service=+ep /usr/bin/caddy
2607
+ # Ensure it's in PATH
2608
+ ln -sf /usr/bin/caddy /usr/local/bin/caddy || true
2609
+
2610
+ # Install Node.js
2611
+ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
2612
+ export NVM_DIR="$HOME/.nvm"
2613
+ [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
2614
+ nvm install 18
2615
+ nvm use 18
2616
+
2617
+ # Create system-wide symlink for Node.js (so root can access it)
2618
+ NODE_VERSION=$(nvm list | grep -oE 'v18\.[0-9]+\.[0-9]+' | head -1)
2619
+ if [ -n "$NODE_VERSION" ]; then
2620
+ sudo ln -sf "/home/ec2-user/.nvm/versions/node/$NODE_VERSION/bin/node" /usr/local/bin/node
2621
+ sudo ln -sf "/home/ec2-user/.nvm/versions/node/$NODE_VERSION/bin/npm" /usr/local/bin/npm
2622
+ fi
2623
+
2624
+ # Install PM2 for process management (install globally for root as well)
2625
+ npm install -g pm2
2626
+ sudo npm install -g pm2 || true
2627
+
2628
+ # Create directory for agent (owned by root)
2629
+ sudo mkdir -p /opt/edgible-agent
2630
+ cd /opt/edgible-agent
2631
+ sudo chown root:root /opt/edgible-agent
2632
+
2633
+ # Set up systemd service for agent (running as root)
2634
+ cat > /tmp/edgible-agent.service << 'EOF'
2635
+ [Unit]
2636
+ Description=Edgible Agent
2637
+ After=network.target
2638
+
2639
+ [Service]
2640
+ Type=simple
2641
+ User=root
2642
+ WorkingDirectory=/opt/edgible-agent
2643
+ ExecStart=/usr/local/bin/node index.js start -c /opt/edgible-agent/agent.config.json
2644
+ Restart=always
2645
+ RestartSec=10
2646
+ Environment=NODE_ENV=production
2647
+ Environment=EDGIBLE_CONFIG_PATH=${systemAgentPath}
2648
+ Environment=DEVICE_ID=${deviceId}
2649
+ Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
2650
+
2651
+ [Install]
2652
+ WantedBy=multi-user.target
2653
+ EOF
2654
+ sudo mv /tmp/edgible-agent.service /etc/systemd/system/edgible-agent.service
2655
+
2656
+ # Enable service
2657
+ systemctl enable edgible-agent.service
2658
+
2659
+ echo "Edgible Agent setup complete for device: ${deviceId}"
2660
+ `;
2661
+ }
2662
+ /**
2663
+ * Generate placeholder agent code
2664
+ */
2665
+ generateAgentCode(deviceId) {
2666
+ return `#!/usr/bin/env node
2667
+
2668
+ console.log('Edgible Agent starting...');
2669
+ console.log('Device ID:', process.env.DEVICE_ID || '${deviceId}');
2670
+
2671
+ // Placeholder agent code
2672
+ // In a real implementation, this would be the actual agent-v2 code
2673
+
2674
+ setInterval(() => {
2675
+ console.log('Agent heartbeat:', new Date().toISOString());
2676
+ }, 30000);
2677
+
2678
+ console.log('Edgible Agent started successfully');
2679
+ `;
2680
+ }
2681
+ // Enhanced Application Setup with Validation
2682
+ /**
2683
+ * Enhanced setup with full validation
2684
+ */
2685
+ async setupApplicationWithValidation(workload, port, protocol = 'http', description, gatewayId) {
2686
+ const startTime = Date.now();
2687
+ const warnings = [];
2688
+ const errors = [];
2689
+ try {
2690
+ // Attempt auto re-login if tokens are missing but credentials are available
2691
+ await this.attemptAutoRelogin();
2692
+ console.log(chalk_1.default.blue('Setting up application with validation...'));
2693
+ // Step 1: Check local agent status
2694
+ console.log(chalk_1.default.gray('Checking local agent status...'));
2695
+ const localAgentManager = new LocalAgentManager_1.LocalAgentManager();
2696
+ let localAgentStatus = await localAgentManager.checkLocalAgentStatus();
2697
+ if (!localAgentStatus.installed) {
2698
+ warnings.push('Local serving agent is not installed');
2699
+ console.log(chalk_1.default.yellow('⚠ Local serving agent not found. Installing...'));
2700
+ const installResult = await localAgentManager.installLocalAgent({
2701
+ autoStart: true
2702
+ });
2703
+ if (!installResult.success) {
2704
+ errors.push(`Failed to install local agent: ${installResult.error}`);
2705
+ }
2706
+ else {
2707
+ console.log(chalk_1.default.green('✓ Local agent installed successfully'));
2708
+ // Wait and verify the agent is running
2709
+ console.log(chalk_1.default.gray('Verifying agent is running...'));
2710
+ let retries = 0;
2711
+ const maxRetries = 5;
2712
+ let isRunning = false;
2713
+ while (retries < maxRetries && !isRunning) {
2714
+ await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second
2715
+ localAgentStatus = await localAgentManager.checkLocalAgentStatus();
2716
+ isRunning = localAgentStatus.running;
2717
+ retries++;
2718
+ if (!isRunning) {
2719
+ console.log(chalk_1.default.gray(`Waiting for agent to start... (${retries}/${maxRetries})`));
2720
+ }
2721
+ }
2722
+ if (!isRunning) {
2723
+ warnings.push('Local serving agent installation completed but may not be running');
2724
+ }
2725
+ }
2726
+ }
2727
+ else if (!localAgentStatus.running) {
2728
+ warnings.push('Local serving agent is not running');
2729
+ console.log(chalk_1.default.yellow('⚠ Starting local serving agent...'));
2730
+ const started = await localAgentManager.startLocalAgent();
2731
+ if (!started) {
2732
+ errors.push('Failed to start local serving agent');
2733
+ }
2734
+ else {
2735
+ console.log(chalk_1.default.green('✓ Local serving agent started'));
2736
+ // Verify the agent is actually running
2737
+ localAgentStatus = await localAgentManager.checkLocalAgentStatus();
2738
+ if (!localAgentStatus.running) {
2739
+ warnings.push('Agent start command succeeded but agent may not be running');
2740
+ }
2741
+ }
2742
+ }
2743
+ else {
2744
+ console.log(chalk_1.default.green('✓ Local serving agent is running'));
2745
+ }
2746
+ // Final status check
2747
+ if (errors.length === 0 && !localAgentStatus.running) {
2748
+ warnings.push('Local serving agent may not be running properly');
2749
+ }
2750
+ // Step 2: Create application
2751
+ console.log(chalk_1.default.gray('Creating application...'));
2752
+ const application = await this.setupApplication(workload, port, protocol, description, gatewayId);
2753
+ // Step 3: Configure local agent for application
2754
+ console.log(chalk_1.default.gray('Configuring local agent for application...'));
2755
+ const configResult = await localAgentManager.configureAgentForApplication(application);
2756
+ if (!configResult.success) {
2757
+ warnings.push(`Failed to configure agent: ${configResult.error}`);
2758
+ }
2759
+ // Step 4: Test connectivity
2760
+ console.log(chalk_1.default.gray('Testing connectivity...'));
2761
+ const connectivityTester = new ConnectivityTester_1.ConnectivityTester();
2762
+ let connectivityTest;
2763
+ if (gatewayId) {
2764
+ // Get gateway information
2765
+ const gatewaysResponse = await this.listGateways();
2766
+ const gateway = gatewaysResponse.gateways.find((g) => g.device.id === gatewayId);
2767
+ if (gateway) {
2768
+ const gatewayInfo = {
2769
+ id: gateway.device.id,
2770
+ name: gateway.device.name,
2771
+ publicIp: gateway.ec2Instance?.publicIp || '',
2772
+ privateIp: gateway.ec2Instance?.privateIp,
2773
+ region: gateway.ec2Instance?.region || 'us-east-1',
2774
+ status: gateway.device.status,
2775
+ deviceId: gateway.device.id,
2776
+ ec2InstanceId: gateway.ec2Instance?.instanceId
2777
+ };
2778
+ connectivityTest = await connectivityTester.testGatewayToWorkloadConnectivity(gatewayInfo, workload, port);
2779
+ }
2780
+ else {
2781
+ // Fallback connectivity test without gateway
2782
+ const portTest = await connectivityTester.testPortConnectivity(workload.ipAddress || 'localhost', port, 'tcp', 5000);
2783
+ connectivityTest = {
2784
+ overall: portTest.success,
2785
+ tests: [{
2786
+ success: portTest.success,
2787
+ applicationId: application.id,
2788
+ gatewayIp: 'localhost',
2789
+ workloadIp: workload.ipAddress || 'localhost',
2790
+ port,
2791
+ protocol: 'tcp',
2792
+ testType: 'connectivity',
2793
+ host: workload.ipAddress || 'localhost',
2794
+ latency: portTest.latency,
2795
+ error: portTest.error,
2796
+ timestamp: portTest.timestamp,
2797
+ url: `tcp://${workload.ipAddress || 'localhost'}:${port}`,
2798
+ method: 'CONNECT'
2799
+ }],
2800
+ diagnostics: {
2801
+ networkIssues: [],
2802
+ configurationIssues: [],
2803
+ serviceIssues: [],
2804
+ suggestions: []
2805
+ },
2806
+ recommendations: [],
2807
+ timestamp: new Date(),
2808
+ duration: 0
2809
+ };
2810
+ }
2811
+ }
2812
+ else {
2813
+ // Test local connectivity only
2814
+ const portTest = await connectivityTester.testPortConnectivity(workload.ipAddress || 'localhost', port, 'tcp', 5000);
2815
+ connectivityTest = {
2816
+ overall: portTest.success,
2817
+ tests: [{
2818
+ success: portTest.success,
2819
+ applicationId: application.id,
2820
+ gatewayIp: 'localhost',
2821
+ workloadIp: workload.ipAddress || 'localhost',
2822
+ port,
2823
+ protocol: 'tcp',
2824
+ testType: 'connectivity',
2825
+ host: workload.ipAddress || 'localhost',
2826
+ latency: portTest.latency,
2827
+ error: portTest.error,
2828
+ timestamp: portTest.timestamp,
2829
+ url: `tcp://${workload.ipAddress || 'localhost'}:${port}`,
2830
+ method: 'CONNECT'
2831
+ }],
2832
+ diagnostics: {
2833
+ networkIssues: [],
2834
+ configurationIssues: [],
2835
+ serviceIssues: [],
2836
+ suggestions: []
2837
+ },
2838
+ recommendations: [],
2839
+ timestamp: new Date(),
2840
+ duration: 0
2841
+ };
2842
+ }
2843
+ if (!connectivityTest.overall) {
2844
+ errors.push('Connectivity test failed');
2845
+ console.log(chalk_1.default.red('✗ Connectivity test failed'));
2846
+ connectivityTest.recommendations.forEach(rec => {
2847
+ console.log(chalk_1.default.yellow(` • ${rec}`));
2848
+ });
2849
+ }
2850
+ else {
2851
+ console.log(chalk_1.default.green('✓ Connectivity test passed'));
2852
+ }
2853
+ // Step 5: Health validation
2854
+ console.log(chalk_1.default.gray('Performing health validation...'));
2855
+ const healthValidation = await this.getApplicationHealth(application.id);
2856
+ if (healthValidation.overall !== 'healthy') {
2857
+ warnings.push('Application health check failed');
2858
+ console.log(chalk_1.default.yellow('⚠ Application health check failed'));
2859
+ }
2860
+ else {
2861
+ console.log(chalk_1.default.green('✓ Application health check passed'));
2862
+ }
2863
+ // Step 6: Final status
2864
+ const setupComplete = errors.length === 0;
2865
+ const duration = Date.now() - startTime;
2866
+ if (setupComplete) {
2867
+ console.log(chalk_1.default.green('\n✓ Application setup completed successfully!'));
2868
+ }
2869
+ else {
2870
+ console.log(chalk_1.default.red('\n✗ Application setup completed with errors'));
2871
+ }
2872
+ return {
2873
+ application: application,
2874
+ localAgentStatus: localAgentStatus,
2875
+ connectivityTest,
2876
+ healthValidation,
2877
+ setupComplete,
2878
+ warnings,
2879
+ errors,
2880
+ duration,
2881
+ timestamp: new Date()
2882
+ };
2883
+ }
2884
+ catch (error) {
2885
+ const duration = Date.now() - startTime;
2886
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
2887
+ return {
2888
+ application: {},
2889
+ localAgentStatus: {
2890
+ running: false,
2891
+ health: 'unknown',
2892
+ uptime: 0,
2893
+ version: 'unknown',
2894
+ timestamp: Date.now(),
2895
+ deviceId: '',
2896
+ deviceType: 'serving',
2897
+ apiConnected: false,
2898
+ apiAuthenticated: false,
2899
+ applications: [],
2900
+ configurationValid: false,
2901
+ installed: false,
2902
+ lastError: errorMessage,
2903
+ pid: 0
2904
+ },
2905
+ connectivityTest: {
2906
+ overall: false,
2907
+ tests: [],
2908
+ diagnostics: {
2909
+ networkIssues: [],
2910
+ configurationIssues: [],
2911
+ serviceIssues: [],
2912
+ suggestions: []
2913
+ },
2914
+ recommendations: [],
2915
+ timestamp: new Date(),
2916
+ duration: 0
2917
+ },
2918
+ healthValidation: {
2919
+ overall: 'unknown',
2920
+ checks: [],
2921
+ lastChecked: new Date(),
2922
+ uptime: 0,
2923
+ version: 'unknown'
2924
+ },
2925
+ setupComplete: false,
2926
+ warnings,
2927
+ errors: [errorMessage],
2928
+ duration,
2929
+ timestamp: new Date()
2930
+ };
2931
+ }
2932
+ }
2933
+ /**
2934
+ * Test application after setup
2935
+ */
2936
+ async validateApplicationSetup(application) {
2937
+ const checks = [];
2938
+ const recommendations = [];
2939
+ try {
2940
+ // Check 1: Application exists
2941
+ checks.push({
2942
+ name: 'application_exists',
2943
+ status: application.id ? 'pass' : 'fail',
2944
+ message: application.id ? 'Application exists' : 'Application not found',
2945
+ duration: 0
2946
+ });
2947
+ // Check 2: Port connectivity
2948
+ const connectivityTester = new ConnectivityTester_1.ConnectivityTester();
2949
+ const portTest = await connectivityTester.testPortConnectivity(application.servingIp, application.port, 'tcp', 5000);
2950
+ checks.push({
2951
+ name: 'port_connectivity',
2952
+ status: portTest.success ? 'pass' : 'fail',
2953
+ message: portTest.success ? 'Port is accessible' : `Port connectivity failed: ${portTest.error}`,
2954
+ duration: portTest.latency || 0
2955
+ });
2956
+ // Check 3: HTTP endpoint (if applicable)
2957
+ if (application.protocol === 'http' || application.protocol === 'https') {
2958
+ const url = `${application.protocol}://${application.servingIp}:${application.port}`;
2959
+ const httpTest = await connectivityTester.testHttpEndpoint(url, 200, 5000);
2960
+ checks.push({
2961
+ name: 'http_endpoint',
2962
+ status: httpTest.success ? 'pass' : 'fail',
2963
+ message: httpTest.success ? 'HTTP endpoint is responding' : `HTTP endpoint failed: ${httpTest.error}`,
2964
+ duration: httpTest.responseTime || 0
2965
+ });
2966
+ }
2967
+ // Check 4: Local agent status
2968
+ const localAgentManager = new LocalAgentManager_1.LocalAgentManager();
2969
+ const agentStatus = await localAgentManager.checkLocalAgentStatus();
2970
+ checks.push({
2971
+ name: 'local_agent',
2972
+ status: agentStatus.running ? 'pass' : 'warn',
2973
+ message: agentStatus.running ? 'Local agent is running' : 'Local agent is not running',
2974
+ duration: 0
2975
+ });
2976
+ // Generate recommendations
2977
+ if (!portTest.success) {
2978
+ recommendations.push('Check if the workload service is running and listening on the correct port');
2979
+ recommendations.push('Verify firewall settings allow connections to the port');
2980
+ }
2981
+ if (agentStatus.installed && !agentStatus.running) {
2982
+ recommendations.push('Start the local serving agent');
2983
+ }
2984
+ const overall = checks.every(c => c.status === 'pass') ? 'pass' :
2985
+ checks.some(c => c.status === 'fail') ? 'fail' : 'warn';
2986
+ return {
2987
+ success: overall === 'pass',
2988
+ checks,
2989
+ overall,
2990
+ recommendations
2991
+ };
2992
+ }
2993
+ catch (error) {
2994
+ return {
2995
+ success: false,
2996
+ checks: [{
2997
+ name: 'validation_error',
2998
+ status: 'fail',
2999
+ message: error instanceof Error ? error.message : 'Validation failed',
3000
+ duration: 0
3001
+ }],
3002
+ overall: 'fail',
3003
+ recommendations: ['Check application configuration and try again']
3004
+ };
3005
+ }
3006
+ }
3007
+ /**
3008
+ * Get application health status
3009
+ */
3010
+ async getApplicationHealth(applicationId) {
3011
+ try {
3012
+ const localAgentManager = new LocalAgentManager_1.LocalAgentManager();
3013
+ return await localAgentManager.monitorAgentHealth();
3014
+ }
3015
+ catch (error) {
3016
+ return {
3017
+ overall: 'unknown',
3018
+ checks: [{
3019
+ name: 'health_check_error',
3020
+ status: 'fail',
3021
+ message: error instanceof Error ? error.message : 'Health check failed',
3022
+ timestamp: new Date()
3023
+ }],
3024
+ lastChecked: new Date(),
3025
+ uptime: 0,
3026
+ version: 'unknown'
3027
+ };
3028
+ }
3029
+ }
3030
+ /**
3031
+ * Create application programmatically without interactive prompts
3032
+ */
3033
+ async createApplicationProgrammatically(params) {
3034
+ try {
3035
+ // Attempt auto re-login if tokens are missing but credentials are available
3036
+ await this.attemptAutoRelogin();
3037
+ const config = this.configManager.getConfig();
3038
+ if (!config.organizationId) {
3039
+ throw new Error('Not logged in. Please run "edgible login" first.');
3040
+ }
3041
+ const createRequest = {
3042
+ name: params.name,
3043
+ description: params.description,
3044
+ organizationId: config.organizationId,
3045
+ configuration: { port: params.port, protocol: params.protocol },
3046
+ hostnames: params.hostnames,
3047
+ deviceIds: params.deviceIds,
3048
+ gatewayIds: params.gatewayIds,
3049
+ subtype: params.subtype
3050
+ };
3051
+ const response = await this.apiClient.createApplication(createRequest);
3052
+ return response.application;
3053
+ }
3054
+ catch (error) {
3055
+ throw new Error(`Failed to create application: ${error instanceof Error ? error.message : 'Unknown error'}`);
3056
+ }
3057
+ }
3058
+ }
3059
+ exports.EdgibleService = EdgibleService;
3060
+ //# sourceMappingURL=edgible.js.map