@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.
@@ -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);