termify-agent 1.0.41 → 1.0.42
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/dist/agent.d.ts +43 -35
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +405 -307
- package/dist/agent.js.map +1 -1
- package/dist/auth.d.ts +0 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +38 -14
- package/dist/auth.js.map +1 -1
- package/dist/config.d.ts +41 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +140 -14
- package/dist/config.js.map +1 -1
- package/dist/index.js +63 -19
- package/dist/index.js.map +1 -1
- package/dist/setup.d.ts +2 -2
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +3 -3
- package/dist/setup.js.map +1 -1
- package/dist/stats-manager.d.ts +10 -0
- package/dist/stats-manager.d.ts.map +1 -1
- package/dist/stats-manager.js +9 -0
- package/dist/stats-manager.js.map +1 -1
- package/dist/updater.d.ts +27 -0
- package/dist/updater.d.ts.map +1 -0
- package/dist/updater.js +153 -0
- package/dist/updater.js.map +1 -0
- package/dist/ws-client.d.ts +13 -1
- package/dist/ws-client.d.ts.map +1 -1
- package/dist/ws-client.js +56 -15
- package/dist/ws-client.js.map +1 -1
- package/mcp/termify-mcp-bundle.mjs +1715 -230
- package/package.json +1 -1
- package/scripts/install.sh +374 -0
- package/scripts/package.sh +166 -0
package/dist/agent.js
CHANGED
|
@@ -12,16 +12,21 @@ import { WireGuardManager } from './network/wireguard-manager.js';
|
|
|
12
12
|
import { NatTraversal } from './network/nat-traversal.js';
|
|
13
13
|
import { KeyRotation } from './network/key-rotation.js';
|
|
14
14
|
import { getDaemonClient } from './daemon-client.js';
|
|
15
|
-
import { getConfig, isConfigured } from './config.js';
|
|
15
|
+
import { getConfig, getServers, isConfigured } from './config.js';
|
|
16
16
|
import { logger } from './utils/logger.js';
|
|
17
17
|
/**
|
|
18
|
-
* Main agent class that coordinates WebSocket
|
|
18
|
+
* Main agent class that coordinates WebSocket connections and PTY/SSH management.
|
|
19
|
+
* Supports multiple simultaneous server connections.
|
|
19
20
|
*/
|
|
20
21
|
export class Agent extends EventEmitter {
|
|
21
|
-
|
|
22
|
+
/** Map of serverUrl -> WSClient for multi-server support */
|
|
23
|
+
wsClients = new Map();
|
|
24
|
+
/** Map of serverUrl -> TunnelManager (tunnels are per-server) */
|
|
25
|
+
tunnelManagers = new Map();
|
|
26
|
+
/** Map of terminalId -> serverUrl (routes terminal I/O to the server that created it) */
|
|
27
|
+
terminalServerMap = new Map();
|
|
22
28
|
ptyManager;
|
|
23
29
|
sshManager;
|
|
24
|
-
tunnelManager;
|
|
25
30
|
statsManager;
|
|
26
31
|
portScanner;
|
|
27
32
|
daemonClient;
|
|
@@ -31,26 +36,28 @@ export class Agent extends EventEmitter {
|
|
|
31
36
|
isRunning = false;
|
|
32
37
|
agentId = null;
|
|
33
38
|
agentName = null;
|
|
39
|
+
/** Track which servers have connected (for events) */
|
|
40
|
+
connectedServers = new Set();
|
|
41
|
+
primaryServerUrl = null;
|
|
34
42
|
constructor() {
|
|
35
43
|
super();
|
|
36
|
-
this.wsClient = new WSClient();
|
|
37
44
|
this.ptyManager = new PTYManager();
|
|
38
45
|
this.sshManager = new SSHManager();
|
|
39
|
-
this.
|
|
40
|
-
this.statsManager = new StatsManager(5); // Collect stats every 5 seconds
|
|
46
|
+
this.statsManager = new StatsManager(5);
|
|
41
47
|
this.portScanner = new PortScanner();
|
|
42
48
|
this.daemonClient = getDaemonClient();
|
|
43
49
|
this.wgManager = new WireGuardManager();
|
|
44
50
|
this.natTraversal = new NatTraversal();
|
|
45
51
|
this.keyRotation = new KeyRotation();
|
|
46
|
-
// Key rotation: timer fires -> rotate key -> propose to server
|
|
52
|
+
// Key rotation: timer fires -> rotate key -> propose to primary server
|
|
47
53
|
this.keyRotation.on('rotate-needed', async () => {
|
|
48
54
|
if (!this.wgManager.getStatus().available)
|
|
49
55
|
return;
|
|
50
56
|
try {
|
|
51
57
|
const result = await this.wgManager.rotateKey();
|
|
52
58
|
const version = this.keyRotation.proposeRotation();
|
|
53
|
-
this.
|
|
59
|
+
const primaryWs = this.getPrimaryWsClient();
|
|
60
|
+
primaryWs?.sendWgRotatePropose(result.publicKey, version);
|
|
54
61
|
logger.info(`Key rotation proposed: v${version}`);
|
|
55
62
|
}
|
|
56
63
|
catch (err) {
|
|
@@ -61,39 +68,152 @@ export class Agent extends EventEmitter {
|
|
|
61
68
|
this.wgManager.on('ready', () => {
|
|
62
69
|
logger.info('WireGuard interface ready');
|
|
63
70
|
this.reportWgStatus();
|
|
64
|
-
// Start periodic key rotation once the interface is up
|
|
65
71
|
this.keyRotation.start();
|
|
66
72
|
});
|
|
67
73
|
this.wgManager.on('error', (err) => {
|
|
68
74
|
logger.error('WireGuard error:', err.message);
|
|
69
75
|
this.reportWgStatus();
|
|
70
76
|
});
|
|
71
|
-
// Pass
|
|
72
|
-
this.ptyManager.setWSClient(this.
|
|
77
|
+
// Pass WSClient shim to PTYManager for per-terminal git/MCP routing
|
|
78
|
+
this.ptyManager.setWSClient(this.createPtyWsShim());
|
|
73
79
|
// Pass daemon client to PTYManager for process monitoring
|
|
74
80
|
this.ptyManager.setDaemonClient(this.daemonClient);
|
|
75
|
-
// Start periodic cleanup of ephemeral terminals
|
|
81
|
+
// Start periodic cleanup of ephemeral terminals
|
|
76
82
|
this.ptyManager.startEphemeralCleanup();
|
|
77
|
-
this.
|
|
83
|
+
this.setupSharedHandlers();
|
|
78
84
|
}
|
|
79
85
|
/**
|
|
80
|
-
*
|
|
86
|
+
* Create a WSClient-like shim that routes per-terminal calls to the correct server
|
|
81
87
|
*/
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
createPtyWsShim() {
|
|
89
|
+
const self = this;
|
|
90
|
+
return {
|
|
91
|
+
sendGitBranch(terminalId, branch, isGitRepo) {
|
|
92
|
+
self.getWsClientForTerminal(terminalId)?.sendGitBranch(terminalId, branch, isGitRepo);
|
|
93
|
+
},
|
|
94
|
+
sendGitBranches(terminalId, branches) {
|
|
95
|
+
self.getWsClientForTerminal(terminalId)?.sendGitBranches(terminalId, branches);
|
|
96
|
+
},
|
|
97
|
+
sendGitLog(terminalId, commits) {
|
|
98
|
+
self.getWsClientForTerminal(terminalId)?.sendGitLog(terminalId, commits);
|
|
99
|
+
},
|
|
100
|
+
sendGitStatus(terminalId, status) {
|
|
101
|
+
self.getWsClientForTerminal(terminalId)?.sendGitStatus(terminalId, status);
|
|
102
|
+
},
|
|
103
|
+
sendGitOperationResult(terminalId, operation, success, error) {
|
|
104
|
+
self.getWsClientForTerminal(terminalId)?.sendGitOperationResult(terminalId, operation, success, error);
|
|
105
|
+
},
|
|
106
|
+
sendTerminalMcpStatus(terminalId, state, message, needsInputPrompt) {
|
|
107
|
+
self.getWsClientForTerminal(terminalId)?.sendTerminalMcpStatus(terminalId, state, message, needsInputPrompt);
|
|
108
|
+
},
|
|
109
|
+
sendTerminalCwd(terminalId, cwd) {
|
|
110
|
+
self.getWsClientForTerminal(terminalId)?.sendTerminalCwd(terminalId, cwd);
|
|
111
|
+
},
|
|
112
|
+
get connected() {
|
|
113
|
+
// Return true if any server is connected (PTYManager uses this to gate operations)
|
|
114
|
+
return self.connectedServers.size > 0;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get the WSClient responsible for a given terminal
|
|
120
|
+
*/
|
|
121
|
+
getWsClientForTerminal(terminalId) {
|
|
122
|
+
const serverUrl = this.terminalServerMap.get(terminalId);
|
|
123
|
+
if (serverUrl)
|
|
124
|
+
return this.wsClients.get(serverUrl);
|
|
125
|
+
// Fallback to primary
|
|
126
|
+
return this.getPrimaryWsClient() || undefined;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Get the primary server's WSClient
|
|
130
|
+
*/
|
|
131
|
+
getPrimaryWsClient() {
|
|
132
|
+
if (this.primaryServerUrl) {
|
|
133
|
+
return this.wsClients.get(this.primaryServerUrl) || null;
|
|
134
|
+
}
|
|
135
|
+
// Fallback: first client
|
|
136
|
+
const first = this.wsClients.values().next();
|
|
137
|
+
return first.done ? null : first.value;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Broadcast to ALL connected WSClients
|
|
141
|
+
*/
|
|
142
|
+
broadcastAll(fn) {
|
|
143
|
+
for (const ws of this.wsClients.values()) {
|
|
144
|
+
if (ws.connected)
|
|
145
|
+
fn(ws);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Setup shared PTY/SSH data handlers (these route output to the correct server)
|
|
150
|
+
*/
|
|
151
|
+
setupSharedHandlers() {
|
|
152
|
+
// PTY -> WebSocket: Forward output to the server that owns the terminal
|
|
153
|
+
this.ptyManager.setDataHandler((terminalId, data) => {
|
|
154
|
+
this.getWsClientForTerminal(terminalId)?.sendTerminalOutput(terminalId, data);
|
|
155
|
+
});
|
|
156
|
+
this.ptyManager.setExitHandler((terminalId, exitCode) => {
|
|
157
|
+
const ws = this.getWsClientForTerminal(terminalId);
|
|
158
|
+
ws?.sendTerminalExit(terminalId, exitCode);
|
|
159
|
+
ws?.sendTerminalStatus(terminalId, 'STOPPED');
|
|
160
|
+
this.terminalServerMap.delete(terminalId);
|
|
161
|
+
this.emit('terminalsChanged');
|
|
162
|
+
});
|
|
163
|
+
this.ptyManager.setWorkingHandler((terminalId, isWorking, activeProcess) => {
|
|
164
|
+
this.getWsClientForTerminal(terminalId)?.sendTerminalWorking(terminalId, isWorking, activeProcess);
|
|
165
|
+
this.emit('terminalsChanged');
|
|
166
|
+
});
|
|
167
|
+
// SSH -> WebSocket: Forward output to the server that owns the terminal
|
|
168
|
+
this.sshManager.setDataHandler((terminalId, data) => {
|
|
169
|
+
this.getWsClientForTerminal(terminalId)?.sendTerminalOutput(terminalId, data);
|
|
170
|
+
});
|
|
171
|
+
this.sshManager.setExitHandler((terminalId, exitCode) => {
|
|
172
|
+
const ws = this.getWsClientForTerminal(terminalId);
|
|
173
|
+
ws?.sendTerminalExit(terminalId, exitCode);
|
|
174
|
+
ws?.sendTerminalStatus(terminalId, 'STOPPED');
|
|
175
|
+
this.terminalServerMap.delete(terminalId);
|
|
176
|
+
this.emit('terminalsChanged');
|
|
177
|
+
});
|
|
178
|
+
this.sshManager.setErrorHandler((terminalId, error) => {
|
|
179
|
+
logger.error(`SSH error for ${terminalId}: ${error}`);
|
|
180
|
+
this.getWsClientForTerminal(terminalId)?.sendTerminalStatus(terminalId, 'CRASHED', error);
|
|
181
|
+
});
|
|
182
|
+
// Port scanning — broadcast to ALL servers
|
|
183
|
+
this.portScanner.on('ports', (ports) => {
|
|
184
|
+
this.broadcastAll(ws => ws.sendPorts(ports));
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Add a server connection with all event wiring
|
|
189
|
+
*/
|
|
190
|
+
addServerConnection(entry) {
|
|
191
|
+
if (this.wsClients.has(entry.serverUrl)) {
|
|
192
|
+
logger.warn(`Already connected to ${entry.serverUrl}`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const wsClient = new WSClient(entry);
|
|
196
|
+
const tunnelManager = new TunnelManager(wsClient);
|
|
197
|
+
const serverUrl = entry.serverUrl;
|
|
198
|
+
this.wsClients.set(serverUrl, wsClient);
|
|
199
|
+
this.tunnelManagers.set(serverUrl, tunnelManager);
|
|
200
|
+
if (entry.isPrimary) {
|
|
201
|
+
this.primaryServerUrl = serverUrl;
|
|
202
|
+
}
|
|
203
|
+
// === Terminal creation: register terminalServerMap ===
|
|
204
|
+
wsClient.on('terminalCreate', (terminalId, cols, rows, options) => {
|
|
86
205
|
if (this.ptyManager.has(terminalId)) {
|
|
87
206
|
logger.info(`Terminal ${terminalId} already exists, reporting RUNNING`);
|
|
88
|
-
|
|
207
|
+
wsClient.sendTerminalStatus(terminalId, 'RUNNING');
|
|
89
208
|
return;
|
|
90
209
|
}
|
|
91
|
-
|
|
210
|
+
this.terminalServerMap.set(terminalId, serverUrl);
|
|
211
|
+
logger.info(`Creating local terminal ${terminalId} (server: ${entry.name})`);
|
|
92
212
|
const config = getConfig();
|
|
93
|
-
const
|
|
94
|
-
const termifyStatusUrl =
|
|
95
|
-
const termifySessionId =
|
|
96
|
-
const termifyApiUrl =
|
|
213
|
+
const srvUrl = (entry.serverUrl || '').replace(/\/$/, '');
|
|
214
|
+
const termifyStatusUrl = srvUrl ? `${srvUrl}/api/mcp/status` : undefined;
|
|
215
|
+
const termifySessionId = entry.agentId || config.machineId || terminalId;
|
|
216
|
+
const termifyApiUrl = srvUrl ? `${srvUrl}/api` : undefined;
|
|
97
217
|
const env = {
|
|
98
218
|
...(options.env || {}),
|
|
99
219
|
};
|
|
@@ -103,8 +223,8 @@ export class Agent extends EventEmitter {
|
|
|
103
223
|
env.TERMIFY_TERMINAL_ID = terminalId;
|
|
104
224
|
if (!env.TERMIFY_SESSION_ID)
|
|
105
225
|
env.TERMIFY_SESSION_ID = termifySessionId;
|
|
106
|
-
if (!env.TERMIFY_TOKEN &&
|
|
107
|
-
env.TERMIFY_TOKEN =
|
|
226
|
+
if (!env.TERMIFY_TOKEN && entry.accessToken)
|
|
227
|
+
env.TERMIFY_TOKEN = entry.accessToken;
|
|
108
228
|
if (!env.TERMIFY_API_URL && termifyApiUrl)
|
|
109
229
|
env.TERMIFY_API_URL = termifyApiUrl;
|
|
110
230
|
const created = this.ptyManager.create(terminalId, {
|
|
@@ -115,45 +235,51 @@ export class Agent extends EventEmitter {
|
|
|
115
235
|
env,
|
|
116
236
|
});
|
|
117
237
|
if (created) {
|
|
118
|
-
|
|
238
|
+
wsClient.sendTerminalStatus(terminalId, 'RUNNING');
|
|
119
239
|
this.emit('terminalsChanged');
|
|
120
240
|
}
|
|
121
241
|
else {
|
|
122
|
-
|
|
242
|
+
wsClient.sendTerminalStatus(terminalId, 'CRASHED', 'Failed to create terminal');
|
|
243
|
+
this.terminalServerMap.delete(terminalId);
|
|
123
244
|
}
|
|
124
245
|
});
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
246
|
+
// SSH terminal creation
|
|
247
|
+
wsClient.on('terminalCreateSSH', async (terminalId, cols, rows, sshConfig) => {
|
|
248
|
+
this.terminalServerMap.set(terminalId, serverUrl);
|
|
249
|
+
logger.info(`Creating SSH terminal ${terminalId} -> ${sshConfig.host}:${sshConfig.port} (server: ${entry.name})`);
|
|
128
250
|
const connected = await this.sshManager.connect(terminalId, cols, rows, sshConfig);
|
|
129
251
|
if (connected) {
|
|
130
|
-
|
|
252
|
+
wsClient.sendTerminalStatus(terminalId, 'RUNNING');
|
|
131
253
|
this.emit('terminalsChanged');
|
|
132
254
|
}
|
|
133
255
|
else {
|
|
134
|
-
|
|
256
|
+
wsClient.sendTerminalStatus(terminalId, 'CRASHED', `Failed to connect to ${sshConfig.host}`);
|
|
257
|
+
this.terminalServerMap.delete(terminalId);
|
|
135
258
|
}
|
|
136
259
|
});
|
|
137
|
-
// SSH over tunnel
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
260
|
+
// SSH over tunnel
|
|
261
|
+
wsClient.on('terminalSSHTunnel', async (terminalId, tunnelId, cols, rows, sshConfig) => {
|
|
262
|
+
this.terminalServerMap.set(terminalId, serverUrl);
|
|
263
|
+
logger.info(`Creating SSH tunnel terminal ${terminalId} via tunnel ${tunnelId} (server: ${entry.name})`);
|
|
264
|
+
if (!tunnelManager.has(tunnelId)) {
|
|
141
265
|
logger.error(`Tunnel ${tunnelId} not found for SSH terminal ${terminalId}`);
|
|
142
|
-
|
|
266
|
+
wsClient.sendTerminalStatus(terminalId, 'CRASHED', `Tunnel ${tunnelId} not found`);
|
|
267
|
+
this.terminalServerMap.delete(terminalId);
|
|
143
268
|
return;
|
|
144
269
|
}
|
|
145
|
-
const tunnelStream =
|
|
270
|
+
const tunnelStream = tunnelManager.createTunnelStream(tunnelId);
|
|
146
271
|
const connected = await this.sshManager.connectOverTunnel(terminalId, cols, rows, sshConfig, tunnelStream);
|
|
147
272
|
if (connected) {
|
|
148
|
-
|
|
273
|
+
wsClient.sendTerminalStatus(terminalId, 'RUNNING');
|
|
149
274
|
this.emit('terminalsChanged');
|
|
150
275
|
}
|
|
151
276
|
else {
|
|
152
|
-
|
|
277
|
+
wsClient.sendTerminalStatus(terminalId, 'CRASHED', `Failed to SSH over tunnel to ${sshConfig.host}`);
|
|
278
|
+
this.terminalServerMap.delete(terminalId);
|
|
153
279
|
}
|
|
154
280
|
});
|
|
155
|
-
// Terminal input - route to appropriate manager
|
|
156
|
-
|
|
281
|
+
// Terminal input - route to appropriate manager (terminals are shared)
|
|
282
|
+
wsClient.on('terminalInput', (terminalId, data) => {
|
|
157
283
|
if (this.sshManager.has(terminalId)) {
|
|
158
284
|
this.sshManager.write(terminalId, data);
|
|
159
285
|
}
|
|
@@ -161,8 +287,8 @@ export class Agent extends EventEmitter {
|
|
|
161
287
|
this.ptyManager.write(terminalId, data);
|
|
162
288
|
}
|
|
163
289
|
});
|
|
164
|
-
// Terminal paste files
|
|
165
|
-
|
|
290
|
+
// Terminal paste files
|
|
291
|
+
wsClient.on('terminalPasteFiles', async (terminalId, files) => {
|
|
166
292
|
try {
|
|
167
293
|
if (!Array.isArray(files) || files.length === 0)
|
|
168
294
|
return;
|
|
@@ -189,8 +315,6 @@ export class Agent extends EventEmitter {
|
|
|
189
315
|
await fs.writeFile(outPath, buf);
|
|
190
316
|
writtenPaths.push(outPath);
|
|
191
317
|
}
|
|
192
|
-
// Paste paths into whichever terminal type is active.
|
|
193
|
-
// NOTE: For SSH terminals, these files exist locally on the agent, not on the remote host.
|
|
194
318
|
for (const p of writtenPaths) {
|
|
195
319
|
const text = `@${p}\r`;
|
|
196
320
|
if (this.sshManager.has(terminalId)) {
|
|
@@ -205,8 +329,8 @@ export class Agent extends EventEmitter {
|
|
|
205
329
|
logger.warn(`[PasteFiles] Failed for terminal ${terminalId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
206
330
|
}
|
|
207
331
|
});
|
|
208
|
-
// Terminal resize
|
|
209
|
-
|
|
332
|
+
// Terminal resize
|
|
333
|
+
wsClient.on('terminalResize', (terminalId, cols, rows) => {
|
|
210
334
|
if (this.sshManager.has(terminalId)) {
|
|
211
335
|
this.sshManager.resize(terminalId, cols, rows);
|
|
212
336
|
}
|
|
@@ -214,8 +338,8 @@ export class Agent extends EventEmitter {
|
|
|
214
338
|
this.ptyManager.resize(terminalId, cols, rows);
|
|
215
339
|
}
|
|
216
340
|
});
|
|
217
|
-
// Terminal stop
|
|
218
|
-
|
|
341
|
+
// Terminal stop
|
|
342
|
+
wsClient.on('terminalStop', (terminalId) => {
|
|
219
343
|
if (this.sshManager.has(terminalId)) {
|
|
220
344
|
this.sshManager.kill(terminalId);
|
|
221
345
|
}
|
|
@@ -223,109 +347,78 @@ export class Agent extends EventEmitter {
|
|
|
223
347
|
this.ptyManager.kill(terminalId);
|
|
224
348
|
}
|
|
225
349
|
});
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
});
|
|
230
|
-
this.ptyManager.setExitHandler((terminalId, exitCode) => {
|
|
231
|
-
this.wsClient.sendTerminalExit(terminalId, exitCode);
|
|
232
|
-
this.wsClient.sendTerminalStatus(terminalId, 'STOPPED');
|
|
233
|
-
this.emit('terminalsChanged');
|
|
234
|
-
});
|
|
235
|
-
// PTY -> WebSocket: Forward working state changes with active process name
|
|
236
|
-
this.ptyManager.setWorkingHandler((terminalId, isWorking, activeProcess) => {
|
|
237
|
-
this.wsClient.sendTerminalWorking(terminalId, isWorking, activeProcess);
|
|
238
|
-
this.emit('terminalsChanged');
|
|
239
|
-
});
|
|
240
|
-
// SSH -> WebSocket: Forward output to server
|
|
241
|
-
this.sshManager.setDataHandler((terminalId, data) => {
|
|
242
|
-
this.wsClient.sendTerminalOutput(terminalId, data);
|
|
243
|
-
});
|
|
244
|
-
this.sshManager.setExitHandler((terminalId, exitCode) => {
|
|
245
|
-
this.wsClient.sendTerminalExit(terminalId, exitCode);
|
|
246
|
-
this.wsClient.sendTerminalStatus(terminalId, 'STOPPED');
|
|
247
|
-
this.emit('terminalsChanged');
|
|
248
|
-
});
|
|
249
|
-
this.sshManager.setErrorHandler((terminalId, error) => {
|
|
250
|
-
logger.error(`SSH error for ${terminalId}: ${error}`);
|
|
251
|
-
this.wsClient.sendTerminalStatus(terminalId, 'CRASHED', error);
|
|
252
|
-
});
|
|
253
|
-
// Tunnel operations: WSClient -> TunnelManager
|
|
254
|
-
this.wsClient.on('tunnelCreate', (tunnelId, sourceAgentId, targetAgentId, role, targetHost, targetPort, protocol, idleTimeoutMs) => {
|
|
255
|
-
this.tunnelManager.handleCreate(tunnelId, sourceAgentId, targetAgentId, role, targetHost, targetPort, protocol, idleTimeoutMs);
|
|
350
|
+
// Tunnel operations
|
|
351
|
+
wsClient.on('tunnelCreate', (tunnelId, sourceAgentId, targetAgentId, role, targetHost, targetPort, protocol, idleTimeoutMs) => {
|
|
352
|
+
tunnelManager.handleCreate(tunnelId, sourceAgentId, targetAgentId, role, targetHost, targetPort, protocol, idleTimeoutMs);
|
|
256
353
|
});
|
|
257
|
-
|
|
258
|
-
|
|
354
|
+
wsClient.on('tunnelClose', (tunnelId, reason) => {
|
|
355
|
+
tunnelManager.handleClose(tunnelId, reason);
|
|
259
356
|
});
|
|
260
|
-
|
|
261
|
-
|
|
357
|
+
wsClient.on('tunnelFlow', (tunnelId, action) => {
|
|
358
|
+
tunnelManager.handleFlow(tunnelId, action);
|
|
262
359
|
});
|
|
263
|
-
|
|
264
|
-
|
|
360
|
+
wsClient.on('tunnelBinaryData', (data) => {
|
|
361
|
+
tunnelManager.handleBinaryFrame(data);
|
|
265
362
|
});
|
|
266
|
-
// Git operations
|
|
267
|
-
|
|
363
|
+
// Git operations → PTYManager (response goes back to the same server via shim)
|
|
364
|
+
wsClient.on('gitCheckout', async (terminalId, branch) => {
|
|
268
365
|
const result = await this.ptyManager.gitCheckout(terminalId, branch);
|
|
269
366
|
if (result.success) {
|
|
270
367
|
const branchInfo = await this.ptyManager.getGitBranches(terminalId);
|
|
271
|
-
|
|
368
|
+
wsClient.sendGitBranches(terminalId, branchInfo);
|
|
272
369
|
const status = await this.ptyManager.getGitStatus(terminalId);
|
|
273
|
-
|
|
370
|
+
wsClient.sendGitStatus(terminalId, status);
|
|
274
371
|
}
|
|
275
372
|
});
|
|
276
|
-
|
|
373
|
+
wsClient.on('gitCreateBranch', async (terminalId, name) => {
|
|
277
374
|
const result = await this.ptyManager.gitCreateBranch(terminalId, name);
|
|
278
375
|
if (result.success) {
|
|
279
376
|
const branchInfo = await this.ptyManager.getGitBranches(terminalId);
|
|
280
|
-
|
|
377
|
+
wsClient.sendGitBranches(terminalId, branchInfo);
|
|
281
378
|
const status = await this.ptyManager.getGitStatus(terminalId);
|
|
282
|
-
|
|
379
|
+
wsClient.sendGitStatus(terminalId, status);
|
|
283
380
|
}
|
|
284
381
|
});
|
|
285
|
-
|
|
382
|
+
wsClient.on('gitRefresh', async (terminalId) => {
|
|
286
383
|
const branchInfo = await this.ptyManager.getGitBranches(terminalId);
|
|
287
|
-
|
|
384
|
+
wsClient.sendGitBranches(terminalId, branchInfo);
|
|
288
385
|
const commits = await this.ptyManager.getGitLog(terminalId);
|
|
289
|
-
|
|
386
|
+
wsClient.sendGitLog(terminalId, commits);
|
|
290
387
|
const status = await this.ptyManager.getGitStatus(terminalId);
|
|
291
|
-
|
|
388
|
+
wsClient.sendGitStatus(terminalId, status);
|
|
292
389
|
});
|
|
293
|
-
|
|
390
|
+
wsClient.on('gitFetch', async (terminalId) => {
|
|
294
391
|
const result = await this.ptyManager.gitFetch(terminalId);
|
|
295
|
-
|
|
392
|
+
wsClient.sendGitOperationResult(terminalId, 'fetch', result.success, result.error);
|
|
296
393
|
if (result.success) {
|
|
297
394
|
const status = await this.ptyManager.getGitStatus(terminalId);
|
|
298
|
-
|
|
395
|
+
wsClient.sendGitStatus(terminalId, status);
|
|
299
396
|
}
|
|
300
397
|
});
|
|
301
|
-
|
|
398
|
+
wsClient.on('gitPush', async (terminalId) => {
|
|
302
399
|
const result = await this.ptyManager.gitPush(terminalId);
|
|
303
|
-
|
|
400
|
+
wsClient.sendGitOperationResult(terminalId, 'push', result.success, result.error);
|
|
304
401
|
if (result.success) {
|
|
305
402
|
const status = await this.ptyManager.getGitStatus(terminalId);
|
|
306
|
-
|
|
403
|
+
wsClient.sendGitStatus(terminalId, status);
|
|
307
404
|
}
|
|
308
405
|
});
|
|
309
|
-
|
|
406
|
+
wsClient.on('gitPull', async (terminalId) => {
|
|
310
407
|
const result = await this.ptyManager.gitPull(terminalId);
|
|
311
|
-
|
|
408
|
+
wsClient.sendGitOperationResult(terminalId, 'pull', result.success, result.error);
|
|
312
409
|
if (result.success) {
|
|
313
410
|
const branchInfo = await this.ptyManager.getGitBranches(terminalId);
|
|
314
|
-
|
|
411
|
+
wsClient.sendGitBranches(terminalId, branchInfo);
|
|
315
412
|
const status = await this.ptyManager.getGitStatus(terminalId);
|
|
316
|
-
|
|
413
|
+
wsClient.sendGitStatus(terminalId, status);
|
|
317
414
|
}
|
|
318
415
|
});
|
|
319
|
-
|
|
416
|
+
wsClient.on('gitStatus', async (terminalId) => {
|
|
320
417
|
const status = await this.ptyManager.getGitStatus(terminalId);
|
|
321
|
-
|
|
322
|
-
});
|
|
323
|
-
// Port scanning
|
|
324
|
-
this.portScanner.on('ports', (ports) => {
|
|
325
|
-
this.wsClient.sendPorts(ports);
|
|
418
|
+
wsClient.sendGitStatus(terminalId, status);
|
|
326
419
|
});
|
|
327
420
|
// Preview proxy requests
|
|
328
|
-
|
|
421
|
+
wsClient.on('previewRequest', async (requestId, port, reqPath, method, headers, body) => {
|
|
329
422
|
try {
|
|
330
423
|
const url = `http://localhost:${port}${reqPath}`;
|
|
331
424
|
const fetchOpts = { method, headers };
|
|
@@ -341,155 +434,159 @@ export class Agent extends EventEmitter {
|
|
|
341
434
|
});
|
|
342
435
|
if (isText) {
|
|
343
436
|
const text = await response.text();
|
|
344
|
-
|
|
437
|
+
wsClient.sendPreviewResponse(requestId, response.status, responseHeaders, text, false);
|
|
345
438
|
}
|
|
346
439
|
else {
|
|
347
440
|
const buf = Buffer.from(await response.arrayBuffer());
|
|
348
|
-
|
|
441
|
+
wsClient.sendPreviewResponse(requestId, response.status, responseHeaders, buf.toString('base64'), true);
|
|
349
442
|
}
|
|
350
443
|
}
|
|
351
444
|
catch (err) {
|
|
352
|
-
|
|
445
|
+
wsClient.sendPreviewResponse(requestId, 502, {}, `Agent could not reach localhost:${port} - ${err.message}`, false);
|
|
353
446
|
}
|
|
354
447
|
});
|
|
355
|
-
// WireGuard
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
this.wsClient.sendWgReady(status.interfaceName || 'termify0', status.publicKey, status.listenPort);
|
|
448
|
+
// WireGuard: only wire to primary server
|
|
449
|
+
if (entry.isPrimary) {
|
|
450
|
+
wsClient.on('wgFeature', (enabled, version) => {
|
|
451
|
+
logger.info(`WireGuard feature: enabled=${enabled} version=${version}`);
|
|
452
|
+
});
|
|
453
|
+
wsClient.on('wgPeersFull', async (selfVirtualIp, peers) => {
|
|
454
|
+
if (!this.wgManager.getStatus().available)
|
|
455
|
+
return;
|
|
456
|
+
try {
|
|
457
|
+
await this.wgManager.ensureInterface(selfVirtualIp);
|
|
458
|
+
const wgPeers = peers.map((p) => ({
|
|
459
|
+
agentId: p.agentId,
|
|
460
|
+
publicKey: p.publicKey,
|
|
461
|
+
allowedIps: `${p.virtualIp}/32`,
|
|
462
|
+
endpoint: p.endpointHint,
|
|
463
|
+
persistentKeepalive: 25,
|
|
464
|
+
}));
|
|
465
|
+
await this.wgManager.setPeers(wgPeers);
|
|
466
|
+
const status = this.wgManager.getStatus();
|
|
467
|
+
if (status.publicKey && status.listenPort) {
|
|
468
|
+
wsClient.sendWgReady(status.interfaceName || 'termify0', status.publicKey, status.listenPort);
|
|
469
|
+
}
|
|
470
|
+
logger.info(`WG peers configured: ${peers.length} peers, virtualIp=${selfVirtualIp}`);
|
|
379
471
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
472
|
+
catch (err) {
|
|
473
|
+
logger.error('Failed to configure WG peers:', err);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
wsClient.on('wgPeerAdd', async (peer) => {
|
|
477
|
+
if (!this.wgManager.getStatus().available)
|
|
478
|
+
return;
|
|
479
|
+
try {
|
|
480
|
+
await this.wgManager.addPeer({
|
|
481
|
+
agentId: peer.agentId,
|
|
482
|
+
publicKey: peer.publicKey,
|
|
483
|
+
allowedIps: `${peer.virtualIp}/32`,
|
|
484
|
+
endpoint: peer.endpointHint,
|
|
485
|
+
persistentKeepalive: 25,
|
|
486
|
+
});
|
|
487
|
+
logger.info(`WG peer added: ${peer.agentId}`);
|
|
488
|
+
}
|
|
489
|
+
catch (err) {
|
|
490
|
+
logger.error(`Failed to add WG peer ${peer.agentId}:`, err);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
wsClient.on('wgPeerRemove', async (_agentId, publicKey) => {
|
|
494
|
+
if (!this.wgManager.getStatus().available)
|
|
495
|
+
return;
|
|
496
|
+
try {
|
|
497
|
+
await this.wgManager.removePeer(publicKey);
|
|
498
|
+
logger.info(`WG peer removed: ${_agentId}`);
|
|
499
|
+
}
|
|
500
|
+
catch (err) {
|
|
501
|
+
logger.error(`Failed to remove WG peer:`, err);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
wsClient.on('wgNatProbe', async (token, servers) => {
|
|
505
|
+
if (!this.wgManager.getStatus().available)
|
|
506
|
+
return;
|
|
507
|
+
try {
|
|
508
|
+
await this.natTraversal.probe({ token, servers });
|
|
509
|
+
logger.info('NAT probe sent to ' + servers.length + ' discovery servers');
|
|
510
|
+
}
|
|
511
|
+
catch (err) {
|
|
512
|
+
logger.error('NAT probe failed:', err);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
wsClient.on('wgEndpointCandidates', async (peerAgentId, peerPublicKey, candidates) => {
|
|
516
|
+
if (!this.wgManager.getStatus().available)
|
|
517
|
+
return;
|
|
518
|
+
try {
|
|
519
|
+
if (candidates.length > 0) {
|
|
520
|
+
const best = candidates[0];
|
|
521
|
+
const endpoint = `${best.ip}:${best.port}`;
|
|
522
|
+
await this.wgManager.updatePeerEndpoint(peerPublicKey, endpoint);
|
|
523
|
+
logger.info(`Updated endpoint for peer ${peerAgentId}: ${endpoint}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
logger.error(`Failed to update endpoint for peer ${peerAgentId}:`, err);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
wsClient.on('wgPeerUpdate', async (agentId, oldPublicKey, newPublicKey, keyVersion) => {
|
|
531
|
+
if (!this.wgManager.getStatus().available)
|
|
532
|
+
return;
|
|
533
|
+
try {
|
|
534
|
+
const health = await this.wgManager.getHealth();
|
|
535
|
+
const existingPeer = health?.peers.find(p => p.publicKey === oldPublicKey);
|
|
536
|
+
const allowedIps = existingPeer?.allowedIps || '';
|
|
537
|
+
const endpoint = existingPeer?.endpoint;
|
|
538
|
+
await this.wgManager.updatePeerKey(oldPublicKey, newPublicKey, allowedIps, endpoint);
|
|
539
|
+
wsClient.sendWgRotateApplied(keyVersion);
|
|
540
|
+
logger.info(`Peer ${agentId} key updated to v${keyVersion}`);
|
|
541
|
+
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
logger.error(`Failed to update peer ${agentId} key:`, err);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
wsClient.on('wgRotateCommit', (keyVersion) => {
|
|
547
|
+
this.keyRotation.confirmRotation(keyVersion);
|
|
548
|
+
this.reportWgStatus();
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
// Connection events
|
|
552
|
+
wsClient.on('connected', (agentId, agentName) => {
|
|
553
|
+
this.connectedServers.add(serverUrl);
|
|
554
|
+
// Use primary server's agentId/name for dashboard
|
|
555
|
+
if (entry.isPrimary) {
|
|
556
|
+
this.agentId = agentId;
|
|
557
|
+
this.agentName = agentName;
|
|
558
|
+
}
|
|
559
|
+
this.emit('connected', agentId, agentName, serverUrl);
|
|
560
|
+
// Start stats/ports only once (on first server connection)
|
|
561
|
+
if (this.connectedServers.size === 1) {
|
|
562
|
+
this.startStatsCollection();
|
|
563
|
+
this.portScanner.start();
|
|
564
|
+
if (entry.isPrimary) {
|
|
565
|
+
this.initWireGuard().catch(err => {
|
|
566
|
+
logger.warn('WireGuard initialization failed:', err);
|
|
567
|
+
});
|
|
435
568
|
}
|
|
436
|
-
}
|
|
437
|
-
catch (err) {
|
|
438
|
-
logger.error(`Failed to update endpoint for peer ${peerAgentId}:`, err);
|
|
439
569
|
}
|
|
440
570
|
});
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const existingPeer = health?.peers.find(p => p.publicKey === oldPublicKey);
|
|
449
|
-
const allowedIps = existingPeer?.allowedIps || '';
|
|
450
|
-
const endpoint = existingPeer?.endpoint;
|
|
451
|
-
await this.wgManager.updatePeerKey(oldPublicKey, newPublicKey, allowedIps, endpoint);
|
|
452
|
-
// ACK the update
|
|
453
|
-
this.wsClient.sendWgRotateApplied(keyVersion);
|
|
454
|
-
logger.info(`Peer ${agentId} key updated to v${keyVersion}`);
|
|
455
|
-
}
|
|
456
|
-
catch (err) {
|
|
457
|
-
logger.error(`Failed to update peer ${agentId} key:`, err);
|
|
571
|
+
wsClient.on('disconnected', (reason) => {
|
|
572
|
+
this.connectedServers.delete(serverUrl);
|
|
573
|
+
this.emit('disconnected', reason, serverUrl);
|
|
574
|
+
// Stop stats/ports only when ALL servers are disconnected
|
|
575
|
+
if (this.connectedServers.size === 0) {
|
|
576
|
+
this.statsManager.stop();
|
|
577
|
+
this.portScanner.stop();
|
|
458
578
|
}
|
|
459
579
|
});
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
// Connection events
|
|
467
|
-
this.wsClient.on('connected', (agentId, agentName) => {
|
|
468
|
-
this.agentId = agentId;
|
|
469
|
-
this.agentName = agentName;
|
|
470
|
-
this.emit('connected', agentId, agentName);
|
|
471
|
-
// Start stats collection and port scanning when connected
|
|
472
|
-
this.startStatsCollection();
|
|
473
|
-
this.portScanner.start();
|
|
474
|
-
// Initialize WireGuard (non-blocking, don't delay connection)
|
|
475
|
-
this.initWireGuard().catch(err => {
|
|
476
|
-
logger.warn('WireGuard initialization failed:', err);
|
|
477
|
-
});
|
|
478
|
-
});
|
|
479
|
-
this.wsClient.on('disconnected', (reason) => {
|
|
480
|
-
// Stop stats and port scanning when disconnected
|
|
481
|
-
this.statsManager.stop();
|
|
482
|
-
this.portScanner.stop();
|
|
483
|
-
this.emit('disconnected', reason);
|
|
484
|
-
});
|
|
485
|
-
this.wsClient.on('authFailed', (error) => {
|
|
486
|
-
logger.error(`Authentication failed: ${error}`);
|
|
487
|
-
this.emit('authFailed', error);
|
|
488
|
-
this.stop();
|
|
580
|
+
wsClient.on('authFailed', (error) => {
|
|
581
|
+
logger.error(`Authentication failed for ${entry.name}: ${error}`);
|
|
582
|
+
if (entry.isPrimary) {
|
|
583
|
+
this.emit('authFailed', error);
|
|
584
|
+
this.stop();
|
|
585
|
+
}
|
|
489
586
|
});
|
|
490
587
|
}
|
|
491
588
|
/**
|
|
492
|
-
* Start the agent
|
|
589
|
+
* Start the agent — connects to all configured servers
|
|
493
590
|
*/
|
|
494
591
|
async start(options = {}) {
|
|
495
592
|
if (!isConfigured()) {
|
|
@@ -513,78 +610,99 @@ export class Agent extends EventEmitter {
|
|
|
513
610
|
else {
|
|
514
611
|
logger.warn('termify-daemon not available, falling back to stats-agent and pgrep');
|
|
515
612
|
}
|
|
516
|
-
// Connect to
|
|
517
|
-
|
|
613
|
+
// Connect to all configured servers
|
|
614
|
+
const servers = getServers();
|
|
615
|
+
if (servers.length === 0) {
|
|
616
|
+
// Backward compat: no servers array, use legacy config
|
|
617
|
+
const cfg = getConfig();
|
|
618
|
+
const legacy = {
|
|
619
|
+
name: 'primary',
|
|
620
|
+
serverUrl: cfg.serverUrl,
|
|
621
|
+
wsUrl: cfg.wsUrl,
|
|
622
|
+
accessToken: cfg.accessToken,
|
|
623
|
+
agentId: cfg.agentId,
|
|
624
|
+
lastConnected: cfg.lastConnected,
|
|
625
|
+
isPrimary: true,
|
|
626
|
+
};
|
|
627
|
+
this.addServerConnection(legacy);
|
|
628
|
+
this.wsClients.get(legacy.serverUrl)?.connect();
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
for (const entry of servers) {
|
|
632
|
+
if (!entry.accessToken)
|
|
633
|
+
continue; // Skip unconfigured servers
|
|
634
|
+
this.addServerConnection(entry);
|
|
635
|
+
this.wsClients.get(entry.serverUrl)?.connect();
|
|
636
|
+
}
|
|
637
|
+
}
|
|
518
638
|
return true;
|
|
519
639
|
}
|
|
520
640
|
/**
|
|
521
|
-
* Stop the agent
|
|
641
|
+
* Stop the agent — disconnects from ALL servers
|
|
522
642
|
*/
|
|
523
643
|
stop() {
|
|
524
|
-
if (!this.isRunning)
|
|
644
|
+
if (!this.isRunning)
|
|
525
645
|
return;
|
|
526
|
-
}
|
|
527
646
|
logger.info('Stopping agent...');
|
|
528
647
|
this.isRunning = false;
|
|
529
|
-
// Stop stats collection and port scanning
|
|
530
648
|
this.statsManager.stop();
|
|
531
649
|
this.portScanner.stop();
|
|
532
650
|
this.daemonClient.stopStatsCollection();
|
|
533
651
|
this.daemonClient.disconnect();
|
|
534
|
-
// Shut down NAT traversal, key rotation, and WireGuard interface
|
|
535
652
|
this.keyRotation.stop();
|
|
536
653
|
this.natTraversal.shutdown();
|
|
537
654
|
this.wgManager.shutdown().catch(err => {
|
|
538
655
|
logger.warn('WireGuard shutdown error:', err);
|
|
539
656
|
});
|
|
540
657
|
// Close all tunnels
|
|
541
|
-
this.
|
|
542
|
-
|
|
658
|
+
for (const tm of this.tunnelManagers.values()) {
|
|
659
|
+
tm.closeAll();
|
|
660
|
+
}
|
|
543
661
|
this.ptyManager.killAll();
|
|
544
662
|
this.sshManager.killAll();
|
|
545
|
-
// Disconnect
|
|
546
|
-
this.
|
|
663
|
+
// Disconnect ALL servers
|
|
664
|
+
for (const ws of this.wsClients.values()) {
|
|
665
|
+
ws.disconnect();
|
|
666
|
+
}
|
|
667
|
+
this.connectedServers.clear();
|
|
668
|
+
this.terminalServerMap.clear();
|
|
547
669
|
this.emit('stopped');
|
|
548
670
|
}
|
|
549
671
|
/**
|
|
550
|
-
* Hibernate the agent
|
|
551
|
-
* Used during graceful shutdown (Cmd+Q, SIGTERM) to preserve terminal state.
|
|
672
|
+
* Hibernate the agent — kills PTYs silently, disconnects ALL servers
|
|
552
673
|
*/
|
|
553
674
|
hibernate() {
|
|
554
|
-
if (!this.isRunning)
|
|
675
|
+
if (!this.isRunning)
|
|
555
676
|
return;
|
|
556
|
-
}
|
|
557
677
|
logger.info('Hibernating agent...');
|
|
558
678
|
this.isRunning = false;
|
|
559
|
-
// Stop stats collection and port scanning
|
|
560
679
|
this.statsManager.stop();
|
|
561
680
|
this.portScanner.stop();
|
|
562
681
|
this.daemonClient.stopStatsCollection();
|
|
563
682
|
this.daemonClient.disconnect();
|
|
564
|
-
// Shut down NAT traversal, key rotation, and WireGuard interface
|
|
565
683
|
this.keyRotation.stop();
|
|
566
684
|
this.natTraversal.shutdown();
|
|
567
685
|
this.wgManager.shutdown().catch(err => {
|
|
568
686
|
logger.warn('WireGuard shutdown error:', err);
|
|
569
687
|
});
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
688
|
+
for (const tm of this.tunnelManagers.values()) {
|
|
689
|
+
tm.closeAll();
|
|
690
|
+
}
|
|
573
691
|
this.ptyManager.hibernateAll();
|
|
574
|
-
// SSH can't hibernate - just kill them
|
|
575
692
|
this.sshManager.killAll();
|
|
576
|
-
|
|
577
|
-
|
|
693
|
+
for (const ws of this.wsClients.values()) {
|
|
694
|
+
ws.disconnect();
|
|
695
|
+
}
|
|
696
|
+
this.connectedServers.clear();
|
|
697
|
+
this.terminalServerMap.clear();
|
|
578
698
|
this.emit('stopped');
|
|
579
699
|
}
|
|
580
700
|
/**
|
|
581
|
-
* Start collecting and sending system stats
|
|
701
|
+
* Start collecting and sending system stats — broadcast to ALL servers
|
|
582
702
|
*/
|
|
583
703
|
startStatsCollection() {
|
|
584
|
-
// Always use stats-agent for complete system metrics
|
|
585
|
-
// (daemon only provides basic CPU/RAM, missing disks, network, per-core CPU, swap)
|
|
586
704
|
this.statsManager.setStatsCallback((stats) => {
|
|
587
|
-
this.
|
|
705
|
+
this.broadcastAll(ws => ws.sendStats(stats));
|
|
588
706
|
this.emit('stats', stats);
|
|
589
707
|
});
|
|
590
708
|
const started = this.statsManager.start();
|
|
@@ -596,12 +714,10 @@ export class Agent extends EventEmitter {
|
|
|
596
714
|
}
|
|
597
715
|
}
|
|
598
716
|
/**
|
|
599
|
-
* Initialize WireGuard
|
|
600
|
-
* Non-blocking — called with .catch() so it never delays the connection.
|
|
717
|
+
* Initialize WireGuard (primary server only)
|
|
601
718
|
*/
|
|
602
719
|
async initWireGuard() {
|
|
603
720
|
const installResult = await this.wgManager.ensureInstalled();
|
|
604
|
-
// Report initial status regardless of availability
|
|
605
721
|
this.reportWgStatus();
|
|
606
722
|
if (installResult.available) {
|
|
607
723
|
const status = this.wgManager.getStatus();
|
|
@@ -613,15 +729,15 @@ export class Agent extends EventEmitter {
|
|
|
613
729
|
logger.info(`WireGuard not available: ${installResult.availability}`);
|
|
614
730
|
return;
|
|
615
731
|
}
|
|
616
|
-
// Interface setup will happen when we receive our virtual IP from signaling (Phase 2)
|
|
617
732
|
logger.info(`WireGuard detected: backend=${installResult.backend}, version=${installResult.version}`);
|
|
618
733
|
}
|
|
619
734
|
/**
|
|
620
|
-
* Report WireGuard status to server
|
|
735
|
+
* Report WireGuard status to primary server
|
|
621
736
|
*/
|
|
622
737
|
reportWgStatus() {
|
|
623
738
|
const status = this.wgManager.getStatus();
|
|
624
|
-
this.
|
|
739
|
+
const primaryWs = this.getPrimaryWsClient();
|
|
740
|
+
primaryWs?.sendWgStatus({
|
|
625
741
|
type: 'agent.wg.status',
|
|
626
742
|
available: status.available,
|
|
627
743
|
availability: status.availability,
|
|
@@ -629,7 +745,7 @@ export class Agent extends EventEmitter {
|
|
|
629
745
|
version: status.version,
|
|
630
746
|
publicKey: status.publicKey,
|
|
631
747
|
listenPort: status.listenPort,
|
|
632
|
-
natType: 'unknown',
|
|
748
|
+
natType: 'unknown',
|
|
633
749
|
connectedPeers: status.connectedPeers,
|
|
634
750
|
});
|
|
635
751
|
}
|
|
@@ -639,50 +755,38 @@ export class Agent extends EventEmitter {
|
|
|
639
755
|
handleShutdown(signal) {
|
|
640
756
|
logger.info(`Received ${signal}, hibernating...`);
|
|
641
757
|
this.hibernate();
|
|
642
|
-
// Small delay to let WS disconnect message flush before exiting
|
|
643
758
|
setTimeout(() => process.exit(0), 200);
|
|
644
759
|
}
|
|
645
|
-
/**
|
|
646
|
-
* Check if agent is running
|
|
647
|
-
*/
|
|
648
760
|
get running() {
|
|
649
761
|
return this.isRunning;
|
|
650
762
|
}
|
|
651
|
-
/**
|
|
652
|
-
* Check if connected to server
|
|
653
|
-
*/
|
|
654
763
|
get connected() {
|
|
655
|
-
return this.
|
|
764
|
+
return this.connectedServers.size > 0;
|
|
656
765
|
}
|
|
657
|
-
/**
|
|
658
|
-
* Get current agent ID
|
|
659
|
-
*/
|
|
660
766
|
get id() {
|
|
661
767
|
return this.agentId;
|
|
662
768
|
}
|
|
663
|
-
/**
|
|
664
|
-
* Get current agent name
|
|
665
|
-
*/
|
|
666
769
|
get name() {
|
|
667
770
|
return this.agentName;
|
|
668
771
|
}
|
|
669
|
-
/**
|
|
670
|
-
* Get number of active terminals (local + SSH)
|
|
671
|
-
*/
|
|
672
772
|
get terminalCount() {
|
|
673
773
|
return this.ptyManager.count + this.sshManager.count;
|
|
674
774
|
}
|
|
675
775
|
/**
|
|
676
|
-
* Get
|
|
776
|
+
* Get number of connected servers
|
|
677
777
|
*/
|
|
778
|
+
get serverCount() {
|
|
779
|
+
return this.connectedServers.size;
|
|
780
|
+
}
|
|
678
781
|
getStatus() {
|
|
679
782
|
const config = getConfig();
|
|
680
783
|
return {
|
|
681
784
|
running: this.isRunning,
|
|
682
|
-
connected: this.
|
|
785
|
+
connected: this.connectedServers.size > 0,
|
|
683
786
|
agentId: this.agentId,
|
|
684
787
|
agentName: this.agentName,
|
|
685
788
|
terminalCount: this.ptyManager.count,
|
|
789
|
+
serverCount: this.connectedServers.size,
|
|
686
790
|
config: {
|
|
687
791
|
serverUrl: config.serverUrl,
|
|
688
792
|
machineId: config.machineId,
|
|
@@ -690,9 +794,6 @@ export class Agent extends EventEmitter {
|
|
|
690
794
|
},
|
|
691
795
|
};
|
|
692
796
|
}
|
|
693
|
-
/**
|
|
694
|
-
* Get combined list of all terminals for dashboard display
|
|
695
|
-
*/
|
|
696
797
|
getTerminals() {
|
|
697
798
|
const locals = this.ptyManager.listTerminals().map(t => ({
|
|
698
799
|
id: t.id,
|
|
@@ -710,9 +811,6 @@ export class Agent extends EventEmitter {
|
|
|
710
811
|
}));
|
|
711
812
|
return [...locals, ...sshs];
|
|
712
813
|
}
|
|
713
|
-
/**
|
|
714
|
-
* Get WireGuard info for dashboard display
|
|
715
|
-
*/
|
|
716
814
|
getWireGuardInfo() {
|
|
717
815
|
const status = this.wgManager.getStatus();
|
|
718
816
|
return {
|