termites 1.0.0

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/index.js ADDED
@@ -0,0 +1,472 @@
1
+ #!/usr/bin/env node
2
+ const http = require('http');
3
+ const os = require('os');
4
+ const WebSocket = require('ws');
5
+
6
+ const PORT = process.env.PORT || 3456;
7
+ const SHELL = process.env.SHELL || '/bin/bash';
8
+
9
+ function getSystemInfo() {
10
+ const username = os.userInfo().username;
11
+ const hostname = os.hostname();
12
+ const cwd = process.cwd().replace(os.homedir(), '~');
13
+ return { username, hostname, cwd };
14
+ }
15
+
16
+ class WebTerminalServer {
17
+ constructor(port) {
18
+ this.port = port;
19
+ this.pty = null;
20
+ this.outputBuffer = [];
21
+ this.clients = new Set();
22
+ this.isRunning = false;
23
+ this.systemInfo = getSystemInfo();
24
+
25
+ this.startServer();
26
+ }
27
+
28
+ startServer() {
29
+ this.httpServer = http.createServer((req, res) => {
30
+ this.handleHttpRequest(req, res);
31
+ });
32
+
33
+ this.wss = new WebSocket.Server({ server: this.httpServer });
34
+
35
+ this.wss.on('connection', (ws) => {
36
+ console.log('新的 WebSocket 连接');
37
+ this.clients.add(ws);
38
+
39
+ ws.send(JSON.stringify({
40
+ type: 'status',
41
+ isRunning: this.isRunning,
42
+ systemInfo: this.systemInfo
43
+ }));
44
+
45
+ this.outputBuffer.forEach(data => {
46
+ ws.send(JSON.stringify({ type: 'output', data }));
47
+ });
48
+
49
+ ws.on('message', (message) => {
50
+ try {
51
+ const data = JSON.parse(message);
52
+ this.handleClientMessage(data);
53
+ } catch (e) {
54
+ console.error('解析消息失败:', e);
55
+ }
56
+ });
57
+
58
+ ws.on('close', () => {
59
+ this.clients.delete(ws);
60
+ });
61
+ });
62
+
63
+ this.httpServer.listen(this.port, () => {
64
+ console.log(`Web Terminal 启动: http://localhost:${this.port}`);
65
+ // 自动启动终端
66
+ this.startTerminal();
67
+ });
68
+ }
69
+
70
+ handleClientMessage(data) {
71
+ switch (data.type) {
72
+ case 'start':
73
+ this.startTerminal();
74
+ break;
75
+ case 'stop':
76
+ this.stopTerminal();
77
+ break;
78
+ case 'input':
79
+ if (this.pty) this.pty.write(data.text);
80
+ break;
81
+ case 'resize':
82
+ if (this.pty) this.pty.resize(data.cols, data.rows);
83
+ break;
84
+ }
85
+ }
86
+
87
+ broadcast(data) {
88
+ const msg = JSON.stringify(data);
89
+ this.clients.forEach(c => {
90
+ if (c.readyState === WebSocket.OPEN) c.send(msg);
91
+ });
92
+ }
93
+
94
+ startTerminal() {
95
+ if (this.isRunning) return;
96
+
97
+ const pty = require('node-pty');
98
+ this.pty = pty.spawn(SHELL, ['-l'], {
99
+ name: 'xterm-256color',
100
+ cols: 120,
101
+ rows: 40,
102
+ cwd: process.cwd(),
103
+ env: process.env
104
+ });
105
+
106
+ this.isRunning = true;
107
+ this.broadcast({ type: 'started' });
108
+
109
+ this.pty.onData((data) => {
110
+ this.outputBuffer.push(data);
111
+ if (this.outputBuffer.length > 1000) {
112
+ this.outputBuffer = this.outputBuffer.slice(-500);
113
+ }
114
+ this.broadcast({ type: 'output', data });
115
+ });
116
+
117
+ this.pty.onExit(() => {
118
+ this.isRunning = false;
119
+ this.pty = null;
120
+ this.broadcast({ type: 'stopped' });
121
+ });
122
+ }
123
+
124
+ stopTerminal() {
125
+ if (this.pty) {
126
+ this.pty.kill();
127
+ this.pty = null;
128
+ this.isRunning = false;
129
+ this.broadcast({ type: 'stopped' });
130
+ }
131
+ }
132
+
133
+ handleHttpRequest(req, res) {
134
+ if (req.url === '/' || req.url === '/index.html') {
135
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
136
+ res.end(this.getHtmlPage());
137
+ } else {
138
+ res.writeHead(404);
139
+ res.end('Not Found');
140
+ }
141
+ }
142
+
143
+ getHtmlPage() {
144
+ return `<!DOCTYPE html>
145
+ <html lang="zh-CN">
146
+ <head>
147
+ <meta charset="UTF-8">
148
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
149
+ <title>Web Terminal</title>
150
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css">
151
+ <style>
152
+ * { margin: 0; padding: 0; box-sizing: border-box; }
153
+ body { height: 100vh; display: flex; flex-direction: column; transition: background 0.3s; }
154
+ .header {
155
+ padding: 8px 16px;
156
+ display: flex; justify-content: space-between; align-items: center;
157
+ border-bottom: 1px solid; font-family: monospace; transition: all 0.3s;
158
+ }
159
+ .header-left { display: flex; align-items: center; gap: 12px; }
160
+ .path-info { font-size: 14px; }
161
+ .path-info .user { font-weight: bold; color: var(--user-color, #859900); }
162
+ .path-info .host { color: var(--host-color, #859900); }
163
+ .path-info .sep { color: var(--sep-color, #657b83); }
164
+ .path-info .path { color: var(--path-color, #268bd2); }
165
+ .status.running { background: var(--status-running, #859900); color: #fff; }
166
+ .status.stopped { background: var(--status-stopped, #dc322f); color: #fff; }
167
+ .status { padding: 3px 10px; border-radius: 10px; font-size: 11px; }
168
+ #terminal-container { flex: 1; padding: 5px; transition: background 0.3s; }
169
+ .xterm { height: 100%; }
170
+ .settings-btn {
171
+ background: none; border: 1px solid; padding: 4px 10px;
172
+ border-radius: 4px; cursor: pointer; font-size: 12px;
173
+ font-family: monospace; transition: all 0.2s;
174
+ }
175
+ .settings-btn:hover { opacity: 0.8; }
176
+ .settings-panel {
177
+ position: fixed; top: 0; right: -320px; width: 300px; height: 100%;
178
+ padding: 20px; box-shadow: -2px 0 10px rgba(0,0,0,0.2);
179
+ transition: right 0.3s; z-index: 100; overflow-y: auto;
180
+ }
181
+ .settings-panel.open { right: 0; }
182
+ .settings-panel h3 { margin-bottom: 20px; font-size: 16px; }
183
+ .setting-group { margin-bottom: 20px; }
184
+ .setting-group label { display: block; margin-bottom: 8px; font-size: 13px; font-weight: bold; }
185
+ .setting-group select, .setting-group input {
186
+ width: 100%; padding: 8px; border: 1px solid; border-radius: 4px;
187
+ font-size: 13px; background: inherit; color: inherit;
188
+ }
189
+ .close-btn {
190
+ position: absolute; top: 15px; right: 15px; background: none;
191
+ border: none; font-size: 20px; cursor: pointer; color: inherit;
192
+ }
193
+ .overlay {
194
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
195
+ background: rgba(0,0,0,0.3); z-index: 99; display: none;
196
+ }
197
+ .overlay.open { display: block; }
198
+ </style>
199
+ </head>
200
+ <body>
201
+ <div class="overlay" id="overlay"></div>
202
+ <div class="settings-panel" id="settings-panel">
203
+ <button class="close-btn" id="close-settings">&times;</button>
204
+ <h3>设置</h3>
205
+ <div class="setting-group">
206
+ <label>主题</label>
207
+ <select id="theme-select">
208
+ <option value="solarized-light">Solarized Light</option>
209
+ <option value="solarized-dark">Solarized Dark</option>
210
+ <option value="monokai">Monokai</option>
211
+ <option value="dracula">Dracula</option>
212
+ <option value="nord">Nord</option>
213
+ <option value="github-dark">GitHub Dark</option>
214
+ </select>
215
+ </div>
216
+ <div class="setting-group">
217
+ <label>字体</label>
218
+ <select id="font-select">
219
+ <option value="Monaco, Menlo, monospace">Monaco</option>
220
+ <option value="'Fira Code', monospace">Fira Code</option>
221
+ <option value="'JetBrains Mono', monospace">JetBrains Mono</option>
222
+ <option value="'Source Code Pro', monospace">Source Code Pro</option>
223
+ <option value="Consolas, monospace">Consolas</option>
224
+ <option value="'Courier New', monospace">Courier New</option>
225
+ </select>
226
+ </div>
227
+ <div class="setting-group">
228
+ <label>字体大小</label>
229
+ <input type="range" id="font-size" min="10" max="24" value="14">
230
+ <span id="font-size-value">14px</span>
231
+ </div>
232
+ </div>
233
+ <div class="header">
234
+ <div class="header-left">
235
+ <span id="path-info" class="path-info"></span>
236
+ <span id="status" class="status stopped">已停止</span>
237
+ </div>
238
+ <button class="settings-btn" id="settings-btn">⚙ 设置</button>
239
+ </div>
240
+ <div id="terminal-container"></div>
241
+
242
+ <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
243
+ <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
244
+ <script>
245
+ let ws, term, fitAddon, isRunning = false;
246
+
247
+ const themes = {
248
+ 'solarized-light': {
249
+ background: '#fdf6e3', foreground: '#657b83', cursor: '#657b83',
250
+ cursorAccent: '#fdf6e3', selectionBackground: '#93a1a1',
251
+ black: '#073642', red: '#dc322f', green: '#859900', yellow: '#b58900',
252
+ blue: '#268bd2', magenta: '#d33682', cyan: '#2aa198', white: '#eee8d5',
253
+ brightBlack: '#002b36', brightRed: '#cb4b16', brightGreen: '#586e75',
254
+ brightYellow: '#657b83', brightBlue: '#839496', brightMagenta: '#6c71c4',
255
+ brightCyan: '#93a1a1', brightWhite: '#fdf6e3',
256
+ headerBg: '#eee8d5', headerBorder: '#93a1a1', userColor: '#859900',
257
+ hostColor: '#859900', sepColor: '#657b83', pathColor: '#268bd2',
258
+ statusRunning: '#859900', statusStopped: '#dc322f'
259
+ },
260
+ 'solarized-dark': {
261
+ background: '#002b36', foreground: '#839496', cursor: '#839496',
262
+ cursorAccent: '#002b36', selectionBackground: '#073642',
263
+ black: '#073642', red: '#dc322f', green: '#859900', yellow: '#b58900',
264
+ blue: '#268bd2', magenta: '#d33682', cyan: '#2aa198', white: '#eee8d5',
265
+ brightBlack: '#002b36', brightRed: '#cb4b16', brightGreen: '#586e75',
266
+ brightYellow: '#657b83', brightBlue: '#839496', brightMagenta: '#6c71c4',
267
+ brightCyan: '#93a1a1', brightWhite: '#fdf6e3',
268
+ headerBg: '#073642', headerBorder: '#586e75', userColor: '#859900',
269
+ hostColor: '#859900', sepColor: '#839496', pathColor: '#268bd2',
270
+ statusRunning: '#859900', statusStopped: '#dc322f'
271
+ },
272
+ 'monokai': {
273
+ background: '#272822', foreground: '#f8f8f2', cursor: '#f8f8f2',
274
+ cursorAccent: '#272822', selectionBackground: '#49483e',
275
+ black: '#272822', red: '#f92672', green: '#a6e22e', yellow: '#f4bf75',
276
+ blue: '#66d9ef', magenta: '#ae81ff', cyan: '#a1efe4', white: '#f8f8f2',
277
+ brightBlack: '#75715e', brightRed: '#f92672', brightGreen: '#a6e22e',
278
+ brightYellow: '#f4bf75', brightBlue: '#66d9ef', brightMagenta: '#ae81ff',
279
+ brightCyan: '#a1efe4', brightWhite: '#f9f8f5',
280
+ headerBg: '#1e1f1c', headerBorder: '#49483e', userColor: '#a6e22e',
281
+ hostColor: '#a6e22e', sepColor: '#f8f8f2', pathColor: '#66d9ef',
282
+ statusRunning: '#a6e22e', statusStopped: '#f92672'
283
+ },
284
+ 'dracula': {
285
+ background: '#282a36', foreground: '#f8f8f2', cursor: '#f8f8f2',
286
+ cursorAccent: '#282a36', selectionBackground: '#44475a',
287
+ black: '#21222c', red: '#ff5555', green: '#50fa7b', yellow: '#f1fa8c',
288
+ blue: '#bd93f9', magenta: '#ff79c6', cyan: '#8be9fd', white: '#f8f8f2',
289
+ brightBlack: '#6272a4', brightRed: '#ff6e6e', brightGreen: '#69ff94',
290
+ brightYellow: '#ffffa5', brightBlue: '#d6acff', brightMagenta: '#ff92df',
291
+ brightCyan: '#a4ffff', brightWhite: '#ffffff',
292
+ headerBg: '#21222c', headerBorder: '#44475a', userColor: '#50fa7b',
293
+ hostColor: '#50fa7b', sepColor: '#f8f8f2', pathColor: '#bd93f9',
294
+ statusRunning: '#50fa7b', statusStopped: '#ff5555'
295
+ },
296
+ 'nord': {
297
+ background: '#2e3440', foreground: '#d8dee9', cursor: '#d8dee9',
298
+ cursorAccent: '#2e3440', selectionBackground: '#434c5e',
299
+ black: '#3b4252', red: '#bf616a', green: '#a3be8c', yellow: '#ebcb8b',
300
+ blue: '#81a1c1', magenta: '#b48ead', cyan: '#88c0d0', white: '#e5e9f0',
301
+ brightBlack: '#4c566a', brightRed: '#bf616a', brightGreen: '#a3be8c',
302
+ brightYellow: '#ebcb8b', brightBlue: '#81a1c1', brightMagenta: '#b48ead',
303
+ brightCyan: '#8fbcbb', brightWhite: '#eceff4',
304
+ headerBg: '#3b4252', headerBorder: '#4c566a', userColor: '#a3be8c',
305
+ hostColor: '#a3be8c', sepColor: '#d8dee9', pathColor: '#81a1c1',
306
+ statusRunning: '#a3be8c', statusStopped: '#bf616a'
307
+ },
308
+ 'github-dark': {
309
+ background: '#0d1117', foreground: '#c9d1d9', cursor: '#c9d1d9',
310
+ cursorAccent: '#0d1117', selectionBackground: '#3b5070',
311
+ black: '#0d1117', red: '#ff7b72', green: '#7ee787', yellow: '#d29922',
312
+ blue: '#79c0ff', magenta: '#d2a8ff', cyan: '#a5d6ff', white: '#c9d1d9',
313
+ brightBlack: '#484f58', brightRed: '#ffa198', brightGreen: '#aff5b4',
314
+ brightYellow: '#e3b341', brightBlue: '#a5d6ff', brightMagenta: '#d2a8ff',
315
+ brightCyan: '#b6e3ff', brightWhite: '#f0f6fc',
316
+ headerBg: '#161b22', headerBorder: '#30363d', userColor: '#7ee787',
317
+ hostColor: '#7ee787', sepColor: '#c9d1d9', pathColor: '#79c0ff',
318
+ statusRunning: '#7ee787', statusStopped: '#ff7b72'
319
+ }
320
+ };
321
+
322
+ let currentTheme = 'solarized-light';
323
+ let currentFont = 'Monaco, Menlo, monospace';
324
+ let currentFontSize = 14;
325
+
326
+ function loadSettings() {
327
+ const saved = localStorage.getItem('webterm-settings');
328
+ if (saved) {
329
+ const s = JSON.parse(saved);
330
+ currentTheme = s.theme || currentTheme;
331
+ currentFont = s.font || currentFont;
332
+ currentFontSize = s.fontSize || currentFontSize;
333
+ }
334
+ document.getElementById('theme-select').value = currentTheme;
335
+ document.getElementById('font-select').value = currentFont;
336
+ document.getElementById('font-size').value = currentFontSize;
337
+ document.getElementById('font-size-value').textContent = currentFontSize + 'px';
338
+ }
339
+
340
+ function saveSettings() {
341
+ localStorage.setItem('webterm-settings', JSON.stringify({
342
+ theme: currentTheme, font: currentFont, fontSize: currentFontSize
343
+ }));
344
+ }
345
+
346
+ function applyTheme(themeName) {
347
+ const t = themes[themeName];
348
+ if (!t) return;
349
+ currentTheme = themeName;
350
+ document.body.style.background = t.background;
351
+ document.getElementById('terminal-container').style.background = t.background;
352
+ const header = document.querySelector('.header');
353
+ header.style.background = t.headerBg;
354
+ header.style.borderColor = t.headerBorder;
355
+ header.style.color = t.foreground;
356
+ document.querySelector('.settings-btn').style.borderColor = t.foreground;
357
+ document.querySelector('.settings-btn').style.color = t.foreground;
358
+ const panel = document.getElementById('settings-panel');
359
+ panel.style.background = t.headerBg;
360
+ panel.style.color = t.foreground;
361
+ const style = document.documentElement.style;
362
+ style.setProperty('--user-color', t.userColor);
363
+ style.setProperty('--host-color', t.hostColor);
364
+ style.setProperty('--sep-color', t.sepColor);
365
+ style.setProperty('--path-color', t.pathColor);
366
+ style.setProperty('--status-running', t.statusRunning);
367
+ style.setProperty('--status-stopped', t.statusStopped);
368
+ if (term) {
369
+ term.options.theme = t;
370
+ }
371
+ saveSettings();
372
+ }
373
+
374
+ function applyFont(font) {
375
+ currentFont = font;
376
+ if (term) {
377
+ term.options.fontFamily = font;
378
+ fitAddon.fit();
379
+ }
380
+ saveSettings();
381
+ }
382
+
383
+ function applyFontSize(size) {
384
+ currentFontSize = size;
385
+ document.getElementById('font-size-value').textContent = size + 'px';
386
+ if (term) {
387
+ term.options.fontSize = size;
388
+ fitAddon.fit();
389
+ }
390
+ saveSettings();
391
+ }
392
+
393
+ function setupSettings() {
394
+ const btn = document.getElementById('settings-btn');
395
+ const panel = document.getElementById('settings-panel');
396
+ const overlay = document.getElementById('overlay');
397
+ const closeBtn = document.getElementById('close-settings');
398
+
399
+ btn.onclick = () => { panel.classList.add('open'); overlay.classList.add('open'); };
400
+ closeBtn.onclick = () => { panel.classList.remove('open'); overlay.classList.remove('open'); };
401
+ overlay.onclick = () => { panel.classList.remove('open'); overlay.classList.remove('open'); };
402
+
403
+ document.getElementById('theme-select').onchange = e => applyTheme(e.target.value);
404
+ document.getElementById('font-select').onchange = e => applyFont(e.target.value);
405
+ document.getElementById('font-size').oninput = e => applyFontSize(parseInt(e.target.value));
406
+ }
407
+
408
+ function init() {
409
+ loadSettings();
410
+ setupSettings();
411
+
412
+ term = new Terminal({
413
+ theme: themes[currentTheme], fontFamily: currentFont,
414
+ fontSize: currentFontSize, cursorBlink: true
415
+ });
416
+ fitAddon = new FitAddon.FitAddon();
417
+ term.loadAddon(fitAddon);
418
+ term.open(document.getElementById('terminal-container'));
419
+ fitAddon.fit();
420
+
421
+ term.onData(data => {
422
+ if (ws?.readyState === WebSocket.OPEN) {
423
+ ws.send(JSON.stringify({ type: 'input', text: data }));
424
+ }
425
+ });
426
+
427
+ window.addEventListener('resize', () => {
428
+ fitAddon.fit();
429
+ if (ws?.readyState === WebSocket.OPEN) {
430
+ ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
431
+ }
432
+ });
433
+
434
+ connect();
435
+ applyTheme(currentTheme);
436
+ }
437
+
438
+ function connect() {
439
+ ws = new WebSocket('ws://' + location.host);
440
+ ws.onopen = () => {
441
+ ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
442
+ };
443
+ ws.onmessage = e => {
444
+ const d = JSON.parse(e.data);
445
+ if (d.type === 'output') term.write(d.data);
446
+ else if (d.type === 'status' || d.type === 'started' || d.type === 'stopped') {
447
+ isRunning = d.type === 'started' || (d.type === 'status' && d.isRunning);
448
+ document.getElementById('status').textContent = isRunning ? '运行中' : '已停止';
449
+ document.getElementById('status').className = 'status ' + (isRunning ? 'running' : 'stopped');
450
+ if (d.systemInfo) {
451
+ const i = d.systemInfo;
452
+ document.getElementById('path-info').innerHTML =
453
+ '<span class="user">' + i.username + '</span>' +
454
+ '<span class="sep">@</span><span class="host">' + i.hostname + '</span>' +
455
+ '<span class="sep">:</span><span class="path">' + i.cwd + '</span>';
456
+ }
457
+ }
458
+ };
459
+ ws.onclose = () => setTimeout(connect, 3000);
460
+ }
461
+
462
+ init();
463
+ </script>
464
+ </body>
465
+ </html>`;
466
+ }
467
+ }
468
+
469
+ new WebTerminalServer(PORT);
470
+
471
+ process.on('SIGINT', () => process.exit(0));
472
+ process.on('SIGTERM', () => process.exit(0));
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "termites",
3
+ "version": "1.0.0",
4
+ "description": "Web terminal with server-client architecture for remote shell access",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "start": "node index.js",
8
+ "server": "node server.js",
9
+ "client": "node client.js"
10
+ },
11
+ "bin": {
12
+ "termites": "bin/termites.js"
13
+ },
14
+ "dependencies": {
15
+ "node-pty": "^0.10.1",
16
+ "ws": "^8.19.0"
17
+ },
18
+ "keywords": [
19
+ "terminal",
20
+ "shell",
21
+ "remote",
22
+ "web",
23
+ "pty"
24
+ ],
25
+ "author": "",
26
+ "license": "MIT"
27
+ }