@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.
- package/LICENSE +136 -0
- package/README.md +450 -0
- package/dist/client/api-client.js +1057 -0
- package/dist/client/index.js +21 -0
- package/dist/commands/agent.js +1280 -0
- package/dist/commands/ai.js +608 -0
- package/dist/commands/application.js +885 -0
- package/dist/commands/auth.js +570 -0
- package/dist/commands/base/BaseCommand.js +93 -0
- package/dist/commands/base/CommandHandler.js +7 -0
- package/dist/commands/base/command-wrapper.js +58 -0
- package/dist/commands/base/middleware.js +77 -0
- package/dist/commands/config.js +116 -0
- package/dist/commands/connectivity.js +59 -0
- package/dist/commands/debug.js +98 -0
- package/dist/commands/discover.js +144 -0
- package/dist/commands/examples/migrated-command-example.js +180 -0
- package/dist/commands/gateway.js +494 -0
- package/dist/commands/managedGateway.js +787 -0
- package/dist/commands/utils/config-validator.js +76 -0
- package/dist/commands/utils/gateway-prompt.js +79 -0
- package/dist/commands/utils/input-parser.js +120 -0
- package/dist/commands/utils/output-formatter.js +109 -0
- package/dist/config/app-config.js +99 -0
- package/dist/detection/SystemCapabilityDetector.js +1244 -0
- package/dist/detection/ToolDetector.js +305 -0
- package/dist/detection/WorkloadDetector.js +314 -0
- package/dist/di/bindings.js +99 -0
- package/dist/di/container.js +88 -0
- package/dist/di/types.js +32 -0
- package/dist/index.js +52 -0
- package/dist/interfaces/IDaemonManager.js +3 -0
- package/dist/repositories/config-repository.js +62 -0
- package/dist/repositories/gateway-repository.js +35 -0
- package/dist/scripts/postinstall.js +101 -0
- package/dist/services/AgentStatusManager.js +299 -0
- package/dist/services/ConnectivityTester.js +271 -0
- package/dist/services/DependencyInstaller.js +475 -0
- package/dist/services/LocalAgentManager.js +2216 -0
- package/dist/services/application/ApplicationService.js +299 -0
- package/dist/services/auth/AuthService.js +214 -0
- package/dist/services/aws.js +644 -0
- package/dist/services/daemon/DaemonManagerFactory.js +65 -0
- package/dist/services/daemon/DockerDaemonManager.js +395 -0
- package/dist/services/daemon/LaunchdDaemonManager.js +257 -0
- package/dist/services/daemon/PodmanDaemonManager.js +369 -0
- package/dist/services/daemon/SystemdDaemonManager.js +221 -0
- package/dist/services/daemon/WindowsServiceDaemonManager.js +210 -0
- package/dist/services/daemon/index.js +16 -0
- package/dist/services/edgible.js +3060 -0
- package/dist/services/gateway/GatewayService.js +334 -0
- package/dist/state/config.js +146 -0
- package/dist/types/AgentConfig.js +5 -0
- package/dist/types/AgentStatus.js +5 -0
- package/dist/types/ApiClient.js +5 -0
- package/dist/types/ApiRequests.js +5 -0
- package/dist/types/ApiResponses.js +5 -0
- package/dist/types/Application.js +5 -0
- package/dist/types/CaddyJson.js +5 -0
- package/dist/types/UnifiedAgentStatus.js +56 -0
- package/dist/types/WireGuard.js +5 -0
- package/dist/types/Workload.js +5 -0
- package/dist/types/agent.js +5 -0
- package/dist/types/command-options.js +5 -0
- package/dist/types/connectivity.js +5 -0
- package/dist/types/errors.js +250 -0
- package/dist/types/gateway-types.js +5 -0
- package/dist/types/index.js +48 -0
- package/dist/types/models/ApplicationData.js +5 -0
- package/dist/types/models/CertificateData.js +5 -0
- package/dist/types/models/DeviceData.js +5 -0
- package/dist/types/models/DevicePoolData.js +5 -0
- package/dist/types/models/OrganizationData.js +5 -0
- package/dist/types/models/OrganizationInviteData.js +5 -0
- package/dist/types/models/ProviderConfiguration.js +5 -0
- package/dist/types/models/ResourceData.js +5 -0
- package/dist/types/models/ServiceResourceData.js +5 -0
- package/dist/types/models/UserData.js +5 -0
- package/dist/types/route.js +5 -0
- package/dist/types/validation/schemas.js +218 -0
- package/dist/types/validation.js +5 -0
- package/dist/utils/FileIntegrityManager.js +256 -0
- package/dist/utils/PathMigration.js +219 -0
- package/dist/utils/PathResolver.js +235 -0
- package/dist/utils/PlatformDetector.js +277 -0
- package/dist/utils/console-logger.js +130 -0
- package/dist/utils/docker-compose-parser.js +179 -0
- package/dist/utils/errors.js +130 -0
- package/dist/utils/health-checker.js +155 -0
- package/dist/utils/json-logger.js +72 -0
- package/dist/utils/log-formatter.js +293 -0
- package/dist/utils/logger.js +59 -0
- package/dist/utils/network-utils.js +217 -0
- package/dist/utils/output.js +182 -0
- package/dist/utils/passwordValidation.js +91 -0
- package/dist/utils/progress.js +167 -0
- package/dist/utils/sudo-checker.js +22 -0
- package/dist/utils/urls.js +32 -0
- package/dist/utils/validation.js +31 -0
- package/dist/validation/schemas.js +175 -0
- package/dist/validation/validator.js +67 -0
- 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
|