@darksol/terminal 0.4.1 → 0.4.2
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/package.json +3 -2
- package/src/cli.js +48 -0
- package/src/services/builders.js +4 -7
- package/src/services/cards.js +7 -8
- package/src/services/casino.js +6 -8
- package/src/services/facilitator.js +4 -7
- package/src/services/oracle.js +6 -10
- package/src/ui/banner.js +5 -2
- package/src/utils/fetch.js +28 -0
- package/src/web/commands.js +532 -0
- package/src/web/index.html +49 -0
- package/src/web/server.js +247 -0
- package/src/web/terminal.css +177 -0
- package/src/web/terminal.js +262 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { WebSocketServer } from 'ws';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
|
+
import open from 'open';
|
|
7
|
+
import { theme } from '../ui/theme.js';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
|
|
12
|
+
// ══════════════════════════════════════════════════
|
|
13
|
+
// DARKSOL WEB SHELL — Terminal in the browser
|
|
14
|
+
// ══════════════════════════════════════════════════
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Command handler registry — maps command strings to async functions
|
|
18
|
+
* that return { output: string } with ANSI stripped for web
|
|
19
|
+
*/
|
|
20
|
+
import { handleCommand } from './commands.js';
|
|
21
|
+
|
|
22
|
+
export async function startWebShell(opts = {}) {
|
|
23
|
+
process.on('uncaughtException', (err) => {
|
|
24
|
+
console.error('Uncaught:', err.message, err.stack);
|
|
25
|
+
});
|
|
26
|
+
const port = parseInt(opts.port || '18791');
|
|
27
|
+
const noOpen = opts.noOpen || false;
|
|
28
|
+
|
|
29
|
+
// Serve static files
|
|
30
|
+
const html = readFileSync(join(__dirname, 'index.html'), 'utf-8');
|
|
31
|
+
const css = readFileSync(join(__dirname, 'terminal.css'), 'utf-8');
|
|
32
|
+
const js = readFileSync(join(__dirname, 'terminal.js'), 'utf-8');
|
|
33
|
+
|
|
34
|
+
const server = createServer((req, res) => {
|
|
35
|
+
try {
|
|
36
|
+
const pathname = req.url.split('?')[0];
|
|
37
|
+
|
|
38
|
+
if (pathname === '/' || pathname === '/index.html') {
|
|
39
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
40
|
+
res.end(html);
|
|
41
|
+
} else if (pathname === '/terminal.css') {
|
|
42
|
+
res.writeHead(200, { 'Content-Type': 'text/css' });
|
|
43
|
+
res.end(css);
|
|
44
|
+
} else if (pathname === '/terminal.js') {
|
|
45
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
|
46
|
+
res.end(js);
|
|
47
|
+
} else if (pathname === '/health') {
|
|
48
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
49
|
+
res.end(JSON.stringify({ status: 'ok', version: '0.4.0' }));
|
|
50
|
+
} else {
|
|
51
|
+
res.writeHead(404);
|
|
52
|
+
res.end('Not found');
|
|
53
|
+
}
|
|
54
|
+
} catch (err) {
|
|
55
|
+
res.writeHead(500);
|
|
56
|
+
res.end('Internal error');
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// WebSocket for terminal I/O
|
|
61
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
62
|
+
|
|
63
|
+
server.on('upgrade', (request, socket, head) => {
|
|
64
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
65
|
+
wss.emit('connection', ws, request);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
server.on('error', (err) => {
|
|
70
|
+
if (err.code === 'EADDRINUSE') {
|
|
71
|
+
console.error(` ✗ Port ${port} is in use. Try: darksol serve --port ${port + 1}`);
|
|
72
|
+
} else {
|
|
73
|
+
console.error(` ✗ Server error: ${err.message}`);
|
|
74
|
+
}
|
|
75
|
+
process.exit(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
wss.on('error', (err) => {
|
|
79
|
+
console.error(` ✗ WebSocket error: ${err.message}`);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
wss.on('connection', (ws) => {
|
|
83
|
+
// Send welcome banner
|
|
84
|
+
ws.send(JSON.stringify({
|
|
85
|
+
type: 'output',
|
|
86
|
+
data: getBanner(),
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
ws.on('message', async (raw) => {
|
|
90
|
+
try {
|
|
91
|
+
const msg = JSON.parse(raw.toString());
|
|
92
|
+
|
|
93
|
+
if (msg.type === 'command') {
|
|
94
|
+
const cmd = msg.data.trim();
|
|
95
|
+
|
|
96
|
+
if (!cmd) return;
|
|
97
|
+
|
|
98
|
+
if (cmd === 'clear' || cmd === 'cls') {
|
|
99
|
+
ws.send(JSON.stringify({ type: 'clear' }));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (cmd === 'help') {
|
|
104
|
+
ws.send(JSON.stringify({ type: 'output', data: getHelp() }));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (cmd === 'banner') {
|
|
109
|
+
ws.send(JSON.stringify({ type: 'output', data: getBanner() }));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (cmd === 'exit' || cmd === 'quit') {
|
|
114
|
+
ws.send(JSON.stringify({ type: 'output', data: '\r\n 👋 Goodbye.\r\n' }));
|
|
115
|
+
ws.close();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Route to command handler
|
|
120
|
+
try {
|
|
121
|
+
ws.send(JSON.stringify({ type: 'output', data: '\r\n' }));
|
|
122
|
+
|
|
123
|
+
const result = await handleCommand(cmd, {
|
|
124
|
+
send: (text) => ws.send(JSON.stringify({ type: 'output', data: text })),
|
|
125
|
+
sendLine: (text) => ws.send(JSON.stringify({ type: 'output', data: text + '\r\n' })),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (result?.output) {
|
|
129
|
+
ws.send(JSON.stringify({ type: 'output', data: result.output }));
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
ws.send(JSON.stringify({
|
|
133
|
+
type: 'output',
|
|
134
|
+
data: `\r\n \x1b[31m✗ Error: ${err.message}\x1b[0m\r\n`,
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// Ignore malformed messages
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
ws.on('close', () => {
|
|
144
|
+
// Client disconnected
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
server.listen(port, '0.0.0.0', () => {
|
|
149
|
+
console.log('');
|
|
150
|
+
console.log(theme.gold.bold(' 🌑 DARKSOL WEB SHELL'));
|
|
151
|
+
console.log(theme.dim(' ─────────────────────────────'));
|
|
152
|
+
console.log('');
|
|
153
|
+
console.log(theme.dim(' Server: ') + theme.gold(`http://127.0.0.1:${port}`));
|
|
154
|
+
console.log(theme.dim(' WebSocket: ') + theme.gold(`ws://127.0.0.1:${port}`));
|
|
155
|
+
console.log('');
|
|
156
|
+
console.log(theme.dim(' Press Ctrl+C to stop'));
|
|
157
|
+
console.log('');
|
|
158
|
+
|
|
159
|
+
if (!noOpen) {
|
|
160
|
+
open(`http://127.0.0.1:${port}`).catch(() => {});
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Keep alive
|
|
165
|
+
await new Promise((resolve) => {
|
|
166
|
+
process.on('SIGINT', () => {
|
|
167
|
+
console.log(theme.dim('\n Shutting down web shell...'));
|
|
168
|
+
wss.close();
|
|
169
|
+
server.close();
|
|
170
|
+
resolve();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ══════════════════════════════════════════════════
|
|
176
|
+
// Banner & Help (ANSI formatted for xterm.js)
|
|
177
|
+
// ══════════════════════════════════════════════════
|
|
178
|
+
|
|
179
|
+
function getBanner() {
|
|
180
|
+
const gold = '\x1b[38;2;255;215;0m';
|
|
181
|
+
const dim = '\x1b[38;2;102;102;102m';
|
|
182
|
+
const white = '\x1b[1;37m';
|
|
183
|
+
const reset = '\x1b[0m';
|
|
184
|
+
const darkGold = '\x1b[38;2;184;134;11m';
|
|
185
|
+
|
|
186
|
+
return [
|
|
187
|
+
'',
|
|
188
|
+
`${gold} ██████╗ █████╗ ██████╗ ██╗ ██╗███████╗ ██████╗ ██╗ ${reset}`,
|
|
189
|
+
`${gold} ██╔══██╗██╔══██╗██╔══██╗██║ ██╔╝██╔════╝██╔═══██╗██║ ${reset}`,
|
|
190
|
+
`${darkGold} ██║ ██║███████║██████╔╝█████╔╝ ███████╗██║ ██║██║ ${reset}`,
|
|
191
|
+
`${darkGold} ██║ ██║██╔══██║██╔══██╗██╔═██╗ ╚════██║██║ ██║██║ ${reset}`,
|
|
192
|
+
`${gold} ██████╔╝██║ ██║██║ ██║██║ ██╗███████║╚██████╔╝███████╗${reset}`,
|
|
193
|
+
`${gold} ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚══════╝${reset}`,
|
|
194
|
+
'',
|
|
195
|
+
`${dim} ╔══════════════════════════════════════════════════════════╗${reset}`,
|
|
196
|
+
`${dim} ║${reset} ${gold}${white} DARKSOL TERMINAL${reset}${dim} — ${reset}${dim}Ghost in the machine with teeth${reset}${dim} ║${reset}`,
|
|
197
|
+
`${dim} ║${reset}${dim} v0.4.0 ${reset}${gold}🌑${reset}${dim} ║${reset}`,
|
|
198
|
+
`${dim} ╚══════════════════════════════════════════════════════════╝${reset}`,
|
|
199
|
+
'',
|
|
200
|
+
`${dim} All services. One terminal. Zero trust required.${reset}`,
|
|
201
|
+
'',
|
|
202
|
+
`${dim} Type ${gold}help${dim} for commands. Tab to autocomplete.${reset}`,
|
|
203
|
+
'',
|
|
204
|
+
].join('\r\n');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function getHelp() {
|
|
208
|
+
const gold = '\x1b[38;2;255;215;0m';
|
|
209
|
+
const dim = '\x1b[38;2;102;102;102m';
|
|
210
|
+
const green = '\x1b[38;2;0;255;136m';
|
|
211
|
+
const reset = '\x1b[0m';
|
|
212
|
+
|
|
213
|
+
const cmds = [
|
|
214
|
+
['price <token...>', 'Quick price check'],
|
|
215
|
+
['watch <token>', 'Live price monitor'],
|
|
216
|
+
['gas [chain]', 'Gas prices & estimates'],
|
|
217
|
+
['portfolio', 'Multi-chain balances'],
|
|
218
|
+
['history', 'Transaction history'],
|
|
219
|
+
['market <token>', 'Market intel & data'],
|
|
220
|
+
['mail status', 'AgentMail status'],
|
|
221
|
+
['mail inbox', 'Check email inbox'],
|
|
222
|
+
['mail send', 'Send an email'],
|
|
223
|
+
['oracle roll', 'On-chain random oracle'],
|
|
224
|
+
['casino status', 'Casino status'],
|
|
225
|
+
['wallet list', 'List wallets'],
|
|
226
|
+
['wallet balance', 'Wallet balance'],
|
|
227
|
+
['config', 'Show configuration'],
|
|
228
|
+
['banner', 'Show banner'],
|
|
229
|
+
['clear', 'Clear screen'],
|
|
230
|
+
['help', 'This help message'],
|
|
231
|
+
['exit', 'Close session'],
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
let out = '\r\n';
|
|
235
|
+
out += `${gold} ◆ COMMANDS${reset}\r\n`;
|
|
236
|
+
out += `${dim} ${'─'.repeat(50)}${reset}\r\n`;
|
|
237
|
+
|
|
238
|
+
for (const [cmd, desc] of cmds) {
|
|
239
|
+
out += ` ${green}${cmd.padEnd(22)}${reset}${dim}${desc}${reset}\r\n`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
out += '\r\n';
|
|
243
|
+
out += `${dim} Full CLI: npm i -g @darksol/terminal${reset}\r\n`;
|
|
244
|
+
out += '\r\n';
|
|
245
|
+
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/* ══════════════════════════════════════════════════
|
|
2
|
+
DARKSOL WEB SHELL — Terminal Theme
|
|
3
|
+
══════════════════════════════════════════════════ */
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
--bg: #0a0a1a;
|
|
7
|
+
--bg-surface: #111128;
|
|
8
|
+
--gold: #FFD700;
|
|
9
|
+
--dark-gold: #B8860B;
|
|
10
|
+
--text: #e0e0e0;
|
|
11
|
+
--text-dim: #666666;
|
|
12
|
+
--accent: #e94560;
|
|
13
|
+
--green: #00ff88;
|
|
14
|
+
--blue: #4488ff;
|
|
15
|
+
--border: #1a1a3e;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
* {
|
|
19
|
+
margin: 0;
|
|
20
|
+
padding: 0;
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
html, body {
|
|
25
|
+
height: 100%;
|
|
26
|
+
background: var(--bg);
|
|
27
|
+
color: var(--text);
|
|
28
|
+
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
|
|
29
|
+
overflow: hidden;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Top bar */
|
|
33
|
+
.top-bar {
|
|
34
|
+
height: 36px;
|
|
35
|
+
background: var(--bg-surface);
|
|
36
|
+
border-bottom: 1px solid var(--border);
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
padding: 0 16px;
|
|
40
|
+
gap: 12px;
|
|
41
|
+
user-select: none;
|
|
42
|
+
-webkit-app-region: drag;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.top-bar .dots {
|
|
46
|
+
display: flex;
|
|
47
|
+
gap: 6px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.top-bar .dot {
|
|
51
|
+
width: 10px;
|
|
52
|
+
height: 10px;
|
|
53
|
+
border-radius: 50%;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.dot.red { background: #ff5f57; }
|
|
57
|
+
.dot.yellow { background: #febc2e; }
|
|
58
|
+
.dot.green { background: #28c840; }
|
|
59
|
+
|
|
60
|
+
.top-bar .title {
|
|
61
|
+
color: var(--text-dim);
|
|
62
|
+
font-size: 12px;
|
|
63
|
+
letter-spacing: 1px;
|
|
64
|
+
flex: 1;
|
|
65
|
+
text-align: center;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.top-bar .badge {
|
|
69
|
+
background: rgba(255, 215, 0, 0.15);
|
|
70
|
+
color: var(--gold);
|
|
71
|
+
font-size: 10px;
|
|
72
|
+
padding: 2px 8px;
|
|
73
|
+
border-radius: 3px;
|
|
74
|
+
letter-spacing: 1px;
|
|
75
|
+
font-weight: bold;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Terminal container */
|
|
79
|
+
#terminal-container {
|
|
80
|
+
height: calc(100vh - 36px);
|
|
81
|
+
background: var(--bg);
|
|
82
|
+
padding: 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Status bar */
|
|
86
|
+
.status-bar {
|
|
87
|
+
height: 24px;
|
|
88
|
+
background: var(--bg-surface);
|
|
89
|
+
border-top: 1px solid var(--border);
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
padding: 0 16px;
|
|
93
|
+
font-size: 11px;
|
|
94
|
+
color: var(--text-dim);
|
|
95
|
+
gap: 16px;
|
|
96
|
+
position: fixed;
|
|
97
|
+
bottom: 0;
|
|
98
|
+
left: 0;
|
|
99
|
+
right: 0;
|
|
100
|
+
z-index: 10;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.status-bar .status-dot {
|
|
104
|
+
width: 6px;
|
|
105
|
+
height: 6px;
|
|
106
|
+
border-radius: 50%;
|
|
107
|
+
background: var(--green);
|
|
108
|
+
display: inline-block;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.status-bar .status-dot.disconnected {
|
|
112
|
+
background: var(--accent);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* xterm.js overrides */
|
|
116
|
+
.xterm {
|
|
117
|
+
padding: 8px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.xterm-viewport::-webkit-scrollbar {
|
|
121
|
+
width: 6px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.xterm-viewport::-webkit-scrollbar-track {
|
|
125
|
+
background: var(--bg);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.xterm-viewport::-webkit-scrollbar-thumb {
|
|
129
|
+
background: var(--border);
|
|
130
|
+
border-radius: 3px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.xterm-viewport::-webkit-scrollbar-thumb:hover {
|
|
134
|
+
background: var(--text-dim);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* Selection */
|
|
138
|
+
::selection {
|
|
139
|
+
background: rgba(255, 215, 0, 0.2);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Loading state */
|
|
143
|
+
.loading {
|
|
144
|
+
display: flex;
|
|
145
|
+
align-items: center;
|
|
146
|
+
justify-content: center;
|
|
147
|
+
height: 100%;
|
|
148
|
+
color: var(--gold);
|
|
149
|
+
font-size: 14px;
|
|
150
|
+
letter-spacing: 2px;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.loading .spinner {
|
|
154
|
+
display: inline-block;
|
|
155
|
+
animation: pulse 1.5s ease-in-out infinite;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@keyframes pulse {
|
|
159
|
+
0%, 100% { opacity: 0.3; }
|
|
160
|
+
50% { opacity: 1; }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* Responsive */
|
|
164
|
+
@media (max-width: 600px) {
|
|
165
|
+
.top-bar {
|
|
166
|
+
height: 30px;
|
|
167
|
+
padding: 0 10px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.top-bar .title {
|
|
171
|
+
font-size: 10px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#terminal-container {
|
|
175
|
+
height: calc(100vh - 30px);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// ══════════════════════════════════════════════════
|
|
2
|
+
// DARKSOL WEB SHELL — Client Terminal
|
|
3
|
+
// ══════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
const PROMPT = '\x1b[38;2;255;215;0m❯\x1b[0m ';
|
|
6
|
+
const PROMPT_LENGTH = 2; // visible chars: ❯ + space
|
|
7
|
+
|
|
8
|
+
const COMMANDS = [
|
|
9
|
+
'price', 'watch', 'gas', 'portfolio', 'history', 'market',
|
|
10
|
+
'wallet', 'mail', 'oracle', 'casino', 'facilitator', 'config',
|
|
11
|
+
'help', 'clear', 'banner', 'exit',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
let term;
|
|
15
|
+
let ws;
|
|
16
|
+
let currentLine = '';
|
|
17
|
+
let commandHistory = [];
|
|
18
|
+
let historyIndex = -1;
|
|
19
|
+
let connected = false;
|
|
20
|
+
|
|
21
|
+
async function init() {
|
|
22
|
+
// Load xterm.js
|
|
23
|
+
term = new Terminal({
|
|
24
|
+
theme: {
|
|
25
|
+
background: '#0a0a1a',
|
|
26
|
+
foreground: '#e0e0e0',
|
|
27
|
+
cursor: '#FFD700',
|
|
28
|
+
cursorAccent: '#0a0a1a',
|
|
29
|
+
selectionBackground: 'rgba(255, 215, 0, 0.2)',
|
|
30
|
+
selectionForeground: '#ffffff',
|
|
31
|
+
black: '#0a0a1a',
|
|
32
|
+
red: '#e94560',
|
|
33
|
+
green: '#00ff88',
|
|
34
|
+
yellow: '#FFD700',
|
|
35
|
+
blue: '#4488ff',
|
|
36
|
+
magenta: '#B8860B',
|
|
37
|
+
cyan: '#00bcd4',
|
|
38
|
+
white: '#e0e0e0',
|
|
39
|
+
brightBlack: '#666666',
|
|
40
|
+
brightRed: '#ff6b81',
|
|
41
|
+
brightGreen: '#69ff9e',
|
|
42
|
+
brightYellow: '#ffd700',
|
|
43
|
+
brightBlue: '#7cb3ff',
|
|
44
|
+
brightMagenta: '#d4a017',
|
|
45
|
+
brightCyan: '#40e0d0',
|
|
46
|
+
brightWhite: '#ffffff',
|
|
47
|
+
},
|
|
48
|
+
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace",
|
|
49
|
+
fontSize: 14,
|
|
50
|
+
lineHeight: 1.3,
|
|
51
|
+
cursorBlink: true,
|
|
52
|
+
cursorStyle: 'bar',
|
|
53
|
+
scrollback: 5000,
|
|
54
|
+
allowTransparency: true,
|
|
55
|
+
bellStyle: 'none',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const fitAddon = new FitAddon.FitAddon();
|
|
59
|
+
term.loadAddon(fitAddon);
|
|
60
|
+
|
|
61
|
+
const container = document.getElementById('terminal-container');
|
|
62
|
+
term.open(container);
|
|
63
|
+
fitAddon.fit();
|
|
64
|
+
|
|
65
|
+
window.addEventListener('resize', () => fitAddon.fit());
|
|
66
|
+
|
|
67
|
+
// Connect WebSocket
|
|
68
|
+
connectWS();
|
|
69
|
+
|
|
70
|
+
// Input handling
|
|
71
|
+
term.onKey(({ key, domEvent }) => {
|
|
72
|
+
const code = domEvent.keyCode;
|
|
73
|
+
const ctrl = domEvent.ctrlKey;
|
|
74
|
+
|
|
75
|
+
if (ctrl && code === 67) {
|
|
76
|
+
// Ctrl+C — cancel
|
|
77
|
+
currentLine = '';
|
|
78
|
+
term.write('^C\r\n');
|
|
79
|
+
writePrompt();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (ctrl && code === 76) {
|
|
84
|
+
// Ctrl+L — clear
|
|
85
|
+
term.clear();
|
|
86
|
+
writePrompt();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (code === 13) {
|
|
91
|
+
// Enter
|
|
92
|
+
term.write('\r\n');
|
|
93
|
+
const cmd = currentLine.trim();
|
|
94
|
+
|
|
95
|
+
if (cmd) {
|
|
96
|
+
commandHistory.unshift(cmd);
|
|
97
|
+
if (commandHistory.length > 50) commandHistory.pop();
|
|
98
|
+
sendCommand(cmd);
|
|
99
|
+
} else {
|
|
100
|
+
writePrompt();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
currentLine = '';
|
|
104
|
+
historyIndex = -1;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (code === 8) {
|
|
109
|
+
// Backspace
|
|
110
|
+
if (currentLine.length > 0) {
|
|
111
|
+
currentLine = currentLine.slice(0, -1);
|
|
112
|
+
term.write('\b \b');
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (code === 38) {
|
|
118
|
+
// Arrow up — history
|
|
119
|
+
if (historyIndex < commandHistory.length - 1) {
|
|
120
|
+
historyIndex++;
|
|
121
|
+
replaceInput(commandHistory[historyIndex]);
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (code === 40) {
|
|
127
|
+
// Arrow down — history
|
|
128
|
+
if (historyIndex > 0) {
|
|
129
|
+
historyIndex--;
|
|
130
|
+
replaceInput(commandHistory[historyIndex]);
|
|
131
|
+
} else if (historyIndex === 0) {
|
|
132
|
+
historyIndex = -1;
|
|
133
|
+
replaceInput('');
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (code === 9) {
|
|
139
|
+
// Tab — autocomplete
|
|
140
|
+
domEvent.preventDefault();
|
|
141
|
+
autocomplete();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Printable characters
|
|
146
|
+
if (key.length === 1 && !ctrl) {
|
|
147
|
+
currentLine += key;
|
|
148
|
+
term.write(key);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Paste support
|
|
153
|
+
term.onData((data) => {
|
|
154
|
+
// Only handle paste (multi-char input)
|
|
155
|
+
if (data.length > 1 && !data.startsWith('\x1b')) {
|
|
156
|
+
const clean = data.replace(/[\r\n]/g, '');
|
|
157
|
+
currentLine += clean;
|
|
158
|
+
term.write(clean);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
term.focus();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function connectWS() {
|
|
166
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
167
|
+
ws = new WebSocket(`${protocol}//${window.location.host}`);
|
|
168
|
+
|
|
169
|
+
ws.onopen = () => {
|
|
170
|
+
connected = true;
|
|
171
|
+
updateStatus(true);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
ws.onmessage = (event) => {
|
|
175
|
+
try {
|
|
176
|
+
const msg = JSON.parse(event.data);
|
|
177
|
+
|
|
178
|
+
if (msg.type === 'output') {
|
|
179
|
+
term.write(msg.data);
|
|
180
|
+
writePrompt();
|
|
181
|
+
} else if (msg.type === 'clear') {
|
|
182
|
+
term.clear();
|
|
183
|
+
writePrompt();
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
// Raw text fallback
|
|
187
|
+
term.write(event.data);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
ws.onclose = () => {
|
|
192
|
+
connected = false;
|
|
193
|
+
updateStatus(false);
|
|
194
|
+
term.write('\r\n\x1b[38;2;233;69;96m ⚡ Connection lost. Reconnecting...\x1b[0m\r\n');
|
|
195
|
+
|
|
196
|
+
// Reconnect after 3s
|
|
197
|
+
setTimeout(() => {
|
|
198
|
+
connectWS();
|
|
199
|
+
}, 3000);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
ws.onerror = () => {
|
|
203
|
+
connected = false;
|
|
204
|
+
updateStatus(false);
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function sendCommand(cmd) {
|
|
209
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
210
|
+
ws.send(JSON.stringify({ type: 'command', data: cmd }));
|
|
211
|
+
} else {
|
|
212
|
+
term.write('\x1b[38;2;233;69;96m ✗ Not connected\x1b[0m\r\n');
|
|
213
|
+
writePrompt();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function writePrompt() {
|
|
218
|
+
term.write(PROMPT);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function replaceInput(text) {
|
|
222
|
+
// Clear current input
|
|
223
|
+
const clearLen = currentLine.length;
|
|
224
|
+
for (let i = 0; i < clearLen; i++) {
|
|
225
|
+
term.write('\b \b');
|
|
226
|
+
}
|
|
227
|
+
currentLine = text;
|
|
228
|
+
term.write(text);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function autocomplete() {
|
|
232
|
+
if (!currentLine) return;
|
|
233
|
+
|
|
234
|
+
const matches = COMMANDS.filter((c) => c.startsWith(currentLine.toLowerCase()));
|
|
235
|
+
|
|
236
|
+
if (matches.length === 1) {
|
|
237
|
+
replaceInput(matches[0]);
|
|
238
|
+
} else if (matches.length > 1) {
|
|
239
|
+
// Show options
|
|
240
|
+
term.write('\r\n');
|
|
241
|
+
term.write(
|
|
242
|
+
matches.map((m) => ` \x1b[38;2;102;102;102m${m}\x1b[0m`).join(' ')
|
|
243
|
+
);
|
|
244
|
+
term.write('\r\n');
|
|
245
|
+
writePrompt();
|
|
246
|
+
term.write(currentLine);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function updateStatus(isConnected) {
|
|
251
|
+
const dot = document.querySelector('.status-dot');
|
|
252
|
+
const text = document.querySelector('.status-text');
|
|
253
|
+
if (dot) {
|
|
254
|
+
dot.className = isConnected ? 'status-dot' : 'status-dot disconnected';
|
|
255
|
+
}
|
|
256
|
+
if (text) {
|
|
257
|
+
text.textContent = isConnected ? 'Connected' : 'Disconnected';
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Boot
|
|
262
|
+
document.addEventListener('DOMContentLoaded', init);
|