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.
@@ -7,7 +7,9 @@
7
7
  "Bash(npm view ptyshare)",
8
8
  "Bash(npm view turtleshell)",
9
9
  "Bash(npm view snailshell)",
10
- "Bash(npm view crabshell)"
10
+ "Bash(npm view crabshell)",
11
+ "Bash(node:*)",
12
+ "Bash(curl:*)"
11
13
  ]
12
14
  }
13
15
  }
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 - Web terminal with server-client architecture
15
+ Termites - Local multi-terminal manager with web interface
16
16
 
17
17
  Usage:
18
- termites server [options] Start the server
19
- termites client [url] Connect to a server
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 server Start server with local client
29
- termites server --no-client Start server only
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 (!command || command === '-h' || command === '--help' || command === 'help') {
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.29",
4
- "description": "Web terminal with server-client architecture for remote shell access",
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
- "remote",
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.clients = new Map(); // clientId -> { ws, info, outputBuffer }
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
- const isClient = req.url === '/client';
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 (!isClient && this.config.passwordHash && !this.checkSession(req)) {
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
- if (isClient) {
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-start local client if enabled
85
- if (START_CLIENT) {
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
- startLocalClient() {
112
- console.log('Starting local client...');
113
- const clientPath = path.join(__dirname, 'client.js');
114
- const serverUrl = `ws://localhost:${this.port}/client`;
115
-
116
- this.localClient = spawn(process.execPath, [clientPath, serverUrl], {
117
- stdio: 'inherit'
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
- // Handle shell client connections
133
- handleClientConnection(ws) {
134
- const clientId = crypto.randomUUID();
135
- console.log(`New client connected: ${clientId}`);
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 clientData = {
138
- ws,
139
- info: null,
121
+ const terminal = {
122
+ pty: ptyProcess,
123
+ info,
140
124
  outputBuffer: []
141
125
  };
142
- this.clients.set(clientId, clientData);
143
126
 
144
- ws.on('message', (message) => {
145
- try {
146
- const data = JSON.parse(message);
147
- this.handleClientMessage(clientId, data);
148
- } catch (e) {
149
- console.error('Failed to parse client message:', e);
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
- ws.on('close', () => {
154
- console.log(`Client disconnected: ${clientId}`);
155
- this.clients.delete(clientId);
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
- handleClientMessage(clientId, data) {
164
- const client = this.clients.get(clientId);
165
- if (!client) return;
198
+ // Notify browsers
199
+ this.broadcastToBrowsers({
200
+ type: 'client-connected',
201
+ client: { id: terminalId, ...info }
202
+ });
203
+ this.broadcastTerminalList();
166
204
 
167
- switch (data.type) {
168
- case 'register':
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
- case 'output':
179
- client.outputBuffer.push(data.data);
180
- if (client.outputBuffer.length > 1000) {
181
- client.outputBuffer = client.outputBuffer.slice(-500);
182
- }
183
- this.broadcastToBrowsers({
184
- type: 'output',
185
- clientId,
186
- data: data.data
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
- case 'exit':
191
- console.log(`Client shell exited: ${clientId}`);
192
- break;
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 clients count:', this.clients.size);
247
+ console.log('Current terminals count:', this.terminals.size);
200
248
  this.browsers.add(ws);
201
249
 
202
- // Send current client list
203
- const clientList = this.getClientList();
204
- console.log('Sending client list:', clientList.length, 'clients');
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: clientList
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 client = this.clients.get(data.clientId);
228
- if (client) {
285
+ const terminal = this.terminals.get(data.clientId);
286
+ if (terminal) {
229
287
  // Send buffered output to browser
230
- client.outputBuffer.forEach(output => {
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 targetClient = this.clients.get(data.clientId);
243
- if (targetClient && targetClient.ws.readyState === WebSocket.OPEN) {
244
- targetClient.ws.send(JSON.stringify({
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 targetClient = this.clients.get(data.clientId);
255
- if (targetClient && targetClient.ws.readyState === WebSocket.OPEN) {
256
- targetClient.ws.send(JSON.stringify({
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
- getClientList() {
318
+ getTerminalList() {
268
319
  const list = [];
269
- this.clients.forEach((client, id) => {
270
- if (client.info) {
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
- broadcastClientList() {
326
+ broadcastTerminalList() {
278
327
  this.broadcastToBrowsers({
279
328
  type: 'clients',
280
- list: this.getClientList()
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">◉ Clients</div>
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">Waiting for clients...</div>
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">Waiting for clients...</div>';
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));