@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,2216 @@
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.LocalAgentManager = void 0;
40
+ const DependencyInstaller_1 = require("./DependencyInstaller");
41
+ const child_process_1 = require("child_process");
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const os = __importStar(require("os"));
45
+ const PathResolver_1 = require("../utils/PathResolver");
46
+ const util_1 = require("util");
47
+ const chalk_1 = __importDefault(require("chalk"));
48
+ const find_process_1 = __importDefault(require("find-process"));
49
+ const fs_1 = require("fs");
50
+ const urls_1 = require("../utils/urls");
51
+ const config_1 = require("../state/config");
52
+ const AgentStatusManager_1 = require("./AgentStatusManager");
53
+ const DaemonManagerFactory_1 = require("./daemon/DaemonManagerFactory");
54
+ const writeFile = (0, util_1.promisify)(fs.writeFile);
55
+ const readFile = (0, util_1.promisify)(fs.readFile);
56
+ const mkdir = (0, util_1.promisify)(fs.mkdir);
57
+ class LocalAgentManager {
58
+ constructor() {
59
+ this.agentProcess = null;
60
+ // Get user config to determine installation type
61
+ const tempConfigManager = new config_1.ConfigManager();
62
+ const userConfig = tempConfigManager.getConfig();
63
+ // Resolve agent config path based on installation type
64
+ this.configPath = PathResolver_1.PathResolver.resolveAgentConfigPath(userConfig.agentInstallationType);
65
+ // Check for the actual agent binary location
66
+ const possibleAgentPaths = [
67
+ path.join(this.configPath, 'index.js'), // S3 distribution location
68
+ path.join(this.configPath, 'dist/index.js'), // Alternative build location
69
+ path.join(this.configPath, 'agent') // Legacy location
70
+ ];
71
+ this.agentPath = possibleAgentPaths.find(p => fs.existsSync(p)) || possibleAgentPaths[0];
72
+ this.serviceName = 'edgible-agent';
73
+ this.statePath = path.join(this.configPath, 'state.json');
74
+ this.configManager = new config_1.ConfigManager();
75
+ this.statusManager = new AgentStatusManager_1.AgentStatusManager();
76
+ }
77
+ /**
78
+ * Update agent configuration with current CLI device credentials
79
+ * @param configPath Optional config path to use. If not provided, uses the instance's configPath.
80
+ */
81
+ async updateAgentConfig(configPath) {
82
+ try {
83
+ // Get current device credentials from CLI config
84
+ const cliConfig = this.configManager.getConfig();
85
+ if (!cliConfig.deviceId || !cliConfig.devicePassword) {
86
+ throw new Error('No device credentials found in CLI config');
87
+ }
88
+ // Create updated agent-v2 configuration
89
+ const agentV2Config = {
90
+ deviceId: cliConfig.deviceId,
91
+ devicePassword: cliConfig.devicePassword,
92
+ deviceType: cliConfig.deviceType || 'serving',
93
+ apiBaseUrl: (0, urls_1.getApiBaseUrl)(),
94
+ organizationId: cliConfig.organizationId,
95
+ firewallEnabled: true,
96
+ pollingInterval: 60000,
97
+ healthCheckTimeout: 5000,
98
+ maxRetries: 3,
99
+ logLevel: 'info',
100
+ updateEnabled: true,
101
+ updateCheckInterval: 3600000,
102
+ wireguardMode: cliConfig.wireguardMode || 'kernel',
103
+ wireguardGoBinary: cliConfig.wireguardGoBinary || 'wireguard-go'
104
+ };
105
+ // Use provided configPath or fall back to instance configPath
106
+ const targetConfigPath = configPath || this.configPath;
107
+ // Ensure the config directory exists
108
+ if (!fs.existsSync(targetConfigPath)) {
109
+ await mkdir(targetConfigPath, { recursive: true });
110
+ }
111
+ const agentV2ConfigPath = path.join(targetConfigPath, 'agent.config.json');
112
+ await writeFile(agentV2ConfigPath, JSON.stringify(agentV2Config, null, 2));
113
+ console.log('✓ Agent configuration updated with current device credentials');
114
+ }
115
+ catch (error) {
116
+ console.error('Failed to update agent configuration:', error);
117
+ throw error;
118
+ }
119
+ }
120
+ /**
121
+ * Check if agent is running in Docker
122
+ */
123
+ async isDockerAgentRunning(deviceId) {
124
+ try {
125
+ const status = await this.checkDockerAgentStatus(deviceId);
126
+ return status.running;
127
+ }
128
+ catch (error) {
129
+ return false;
130
+ }
131
+ }
132
+ /**
133
+ * Check if local agent is installed and running using file-based status
134
+ */
135
+ async checkLocalAgentStatus() {
136
+ try {
137
+ // First check if agent is running in Docker
138
+ const cliConfig = this.configManager.getConfig();
139
+ if (cliConfig.deviceId) {
140
+ const dockerStatus = await this.checkDockerAgentStatus(cliConfig.deviceId);
141
+ if (dockerStatus.running) {
142
+ return {
143
+ pid: 0,
144
+ running: true,
145
+ health: 'healthy',
146
+ lastPoll: new Date().toISOString(),
147
+ uptime: 0,
148
+ version: 'docker',
149
+ timestamp: Date.now(),
150
+ deviceId: cliConfig.deviceId,
151
+ deviceType: cliConfig.deviceType || 'serving',
152
+ apiConnected: false,
153
+ apiAuthenticated: false,
154
+ applications: [],
155
+ configurationValid: true,
156
+ installed: true,
157
+ lastError: ''
158
+ };
159
+ }
160
+ }
161
+ // Check if agent binary exists
162
+ const installed = fs.existsSync(this.agentPath);
163
+ if (!installed) {
164
+ return {
165
+ pid: 0,
166
+ running: false,
167
+ health: 'unknown',
168
+ lastPoll: new Date().toISOString(),
169
+ uptime: 0,
170
+ version: 'unknown',
171
+ timestamp: Date.now(),
172
+ deviceId: '',
173
+ deviceType: 'serving',
174
+ apiConnected: false,
175
+ apiAuthenticated: false,
176
+ applications: [],
177
+ configurationValid: false,
178
+ installed: false,
179
+ lastError: 'Agent not installed'
180
+ };
181
+ }
182
+ // Get status from file-based status manager
183
+ const statusResult = await this.statusManager.getStatus();
184
+ if (!statusResult.isValid) {
185
+ // Fallback to process-based checking if file status is invalid
186
+ const processRunning = await this.isProcessRunning();
187
+ return {
188
+ pid: 0,
189
+ running: processRunning,
190
+ health: 'unknown',
191
+ lastPoll: new Date().toISOString(),
192
+ uptime: 0,
193
+ version: 'unknown',
194
+ timestamp: Date.now(),
195
+ deviceId: '',
196
+ deviceType: 'serving',
197
+ apiConnected: false,
198
+ apiAuthenticated: false,
199
+ applications: [],
200
+ configurationValid: false,
201
+ installed: true,
202
+ lastError: statusResult.error || 'Status file invalid'
203
+ };
204
+ }
205
+ const fileStatus = statusResult.status;
206
+ // Return the unified status directly
207
+ return fileStatus;
208
+ }
209
+ catch (error) {
210
+ // Fallback to basic process checking
211
+ const processRunning = await this.isProcessRunning();
212
+ return {
213
+ pid: 0,
214
+ running: processRunning,
215
+ health: 'unknown',
216
+ lastPoll: new Date().toISOString(),
217
+ uptime: 0,
218
+ version: 'unknown',
219
+ timestamp: Date.now(),
220
+ deviceId: '',
221
+ deviceType: 'serving',
222
+ apiConnected: false,
223
+ apiAuthenticated: false,
224
+ applications: [],
225
+ configurationValid: false,
226
+ installed: fs.existsSync(this.agentPath),
227
+ lastError: error instanceof Error ? error.message : 'Unknown error'
228
+ };
229
+ }
230
+ }
231
+ /**
232
+ * Check if agent process is running (cross-platform method)
233
+ * Prioritizes status file PID validation over process detection
234
+ */
235
+ async isProcessRunning() {
236
+ try {
237
+ if (this.agentProcess && !this.agentProcess.killed) {
238
+ return true;
239
+ }
240
+ // First, try to get PID from status file and validate it's actually running
241
+ try {
242
+ const statusResult = await this.statusManager.getStatus();
243
+ if (statusResult.isValid && statusResult.status?.pid) {
244
+ const pid = statusResult.status.pid;
245
+ // Validate that this PID is actually a running process
246
+ const list = await (0, find_process_1.default)('pid', pid);
247
+ if (list.length > 0) {
248
+ return true;
249
+ }
250
+ }
251
+ }
252
+ catch (error) {
253
+ // Status file check failed, continue to fallback detection
254
+ }
255
+ // Fallback to process detection
256
+ const list = await (0, find_process_1.default)('name', this.agentPath, true);
257
+ return list.length > 0;
258
+ }
259
+ catch (error) {
260
+ return false;
261
+ }
262
+ }
263
+ /**
264
+ * Check if a specific PID is currently running
265
+ */
266
+ async isPidRunning(pid) {
267
+ try {
268
+ const list = await (0, find_process_1.default)('pid', pid);
269
+ return list.length > 0;
270
+ }
271
+ catch (error) {
272
+ return false;
273
+ }
274
+ }
275
+ /**
276
+ * Validate agent configuration before starting
277
+ */
278
+ async validateAgentConfig() {
279
+ const errors = [];
280
+ const cliConfig = this.configManager.getConfig();
281
+ // Check required fields
282
+ if (!cliConfig.deviceId) {
283
+ errors.push('Device ID is required');
284
+ }
285
+ if (!cliConfig.devicePassword) {
286
+ errors.push('Device password is required');
287
+ }
288
+ // Check API URL
289
+ // API base URL is now hardcoded based on stage, no validation needed
290
+ // Check agent binary exists
291
+ if (!fs.existsSync(this.agentPath)) {
292
+ errors.push('Agent binary not found. Run "edgible agent install" first.');
293
+ }
294
+ // Note: Ports are handled dynamically by the agent, no need to check specific ports
295
+ return {
296
+ valid: errors.length === 0,
297
+ errors
298
+ };
299
+ }
300
+ /**
301
+ * Install local serving agent
302
+ */
303
+ async installLocalAgent(options = {}) {
304
+ try {
305
+ const warnings = [];
306
+ // Determine correct config path based on installation type
307
+ const targetConfigPath = options.installationType
308
+ ? PathResolver_1.PathResolver.resolveAgentConfigPath(options.installationType)
309
+ : this.configPath;
310
+ // Create agent directory
311
+ await mkdir(targetConfigPath, { recursive: true });
312
+ // Determine platform
313
+ const platform = options.platform || this.detectPlatform();
314
+ // Download and install agent based on platform
315
+ const installResult = await this.installAgentForPlatform(platform, options, targetConfigPath);
316
+ if (!installResult.success) {
317
+ return {
318
+ success: false,
319
+ error: installResult.error,
320
+ warnings
321
+ };
322
+ }
323
+ // Update instance paths to reflect the new installation location
324
+ this.configPath = targetConfigPath;
325
+ this.agentPath = path.join(targetConfigPath, 'index.js');
326
+ this.statePath = path.join(targetConfigPath, 'state.json');
327
+ // Create default configuration
328
+ try {
329
+ await this.createDefaultConfig();
330
+ }
331
+ catch (error) {
332
+ warnings.push('Failed to create default configuration');
333
+ }
334
+ // Set up service (if not Docker)
335
+ if (platform !== 'docker') {
336
+ const serviceResult = await this.setupService(options);
337
+ if (!serviceResult.success) {
338
+ warnings.push('Failed to set up service');
339
+ }
340
+ }
341
+ // Start agent if autoStart is enabled
342
+ if (options.autoStart !== false) {
343
+ const startResult = await this.startLocalAgent();
344
+ if (!startResult) {
345
+ warnings.push('Failed to start agent automatically');
346
+ }
347
+ }
348
+ return {
349
+ success: true,
350
+ version: installResult.version,
351
+ path: targetConfigPath,
352
+ warnings
353
+ };
354
+ }
355
+ catch (error) {
356
+ return {
357
+ success: false,
358
+ error: error instanceof Error ? error.message : 'Installation failed',
359
+ warnings: []
360
+ };
361
+ }
362
+ }
363
+ /**
364
+ * Start local agent service
365
+ */
366
+ async startLocalAgent(options = {}) {
367
+ try {
368
+ // If Docker mode is requested, use Docker instead
369
+ if (options.docker) {
370
+ return await this.spawnDockerAgent(options);
371
+ }
372
+ // Check if agent is installed as a daemon (systemd, launchd, etc.)
373
+ const userConfig = this.configManager.getConfig();
374
+ const daemonManager = DaemonManagerFactory_1.DaemonManagerFactory.fromConfig(userConfig.agentInstallationType);
375
+ if (daemonManager) {
376
+ // Agent is installed as a daemon, use daemon manager to start it
377
+ if (options.debug) {
378
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Using daemon manager for installation type: ${userConfig.agentInstallationType}`));
379
+ }
380
+ // Update agent config with current CLI credentials before starting
381
+ await this.updateAgentConfig();
382
+ // Check if already running
383
+ try {
384
+ const status = await daemonManager.status();
385
+ if (status.running) {
386
+ console.log(chalk_1.default.yellow('Agent is already running'));
387
+ return true;
388
+ }
389
+ }
390
+ catch (error) {
391
+ // Status check failed, continue with start attempt
392
+ if (options.debug) {
393
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Status check failed, continuing: ${error instanceof Error ? error.message : String(error)}`));
394
+ }
395
+ }
396
+ // Start using daemon manager
397
+ try {
398
+ await daemonManager.start();
399
+ console.log(chalk_1.default.green('✓ Agent started via daemon'));
400
+ // Wait a moment for agent to initialize
401
+ await new Promise(resolve => setTimeout(resolve, 2000));
402
+ // Verify it's actually running
403
+ try {
404
+ const status = await daemonManager.status();
405
+ if (!status.running) {
406
+ console.log(chalk_1.default.yellow('⚠ Agent service started but may not be running yet'));
407
+ if (userConfig.agentInstallationType === 'systemd') {
408
+ console.log(chalk_1.default.gray(' Check logs with: journalctl -u edgible-agent -n 50'));
409
+ }
410
+ }
411
+ }
412
+ catch (statusError) {
413
+ // Status check failed, but start succeeded, so continue
414
+ if (options.debug) {
415
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Status check after start failed: ${statusError instanceof Error ? statusError.message : String(statusError)}`));
416
+ }
417
+ }
418
+ return true;
419
+ }
420
+ catch (error) {
421
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
422
+ // Provide helpful error messages for systemd
423
+ if (userConfig.agentInstallationType === 'systemd') {
424
+ console.error(chalk_1.default.red(`\n✗ Failed to start systemd service: ${errorMessage}`));
425
+ console.log(chalk_1.default.yellow('\nTroubleshooting:'));
426
+ console.log(chalk_1.default.gray(' 1. Check service status: sudo systemctl status edgible-agent'));
427
+ console.log(chalk_1.default.gray(' 2. Check service logs: sudo journalctl -u edgible-agent -n 50'));
428
+ console.log(chalk_1.default.gray(' 3. Verify service file: cat /etc/systemd/system/edgible-agent.service'));
429
+ }
430
+ throw new Error(`Failed to start agent via daemon: ${errorMessage}`);
431
+ }
432
+ }
433
+ // Fall back to direct process spawning for non-daemon installations
434
+ // Warn if running without root on Linux/Unix (WireGuard and iptables typically require root)
435
+ const platform = os.platform();
436
+ if (!options.root && platform !== 'win32') {
437
+ console.log(chalk_1.default.yellow('⚠ Warning: Running without root privileges.'));
438
+ console.log(chalk_1.default.yellow('⚠ WireGuard and iptables management may not work without sudo.'));
439
+ console.log(chalk_1.default.yellow('⚠ Use --root flag if you need these features.'));
440
+ console.log('');
441
+ }
442
+ const dependencyInstaller = new DependencyInstaller_1.DependencyInstaller();
443
+ await dependencyInstaller.checkAndInstallDependencies();
444
+ // Validate configuration first
445
+ const validation = await this.validateAgentConfig();
446
+ if (!validation.valid) {
447
+ throw new Error(`Configuration validation failed:\n${validation.errors.map(e => ` - ${e}`).join('\n')}`);
448
+ }
449
+ // Check if already running
450
+ const isRunning = await this.isProcessRunning();
451
+ if (isRunning) {
452
+ console.log(chalk_1.default.yellow('Agent is already running'));
453
+ return true;
454
+ }
455
+ // Log debug information
456
+ await this.logDebugInfo(options);
457
+ // Start agent with better error handling
458
+ const started = await this.spawnAgentProcess(options);
459
+ if (!started) {
460
+ throw new Error('Failed to start agent process - check logs for details');
461
+ }
462
+ // Wait for status with better timeout handling
463
+ await this.waitForAgentReady(options.debug);
464
+ return true;
465
+ }
466
+ catch (error) {
467
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
468
+ console.error(chalk_1.default.red('Failed to start agent:'), errorMessage);
469
+ // Provide specific troubleshooting steps
470
+ this.provideTroubleshootingSteps(errorMessage);
471
+ return false;
472
+ }
473
+ }
474
+ /**
475
+ * Spawn the agent process
476
+ */
477
+ async spawnAgentProcess(options) {
478
+ try {
479
+ // Check if agent is already running
480
+ if (this.agentProcess && !this.agentProcess.killed) {
481
+ return true;
482
+ }
483
+ // Check if agent binary exists
484
+ if (!fs.existsSync(this.agentPath)) {
485
+ throw new Error('Agent not installed. Run "edgible agent install" first.');
486
+ }
487
+ // Update agent configuration with current CLI credentials
488
+ await this.updateAgentConfig();
489
+ if (options.debug) {
490
+ console.log(chalk_1.default.cyan('🐛 DEBUG: Agent configuration updated'));
491
+ }
492
+ // Clean up any existing agent processes first
493
+ if (options.debug) {
494
+ console.log(chalk_1.default.cyan('🐛 DEBUG: About to clean up existing agents'));
495
+ }
496
+ try {
497
+ await this.cleanupExistingAgents(options.debug);
498
+ if (options.debug) {
499
+ console.log(chalk_1.default.cyan('🐛 DEBUG: Cleaned up existing agents'));
500
+ }
501
+ }
502
+ catch (error) {
503
+ if (options.debug) {
504
+ console.log(chalk_1.default.red('🐛 DEBUG: Error during cleanup:'), error);
505
+ }
506
+ throw error;
507
+ }
508
+ // Get device credentials from CLI config
509
+ const cliConfig = this.configManager.getConfig();
510
+ if (options.debug) {
511
+ console.log(chalk_1.default.cyan('🐛 DEBUG: CLI Configuration:'));
512
+ console.log(chalk_1.default.cyan(` Device ID: ${cliConfig.deviceId || 'unknown'}`));
513
+ console.log(chalk_1.default.cyan(` Device Type: ${cliConfig.deviceType || 'serving'}`));
514
+ console.log(chalk_1.default.cyan(` API URL: ${(0, urls_1.getApiBaseUrl)()}`));
515
+ console.log(chalk_1.default.cyan(` Organization ID: ${cliConfig.organizationId || 'not set'}`));
516
+ console.log(chalk_1.default.cyan(` Agent Path: ${this.agentPath}`));
517
+ console.log(chalk_1.default.cyan(` Config Path: ${path.join(this.configPath, 'agent.config.json')}`));
518
+ }
519
+ // Start the agent process as detached to prevent blocking
520
+ const { spawn } = require('child_process');
521
+ const configPath = path.join(this.configPath, 'agent.config.json');
522
+ if (options.debug) {
523
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: About to spawn agent process`));
524
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Agent binary exists: ${fs.existsSync(this.agentPath)}`));
525
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Config file exists: ${fs.existsSync(configPath)}`));
526
+ }
527
+ // Build command arguments - pass both config file and individual arguments
528
+ const args = [
529
+ this.agentPath,
530
+ 'start',
531
+ '-c', configPath,
532
+ '--device-id', cliConfig.deviceId || 'unknown',
533
+ '--device-type', cliConfig.deviceType || 'serving',
534
+ '--api-url', (0, urls_1.getApiBaseUrl)()
535
+ ];
536
+ // Add organization ID if available
537
+ if (cliConfig.organizationId) {
538
+ args.push('--organization-id', cliConfig.organizationId);
539
+ }
540
+ // Add debug flag if enabled
541
+ if (options.debug) {
542
+ args.push('--debug');
543
+ }
544
+ if (options.debug) {
545
+ console.log(chalk_1.default.cyan('🐛 DEBUG: Spawn Arguments:'));
546
+ console.log(chalk_1.default.cyan(` Command: node`));
547
+ console.log(chalk_1.default.cyan(` Args: ${JSON.stringify(args, null, 2)}`));
548
+ console.log(chalk_1.default.cyan(` Passthrough: ${options.passthrough}`));
549
+ console.log(chalk_1.default.cyan(` Debug: ${options.debug}`));
550
+ }
551
+ // Use sudo only if --root flag is explicitly passed
552
+ // On Windows, we would need administrator privileges (handled differently)
553
+ const platform = os.platform();
554
+ const useSudo = options.root === true && platform !== 'win32';
555
+ if (useSudo) {
556
+ console.log(chalk_1.default.gray('Starting agent with sudo privileges (required for WireGuard and iptables)...'));
557
+ if (options.debug) {
558
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Using sudo to start agent (platform: ${platform})`));
559
+ }
560
+ }
561
+ else if (options.root && platform === 'win32') {
562
+ console.log(chalk_1.default.yellow('⚠ Windows platform detected. Root privileges are handled differently on Windows.'));
563
+ if (options.debug) {
564
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Platform: ${platform}, Not using sudo (Windows)`));
565
+ }
566
+ }
567
+ else if (options.debug) {
568
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Platform: ${platform}, Not using sudo (--root flag not provided)`));
569
+ }
570
+ // Determine stdio configuration based on options
571
+ // When using sudo, we need to capture stderr to see password prompts or errors
572
+ const logFilePath = this.getLogFilePath();
573
+ const logFile = fs.openSync(logFilePath, 'a');
574
+ let stdioConfig;
575
+ if (options.passthrough) {
576
+ // In passthrough mode, inherit all stdio (user can enter sudo password)
577
+ stdioConfig = 'inherit';
578
+ }
579
+ else if (useSudo) {
580
+ // When using sudo, pipe stderr to see errors/password prompts
581
+ // stdin is 'ignore' since we can't interact in non-passthrough mode
582
+ stdioConfig = ['ignore', logFile, 'pipe']; // stdin: ignore, stdout: log, stderr: pipe to catch errors
583
+ }
584
+ else {
585
+ // Normal mode without sudo - redirect everything to log file
586
+ stdioConfig = ['ignore', logFile, logFile];
587
+ }
588
+ // Build the command - use sudo on Unix/Linux systems
589
+ const command = useSudo ? 'sudo' : 'node';
590
+ const commandArgs = useSudo ? ['node', ...args] : args;
591
+ if (options.debug) {
592
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Command: ${command}`));
593
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Command Args: ${JSON.stringify(commandArgs)}`));
594
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Using sudo: ${useSudo}, Passthrough: ${options.passthrough}`));
595
+ }
596
+ this.agentProcess = spawn(command, commandArgs, {
597
+ detached: !options.passthrough, // Don't detach if passthrough is enabled
598
+ stdio: stdioConfig,
599
+ windowsHide: true // Hide window on Windows
600
+ });
601
+ if (options.debug) {
602
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Agent process spawned with PID: ${this.agentProcess.pid}`));
603
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Process stdio config: ${JSON.stringify(stdioConfig)}`));
604
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Process detached: ${!options.passthrough}`));
605
+ }
606
+ // Unref the process so it doesn't keep the parent alive (unless passthrough is enabled)
607
+ if (!options.passthrough) {
608
+ this.agentProcess.unref();
609
+ if (options.debug) {
610
+ console.log(chalk_1.default.cyan('🐛 DEBUG: Process unref() called'));
611
+ }
612
+ }
613
+ // Handle process events (non-blocking)
614
+ this.agentProcess.on('error', (error) => {
615
+ if (options.debug) {
616
+ console.log(chalk_1.default.red('🐛 DEBUG: Agent process error:'), error);
617
+ }
618
+ console.error('Agent process error:', error);
619
+ this.agentProcess = null;
620
+ });
621
+ this.agentProcess.on('exit', (code, signal) => {
622
+ if (options.debug) {
623
+ console.log(chalk_1.default.yellow(`🐛 DEBUG: Agent process exited with code ${code}${signal ? ` and signal ${signal}` : ''}`));
624
+ }
625
+ if (code !== 0) {
626
+ console.error(chalk_1.default.red(`Agent process exited with code ${code}`));
627
+ if (useSudo && !options.passthrough && code === 1) {
628
+ console.error(chalk_1.default.yellow('\n⚠ Sudo may require a password. Try running with:'));
629
+ console.error(chalk_1.default.cyan(' edgible agent start --passthrough'));
630
+ console.error(chalk_1.default.gray('Or configure passwordless sudo for this command.'));
631
+ }
632
+ }
633
+ else {
634
+ console.log(chalk_1.default.gray(`Agent process exited with code ${code}`));
635
+ }
636
+ this.agentProcess = null;
637
+ });
638
+ // Capture stderr when using sudo (to see password prompts or errors)
639
+ if (useSudo && !options.passthrough && this.agentProcess.stderr) {
640
+ let errorBuffer = '';
641
+ this.agentProcess.stderr.on('data', (data) => {
642
+ const errorText = data.toString();
643
+ errorBuffer += errorText;
644
+ // If we see sudo password prompt or common errors, display them
645
+ if (errorText.includes('password for') ||
646
+ errorText.includes('sudo:') ||
647
+ errorText.includes('Permission denied') ||
648
+ errorText.includes('command not found')) {
649
+ console.error(chalk_1.default.red('Sudo error:'), errorText.trim());
650
+ }
651
+ // Also write to log file
652
+ try {
653
+ fs.appendFileSync(logFilePath, errorText);
654
+ }
655
+ catch (err) {
656
+ // Ignore log file errors
657
+ }
658
+ });
659
+ // On process exit, if there was an error, show the full error buffer
660
+ this.agentProcess.on('exit', (code) => {
661
+ if (code !== 0 && errorBuffer.trim()) {
662
+ if (options.debug) {
663
+ console.log(chalk_1.default.red('🐛 DEBUG: Full stderr output:'), errorBuffer);
664
+ }
665
+ }
666
+ });
667
+ }
668
+ // Add debug logging for stdout/stderr if not in passthrough mode
669
+ if (options.debug && !options.passthrough) {
670
+ this.agentProcess.stdout?.on('data', (data) => {
671
+ console.log(chalk_1.default.blue('🐛 DEBUG: Agent stdout:'), data.toString());
672
+ });
673
+ if (!useSudo) {
674
+ // Only capture stderr in debug if not using sudo (sudo stderr is handled above)
675
+ this.agentProcess.stderr?.on('data', (data) => {
676
+ console.log(chalk_1.default.red('🐛 DEBUG: Agent stderr:'), data.toString());
677
+ });
678
+ }
679
+ }
680
+ // Use a shorter, non-blocking check
681
+ return new Promise(async (resolve) => {
682
+ // Quick check after a short delay
683
+ const delay = options.debug ? 2000 : 100; // Longer delay in debug mode to see error output
684
+ if (options.debug) {
685
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Setting timeout for ${delay}ms to check process status`));
686
+ }
687
+ setTimeout(async () => {
688
+ if (this.agentProcess && !this.agentProcess.killed) {
689
+ if (options.debug) {
690
+ console.log(chalk_1.default.cyan('🐛 DEBUG: Agent process started, waiting for status file...'));
691
+ }
692
+ // Wait for status file to be created and valid
693
+ try {
694
+ await this.waitForStatusFile(options.debug);
695
+ if (options.debug) {
696
+ console.log(chalk_1.default.green('🐛 DEBUG: Status file validated, agent is running'));
697
+ }
698
+ resolve(true);
699
+ }
700
+ catch (error) {
701
+ if (options.debug) {
702
+ console.log(chalk_1.default.red('🐛 DEBUG: Failed to wait for status file:'), error);
703
+ // In debug mode, also check if the process is still running
704
+ if (this.agentProcess && !this.agentProcess.killed) {
705
+ console.log(chalk_1.default.yellow('🐛 DEBUG: Process is still running but status file not created - agent may have failed to initialize'));
706
+ }
707
+ else {
708
+ console.log(chalk_1.default.red('🐛 DEBUG: Process has exited - check stderr output above for error details'));
709
+ }
710
+ }
711
+ console.error('Failed to wait for status file:', error);
712
+ resolve(false);
713
+ }
714
+ }
715
+ else {
716
+ if (options.debug) {
717
+ console.log(chalk_1.default.red('🐛 DEBUG: Agent process failed to start or was killed'));
718
+ }
719
+ resolve(false);
720
+ }
721
+ }, delay);
722
+ });
723
+ }
724
+ catch (error) {
725
+ console.error('Failed to start agent:', error);
726
+ this.agentProcess = null;
727
+ return false;
728
+ }
729
+ }
730
+ /**
731
+ * Wait for agent to be ready
732
+ */
733
+ async waitForAgentReady(debug = false) {
734
+ try {
735
+ await this.waitForStatusFile(debug);
736
+ }
737
+ catch (error) {
738
+ throw new Error(`Agent failed to become ready: ${error instanceof Error ? error.message : 'Unknown error'}`);
739
+ }
740
+ }
741
+ /**
742
+ * Provide troubleshooting steps based on error message
743
+ */
744
+ provideTroubleshootingSteps(errorMessage) {
745
+ console.log(chalk_1.default.yellow('\nTroubleshooting steps:'));
746
+ if (errorMessage.includes('Device ID')) {
747
+ console.log(' 1. Run "edgible login" to authenticate');
748
+ console.log(' 2. Run "edgible device-login" if using device credentials');
749
+ }
750
+ if (errorMessage.includes('Agent binary not found')) {
751
+ console.log(' 1. Run "edgible agent install" to install the agent');
752
+ }
753
+ console.log(' 2. Run "edgible agent start --debug" for detailed logs');
754
+ }
755
+ /**
756
+ * Log debug information
757
+ */
758
+ async logDebugInfo(options) {
759
+ if (!options.debug)
760
+ return;
761
+ console.log(chalk_1.default.cyan('🐛 DEBUG: System Information:'));
762
+ console.log(chalk_1.default.cyan(` Platform: ${os.platform()} ${os.arch()}`));
763
+ console.log(chalk_1.default.cyan(` Node Version: ${process.version}`));
764
+ console.log(chalk_1.default.cyan(` CLI PID: ${process.pid}`));
765
+ console.log(chalk_1.default.cyan(` Agent Path: ${this.agentPath}`));
766
+ console.log(chalk_1.default.cyan(` Config Path: ${this.configPath}`));
767
+ // Check environment
768
+ console.log(chalk_1.default.cyan('🐛 DEBUG: Environment:'));
769
+ console.log(chalk_1.default.cyan(` API_BASE_URL: ${(0, urls_1.getApiBaseUrl)()}`));
770
+ console.log(chalk_1.default.cyan(` NODE_ENV: ${process.env.NODE_ENV || 'not set'}`));
771
+ // Check file permissions
772
+ try {
773
+ await fs.promises.access(this.configPath, fs.constants.W_OK);
774
+ console.log(chalk_1.default.cyan(' Config directory: writable'));
775
+ }
776
+ catch {
777
+ console.log(chalk_1.default.red(' Config directory: not writable'));
778
+ }
779
+ }
780
+ /**
781
+ * Wait for status file to be created and valid
782
+ */
783
+ async waitForStatusFile(debug = false) {
784
+ const maxWait = 15000; // 15 seconds
785
+ const checkInterval = 500; // 500ms
786
+ let waited = 0;
787
+ if (debug) {
788
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Waiting for status file (max ${maxWait}ms, checking every ${checkInterval}ms)`));
789
+ }
790
+ while (waited < maxWait) {
791
+ const statusResult = await this.statusManager.getStatus();
792
+ if (debug) {
793
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Status check ${waited}ms: isValid=${statusResult.isValid}, running=${statusResult.status?.running}`));
794
+ }
795
+ if (statusResult.isValid && statusResult.status?.running) {
796
+ if (debug) {
797
+ console.log(chalk_1.default.green(`🐛 DEBUG: Status file validated after ${waited}ms`));
798
+ }
799
+ return;
800
+ }
801
+ await new Promise(resolve => setTimeout(resolve, checkInterval));
802
+ waited += checkInterval;
803
+ }
804
+ if (debug) {
805
+ console.log(chalk_1.default.red(`🐛 DEBUG: Timeout reached after ${waited}ms - status file not created or invalid`));
806
+ }
807
+ throw new Error('Agent failed to start within timeout - status file not created');
808
+ }
809
+ /**
810
+ * Stop local agent service using hybrid approach
811
+ */
812
+ async stopLocalAgent() {
813
+ try {
814
+ // First check if agent is running in Docker
815
+ const cliConfig = this.configManager.getConfig();
816
+ if (cliConfig.deviceId) {
817
+ const dockerRunning = await this.isDockerAgentRunning(cliConfig.deviceId);
818
+ if (dockerRunning) {
819
+ console.log(chalk_1.default.blue('Agent is running in Docker, stopping container...'));
820
+ return await this.stopDockerAgent(cliConfig.deviceId);
821
+ }
822
+ }
823
+ let processStopped = false;
824
+ // Method 1: Stop the current process if it exists
825
+ if (this.agentProcess && !this.agentProcess.killed) {
826
+ console.log('Stopping agent process from current CLI session...');
827
+ this.agentProcess.kill('SIGTERM');
828
+ // Wait for process to exit
829
+ await new Promise((resolve) => {
830
+ const timeout = setTimeout(() => {
831
+ if (this.agentProcess && !this.agentProcess.killed) {
832
+ this.agentProcess.kill('SIGKILL');
833
+ }
834
+ resolve(true);
835
+ }, 5000);
836
+ this.agentProcess.on('exit', () => {
837
+ clearTimeout(timeout);
838
+ resolve(true);
839
+ });
840
+ });
841
+ this.agentProcess = null;
842
+ processStopped = true;
843
+ }
844
+ // Method 2: Try to stop agent using PID from status file (prioritize this)
845
+ if (!processStopped) {
846
+ try {
847
+ const statusResult = await this.statusManager.getStatus();
848
+ if (statusResult.isValid && statusResult.status?.pid) {
849
+ console.log(`Attempting to stop agent process (PID: ${statusResult.status.pid}) from status file...`);
850
+ const killed = await this.killProcessByPid(statusResult.status.pid);
851
+ if (killed) {
852
+ processStopped = true;
853
+ }
854
+ }
855
+ }
856
+ catch (error) {
857
+ console.log('Could not read status file for PID:', error instanceof Error ? error.message : 'Unknown error');
858
+ }
859
+ }
860
+ // Method 3: Search for and kill any running agent processes (fallback)
861
+ if (!processStopped) {
862
+ console.log('Searching for running agent processes...');
863
+ const killed = await this.findAndKillAgentProcess();
864
+ if (killed) {
865
+ processStopped = true;
866
+ }
867
+ }
868
+ // Method 4: Clean up any existing agent processes from previous sessions
869
+ await this.cleanupExistingAgents();
870
+ // Wait for status file to show stopped (with increased timeout)
871
+ await this.waitForStoppedStatus();
872
+ return true;
873
+ }
874
+ catch (error) {
875
+ console.error('Failed to stop agent:', error);
876
+ return false;
877
+ }
878
+ }
879
+ /**
880
+ * Wait for status file to show agent is stopped
881
+ */
882
+ async waitForStoppedStatus() {
883
+ const maxWait = 30000; // 30 seconds (increased from 10)
884
+ const checkInterval = 1000; // 1 second (increased from 500ms)
885
+ let waited = 0;
886
+ console.log('Waiting for agent to update status file...');
887
+ while (waited < maxWait) {
888
+ const statusResult = await this.statusManager.getStatus();
889
+ if (!statusResult.isValid || !statusResult.status?.running) {
890
+ console.log('Agent status updated - stopped');
891
+ return;
892
+ }
893
+ // Show progress every 5 seconds
894
+ if (waited > 0 && waited % 5000 === 0) {
895
+ console.log(`Still waiting for agent to stop... (${waited / 1000}s elapsed)`);
896
+ }
897
+ await new Promise(resolve => setTimeout(resolve, checkInterval));
898
+ waited += checkInterval;
899
+ }
900
+ // If timeout, continue anyway - the process is likely stopped
901
+ console.warn(`Timeout waiting for status file to show stopped state (${maxWait / 1000}s)`);
902
+ }
903
+ /**
904
+ * Restart local agent service
905
+ */
906
+ async restartLocalAgent() {
907
+ const stopped = await this.stopLocalAgent();
908
+ if (!stopped) {
909
+ return false;
910
+ }
911
+ // Wait a moment before restarting
912
+ await new Promise(resolve => setTimeout(resolve, 2000));
913
+ return await this.startLocalAgent();
914
+ }
915
+ /**
916
+ * Update local agent to latest version
917
+ */
918
+ async updateLocalAgent(options = {}) {
919
+ try {
920
+ const currentStatus = await this.checkLocalAgentStatus();
921
+ if (!currentStatus.installed) {
922
+ return {
923
+ success: false,
924
+ error: 'Agent not installed',
925
+ requiresRestart: false
926
+ };
927
+ }
928
+ const previousVersion = currentStatus.version;
929
+ const wasRunning = currentStatus.running;
930
+ // Stop agent if running
931
+ if (wasRunning) {
932
+ await this.stopLocalAgent();
933
+ }
934
+ // Backup current installation if requested
935
+ if (options.backup) {
936
+ await this.backupCurrentInstallation();
937
+ }
938
+ // Install new version
939
+ const installResult = await this.installLocalAgent({
940
+ version: options.version,
941
+ autoStart: false
942
+ });
943
+ if (!installResult.success) {
944
+ return {
945
+ success: false,
946
+ error: installResult.error,
947
+ requiresRestart: false
948
+ };
949
+ }
950
+ // Migrate configuration if requested
951
+ if (options.configMigration) {
952
+ await this.migrateConfiguration();
953
+ }
954
+ // Restart agent if it was running or if requested
955
+ if (wasRunning || options.restart) {
956
+ await this.startLocalAgent();
957
+ }
958
+ return {
959
+ success: true,
960
+ previousVersion,
961
+ newVersion: installResult.version,
962
+ requiresRestart: !wasRunning && !options.restart
963
+ };
964
+ }
965
+ catch (error) {
966
+ return {
967
+ success: false,
968
+ error: error instanceof Error ? error.message : 'Update failed',
969
+ requiresRestart: false
970
+ };
971
+ }
972
+ }
973
+ /**
974
+ * Configure local agent for specific applications
975
+ */
976
+ async configureAgentForApplication(application) {
977
+ try {
978
+ const configPath = path.join(this.configPath, 'config.json');
979
+ let config;
980
+ // Load existing config or create new one
981
+ if (fs.existsSync(configPath)) {
982
+ const configData = await readFile(configPath, 'utf8');
983
+ config = JSON.parse(configData);
984
+ }
985
+ else {
986
+ config = await this.createDefaultConfig();
987
+ }
988
+ // Add application to config
989
+ if (!application.workloadId) {
990
+ throw new Error(`Application ${application.id} does not have a workloadId`);
991
+ }
992
+ const appConfig = {
993
+ applicationId: application.id,
994
+ workloadId: application.workloadId,
995
+ localPort: application.localPort || application.port,
996
+ targetPort: application.port,
997
+ protocol: application.protocol,
998
+ healthCheck: {
999
+ enabled: true,
1000
+ interval: 30,
1001
+ timeout: 5,
1002
+ retries: 3,
1003
+ expectedStatus: 200
1004
+ },
1005
+ enabled: true
1006
+ };
1007
+ // Check if application already exists
1008
+ const existingIndex = config.applications.findIndex((app) => app.applicationId === application.id);
1009
+ if (existingIndex >= 0) {
1010
+ config.applications[existingIndex] = appConfig;
1011
+ }
1012
+ else {
1013
+ config.applications.push(appConfig);
1014
+ }
1015
+ // Save updated config
1016
+ await writeFile(configPath, JSON.stringify(config, null, 2));
1017
+ return {
1018
+ success: true,
1019
+ configPath,
1020
+ warnings: []
1021
+ };
1022
+ }
1023
+ catch (error) {
1024
+ return {
1025
+ success: false,
1026
+ error: error instanceof Error ? error.message : 'Configuration failed',
1027
+ warnings: []
1028
+ };
1029
+ }
1030
+ }
1031
+ /**
1032
+ * Monitor local agent health
1033
+ */
1034
+ async monitorAgentHealth() {
1035
+ const checks = [];
1036
+ const startTime = Date.now();
1037
+ // Check if agent is running
1038
+ const isRunning = await this.isAgentRunning();
1039
+ checks.push({
1040
+ name: 'agent_running',
1041
+ status: isRunning ? 'pass' : 'fail',
1042
+ message: isRunning ? 'Agent is running' : 'Agent is not running',
1043
+ timestamp: new Date()
1044
+ });
1045
+ // Check agent process health
1046
+ if (isRunning) {
1047
+ const pid = await this.getAgentPID();
1048
+ const processHealth = await this.checkProcessHealth(pid);
1049
+ checks.push({
1050
+ name: 'process_health',
1051
+ status: processHealth ? 'pass' : 'fail',
1052
+ message: processHealth ? 'Process is healthy' : 'Process is unhealthy',
1053
+ timestamp: new Date()
1054
+ });
1055
+ }
1056
+ // Check configuration
1057
+ const configValid = await this.validateConfiguration();
1058
+ checks.push({
1059
+ name: 'configuration',
1060
+ status: configValid ? 'pass' : 'warn',
1061
+ message: configValid ? 'Configuration is valid' : 'Configuration has issues',
1062
+ timestamp: new Date()
1063
+ });
1064
+ // Check network connectivity
1065
+ const networkHealth = await this.checkNetworkHealth();
1066
+ checks.push({
1067
+ name: 'network',
1068
+ status: networkHealth ? 'pass' : 'warn',
1069
+ message: networkHealth ? 'Network connectivity is good' : 'Network issues detected',
1070
+ timestamp: new Date()
1071
+ });
1072
+ const overall = this.determineOverallHealth(checks);
1073
+ const version = await this.getAgentVersion();
1074
+ const uptime = await this.getAgentUptime();
1075
+ return {
1076
+ overall,
1077
+ checks,
1078
+ lastChecked: new Date(),
1079
+ uptime,
1080
+ version: version || 'unknown'
1081
+ };
1082
+ }
1083
+ // Private helper methods
1084
+ async saveAgentState(state) {
1085
+ // Make this non-blocking by not awaiting
1086
+ this.loadAgentState().then(currentState => {
1087
+ const newState = { ...currentState, ...state, timestamp: new Date() };
1088
+ return writeFile(this.statePath, JSON.stringify(newState, null, 2));
1089
+ }).catch(() => {
1090
+ // Ignore errors saving state
1091
+ });
1092
+ }
1093
+ async loadAgentState() {
1094
+ try {
1095
+ if (fs.existsSync(this.statePath)) {
1096
+ const data = await readFile(this.statePath, 'utf8');
1097
+ return JSON.parse(data);
1098
+ }
1099
+ }
1100
+ catch (error) {
1101
+ // Ignore errors loading state
1102
+ }
1103
+ return {
1104
+ installed: false,
1105
+ running: false,
1106
+ health: 'unknown',
1107
+ version: undefined,
1108
+ pid: undefined,
1109
+ uptime: 0,
1110
+ lastHealthCheck: undefined,
1111
+ error: undefined
1112
+ };
1113
+ }
1114
+ detectPlatform() {
1115
+ const platform = os.platform();
1116
+ if (platform === 'darwin')
1117
+ return 'macos';
1118
+ if (platform === 'linux')
1119
+ return 'linux';
1120
+ if (platform === 'win32')
1121
+ return 'windows';
1122
+ return 'linux'; // Default fallback
1123
+ }
1124
+ async isAgentRunning() {
1125
+ try {
1126
+ const platform = this.detectPlatform();
1127
+ switch (platform) {
1128
+ case 'macos':
1129
+ return await this.isMacOSServiceRunning();
1130
+ case 'linux':
1131
+ return await this.isLinuxServiceRunning();
1132
+ case 'windows':
1133
+ return await this.isWindowsServiceRunning();
1134
+ case 'docker':
1135
+ return await this.isDockerServiceRunning();
1136
+ default:
1137
+ return false;
1138
+ }
1139
+ }
1140
+ catch {
1141
+ return false;
1142
+ }
1143
+ }
1144
+ async getAgentVersion() {
1145
+ try {
1146
+ const result = (0, child_process_1.execSync)(`${this.agentPath} --version`, { encoding: 'utf8' });
1147
+ return result.trim();
1148
+ }
1149
+ catch {
1150
+ return undefined;
1151
+ }
1152
+ }
1153
+ async getAgentPID() {
1154
+ try {
1155
+ const platform = this.detectPlatform();
1156
+ switch (platform) {
1157
+ case 'macos':
1158
+ return await this.getMacOSServicePID();
1159
+ case 'linux':
1160
+ return await this.getLinuxServicePID();
1161
+ case 'windows':
1162
+ return await this.getWindowsServicePID();
1163
+ default:
1164
+ return undefined;
1165
+ }
1166
+ }
1167
+ catch {
1168
+ return undefined;
1169
+ }
1170
+ }
1171
+ async getAgentUptime() {
1172
+ try {
1173
+ const pid = await this.getAgentPID();
1174
+ if (!pid)
1175
+ return 0;
1176
+ const platform = this.detectPlatform();
1177
+ switch (platform) {
1178
+ case 'macos':
1179
+ return await this.getMacOSProcessUptime(pid);
1180
+ case 'linux':
1181
+ return await this.getLinuxProcessUptime(pid);
1182
+ case 'windows':
1183
+ return await this.getWindowsProcessUptime(pid);
1184
+ default:
1185
+ return 0;
1186
+ }
1187
+ }
1188
+ catch {
1189
+ return 0;
1190
+ }
1191
+ }
1192
+ async performHealthCheck() {
1193
+ try {
1194
+ // Try to connect to agent health endpoint
1195
+ const healthUrl = 'http://localhost:8080/health';
1196
+ const controller = new AbortController();
1197
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
1198
+ const response = await fetch(healthUrl, { signal: controller.signal });
1199
+ clearTimeout(timeoutId);
1200
+ return { overall: response.ok ? 'healthy' : 'unhealthy' };
1201
+ }
1202
+ catch {
1203
+ return { overall: 'unknown' };
1204
+ }
1205
+ }
1206
+ async copyDirectory(src, dest) {
1207
+ const { execSync } = require('child_process');
1208
+ // Use rsync for efficient directory copying (cross-platform)
1209
+ try {
1210
+ execSync(`rsync -av --exclude=node_modules/.cache --exclude=.git "${src}/" "${dest}/"`, { stdio: 'inherit' });
1211
+ }
1212
+ catch (error) {
1213
+ // Fallback to manual copying if rsync is not available
1214
+ await this.copyDirectoryManual(src, dest);
1215
+ }
1216
+ }
1217
+ async copyDirectoryManual(src, dest) {
1218
+ const readdir = (0, util_1.promisify)(fs.readdir);
1219
+ const stat = (0, util_1.promisify)(fs.stat);
1220
+ const copyFile = (0, util_1.promisify)(fs.copyFile);
1221
+ const items = await readdir(src);
1222
+ for (const item of items) {
1223
+ const srcPath = path.join(src, item);
1224
+ const destPath = path.join(dest, item);
1225
+ const statResult = await stat(srcPath);
1226
+ if (statResult.isDirectory()) {
1227
+ // Skip certain directories that we don't need
1228
+ if (['.git', 'coverage', '__tests__', 'src'].includes(item)) {
1229
+ continue;
1230
+ }
1231
+ await mkdir(destPath, { recursive: true });
1232
+ await this.copyDirectoryManual(srcPath, destPath);
1233
+ }
1234
+ else {
1235
+ // Skip certain files we don't need
1236
+ if (['.gitignore', 'tsconfig.json', 'jest.config.js', 'package-lock.json'].includes(item)) {
1237
+ continue;
1238
+ }
1239
+ await copyFile(srcPath, destPath);
1240
+ }
1241
+ }
1242
+ }
1243
+ async installAgentForPlatform(platform, options, targetConfigPath) {
1244
+ try {
1245
+ // Install from local build if requested
1246
+ if (options.installFromLocal) {
1247
+ return await this.installFromLocalBuild(targetConfigPath);
1248
+ }
1249
+ // Download agent from S3 (primary distribution method)
1250
+ return await this.installFromS3(options, targetConfigPath);
1251
+ }
1252
+ catch (error) {
1253
+ return {
1254
+ success: false,
1255
+ error: error instanceof Error ? error.message : 'Installation failed'
1256
+ };
1257
+ }
1258
+ }
1259
+ async installFromS3(options, targetConfigPath) {
1260
+ try {
1261
+ const DEFAULT_VERSION = 'latest';
1262
+ const distributionUrl = (0, urls_1.getDistributionUrl)();
1263
+ const version = options.version || DEFAULT_VERSION;
1264
+ // Detect device type from config (defaults to 'serving' for local installs)
1265
+ const cliConfig = this.configManager.getConfig();
1266
+ const deviceType = cliConfig.deviceType || 'serving';
1267
+ // Build CloudFront URL with device type prefix
1268
+ // Format: {distributionUrl}/{device-type}/{version}.zip
1269
+ // This matches the S3 path structure we use in deploy-to-s3.sh
1270
+ const downloadUrl = `${distributionUrl}/${deviceType}/${version}.zip`;
1271
+ console.log(chalk_1.default.blue('Downloading agent from CloudFront...'));
1272
+ console.log(chalk_1.default.gray(` Distribution: ${distributionUrl}`));
1273
+ console.log(chalk_1.default.gray(` Device Type: ${deviceType}`));
1274
+ console.log(chalk_1.default.gray(` Version: ${version}`));
1275
+ console.log(chalk_1.default.gray(` URL: ${downloadUrl}`));
1276
+ // Create temp directory for download
1277
+ const tempDir = path.join(os.tmpdir(), `edgible-agent-${Date.now()}`);
1278
+ await mkdir(tempDir, { recursive: true });
1279
+ const zipPath = path.join(tempDir, 'agent.zip');
1280
+ try {
1281
+ // Download directly from CloudFront (no AWS SDK needed)
1282
+ const response = await fetch(downloadUrl);
1283
+ if (!response.ok) {
1284
+ let errorMessage = `Failed to download from CloudFront: ${response.status} ${response.statusText}`;
1285
+ if (response.status === 404) {
1286
+ errorMessage = `Agent version not found at ${downloadUrl}\n\n` +
1287
+ `The agent may not have been deployed to S3 yet.\n` +
1288
+ `To deploy the agent:\n` +
1289
+ ` 1. Navigate to agent-v2 directory\n` +
1290
+ ` 2. Run: npm run build\n` +
1291
+ ` 3. Run: ./scripts/deploy-to-s3.sh --env production\n\n` +
1292
+ `Expected path: ${version}.zip`;
1293
+ }
1294
+ else if (response.status === 403) {
1295
+ errorMessage = `Access denied to ${downloadUrl}\n\n` +
1296
+ `This usually means:\n` +
1297
+ ` 1. Agent not deployed: The file doesn't exist\n` +
1298
+ ` → Deploy agent: cd agent-v2 && ./scripts/deploy-to-s3.sh --env production\n\n` +
1299
+ ` 2. CloudFront misconfiguration: OAC or bucket policy issue\n` +
1300
+ ` → Check backend Pulumi outputs for distribution status`;
1301
+ }
1302
+ throw new Error(errorMessage);
1303
+ }
1304
+ const buffer = await response.arrayBuffer();
1305
+ fs.writeFileSync(zipPath, Buffer.from(buffer));
1306
+ console.log(chalk_1.default.green(`✓ Downloaded to ${zipPath}`));
1307
+ // Extract zip file
1308
+ console.log(chalk_1.default.gray('Extracting agent files...'));
1309
+ const extractPath = path.join(tempDir, 'extracted');
1310
+ await mkdir(extractPath, { recursive: true });
1311
+ // Use unzip command (available on most systems)
1312
+ (0, child_process_1.execSync)(`unzip -q "${zipPath}" -d "${extractPath}"`, {
1313
+ stdio: 'pipe'
1314
+ });
1315
+ // Copy extracted files to agent directory
1316
+ await this.copyDirectory(extractPath, targetConfigPath);
1317
+ // Make the agent binary executable
1318
+ const agentBinaryPath = path.join(targetConfigPath, 'index.js');
1319
+ if (fs.existsSync(agentBinaryPath)) {
1320
+ fs.chmodSync(agentBinaryPath, '755');
1321
+ }
1322
+ // Create logs directory
1323
+ const logsDir = path.join(targetConfigPath, 'logs');
1324
+ if (!fs.existsSync(logsDir)) {
1325
+ await mkdir(logsDir, { recursive: true });
1326
+ }
1327
+ // Clean up temp directory
1328
+ fs.rmSync(tempDir, { recursive: true, force: true });
1329
+ console.log(chalk_1.default.green('✓ Agent downloaded and installed from S3'));
1330
+ return {
1331
+ success: true,
1332
+ version: version
1333
+ };
1334
+ }
1335
+ catch (downloadError) {
1336
+ // Clean up temp directory on error
1337
+ if (fs.existsSync(tempDir)) {
1338
+ fs.rmSync(tempDir, { recursive: true, force: true });
1339
+ }
1340
+ throw downloadError;
1341
+ }
1342
+ }
1343
+ catch (error) {
1344
+ return {
1345
+ success: false,
1346
+ error: error instanceof Error ? error.message : 'S3 download failed'
1347
+ };
1348
+ }
1349
+ }
1350
+ async installFromLocalBuild(targetConfigPath) {
1351
+ try {
1352
+ // Determine the agent-v2 directory path relative to CLI
1353
+ // __dirname in compiled JS will be in cli/dist/services, so we go up to workspace root
1354
+ const cliDir = path.resolve(__dirname, '..', '..');
1355
+ const workspaceRoot = path.resolve(cliDir, '..');
1356
+ const agentV2Path = path.join(workspaceRoot, 'agent-v2');
1357
+ const agentDistPath = path.join(agentV2Path, 'dist');
1358
+ // Detect device type from config (defaults to 'serving' for local installs)
1359
+ const cliConfig = this.configManager.getConfig();
1360
+ const deviceType = cliConfig.deviceType || 'serving';
1361
+ console.log(chalk_1.default.blue('Installing agent from local build...'));
1362
+ console.log(chalk_1.default.gray(` Source: ${agentDistPath}`));
1363
+ console.log(chalk_1.default.gray(` Target: ${targetConfigPath}`));
1364
+ console.log(chalk_1.default.gray(` Device Type: ${deviceType}`));
1365
+ // Check if local build exists, and if not, build it for the correct device type
1366
+ if (!fs.existsSync(agentDistPath) || !fs.existsSync(path.join(agentDistPath, 'index.js'))) {
1367
+ console.log(chalk_1.default.yellow('⚠ Local build not found or incomplete, building agent...'));
1368
+ const { execSync } = require('child_process');
1369
+ const buildCommand = deviceType === 'serving' ? 'npm run build:serving' : 'npm run build:gateway';
1370
+ console.log(chalk_1.default.gray(` Running: ${buildCommand}`));
1371
+ try {
1372
+ execSync(buildCommand, {
1373
+ cwd: agentV2Path,
1374
+ stdio: 'inherit'
1375
+ });
1376
+ console.log(chalk_1.default.green('✓ Build completed'));
1377
+ }
1378
+ catch (buildError) {
1379
+ throw new Error(`Failed to build agent: ${buildError instanceof Error ? buildError.message : String(buildError)}\n\n` +
1380
+ `Please build manually:\n` +
1381
+ ` cd agent-v2\n` +
1382
+ ` npm run build:${deviceType}`);
1383
+ }
1384
+ }
1385
+ else {
1386
+ // Verify the build matches the device type (check if gateway code exists)
1387
+ const gatewayCodePath = path.join(agentDistPath, 'gateway');
1388
+ const hasGatewayCode = fs.existsSync(gatewayCodePath);
1389
+ if (deviceType === 'serving' && hasGatewayCode) {
1390
+ console.log(chalk_1.default.yellow('⚠ Build contains gateway code but device type is serving, rebuilding...'));
1391
+ const { execSync } = require('child_process');
1392
+ console.log(chalk_1.default.gray(` Running: npm run build:serving`));
1393
+ try {
1394
+ execSync('npm run build:serving', {
1395
+ cwd: agentV2Path,
1396
+ stdio: 'inherit'
1397
+ });
1398
+ console.log(chalk_1.default.green('✓ Rebuild completed'));
1399
+ }
1400
+ catch (buildError) {
1401
+ throw new Error(`Failed to rebuild agent for serving device: ${buildError instanceof Error ? buildError.message : String(buildError)}`);
1402
+ }
1403
+ }
1404
+ else if (deviceType === 'gateway' && !hasGatewayCode) {
1405
+ console.log(chalk_1.default.yellow('⚠ Build missing gateway code but device type is gateway, rebuilding...'));
1406
+ const { execSync } = require('child_process');
1407
+ console.log(chalk_1.default.gray(` Running: npm run build:gateway`));
1408
+ try {
1409
+ execSync('npm run build:gateway', {
1410
+ cwd: agentV2Path,
1411
+ stdio: 'inherit'
1412
+ });
1413
+ console.log(chalk_1.default.green('✓ Rebuild completed'));
1414
+ }
1415
+ catch (buildError) {
1416
+ throw new Error(`Failed to rebuild agent for gateway device: ${buildError instanceof Error ? buildError.message : String(buildError)}`);
1417
+ }
1418
+ }
1419
+ }
1420
+ // Check if index.js exists in the build
1421
+ const indexPath = path.join(agentDistPath, 'index.js');
1422
+ if (!fs.existsSync(indexPath)) {
1423
+ throw new Error(`Agent entry point not found at ${indexPath}\n\n` +
1424
+ `The build may be incomplete. Please rebuild:\n` +
1425
+ ` cd agent-v2\n` +
1426
+ ` npm run build`);
1427
+ }
1428
+ // Copy dist directory contents to target
1429
+ console.log(chalk_1.default.gray('Copying agent files...'));
1430
+ await this.copyDirectory(agentDistPath, targetConfigPath);
1431
+ // Copy package.json to target directory (needed for npm install)
1432
+ const packageJsonPath = path.join(agentV2Path, 'package.json');
1433
+ if (fs.existsSync(packageJsonPath)) {
1434
+ const targetPackageJsonPath = path.join(targetConfigPath, 'package.json');
1435
+ fs.copyFileSync(packageJsonPath, targetPackageJsonPath);
1436
+ console.log(chalk_1.default.gray('✓ Copied package.json'));
1437
+ }
1438
+ else {
1439
+ throw new Error(`package.json not found at ${packageJsonPath}\n\n` +
1440
+ `This is required for installing dependencies.`);
1441
+ }
1442
+ // Install production dependencies
1443
+ console.log(chalk_1.default.gray('Installing production dependencies...'));
1444
+ try {
1445
+ (0, child_process_1.execSync)('npm install --production --no-optional', {
1446
+ cwd: targetConfigPath,
1447
+ stdio: 'inherit'
1448
+ });
1449
+ console.log(chalk_1.default.green('✓ Dependencies installed'));
1450
+ }
1451
+ catch (error) {
1452
+ throw new Error(`Failed to install dependencies: ${error instanceof Error ? error.message : String(error)}\n\n` +
1453
+ `Make sure npm is available and you have write permissions to ${targetConfigPath}`);
1454
+ }
1455
+ // Make the agent binary executable
1456
+ const agentBinaryPath = path.join(targetConfigPath, 'index.js');
1457
+ if (fs.existsSync(agentBinaryPath)) {
1458
+ fs.chmodSync(agentBinaryPath, '755');
1459
+ }
1460
+ // Create logs directory
1461
+ const logsDir = path.join(targetConfigPath, 'logs');
1462
+ if (!fs.existsSync(logsDir)) {
1463
+ await mkdir(logsDir, { recursive: true });
1464
+ }
1465
+ console.log(chalk_1.default.green('✓ Agent installed from local build'));
1466
+ return {
1467
+ success: true,
1468
+ version: 'local-dev'
1469
+ };
1470
+ }
1471
+ catch (error) {
1472
+ return {
1473
+ success: false,
1474
+ error: error instanceof Error ? error.message : 'Local installation failed'
1475
+ };
1476
+ }
1477
+ }
1478
+ async createDefaultConfig() {
1479
+ // Get device credentials from CLI config
1480
+ const cliConfig = this.configManager.getConfig();
1481
+ // Create agent-v2 configuration using actual device credentials
1482
+ const agentV2Config = {
1483
+ deviceId: cliConfig.deviceId || 'edgible-device-' + Math.random().toString(36).substr(2, 9),
1484
+ devicePassword: cliConfig.devicePassword || 'edgible-password-' + Math.random().toString(36).substr(2, 9),
1485
+ deviceType: cliConfig.deviceType || 'serving',
1486
+ apiBaseUrl: (0, urls_1.getApiBaseUrl)(),
1487
+ firewallEnabled: true,
1488
+ pollingInterval: 60000,
1489
+ healthCheckTimeout: 5000,
1490
+ maxRetries: 3,
1491
+ logLevel: 'info',
1492
+ updateEnabled: true,
1493
+ updateCheckInterval: 3600000,
1494
+ wireguardMode: cliConfig.wireguardMode || 'kernel',
1495
+ wireguardGoBinary: cliConfig.wireguardGoBinary || 'wireguard-go'
1496
+ };
1497
+ const agentV2ConfigPath = path.join(this.configPath, 'agent.config.json');
1498
+ await writeFile(agentV2ConfigPath, JSON.stringify(agentV2Config, null, 2));
1499
+ // Also create the legacy config for compatibility
1500
+ const config = {
1501
+ version: '1.0.0',
1502
+ applications: [],
1503
+ gateway: {
1504
+ gatewayId: '',
1505
+ publicIp: '',
1506
+ region: 'us-east-1'
1507
+ },
1508
+ monitoring: {
1509
+ enabled: true,
1510
+ interval: 30,
1511
+ alerting: {
1512
+ enabled: false,
1513
+ emailNotifications: false,
1514
+ thresholds: {
1515
+ responseTime: 1000,
1516
+ errorRate: 5,
1517
+ availability: 95
1518
+ }
1519
+ }
1520
+ },
1521
+ logging: {
1522
+ level: 'info',
1523
+ console: true
1524
+ }
1525
+ };
1526
+ const configPath = path.join(this.configPath, 'config.json');
1527
+ await writeFile(configPath, JSON.stringify(config, null, 2));
1528
+ return config;
1529
+ }
1530
+ async setupService(options) {
1531
+ // Platform-specific service setup would go here
1532
+ return { success: true };
1533
+ }
1534
+ async backupCurrentInstallation() {
1535
+ const backupPath = `${this.agentPath}.backup.${Date.now()}`;
1536
+ fs.copyFileSync(this.agentPath, backupPath);
1537
+ }
1538
+ async migrateConfiguration() {
1539
+ // Configuration migration logic would go here
1540
+ }
1541
+ // Platform-specific service management methods (placeholders)
1542
+ async startMacOSService() { return true; }
1543
+ async stopMacOSService() { return true; }
1544
+ async isMacOSServiceRunning() { return false; }
1545
+ async getMacOSServicePID() { return undefined; }
1546
+ async getMacOSProcessUptime(pid) { return 0; }
1547
+ async startLinuxService() { return true; }
1548
+ async stopLinuxService() { return true; }
1549
+ async isLinuxServiceRunning() { return false; }
1550
+ async getLinuxServicePID() { return undefined; }
1551
+ async getLinuxProcessUptime(pid) { return 0; }
1552
+ async startWindowsService() { return true; }
1553
+ async stopWindowsService() { return true; }
1554
+ async isWindowsServiceRunning() { return false; }
1555
+ async getWindowsServicePID() { return undefined; }
1556
+ async getWindowsProcessUptime(pid) { return 0; }
1557
+ async startDockerService() { return true; }
1558
+ async stopDockerService() { return true; }
1559
+ async isDockerServiceRunning() { return false; }
1560
+ async checkProcessHealth(pid) {
1561
+ if (!pid)
1562
+ return false;
1563
+ try {
1564
+ process.kill(pid, 0);
1565
+ return true;
1566
+ }
1567
+ catch {
1568
+ return false;
1569
+ }
1570
+ }
1571
+ async validateConfiguration() {
1572
+ try {
1573
+ const configPath = path.join(this.configPath, 'config.json');
1574
+ if (!fs.existsSync(configPath))
1575
+ return false;
1576
+ const configData = await readFile(configPath, 'utf8');
1577
+ JSON.parse(configData);
1578
+ return true;
1579
+ }
1580
+ catch {
1581
+ return false;
1582
+ }
1583
+ }
1584
+ async checkNetworkHealth() {
1585
+ try {
1586
+ const controller = new AbortController();
1587
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
1588
+ const response = await fetch('http://localhost:8080/health', { signal: controller.signal });
1589
+ clearTimeout(timeoutId);
1590
+ return response.ok;
1591
+ }
1592
+ catch {
1593
+ return false;
1594
+ }
1595
+ }
1596
+ determineOverallHealth(checks) {
1597
+ const failed = checks.filter(c => c.status === 'fail');
1598
+ const warnings = checks.filter(c => c.status === 'warn');
1599
+ if (failed.length > 0)
1600
+ return 'unhealthy';
1601
+ if (warnings.length > 0)
1602
+ return 'degraded';
1603
+ if (checks.every(c => c.status === 'pass'))
1604
+ return 'healthy';
1605
+ return 'unknown';
1606
+ }
1607
+ /**
1608
+ * Display agent status in a user-friendly format
1609
+ */
1610
+ displayAgentStatus(status) {
1611
+ console.log(chalk_1.default.blue('🤖 Local Agent Status:'));
1612
+ console.log(chalk_1.default.white(` Installed: ${status.installed ? chalk_1.default.green('Yes') : chalk_1.default.red('No')}`));
1613
+ console.log(chalk_1.default.white(` Running: ${status.running ? chalk_1.default.green('Yes') : chalk_1.default.red('No')}`));
1614
+ console.log(chalk_1.default.white(` Health: ${status.health === 'healthy' ? chalk_1.default.green('Healthy') :
1615
+ status.health === 'degraded' ? chalk_1.default.yellow('Degraded') :
1616
+ status.health === 'unhealthy' ? chalk_1.default.red('Unhealthy') : chalk_1.default.gray('Unknown')}`));
1617
+ if (status.version) {
1618
+ console.log(chalk_1.default.white(` Version: ${status.version}`));
1619
+ }
1620
+ if (status.pid) {
1621
+ console.log(chalk_1.default.white(` PID: ${status.pid}`));
1622
+ }
1623
+ if (status.uptime) {
1624
+ console.log(chalk_1.default.white(` Uptime: ${Math.floor(status.uptime / 1000)}s`));
1625
+ }
1626
+ if (status.deviceId) {
1627
+ console.log(chalk_1.default.white(` Device ID: ${status.deviceId}`));
1628
+ }
1629
+ if (status.deviceType) {
1630
+ console.log(chalk_1.default.white(` Device Type: ${status.deviceType}`));
1631
+ }
1632
+ if (status.apiConnected !== undefined) {
1633
+ console.log(chalk_1.default.white(` API Connected: ${status.apiConnected ? chalk_1.default.green('Yes') : chalk_1.default.red('No')}`));
1634
+ }
1635
+ if (status.apiAuthenticated !== undefined) {
1636
+ console.log(chalk_1.default.white(` API Authenticated: ${status.apiAuthenticated ? chalk_1.default.green('Yes') : chalk_1.default.red('No')}`));
1637
+ }
1638
+ if (status.applications && status.applications.length > 0) {
1639
+ console.log(chalk_1.default.white(` Applications: ${status.applications.length}`));
1640
+ }
1641
+ if (status.lastError) {
1642
+ console.log(chalk_1.default.red(` Error: ${status.lastError}`));
1643
+ }
1644
+ if (!status.installed) {
1645
+ console.log(chalk_1.default.yellow('\n💡 To install the agent, run: edgible agent install'));
1646
+ }
1647
+ else if (!status.running) {
1648
+ console.log(chalk_1.default.yellow('\n💡 To start the agent, run: edgible agent start'));
1649
+ }
1650
+ }
1651
+ /**
1652
+ * Kill a process by PID using cross-platform approach
1653
+ */
1654
+ async killProcessByPid(pid) {
1655
+ const platform = os.platform();
1656
+ try {
1657
+ if (platform === 'win32') {
1658
+ // Windows: Use taskkill
1659
+ const { exec } = require('child_process');
1660
+ await new Promise((resolve, reject) => {
1661
+ exec(`taskkill /PID ${pid} /T /F`, (error) => {
1662
+ if (error && !error.message.includes('not found')) {
1663
+ reject(error);
1664
+ }
1665
+ else {
1666
+ resolve();
1667
+ }
1668
+ });
1669
+ });
1670
+ }
1671
+ else {
1672
+ // Unix-like: Use kill
1673
+ process.kill(pid, 'SIGTERM');
1674
+ // Wait for graceful shutdown
1675
+ for (let i = 0; i < 50; i++) {
1676
+ try {
1677
+ process.kill(pid, 0);
1678
+ await new Promise(resolve => setTimeout(resolve, 100));
1679
+ }
1680
+ catch {
1681
+ return true; // Process killed
1682
+ }
1683
+ }
1684
+ // Force kill if still running
1685
+ process.kill(pid, 'SIGKILL');
1686
+ }
1687
+ return true;
1688
+ }
1689
+ catch (error) {
1690
+ console.log(`Failed to kill process ${pid}:`, error instanceof Error ? error.message : 'Unknown error');
1691
+ return false;
1692
+ }
1693
+ }
1694
+ /**
1695
+ * Find and kill any running agent processes using cross-platform process search
1696
+ */
1697
+ async findAndKillAgentProcess() {
1698
+ try {
1699
+ const list = await (0, find_process_1.default)('name', this.agentPath, true);
1700
+ if (list.length > 0) {
1701
+ let killedAny = false;
1702
+ for (const process of list) {
1703
+ console.log(`Killing agent process ${process.pid}...`);
1704
+ const killed = await this.killProcessByPid(process.pid);
1705
+ if (killed) {
1706
+ killedAny = true;
1707
+ }
1708
+ }
1709
+ return killedAny;
1710
+ }
1711
+ return false;
1712
+ }
1713
+ catch (error) {
1714
+ console.log('Error searching for agent processes:', error.message);
1715
+ return false;
1716
+ }
1717
+ }
1718
+ /**
1719
+ * Clean up any existing agent processes
1720
+ */
1721
+ async cleanupExistingAgents(debug = false) {
1722
+ try {
1723
+ if (debug) {
1724
+ console.log(chalk_1.default.cyan('🐛 DEBUG: Starting cleanup of existing agents'));
1725
+ }
1726
+ // Use our improved process finding and killing method
1727
+ try {
1728
+ if (debug) {
1729
+ console.log(chalk_1.default.cyan('🐛 DEBUG: Checking for existing agent processes'));
1730
+ }
1731
+ const killed = await this.findAndKillAgentProcess();
1732
+ if (killed) {
1733
+ if (debug) {
1734
+ console.log(chalk_1.default.yellow('🐛 DEBUG: Successfully killed existing agent processes'));
1735
+ }
1736
+ }
1737
+ else {
1738
+ if (debug) {
1739
+ console.log(chalk_1.default.cyan('🐛 DEBUG: No existing agent processes found'));
1740
+ }
1741
+ }
1742
+ }
1743
+ catch (error) {
1744
+ if (debug) {
1745
+ console.log(chalk_1.default.yellow('🐛 DEBUG: Process cleanup failed, continuing anyway'), error);
1746
+ }
1747
+ // Ignore cleanup errors - this is a best-effort cleanup
1748
+ }
1749
+ if (debug) {
1750
+ console.log(chalk_1.default.cyan('🐛 DEBUG: Cleanup completed'));
1751
+ }
1752
+ }
1753
+ catch (error) {
1754
+ if (debug) {
1755
+ console.log(chalk_1.default.red('🐛 DEBUG: Error in cleanup:'), error);
1756
+ }
1757
+ // Ignore cleanup errors
1758
+ }
1759
+ }
1760
+ /**
1761
+ * Get current log configuration
1762
+ */
1763
+ async getLogConfiguration() {
1764
+ try {
1765
+ const configPath = path.join(this.configPath, 'agent.config.json');
1766
+ if (!fs.existsSync(configPath)) {
1767
+ // Return default configuration
1768
+ const defaultLevel = 'info';
1769
+ const moduleLevels = new Map();
1770
+ ['agent', 'caddy', 'firewall', 'health', 'status', 'update'].forEach(module => {
1771
+ moduleLevels.set(module, defaultLevel);
1772
+ });
1773
+ return { globalLevel: defaultLevel, moduleLevels };
1774
+ }
1775
+ const configData = fs.readFileSync(configPath, 'utf8');
1776
+ const config = JSON.parse(configData);
1777
+ const globalLevel = config.logLevel || 'info';
1778
+ const moduleLevels = new Map();
1779
+ // Set default levels for all modules
1780
+ ['agent', 'api-client', 'caddy', 'firewall', 'health', 'status', 'update'].forEach(module => {
1781
+ moduleLevels.set(module, globalLevel);
1782
+ });
1783
+ // Override with module-specific levels if they exist
1784
+ if (config.logModules) {
1785
+ Object.entries(config.logModules).forEach(([module, level]) => {
1786
+ if (level && typeof level === 'string') {
1787
+ moduleLevels.set(module, level);
1788
+ }
1789
+ });
1790
+ }
1791
+ return { globalLevel, moduleLevels };
1792
+ }
1793
+ catch (error) {
1794
+ console.error('Failed to read log configuration:', error);
1795
+ throw error;
1796
+ }
1797
+ }
1798
+ /**
1799
+ * Update global log level
1800
+ */
1801
+ async updateGlobalLogLevel(level) {
1802
+ try {
1803
+ const configPath = path.join(this.configPath, 'agent.config.json');
1804
+ if (!fs.existsSync(configPath)) {
1805
+ throw new Error('Agent configuration file not found');
1806
+ }
1807
+ const configData = fs.readFileSync(configPath, 'utf8');
1808
+ const config = JSON.parse(configData);
1809
+ config.logLevel = level;
1810
+ await writeFile(configPath, JSON.stringify(config, null, 2));
1811
+ }
1812
+ catch (error) {
1813
+ console.error('Failed to update global log level:', error);
1814
+ throw error;
1815
+ }
1816
+ }
1817
+ /**
1818
+ * Update module-specific log level
1819
+ */
1820
+ async updateModuleLogLevel(module, level) {
1821
+ try {
1822
+ const configPath = path.join(this.configPath, 'agent.config.json');
1823
+ if (!fs.existsSync(configPath)) {
1824
+ throw new Error('Agent configuration file not found');
1825
+ }
1826
+ const configData = fs.readFileSync(configPath, 'utf8');
1827
+ const config = JSON.parse(configData);
1828
+ if (!config.logModules) {
1829
+ config.logModules = {};
1830
+ }
1831
+ config.logModules[module] = level;
1832
+ await writeFile(configPath, JSON.stringify(config, null, 2));
1833
+ }
1834
+ catch (error) {
1835
+ console.error('Failed to update module log level:', error);
1836
+ throw error;
1837
+ }
1838
+ }
1839
+ /**
1840
+ * Reset module to use global log level
1841
+ */
1842
+ async resetModuleLogLevel(module) {
1843
+ try {
1844
+ const configPath = path.join(this.configPath, 'agent.config.json');
1845
+ if (!fs.existsSync(configPath)) {
1846
+ throw new Error('Agent configuration file not found');
1847
+ }
1848
+ const configData = fs.readFileSync(configPath, 'utf8');
1849
+ const config = JSON.parse(configData);
1850
+ if (config.logModules && config.logModules[module]) {
1851
+ delete config.logModules[module];
1852
+ // If no more module-specific levels, remove the logModules object
1853
+ if (Object.keys(config.logModules).length === 0) {
1854
+ delete config.logModules;
1855
+ }
1856
+ await writeFile(configPath, JSON.stringify(config, null, 2));
1857
+ }
1858
+ }
1859
+ catch (error) {
1860
+ console.error('Failed to reset module log level:', error);
1861
+ throw error;
1862
+ }
1863
+ }
1864
+ /**
1865
+ * Reset all modules to use global log level
1866
+ */
1867
+ async resetAllModuleLogLevels() {
1868
+ try {
1869
+ const configPath = path.join(this.configPath, 'agent.config.json');
1870
+ if (!fs.existsSync(configPath)) {
1871
+ throw new Error('Agent configuration file not found');
1872
+ }
1873
+ const configData = fs.readFileSync(configPath, 'utf8');
1874
+ const config = JSON.parse(configData);
1875
+ if (config.logModules) {
1876
+ delete config.logModules;
1877
+ await writeFile(configPath, JSON.stringify(config, null, 2));
1878
+ }
1879
+ }
1880
+ catch (error) {
1881
+ console.error('Failed to reset all module log levels:', error);
1882
+ throw error;
1883
+ }
1884
+ }
1885
+ async getStartCommand() {
1886
+ const cliConfig = this.configManager.getConfig();
1887
+ const configPath = path.join(this.configPath, 'agent.config.json');
1888
+ const args = [
1889
+ 'node',
1890
+ this.agentPath,
1891
+ 'start',
1892
+ '-c',
1893
+ configPath,
1894
+ '--device-id',
1895
+ cliConfig.deviceId || 'unknown',
1896
+ '--device-type',
1897
+ cliConfig.deviceType || 'serving',
1898
+ '--api-url',
1899
+ (0, urls_1.getApiBaseUrl)(),
1900
+ ];
1901
+ if (cliConfig.organizationId) {
1902
+ args.push('--organization-id', cliConfig.organizationId);
1903
+ }
1904
+ return args.join(' ');
1905
+ }
1906
+ /**
1907
+ * Get the path to the agent log file
1908
+ */
1909
+ getLogFilePath() {
1910
+ return path.join(this.configPath, 'logs', 'agent.log');
1911
+ }
1912
+ /**
1913
+ * Check if Docker is available
1914
+ */
1915
+ async checkDockerAvailable() {
1916
+ try {
1917
+ (0, child_process_1.execSync)('docker --version', { encoding: 'utf8', timeout: 5000 });
1918
+ return true;
1919
+ }
1920
+ catch (error) {
1921
+ return false;
1922
+ }
1923
+ }
1924
+ /**
1925
+ * Check if Docker image exists locally
1926
+ */
1927
+ async checkDockerImageExists(imageName = 'wireguard-mcp-agent:latest') {
1928
+ try {
1929
+ const output = (0, child_process_1.execSync)(`docker images --format "{{.Repository}}:{{.Tag}}" | grep -q "^${imageName}$" && echo "exists" || echo "not-found"`, { encoding: 'utf8', timeout: 10000 });
1930
+ return output.trim() === 'exists';
1931
+ }
1932
+ catch (error) {
1933
+ return false;
1934
+ }
1935
+ }
1936
+ /**
1937
+ * Recursively copy a directory
1938
+ */
1939
+ copyDirectorySync(source, destination) {
1940
+ // Create destination directory if it doesn't exist
1941
+ if (!fs.existsSync(destination)) {
1942
+ (0, fs_1.mkdirSync)(destination, { recursive: true });
1943
+ }
1944
+ // Read source directory
1945
+ const entries = (0, fs_1.readdirSync)(source, { withFileTypes: true });
1946
+ for (const entry of entries) {
1947
+ const sourcePath = path.join(source, entry.name);
1948
+ const destPath = path.join(destination, entry.name);
1949
+ if (entry.isDirectory()) {
1950
+ // Recursively copy subdirectories
1951
+ this.copyDirectorySync(sourcePath, destPath);
1952
+ }
1953
+ else {
1954
+ // Copy files
1955
+ (0, fs_1.copyFileSync)(sourcePath, destPath);
1956
+ }
1957
+ }
1958
+ }
1959
+ /**
1960
+ * Copy backend directories to temp-sst-copy (required for Docker build)
1961
+ */
1962
+ async copySstDirectories(agentV2Path, debug) {
1963
+ const tempDir = path.join(agentV2Path, 'temp-sst-copy');
1964
+ const backendPath = path.resolve(agentV2Path, '..', 'backend');
1965
+ if (debug) {
1966
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Copying backend directories to temp-sst-copy`));
1967
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Backend path: ${backendPath}`));
1968
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Temp dir: ${tempDir}`));
1969
+ }
1970
+ // Check if backend directory exists
1971
+ if (!fs.existsSync(backendPath)) {
1972
+ throw new Error(`Backend directory not found at ${backendPath}. Expected structure: wireguard-private-mcp/backend and wireguard-private-mcp/agent-v2`);
1973
+ }
1974
+ // Create temp directory structure
1975
+ const tempSrcPath = path.join(tempDir, 'src');
1976
+ await mkdir(tempSrcPath, { recursive: true });
1977
+ // Copy client directory
1978
+ const backendClientPath = path.join(backendPath, 'src', 'client');
1979
+ const tempClientPath = path.join(tempSrcPath, 'client');
1980
+ if (!fs.existsSync(backendClientPath)) {
1981
+ throw new Error(`Client directory not found at ${backendClientPath}`);
1982
+ }
1983
+ this.copyDirectorySync(backendClientPath, tempClientPath);
1984
+ // Copy types directory
1985
+ const backendTypesPath = path.join(backendPath, 'src', 'types');
1986
+ const tempTypesPath = path.join(tempSrcPath, 'types');
1987
+ if (!fs.existsSync(backendTypesPath)) {
1988
+ throw new Error(`Types directory not found at ${backendTypesPath}`);
1989
+ }
1990
+ this.copyDirectorySync(backendTypesPath, tempTypesPath);
1991
+ // Validate copies
1992
+ if (!fs.existsSync(tempClientPath) || !fs.existsSync(tempTypesPath)) {
1993
+ throw new Error(`Failed to validate copied directories. Expected: ${tempClientPath} and ${tempTypesPath}`);
1994
+ }
1995
+ if (debug) {
1996
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Successfully copied backend directories`));
1997
+ }
1998
+ }
1999
+ /**
2000
+ * Build Docker image
2001
+ */
2002
+ async buildDockerImage(imageName = 'wireguard-mcp-agent:latest', debug) {
2003
+ try {
2004
+ // Try to find agent-v2 directory relative to CLI directory
2005
+ // Note: This feature requires development environment setup
2006
+ const cliDir = path.resolve(__dirname, '../..');
2007
+ const workspaceRoot = path.resolve(cliDir, '..');
2008
+ const agentV2Path = path.join(workspaceRoot, 'agent-v2');
2009
+ if (!fs.existsSync(agentV2Path)) {
2010
+ throw new Error('Agent source directory not found. Docker image building requires development environment setup.\n' +
2011
+ 'This feature is not available in the npm-installed version of the CLI.\n' +
2012
+ 'For production use, agents should be deployed via S3 distribution.');
2013
+ }
2014
+ if (debug) {
2015
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Building Docker image: ${imageName}`));
2016
+ console.log(chalk_1.default.cyan(`🐛 DEBUG: Build context: ${agentV2Path}`));
2017
+ }
2018
+ console.log(chalk_1.default.blue(`Building Docker image ${imageName}...`));
2019
+ // Create temp-sst-copy directory with backend directories (required for build)
2020
+ console.log(chalk_1.default.gray('Preparing build dependencies...'));
2021
+ await this.copySstDirectories(agentV2Path, debug);
2022
+ // Build the Docker image
2023
+ (0, child_process_1.execSync)(`docker build -t ${imageName} .`, {
2024
+ cwd: agentV2Path,
2025
+ stdio: debug ? 'inherit' : 'pipe',
2026
+ encoding: 'utf8',
2027
+ timeout: 600000 // 10 minutes
2028
+ });
2029
+ console.log(chalk_1.default.green(`✓ Docker image ${imageName} built successfully`));
2030
+ return true;
2031
+ }
2032
+ catch (error) {
2033
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
2034
+ console.error(chalk_1.default.red(`Failed to build Docker image: ${errorMessage}`));
2035
+ return false;
2036
+ }
2037
+ }
2038
+ /**
2039
+ * Get Docker container name for device
2040
+ */
2041
+ getDockerContainerName(deviceId) {
2042
+ return `wireguard-mcp-agent-${deviceId}`;
2043
+ }
2044
+ /**
2045
+ * Spawn agent in Docker container
2046
+ */
2047
+ async spawnDockerAgent(options) {
2048
+ try {
2049
+ // Check if Docker is available
2050
+ const dockerAvailable = await this.checkDockerAvailable();
2051
+ if (!dockerAvailable) {
2052
+ throw new Error('Docker is not available. Please install Docker and ensure it is running.');
2053
+ }
2054
+ // Get device credentials from CLI config
2055
+ const cliConfig = this.configManager.getConfig();
2056
+ if (!cliConfig.deviceId || !cliConfig.devicePassword) {
2057
+ throw new Error('No device credentials found in CLI config');
2058
+ }
2059
+ // Update agent configuration
2060
+ await this.updateAgentConfig();
2061
+ const imageName = 'wireguard-mcp-agent:latest';
2062
+ const containerName = this.getDockerContainerName(cliConfig.deviceId);
2063
+ // Check if container is already running
2064
+ try {
2065
+ const statusOutput = (0, child_process_1.execSync)(`docker ps --filter "name=${containerName}" --format "{{.Status}}"`, { encoding: 'utf8', timeout: 5000 });
2066
+ if (statusOutput.trim()) {
2067
+ console.log(chalk_1.default.yellow(`Container ${containerName} is already running`));
2068
+ return true;
2069
+ }
2070
+ }
2071
+ catch (error) {
2072
+ // Container not running, continue
2073
+ }
2074
+ // Check if image exists, build if missing
2075
+ const imageExists = await this.checkDockerImageExists(imageName);
2076
+ if (!imageExists) {
2077
+ console.log(chalk_1.default.blue(`Docker image ${imageName} not found, building...`));
2078
+ const built = await this.buildDockerImage(imageName, options.debug);
2079
+ if (!built) {
2080
+ throw new Error('Failed to build Docker image');
2081
+ }
2082
+ }
2083
+ // Prepare environment variables
2084
+ const envVars = [
2085
+ `DEVICE_ID=${cliConfig.deviceId}`,
2086
+ `DEVICE_PASSWORD=${cliConfig.devicePassword}`,
2087
+ `DEVICE_TYPE=${cliConfig.deviceType || 'serving'}`,
2088
+ `API_BASE_URL=${(0, urls_1.getApiBaseUrl)()}`,
2089
+ `LOG_LEVEL=${options.debug ? 'debug' : 'info'}`
2090
+ ];
2091
+ if (cliConfig.organizationId) {
2092
+ envVars.push(`ORGANIZATION_ID=${cliConfig.organizationId}`);
2093
+ }
2094
+ // Prepare docker run command
2095
+ const configPath = path.join(this.configPath, 'agent.config.json');
2096
+ const dockerArgs = [
2097
+ 'run',
2098
+ '-d',
2099
+ '--name', containerName,
2100
+ '--restart', 'unless-stopped',
2101
+ '--network', 'host',
2102
+ '-v', `${configPath}:/app/agent.config.json:ro`,
2103
+ ...envVars.flatMap(env => ['-e', env]),
2104
+ imageName,
2105
+ 'start'
2106
+ ];
2107
+ if (options.debug) {
2108
+ console.log(chalk_1.default.cyan('🐛 DEBUG: Docker command:'));
2109
+ console.log(chalk_1.default.cyan(` docker ${dockerArgs.join(' ')}`));
2110
+ }
2111
+ console.log(chalk_1.default.blue(`Starting agent in Docker container: ${containerName}...`));
2112
+ (0, child_process_1.execSync)(`docker ${dockerArgs.join(' ')}`, {
2113
+ stdio: options.debug ? 'inherit' : 'pipe',
2114
+ encoding: 'utf8',
2115
+ timeout: 30000
2116
+ });
2117
+ console.log(chalk_1.default.green(`✓ Agent started in Docker container: ${containerName}`));
2118
+ console.log(chalk_1.default.gray(` Use 'docker logs ${containerName}' to view logs`));
2119
+ console.log(chalk_1.default.yellow(` Note: Running without privileges - some features may be limited`));
2120
+ return true;
2121
+ }
2122
+ catch (error) {
2123
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
2124
+ console.error(chalk_1.default.red('Failed to start agent in Docker:'), errorMessage);
2125
+ return false;
2126
+ }
2127
+ }
2128
+ /**
2129
+ * Stop Docker agent container
2130
+ */
2131
+ async stopDockerAgent(deviceId) {
2132
+ try {
2133
+ const containerName = this.getDockerContainerName(deviceId);
2134
+ // Check if container exists
2135
+ try {
2136
+ (0, child_process_1.execSync)(`docker ps -a --filter "name=${containerName}" --format "{{.Names}}"`, {
2137
+ encoding: 'utf8',
2138
+ timeout: 5000
2139
+ });
2140
+ }
2141
+ catch (error) {
2142
+ console.log(chalk_1.default.yellow(`Container ${containerName} not found`));
2143
+ return true; // Already stopped/removed
2144
+ }
2145
+ console.log(chalk_1.default.blue(`Stopping Docker container: ${containerName}...`));
2146
+ // Stop container
2147
+ try {
2148
+ (0, child_process_1.execSync)(`docker stop ${containerName}`, {
2149
+ encoding: 'utf8',
2150
+ timeout: 30000,
2151
+ stdio: 'pipe'
2152
+ });
2153
+ }
2154
+ catch (error) {
2155
+ // Container might already be stopped
2156
+ }
2157
+ // Remove container
2158
+ try {
2159
+ (0, child_process_1.execSync)(`docker rm ${containerName}`, {
2160
+ encoding: 'utf8',
2161
+ timeout: 10000,
2162
+ stdio: 'pipe'
2163
+ });
2164
+ }
2165
+ catch (error) {
2166
+ // Container might already be removed
2167
+ }
2168
+ console.log(chalk_1.default.green(`✓ Docker container ${containerName} stopped and removed`));
2169
+ return true;
2170
+ }
2171
+ catch (error) {
2172
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
2173
+ console.error(chalk_1.default.red(`Failed to stop Docker container: ${errorMessage}`));
2174
+ return false;
2175
+ }
2176
+ }
2177
+ /**
2178
+ * Check Docker agent container status
2179
+ */
2180
+ async checkDockerAgentStatus(deviceId) {
2181
+ try {
2182
+ const containerName = this.getDockerContainerName(deviceId);
2183
+ const output = (0, child_process_1.execSync)(`docker ps --filter "name=${containerName}" --format "{{.Status}}"`, { encoding: 'utf8', timeout: 5000 });
2184
+ if (output.trim()) {
2185
+ return { running: true, status: output.trim() };
2186
+ }
2187
+ return { running: false };
2188
+ }
2189
+ catch (error) {
2190
+ return { running: false };
2191
+ }
2192
+ }
2193
+ /**
2194
+ * Get Docker agent container logs
2195
+ */
2196
+ async getDockerAgentLogs(deviceId, follow = false) {
2197
+ try {
2198
+ const containerName = this.getDockerContainerName(deviceId);
2199
+ const args = ['logs'];
2200
+ if (follow) {
2201
+ args.push('-f');
2202
+ }
2203
+ args.push(containerName);
2204
+ (0, child_process_1.execSync)(`docker ${args.join(' ')}`, {
2205
+ stdio: 'inherit',
2206
+ encoding: 'utf8'
2207
+ });
2208
+ }
2209
+ catch (error) {
2210
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
2211
+ console.error(chalk_1.default.red(`Failed to get Docker logs: ${errorMessage}`));
2212
+ }
2213
+ }
2214
+ }
2215
+ exports.LocalAgentManager = LocalAgentManager;
2216
+ //# sourceMappingURL=LocalAgentManager.js.map