termites 1.0.29 → 1.0.31
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/.claude/settings.local.json +3 -1
- package/bin/termites.js +7 -31
- package/package.json +4 -5
- package/server.js +186 -120
- package/client.js +0 -150
package/bin/termites.js
CHANGED
|
@@ -12,54 +12,30 @@ if (command === '-v' || command === '--version' || command === 'version') {
|
|
|
12
12
|
|
|
13
13
|
function showHelp() {
|
|
14
14
|
console.log(`
|
|
15
|
-
Termites -
|
|
15
|
+
Termites - Local multi-terminal manager with web interface
|
|
16
16
|
|
|
17
17
|
Usage:
|
|
18
|
-
termites
|
|
19
|
-
termites
|
|
20
|
-
|
|
21
|
-
Server Options:
|
|
22
|
-
--no-client Don't start local client (default: starts client)
|
|
18
|
+
termites [options] Start the terminal server
|
|
19
|
+
termites server [options] Start the terminal server
|
|
23
20
|
|
|
24
21
|
Environment:
|
|
25
22
|
PORT=<port> Set server port (default: 6789)
|
|
26
23
|
|
|
27
24
|
Examples:
|
|
28
|
-
termites
|
|
29
|
-
termites
|
|
30
|
-
PORT=8080 termites server Start on port 8080
|
|
31
|
-
termites client Connect to ws://localhost:6789
|
|
32
|
-
termites client myserver Connect to ws://myserver:6789
|
|
25
|
+
termites Start server on default port
|
|
26
|
+
PORT=8080 termites Start on port 8080
|
|
33
27
|
`);
|
|
34
28
|
}
|
|
35
29
|
|
|
36
|
-
if (
|
|
30
|
+
if (command === '-h' || command === '--help' || command === 'help') {
|
|
37
31
|
showHelp();
|
|
38
32
|
process.exit(0);
|
|
39
33
|
}
|
|
40
34
|
|
|
41
35
|
const rootDir = path.join(__dirname, '..');
|
|
42
36
|
|
|
43
|
-
if (command === 'server') {
|
|
44
|
-
const noClient = args.includes('--no-client');
|
|
45
|
-
const startClient = !noClient;
|
|
46
|
-
|
|
47
|
-
// Set environment variable for server to know whether to start client
|
|
48
|
-
process.env.START_CLIENT = startClient ? 'true' : 'false';
|
|
49
|
-
|
|
37
|
+
if (!command || command === 'server') {
|
|
50
38
|
require(path.join(rootDir, 'server.js'));
|
|
51
|
-
} else if (command === 'client') {
|
|
52
|
-
let serverUrl = args[1] || 'ws://localhost:6789';
|
|
53
|
-
// Add ws:// prefix if missing
|
|
54
|
-
if (!serverUrl.startsWith('ws://') && !serverUrl.startsWith('wss://')) {
|
|
55
|
-
serverUrl = 'ws://' + serverUrl;
|
|
56
|
-
}
|
|
57
|
-
// Add default port if not specified
|
|
58
|
-
if (!serverUrl.match(/:\d+/)) {
|
|
59
|
-
serverUrl = serverUrl.replace(/^(wss?:\/\/[^\/]+)/, '$1:6789');
|
|
60
|
-
}
|
|
61
|
-
process.argv = [process.argv[0], path.join(rootDir, 'client.js'), serverUrl];
|
|
62
|
-
require(path.join(rootDir, 'client.js'));
|
|
63
39
|
} else {
|
|
64
40
|
console.error(`Unknown command: ${command}`);
|
|
65
41
|
showHelp();
|
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "termites",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.31",
|
|
4
|
+
"description": "Local multi-terminal manager with web interface",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "node index.js",
|
|
8
|
-
"server": "node server.js"
|
|
9
|
-
"client": "node client.js"
|
|
8
|
+
"server": "node server.js"
|
|
10
9
|
},
|
|
11
10
|
"bin": {
|
|
12
11
|
"termites": "bin/termites.js"
|
|
@@ -18,7 +17,7 @@
|
|
|
18
17
|
"keywords": [
|
|
19
18
|
"terminal",
|
|
20
19
|
"shell",
|
|
21
|
-
"
|
|
20
|
+
"multi-terminal",
|
|
22
21
|
"web",
|
|
23
22
|
"pty"
|
|
24
23
|
],
|
package/server.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
const http = require('http');
|
|
3
3
|
const WebSocket = require('ws');
|
|
4
4
|
const crypto = require('crypto');
|
|
5
|
-
const { spawn } = require('child_process');
|
|
6
5
|
const path = require('path');
|
|
7
6
|
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const pty = require('node-pty');
|
|
8
9
|
|
|
9
10
|
const PORT = process.env.PORT || 6789;
|
|
10
|
-
const START_CLIENT = process.env.START_CLIENT !== 'false'; // default true
|
|
11
11
|
const CONFIG_FILE = path.join(__dirname, '.termites.json');
|
|
12
|
+
const SHELL = process.env.SHELL || '/bin/bash';
|
|
12
13
|
|
|
13
14
|
// Load or create config
|
|
14
15
|
function loadConfig() {
|
|
@@ -31,9 +32,8 @@ function hashPassword(password) {
|
|
|
31
32
|
class TermitesServer {
|
|
32
33
|
constructor(port) {
|
|
33
34
|
this.port = port;
|
|
34
|
-
this.
|
|
35
|
+
this.terminals = new Map(); // terminalId -> { pty, info, outputBuffer }
|
|
35
36
|
this.browsers = new Set(); // browser WebSocket connections
|
|
36
|
-
this.selectedClient = null; // currently selected client for each browser
|
|
37
37
|
this.config = loadConfig();
|
|
38
38
|
// Use saved sessionToken or generate new one
|
|
39
39
|
if (this.config.passwordHash) {
|
|
@@ -57,34 +57,26 @@ class TermitesServer {
|
|
|
57
57
|
this.wss = new WebSocket.Server({ server: this.httpServer });
|
|
58
58
|
|
|
59
59
|
this.wss.on('connection', (ws, req) => {
|
|
60
|
-
|
|
61
|
-
console.log(`WebSocket connection: ${req.url}, isClient: ${isClient}`);
|
|
60
|
+
console.log(`WebSocket connection: ${req.url}`);
|
|
62
61
|
|
|
63
62
|
// Browser connections require auth (if password is set)
|
|
64
|
-
if (
|
|
63
|
+
if (this.config.passwordHash && !this.checkSession(req)) {
|
|
65
64
|
console.log('WebSocket rejected: unauthorized browser connection');
|
|
66
65
|
ws.close(1008, 'Unauthorized');
|
|
67
66
|
return;
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
this.handleClientConnection(ws);
|
|
72
|
-
} else {
|
|
73
|
-
this.handleBrowserConnection(ws);
|
|
74
|
-
}
|
|
69
|
+
this.handleBrowserConnection(ws);
|
|
75
70
|
});
|
|
76
71
|
|
|
77
72
|
this.httpServer.listen(this.port, () => {
|
|
78
73
|
console.log(`Termites Server started: http://localhost:${this.port}`);
|
|
79
|
-
console.log(`Client connection: ws://localhost:${this.port}/client`);
|
|
80
74
|
if (!this.config.passwordHash) {
|
|
81
75
|
console.log('Warning: No password set, please visit browser to set password');
|
|
82
76
|
}
|
|
83
77
|
|
|
84
|
-
// Auto-
|
|
85
|
-
|
|
86
|
-
this.startLocalClient();
|
|
87
|
-
}
|
|
78
|
+
// Auto-create first terminal
|
|
79
|
+
this.createTerminal();
|
|
88
80
|
});
|
|
89
81
|
}
|
|
90
82
|
|
|
@@ -108,103 +100,159 @@ class TermitesServer {
|
|
|
108
100
|
return cookies;
|
|
109
101
|
}
|
|
110
102
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
103
|
+
// Create a new local terminal
|
|
104
|
+
createTerminal() {
|
|
105
|
+
const terminalId = crypto.randomUUID();
|
|
106
|
+
const ptyProcess = pty.spawn(SHELL, ['-l'], {
|
|
107
|
+
name: 'xterm-256color',
|
|
108
|
+
cols: 120,
|
|
109
|
+
rows: 40,
|
|
110
|
+
cwd: process.cwd(),
|
|
111
|
+
env: process.env
|
|
118
112
|
});
|
|
119
|
-
global.localClient = this.localClient;
|
|
120
|
-
|
|
121
|
-
this.localClient.on('error', (err) => {
|
|
122
|
-
console.error('Failed to start local client:', err.message);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
this.localClient.on('exit', (code) => {
|
|
126
|
-
console.log(`Local client exited (code: ${code})`);
|
|
127
|
-
this.localClient = null;
|
|
128
|
-
global.localClient = null;
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
113
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
114
|
+
const info = {
|
|
115
|
+
username: os.userInfo().username,
|
|
116
|
+
hostname: os.hostname(),
|
|
117
|
+
cwd: process.cwd().replace(os.homedir(), '~'),
|
|
118
|
+
platform: os.platform()
|
|
119
|
+
};
|
|
136
120
|
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
info
|
|
121
|
+
const terminal = {
|
|
122
|
+
pty: ptyProcess,
|
|
123
|
+
info,
|
|
140
124
|
outputBuffer: []
|
|
141
125
|
};
|
|
142
|
-
this.clients.set(clientId, clientData);
|
|
143
126
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
127
|
+
this.terminals.set(terminalId, terminal);
|
|
128
|
+
console.log(`Terminal created: ${terminalId} (PID: ${ptyProcess.pid})`);
|
|
129
|
+
|
|
130
|
+
ptyProcess.onData(data => {
|
|
131
|
+
terminal.outputBuffer.push(data);
|
|
132
|
+
if (terminal.outputBuffer.length > 1000) {
|
|
133
|
+
terminal.outputBuffer = terminal.outputBuffer.slice(-500);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Parse terminal output to detect user@host changes (e.g., after SSH)
|
|
137
|
+
// Method 1: OSC 0 sequences (window title)
|
|
138
|
+
const oscMatch = data.match(/\x1b\]0;([^\x07\x1b]+)[\x07\x1b]/);
|
|
139
|
+
if (oscMatch) {
|
|
140
|
+
const title = oscMatch[1];
|
|
141
|
+
const match = title.match(/^([^@]+)@([^:]+)(?::(.*))?$/);
|
|
142
|
+
if (match) {
|
|
143
|
+
this.updateTerminalInfo(terminalId, terminal, match[1], match[2], match[3]);
|
|
144
|
+
}
|
|
150
145
|
}
|
|
146
|
+
|
|
147
|
+
// Method 2: Parse shell prompt patterns (works without any config)
|
|
148
|
+
// Look for the LAST (most recent) prompt pattern in output
|
|
149
|
+
const lines = data.split(/\r?\n/);
|
|
150
|
+
let lastMatch = null;
|
|
151
|
+
for (const line of lines) {
|
|
152
|
+
// Strip ANSI escape codes for matching
|
|
153
|
+
const cleanLine = line.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').replace(/\x1b\][^\x07]*\x07/g, '');
|
|
154
|
+
|
|
155
|
+
// Pattern 1: user@host format (e.g., "user@host:path$", "[user@host path]$")
|
|
156
|
+
const userHostMatch = cleanLine.match(/[\[\s]?([a-zA-Z0-9_-]+)@([a-zA-Z0-9_.-]+)[\s:\]]/);
|
|
157
|
+
if (userHostMatch) {
|
|
158
|
+
const [, username, hostname] = userHostMatch;
|
|
159
|
+
if (hostname && !hostname.includes('/') && hostname.length < 64) {
|
|
160
|
+
lastMatch = { username, hostname };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Pattern 2: hostname:path format (e.g., "seis10:/gds/zhfu[22] > ")
|
|
165
|
+
const hostPathMatch = cleanLine.match(/^([a-zA-Z0-9_-]+):([\/~][^\s\[]*)/);
|
|
166
|
+
if (hostPathMatch) {
|
|
167
|
+
const [, hostname, cwd] = hostPathMatch;
|
|
168
|
+
if (hostname && hostname.length < 64) {
|
|
169
|
+
lastMatch = { hostname, cwd, keepUsername: true };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Use only the last (most recent) match
|
|
174
|
+
if (lastMatch) {
|
|
175
|
+
if (lastMatch.keepUsername) {
|
|
176
|
+
this.updateTerminalInfo(terminalId, terminal, null, lastMatch.hostname, lastMatch.cwd);
|
|
177
|
+
} else {
|
|
178
|
+
this.updateTerminalInfo(terminalId, terminal, lastMatch.username, lastMatch.hostname);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.broadcastToBrowsers({
|
|
183
|
+
type: 'output',
|
|
184
|
+
clientId: terminalId,
|
|
185
|
+
data
|
|
186
|
+
});
|
|
151
187
|
});
|
|
152
188
|
|
|
153
|
-
|
|
154
|
-
console.log(`
|
|
155
|
-
this.
|
|
189
|
+
ptyProcess.onExit(() => {
|
|
190
|
+
console.log(`Terminal exited: ${terminalId}`);
|
|
191
|
+
this.terminals.delete(terminalId);
|
|
156
192
|
this.broadcastToBrowsers({
|
|
157
193
|
type: 'client-disconnected',
|
|
158
|
-
clientId
|
|
194
|
+
clientId: terminalId
|
|
159
195
|
});
|
|
160
196
|
});
|
|
161
|
-
}
|
|
162
197
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
198
|
+
// Notify browsers
|
|
199
|
+
this.broadcastToBrowsers({
|
|
200
|
+
type: 'client-connected',
|
|
201
|
+
client: { id: terminalId, ...info }
|
|
202
|
+
});
|
|
203
|
+
this.broadcastTerminalList();
|
|
166
204
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
client.info = data.info;
|
|
170
|
-
console.log(`Client registered: ${data.info.username}@${data.info.hostname}`);
|
|
171
|
-
this.broadcastToBrowsers({
|
|
172
|
-
type: 'client-connected',
|
|
173
|
-
client: { id: clientId, ...data.info }
|
|
174
|
-
});
|
|
175
|
-
this.broadcastClientList();
|
|
176
|
-
break;
|
|
205
|
+
return terminalId;
|
|
206
|
+
}
|
|
177
207
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
});
|
|
188
|
-
break;
|
|
208
|
+
// Close a terminal
|
|
209
|
+
closeTerminal(terminalId) {
|
|
210
|
+
const terminal = this.terminals.get(terminalId);
|
|
211
|
+
if (terminal) {
|
|
212
|
+
terminal.pty.kill();
|
|
213
|
+
this.terminals.delete(terminalId);
|
|
214
|
+
console.log(`Terminal closed: ${terminalId}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
189
217
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
218
|
+
// Update terminal info (user@host) when detected from output
|
|
219
|
+
updateTerminalInfo(terminalId, terminal, username, hostname, cwd) {
|
|
220
|
+
let infoChanged = false;
|
|
221
|
+
if (username && terminal.info.username !== username) {
|
|
222
|
+
terminal.info.username = username;
|
|
223
|
+
infoChanged = true;
|
|
224
|
+
}
|
|
225
|
+
if (hostname && terminal.info.hostname !== hostname) {
|
|
226
|
+
terminal.info.hostname = hostname;
|
|
227
|
+
infoChanged = true;
|
|
228
|
+
}
|
|
229
|
+
if (cwd !== undefined && terminal.info.cwd !== cwd) {
|
|
230
|
+
terminal.info.cwd = cwd || '~';
|
|
231
|
+
infoChanged = true;
|
|
232
|
+
}
|
|
233
|
+
if (infoChanged) {
|
|
234
|
+
console.log(`Terminal ${terminalId} info updated: ${username}@${hostname}`);
|
|
235
|
+
this.broadcastToBrowsers({
|
|
236
|
+
type: 'client-info-updated',
|
|
237
|
+
clientId: terminalId,
|
|
238
|
+
info: terminal.info
|
|
239
|
+
});
|
|
240
|
+
this.broadcastTerminalList();
|
|
193
241
|
}
|
|
194
242
|
}
|
|
195
243
|
|
|
196
244
|
// Handle browser connections
|
|
197
245
|
handleBrowserConnection(ws) {
|
|
198
246
|
console.log('New browser connected');
|
|
199
|
-
console.log('Current
|
|
247
|
+
console.log('Current terminals count:', this.terminals.size);
|
|
200
248
|
this.browsers.add(ws);
|
|
201
249
|
|
|
202
|
-
// Send current
|
|
203
|
-
const
|
|
204
|
-
console.log('Sending
|
|
250
|
+
// Send current terminal list
|
|
251
|
+
const terminalList = this.getTerminalList();
|
|
252
|
+
console.log('Sending terminal list:', terminalList.length, 'terminals');
|
|
205
253
|
ws.send(JSON.stringify({
|
|
206
254
|
type: 'clients',
|
|
207
|
-
list:
|
|
255
|
+
list: terminalList
|
|
208
256
|
}));
|
|
209
257
|
|
|
210
258
|
ws.on('message', (message) => {
|
|
@@ -223,11 +271,21 @@ class TermitesServer {
|
|
|
223
271
|
|
|
224
272
|
handleBrowserMessage(browserWs, data) {
|
|
225
273
|
switch (data.type) {
|
|
274
|
+
case 'create-terminal':
|
|
275
|
+
this.createTerminal();
|
|
276
|
+
break;
|
|
277
|
+
|
|
278
|
+
case 'close-terminal':
|
|
279
|
+
if (data.clientId) {
|
|
280
|
+
this.closeTerminal(data.clientId);
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
|
|
226
284
|
case 'select':
|
|
227
|
-
const
|
|
228
|
-
if (
|
|
285
|
+
const terminal = this.terminals.get(data.clientId);
|
|
286
|
+
if (terminal) {
|
|
229
287
|
// Send buffered output to browser
|
|
230
|
-
|
|
288
|
+
terminal.outputBuffer.forEach(output => {
|
|
231
289
|
browserWs.send(JSON.stringify({
|
|
232
290
|
type: 'output',
|
|
233
291
|
clientId: data.clientId,
|
|
@@ -239,45 +297,36 @@ class TermitesServer {
|
|
|
239
297
|
|
|
240
298
|
case 'input':
|
|
241
299
|
if (data.clientId) {
|
|
242
|
-
const
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
type: 'input',
|
|
246
|
-
text: data.text
|
|
247
|
-
}));
|
|
300
|
+
const targetTerminal = this.terminals.get(data.clientId);
|
|
301
|
+
if (targetTerminal) {
|
|
302
|
+
targetTerminal.pty.write(data.text);
|
|
248
303
|
}
|
|
249
304
|
}
|
|
250
305
|
break;
|
|
251
306
|
|
|
252
307
|
case 'resize':
|
|
253
308
|
if (data.clientId) {
|
|
254
|
-
const
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
type: 'resize',
|
|
258
|
-
cols: data.cols,
|
|
259
|
-
rows: data.rows
|
|
260
|
-
}));
|
|
309
|
+
const targetTerminal = this.terminals.get(data.clientId);
|
|
310
|
+
if (targetTerminal) {
|
|
311
|
+
targetTerminal.pty.resize(data.cols, data.rows);
|
|
261
312
|
}
|
|
262
313
|
}
|
|
263
314
|
break;
|
|
264
315
|
}
|
|
265
316
|
}
|
|
266
317
|
|
|
267
|
-
|
|
318
|
+
getTerminalList() {
|
|
268
319
|
const list = [];
|
|
269
|
-
this.
|
|
270
|
-
|
|
271
|
-
list.push({ id, ...client.info });
|
|
272
|
-
}
|
|
320
|
+
this.terminals.forEach((terminal, id) => {
|
|
321
|
+
list.push({ id, ...terminal.info });
|
|
273
322
|
});
|
|
274
323
|
return list;
|
|
275
324
|
}
|
|
276
325
|
|
|
277
|
-
|
|
326
|
+
broadcastTerminalList() {
|
|
278
327
|
this.broadcastToBrowsers({
|
|
279
328
|
type: 'clients',
|
|
280
|
-
list: this.
|
|
329
|
+
list: this.getTerminalList()
|
|
281
330
|
});
|
|
282
331
|
}
|
|
283
332
|
|
|
@@ -632,6 +681,11 @@ class TermitesServer {
|
|
|
632
681
|
.font-size-row input { flex: 1; }
|
|
633
682
|
.font-size-row span { font-size: 12px; min-width: 36px; }
|
|
634
683
|
.empty-clients { padding: 16px; font-size: 12px; opacity: 0.5; text-align: center; }
|
|
684
|
+
.add-btn {
|
|
685
|
+
margin-left: auto; padding: 4px 12px; border: 1px solid; border-radius: 4px;
|
|
686
|
+
background: transparent; color: inherit; cursor: pointer; font-size: 11px;
|
|
687
|
+
}
|
|
688
|
+
.add-btn:hover { opacity: 0.8; }
|
|
635
689
|
/* Mobile toolbar */
|
|
636
690
|
.mobile-toolbar {
|
|
637
691
|
display: none; flex-shrink: 0; padding: 6px 8px; gap: 6px;
|
|
@@ -736,9 +790,9 @@ class TermitesServer {
|
|
|
736
790
|
</div>
|
|
737
791
|
</div>
|
|
738
792
|
<div class="drawer-section">
|
|
739
|
-
<div class="drawer-section-header">◉
|
|
793
|
+
<div class="drawer-section-header">◉ Terminals <button id="add-terminal-btn" class="add-btn">+ Add</button></div>
|
|
740
794
|
<div class="client-list" id="client-list">
|
|
741
|
-
<div class="empty-clients">
|
|
795
|
+
<div class="empty-clients">No terminals</div>
|
|
742
796
|
</div>
|
|
743
797
|
</div>
|
|
744
798
|
</div>
|
|
@@ -988,11 +1042,17 @@ class TermitesServer {
|
|
|
988
1042
|
const drawer = document.getElementById('drawer');
|
|
989
1043
|
const overlay = document.getElementById('overlay');
|
|
990
1044
|
const closeBtn = document.getElementById('drawer-close');
|
|
1045
|
+
const addTerminalBtn = document.getElementById('add-terminal-btn');
|
|
991
1046
|
const openDrawer = () => { drawer.classList.add('open'); overlay.classList.add('open'); };
|
|
992
1047
|
const closeDrawer = () => { drawer.classList.remove('open'); overlay.classList.remove('open'); };
|
|
993
1048
|
menuBtn.onclick = openDrawer;
|
|
994
1049
|
closeBtn.onclick = closeDrawer;
|
|
995
1050
|
overlay.onclick = closeDrawer;
|
|
1051
|
+
addTerminalBtn.onclick = () => {
|
|
1052
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
1053
|
+
ws.send(JSON.stringify({ type: 'create-terminal' }));
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
996
1056
|
document.getElementById('theme-select').onchange = e => applyTheme(e.target.value);
|
|
997
1057
|
document.getElementById('font-select').onchange = e => applyFont(e.target.value);
|
|
998
1058
|
document.getElementById('font-size').oninput = e => applyFontSize(parseInt(e.target.value));
|
|
@@ -1398,7 +1458,7 @@ class TermitesServer {
|
|
|
1398
1458
|
const listEl = document.getElementById('client-list');
|
|
1399
1459
|
const t = themes[currentTheme];
|
|
1400
1460
|
if (clients.length === 0) {
|
|
1401
|
-
listEl.innerHTML = '<div class="empty-clients">
|
|
1461
|
+
listEl.innerHTML = '<div class="empty-clients">No terminals</div>';
|
|
1402
1462
|
return;
|
|
1403
1463
|
}
|
|
1404
1464
|
listEl.innerHTML = clients.map(c => {
|
|
@@ -1580,6 +1640,18 @@ class TermitesServer {
|
|
|
1580
1640
|
if (clients.length > 0) selectClient(clients[0].id);
|
|
1581
1641
|
}
|
|
1582
1642
|
break;
|
|
1643
|
+
case 'client-info-updated':
|
|
1644
|
+
const clientToUpdate = clients.find(c => c.id === d.clientId);
|
|
1645
|
+
if (clientToUpdate && d.info) {
|
|
1646
|
+
Object.assign(clientToUpdate, d.info);
|
|
1647
|
+
updateClientList();
|
|
1648
|
+
if (selectedClientId === d.clientId) {
|
|
1649
|
+
document.getElementById('header-title').innerHTML =
|
|
1650
|
+
'<span class="user">' + d.info.username + '</span>' +
|
|
1651
|
+
'<span class="sep">@</span><span class="host">' + d.info.hostname + '</span>';
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
break;
|
|
1583
1655
|
case 'output':
|
|
1584
1656
|
if (d.clientId === selectedClientId) {
|
|
1585
1657
|
addToHistory(d.data);
|
|
@@ -1610,10 +1682,4 @@ class TermitesServer {
|
|
|
1610
1682
|
new TermitesServer(PORT);
|
|
1611
1683
|
|
|
1612
1684
|
process.on('SIGINT', () => process.exit(0));
|
|
1613
|
-
process.on('SIGTERM', () => process.exit(0));
|
|
1614
|
-
process.on('exit', () => {
|
|
1615
|
-
// Clean up local client on exit
|
|
1616
|
-
if (global.localClient) {
|
|
1617
|
-
global.localClient.kill();
|
|
1618
|
-
}
|
|
1619
|
-
});
|
|
1685
|
+
process.on('SIGTERM', () => process.exit(0));
|
package/client.js
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
const os = require('os');
|
|
3
|
-
const WebSocket = require('ws');
|
|
4
|
-
|
|
5
|
-
const SHELL = process.env.SHELL || '/bin/bash';
|
|
6
|
-
const SERVER_URL = process.argv[2] || 'ws://localhost:6789/client';
|
|
7
|
-
|
|
8
|
-
function getSystemInfo() {
|
|
9
|
-
const username = os.userInfo().username;
|
|
10
|
-
const hostname = os.hostname();
|
|
11
|
-
const cwd = process.cwd().replace(os.homedir(), '~');
|
|
12
|
-
const platform = os.platform();
|
|
13
|
-
return { username, hostname, cwd, platform };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
class TermitesClient {
|
|
17
|
-
constructor(serverUrl) {
|
|
18
|
-
this.serverUrl = serverUrl.endsWith('/client') ? serverUrl : serverUrl + '/client';
|
|
19
|
-
this.ws = null;
|
|
20
|
-
this.pty = null;
|
|
21
|
-
this.reconnectDelay = 1000;
|
|
22
|
-
this.maxReconnectDelay = 30000;
|
|
23
|
-
this.systemInfo = getSystemInfo();
|
|
24
|
-
|
|
25
|
-
this.connect();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
connect() {
|
|
29
|
-
console.log(`Connecting to server: ${this.serverUrl}`);
|
|
30
|
-
|
|
31
|
-
this.ws = new WebSocket(this.serverUrl);
|
|
32
|
-
|
|
33
|
-
this.ws.on('open', () => {
|
|
34
|
-
console.log('Connected to server');
|
|
35
|
-
this.reconnectDelay = 1000;
|
|
36
|
-
|
|
37
|
-
// Register with server
|
|
38
|
-
this.ws.send(JSON.stringify({
|
|
39
|
-
type: 'register',
|
|
40
|
-
info: this.systemInfo
|
|
41
|
-
}));
|
|
42
|
-
|
|
43
|
-
// Start shell
|
|
44
|
-
this.startShell();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
this.ws.on('message', (message) => {
|
|
48
|
-
try {
|
|
49
|
-
const data = JSON.parse(message);
|
|
50
|
-
this.handleServerMessage(data);
|
|
51
|
-
} catch (e) {
|
|
52
|
-
console.error('Failed to parse server message:', e);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
this.ws.on('close', () => {
|
|
57
|
-
console.log('Disconnected from server');
|
|
58
|
-
this.stopShell();
|
|
59
|
-
this.scheduleReconnect();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
this.ws.on('error', (err) => {
|
|
63
|
-
console.error('WebSocket error:', err.message);
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
scheduleReconnect() {
|
|
68
|
-
console.log(`Reconnecting in ${this.reconnectDelay / 1000}s...`);
|
|
69
|
-
setTimeout(() => {
|
|
70
|
-
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
|
|
71
|
-
this.connect();
|
|
72
|
-
}, this.reconnectDelay);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
handleServerMessage(data) {
|
|
76
|
-
switch (data.type) {
|
|
77
|
-
case 'input':
|
|
78
|
-
if (this.pty) {
|
|
79
|
-
this.pty.write(data.text);
|
|
80
|
-
}
|
|
81
|
-
break;
|
|
82
|
-
|
|
83
|
-
case 'resize':
|
|
84
|
-
if (this.pty) {
|
|
85
|
-
this.pty.resize(data.cols, data.rows);
|
|
86
|
-
}
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
startShell() {
|
|
92
|
-
if (this.pty) return;
|
|
93
|
-
|
|
94
|
-
const pty = require('node-pty');
|
|
95
|
-
this.pty = pty.spawn(SHELL, ['-l'], {
|
|
96
|
-
name: 'xterm-256color',
|
|
97
|
-
cols: 120,
|
|
98
|
-
rows: 40,
|
|
99
|
-
cwd: process.cwd(),
|
|
100
|
-
env: process.env
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
console.log(`Shell started (PID: ${this.pty.pid})`);
|
|
104
|
-
|
|
105
|
-
this.pty.onData((data) => {
|
|
106
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
107
|
-
this.ws.send(JSON.stringify({
|
|
108
|
-
type: 'output',
|
|
109
|
-
data
|
|
110
|
-
}));
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
this.pty.onExit(() => {
|
|
115
|
-
console.log('Shell exited');
|
|
116
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
117
|
-
this.ws.send(JSON.stringify({ type: 'exit' }));
|
|
118
|
-
}
|
|
119
|
-
this.pty = null;
|
|
120
|
-
// Restart shell
|
|
121
|
-
setTimeout(() => this.startShell(), 1000);
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
stopShell() {
|
|
126
|
-
if (this.pty) {
|
|
127
|
-
this.pty.kill();
|
|
128
|
-
this.pty = null;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Parse server URL from command line
|
|
134
|
-
let serverUrl = SERVER_URL;
|
|
135
|
-
if (!serverUrl.startsWith('ws://') && !serverUrl.startsWith('wss://')) {
|
|
136
|
-
serverUrl = 'ws://' + serverUrl;
|
|
137
|
-
}
|
|
138
|
-
// Add default port if not specified
|
|
139
|
-
if (!serverUrl.match(/:\d+/) && !serverUrl.includes('localhost')) {
|
|
140
|
-
serverUrl = serverUrl.replace(/^(wss?:\/\/[^\/]+)/, '$1:6789');
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
console.log('Termites Client');
|
|
144
|
-
console.log(`System: ${getSystemInfo().username}@${getSystemInfo().hostname}`);
|
|
145
|
-
console.log(`Shell: ${SHELL}`);
|
|
146
|
-
|
|
147
|
-
new TermitesClient(serverUrl);
|
|
148
|
-
|
|
149
|
-
process.on('SIGINT', () => process.exit(0));
|
|
150
|
-
process.on('SIGTERM', () => process.exit(0));
|