@ghostty-web/demo 0.2.1 → 0.3.0-next.10.g90c1178
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/README.md +5 -18
- package/bin/demo.js +173 -209
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -5,46 +5,33 @@ Cross-platform demo server for [ghostty-web](https://github.com/coder/ghostty-we
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx @ghostty-web/demo
|
|
8
|
+
npx @ghostty-web/demo@next
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
This starts a local web server with a fully functional terminal connected to your shell.
|
|
12
|
-
Works on **Linux
|
|
12
|
+
Works on **Linux** and **macOS** (no Windows support yet).
|
|
13
13
|
|
|
14
14
|
## What it does
|
|
15
15
|
|
|
16
16
|
- Starts an HTTP server on port 8080 (configurable via `PORT` env var)
|
|
17
17
|
- Starts a WebSocket server on port 3001 for PTY communication
|
|
18
|
-
- Opens a real shell session (bash, zsh,
|
|
18
|
+
- Opens a real shell session (bash, zsh, etc.)
|
|
19
19
|
- Provides full PTY support (colors, cursor positioning, resize, etc.)
|
|
20
20
|
|
|
21
21
|
## Usage
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
24
|
# Default (port 8080)
|
|
25
|
-
npx @ghostty-web/demo
|
|
25
|
+
npx @ghostty-web/demo@next
|
|
26
26
|
|
|
27
27
|
# Custom port
|
|
28
|
-
PORT=3000 npx @ghostty-web/demo
|
|
28
|
+
PORT=3000 npx @ghostty-web/demo@next
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
Then open http://localhost:8080 in your browser.
|
|
32
32
|
|
|
33
|
-
## Features
|
|
34
|
-
|
|
35
|
-
- 🖥️ Real shell sessions with full PTY support
|
|
36
|
-
- 🎨 True color (24-bit) and 256 color support
|
|
37
|
-
- ⌨️ Full keyboard support including special keys
|
|
38
|
-
- 📐 Dynamic terminal resizing
|
|
39
|
-
- 🔄 Auto-reconnection on disconnect
|
|
40
|
-
- 🌐 Cross-platform (Linux, macOS, Windows)
|
|
41
|
-
|
|
42
33
|
## Security Warning
|
|
43
34
|
|
|
44
35
|
⚠️ **This server provides full shell access.**
|
|
45
36
|
|
|
46
37
|
Only use for local development and demos. Do not expose to untrusted networks.
|
|
47
|
-
|
|
48
|
-
## License
|
|
49
|
-
|
|
50
|
-
MIT
|
package/bin/demo.js
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* Run with: npx @ghostty-web/demo
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import crypto from 'crypto';
|
|
11
10
|
import fs from 'fs';
|
|
12
11
|
import http from 'http';
|
|
13
12
|
import { homedir } from 'os';
|
|
@@ -16,40 +15,58 @@ import { fileURLToPath } from 'url';
|
|
|
16
15
|
|
|
17
16
|
// Node-pty for cross-platform PTY support
|
|
18
17
|
import pty from '@lydell/node-pty';
|
|
18
|
+
// WebSocket server
|
|
19
|
+
import { WebSocketServer } from 'ws';
|
|
19
20
|
|
|
20
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
21
22
|
const __dirname = path.dirname(__filename);
|
|
22
23
|
|
|
23
|
-
const
|
|
24
|
+
const DEV_MODE = process.argv.includes('--dev');
|
|
25
|
+
const HTTP_PORT = process.env.PORT || (DEV_MODE ? 8000 : 8080);
|
|
24
26
|
const WS_PORT = 3001;
|
|
25
27
|
|
|
26
28
|
// ============================================================================
|
|
27
29
|
// Locate ghostty-web assets
|
|
28
30
|
// ============================================================================
|
|
29
31
|
|
|
32
|
+
import { createRequire } from 'module';
|
|
33
|
+
const require = createRequire(import.meta.url);
|
|
34
|
+
|
|
30
35
|
function findGhosttyWeb() {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
path.join(__dirname, '..', '..'
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
for (const p of possiblePaths) {
|
|
42
|
-
const jsPath = path.join(p, 'ghostty-web.js');
|
|
43
|
-
if (fs.existsSync(jsPath)) {
|
|
44
|
-
// Find WASM file - check both dist/ and parent directory
|
|
45
|
-
let wasmPath = path.join(p, 'ghostty-vt.wasm');
|
|
46
|
-
if (!fs.existsSync(wasmPath)) {
|
|
47
|
-
wasmPath = path.join(path.dirname(p), 'ghostty-vt.wasm');
|
|
48
|
-
}
|
|
49
|
-
if (fs.existsSync(wasmPath)) {
|
|
50
|
-
return { distPath: p, wasmPath };
|
|
51
|
-
}
|
|
36
|
+
// In dev mode, we use Vite - no need to find built assets
|
|
37
|
+
if (DEV_MODE) {
|
|
38
|
+
const repoRoot = path.join(__dirname, '..', '..');
|
|
39
|
+
const wasmPath = path.join(repoRoot, 'ghostty-vt.wasm');
|
|
40
|
+
if (!fs.existsSync(wasmPath)) {
|
|
41
|
+
console.error('Error: ghostty-vt.wasm not found.');
|
|
42
|
+
console.error('Run: bun run build:wasm');
|
|
43
|
+
process.exit(1);
|
|
52
44
|
}
|
|
45
|
+
return { distPath: null, wasmPath, repoRoot };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// First, check for local development (repo root dist/)
|
|
49
|
+
const localDist = path.join(__dirname, '..', '..', 'dist');
|
|
50
|
+
const localJs = path.join(localDist, 'ghostty-web.js');
|
|
51
|
+
const localWasm = path.join(__dirname, '..', '..', 'ghostty-vt.wasm');
|
|
52
|
+
|
|
53
|
+
if (fs.existsSync(localJs) && fs.existsSync(localWasm)) {
|
|
54
|
+
return { distPath: localDist, wasmPath: localWasm, repoRoot: path.join(__dirname, '..', '..') };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Use require.resolve to find the installed ghostty-web package
|
|
58
|
+
try {
|
|
59
|
+
const ghosttyWebMain = require.resolve('ghostty-web');
|
|
60
|
+
// Strip dist/... from path to get package root (regex already gives us the root)
|
|
61
|
+
const ghosttyWebRoot = ghosttyWebMain.replace(/[/\\]dist[/\\].*$/, '');
|
|
62
|
+
const distPath = path.join(ghosttyWebRoot, 'dist');
|
|
63
|
+
const wasmPath = path.join(ghosttyWebRoot, 'ghostty-vt.wasm');
|
|
64
|
+
|
|
65
|
+
if (fs.existsSync(path.join(distPath, 'ghostty-web.js')) && fs.existsSync(wasmPath)) {
|
|
66
|
+
return { distPath, wasmPath, repoRoot: null };
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// require.resolve failed, package not found
|
|
53
70
|
}
|
|
54
71
|
|
|
55
72
|
console.error('Error: Could not find ghostty-web package.');
|
|
@@ -59,10 +76,7 @@ function findGhosttyWeb() {
|
|
|
59
76
|
process.exit(1);
|
|
60
77
|
}
|
|
61
78
|
|
|
62
|
-
const { distPath, wasmPath } = findGhosttyWeb();
|
|
63
|
-
const isDev =
|
|
64
|
-
distPath.includes(path.join('demo', '..', 'dist')) ||
|
|
65
|
-
distPath === path.join(__dirname, '..', '..', 'dist');
|
|
79
|
+
const { distPath, wasmPath, repoRoot } = findGhosttyWeb();
|
|
66
80
|
|
|
67
81
|
// ============================================================================
|
|
68
82
|
// HTML Template
|
|
@@ -157,14 +171,22 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
157
171
|
}
|
|
158
172
|
|
|
159
173
|
.terminal-content {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
174
|
+
height: 600px;
|
|
175
|
+
padding: 16px;
|
|
176
|
+
background: #1e1e1e;
|
|
177
|
+
position: relative;
|
|
178
|
+
overflow: hidden;
|
|
163
179
|
}
|
|
164
180
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
181
|
+
/* Ensure terminal canvas can handle scrolling */
|
|
182
|
+
.terminal-content canvas {
|
|
183
|
+
display: block;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@media (max-width: 768px) {
|
|
187
|
+
.terminal-content {
|
|
188
|
+
height: 500px;
|
|
189
|
+
}
|
|
168
190
|
}
|
|
169
191
|
</style>
|
|
170
192
|
</head>
|
|
@@ -176,25 +198,28 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
176
198
|
<div class="light yellow"></div>
|
|
177
199
|
<div class="light green"></div>
|
|
178
200
|
</div>
|
|
179
|
-
<span class="title">ghostty-web
|
|
201
|
+
<span class="title">ghostty-web</span>
|
|
180
202
|
<div class="connection-status">
|
|
181
203
|
<div class="status-dot connecting" id="status-dot"></div>
|
|
182
204
|
<span id="status-text">Connecting...</span>
|
|
183
205
|
</div>
|
|
184
206
|
</div>
|
|
185
|
-
<div class="terminal-content">
|
|
186
|
-
<div id="terminal"></div>
|
|
187
|
-
</div>
|
|
207
|
+
<div class="terminal-content" id="terminal"></div>
|
|
188
208
|
</div>
|
|
189
209
|
|
|
190
210
|
<script type="module">
|
|
191
|
-
import { Terminal, FitAddon } from '/dist/ghostty-web.js';
|
|
211
|
+
import { init, Terminal, FitAddon } from '/dist/ghostty-web.js';
|
|
192
212
|
|
|
213
|
+
await init();
|
|
193
214
|
const term = new Terminal({
|
|
194
215
|
cols: 80,
|
|
195
216
|
rows: 24,
|
|
196
217
|
fontFamily: 'JetBrains Mono, Menlo, Monaco, monospace',
|
|
197
218
|
fontSize: 14,
|
|
219
|
+
theme: {
|
|
220
|
+
background: '#1e1e1e',
|
|
221
|
+
foreground: '#d4d4d4',
|
|
222
|
+
},
|
|
198
223
|
});
|
|
199
224
|
|
|
200
225
|
const fitAddon = new FitAddon();
|
|
@@ -203,6 +228,7 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
203
228
|
const container = document.getElementById('terminal');
|
|
204
229
|
await term.open(container);
|
|
205
230
|
fitAddon.fit();
|
|
231
|
+
fitAddon.observeResize(); // Auto-fit when container resizes
|
|
206
232
|
|
|
207
233
|
// Status elements
|
|
208
234
|
const statusDot = document.getElementById('status-dot');
|
|
@@ -249,13 +275,44 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
249
275
|
}
|
|
250
276
|
});
|
|
251
277
|
|
|
252
|
-
// Handle resize
|
|
253
|
-
|
|
254
|
-
fitAddon.fit();
|
|
278
|
+
// Handle resize - notify PTY when terminal dimensions change
|
|
279
|
+
term.onResize(({ cols, rows }) => {
|
|
255
280
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
256
|
-
ws.send(JSON.stringify({ type: 'resize', cols
|
|
281
|
+
ws.send(JSON.stringify({ type: 'resize', cols, rows }));
|
|
257
282
|
}
|
|
258
283
|
});
|
|
284
|
+
|
|
285
|
+
// Also handle window resize (for browsers that don't trigger ResizeObserver on window resize)
|
|
286
|
+
window.addEventListener('resize', () => {
|
|
287
|
+
fitAddon.fit();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Handle mobile keyboard showing/hiding using visualViewport API
|
|
291
|
+
if (window.visualViewport) {
|
|
292
|
+
const terminalContent = document.querySelector('.terminal-content');
|
|
293
|
+
const terminalWindow = document.querySelector('.terminal-window');
|
|
294
|
+
const originalHeight = terminalContent.style.height;
|
|
295
|
+
const body = document.body;
|
|
296
|
+
|
|
297
|
+
window.visualViewport.addEventListener('resize', () => {
|
|
298
|
+
const keyboardHeight = window.innerHeight - window.visualViewport.height;
|
|
299
|
+
if (keyboardHeight > 100) {
|
|
300
|
+
body.style.padding = '0';
|
|
301
|
+
body.style.alignItems = 'flex-start';
|
|
302
|
+
terminalWindow.style.borderRadius = '0';
|
|
303
|
+
terminalWindow.style.maxWidth = '100%';
|
|
304
|
+
terminalContent.style.height = (window.visualViewport.height - 60) + 'px';
|
|
305
|
+
window.scrollTo(0, 0);
|
|
306
|
+
} else {
|
|
307
|
+
body.style.padding = '40px 20px';
|
|
308
|
+
body.style.alignItems = 'center';
|
|
309
|
+
terminalWindow.style.borderRadius = '12px';
|
|
310
|
+
terminalWindow.style.maxWidth = '1000px';
|
|
311
|
+
terminalContent.style.height = originalHeight || '600px';
|
|
312
|
+
}
|
|
313
|
+
fitAddon.fit();
|
|
314
|
+
});
|
|
315
|
+
}
|
|
259
316
|
</script>
|
|
260
317
|
</body>
|
|
261
318
|
</html>`;
|
|
@@ -325,7 +382,7 @@ function serveFile(filePath, res) {
|
|
|
325
382
|
}
|
|
326
383
|
|
|
327
384
|
// ============================================================================
|
|
328
|
-
// WebSocket Server (using
|
|
385
|
+
// WebSocket Server (using ws package)
|
|
329
386
|
// ============================================================================
|
|
330
387
|
|
|
331
388
|
const sessions = new Map();
|
|
@@ -356,225 +413,132 @@ function createPtySession(cols, rows) {
|
|
|
356
413
|
return ptyProcess;
|
|
357
414
|
}
|
|
358
415
|
|
|
359
|
-
// WebSocket server
|
|
360
|
-
const
|
|
416
|
+
// WebSocket server using ws package
|
|
417
|
+
const wss = new WebSocketServer({ port: WS_PORT, path: '/ws' });
|
|
361
418
|
|
|
362
|
-
|
|
419
|
+
wss.on('connection', (ws, req) => {
|
|
363
420
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
364
|
-
|
|
365
|
-
if (url.pathname !== '/ws') {
|
|
366
|
-
socket.destroy();
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
421
|
const cols = Number.parseInt(url.searchParams.get('cols') || '80');
|
|
371
422
|
const rows = Number.parseInt(url.searchParams.get('rows') || '24');
|
|
372
423
|
|
|
373
|
-
// Parse WebSocket key and create accept key
|
|
374
|
-
const key = req.headers['sec-websocket-key'];
|
|
375
|
-
const acceptKey = crypto
|
|
376
|
-
.createHash('sha1')
|
|
377
|
-
.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
|
|
378
|
-
.digest('base64');
|
|
379
|
-
|
|
380
|
-
// Send WebSocket handshake response
|
|
381
|
-
socket.write(
|
|
382
|
-
'HTTP/1.1 101 Switching Protocols\r\n' +
|
|
383
|
-
'Upgrade: websocket\r\n' +
|
|
384
|
-
'Connection: Upgrade\r\n' +
|
|
385
|
-
'Sec-WebSocket-Accept: ' +
|
|
386
|
-
acceptKey +
|
|
387
|
-
'\r\n\r\n'
|
|
388
|
-
);
|
|
389
|
-
|
|
390
|
-
const sessionId = crypto.randomUUID().slice(0, 8);
|
|
391
|
-
|
|
392
424
|
// Create PTY
|
|
393
425
|
const ptyProcess = createPtySession(cols, rows);
|
|
394
|
-
sessions.set(
|
|
426
|
+
sessions.set(ws, { pty: ptyProcess });
|
|
395
427
|
|
|
396
428
|
// PTY -> WebSocket
|
|
397
429
|
ptyProcess.onData((data) => {
|
|
398
|
-
if (
|
|
399
|
-
|
|
430
|
+
if (ws.readyState === ws.OPEN) {
|
|
431
|
+
ws.send(data);
|
|
400
432
|
}
|
|
401
433
|
});
|
|
402
434
|
|
|
403
435
|
ptyProcess.onExit(({ exitCode }) => {
|
|
404
|
-
|
|
405
|
-
|
|
436
|
+
if (ws.readyState === ws.OPEN) {
|
|
437
|
+
ws.send(`\r\n\x1b[33mShell exited (code: ${exitCode})\x1b[0m\r\n`);
|
|
438
|
+
ws.close();
|
|
439
|
+
}
|
|
406
440
|
});
|
|
407
441
|
|
|
408
442
|
// WebSocket -> PTY
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
let offset = 2;
|
|
421
|
-
|
|
422
|
-
if (payloadLength === 126) {
|
|
423
|
-
if (buffer.length < 4) break;
|
|
424
|
-
payloadLength = buffer.readUInt16BE(2);
|
|
425
|
-
offset = 4;
|
|
426
|
-
} else if (payloadLength === 127) {
|
|
427
|
-
if (buffer.length < 10) break;
|
|
428
|
-
payloadLength = Number(buffer.readBigUInt64BE(2));
|
|
429
|
-
offset = 10;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const maskKeyOffset = offset;
|
|
433
|
-
if (masked) offset += 4;
|
|
434
|
-
|
|
435
|
-
const totalLength = offset + payloadLength;
|
|
436
|
-
if (buffer.length < totalLength) break;
|
|
437
|
-
|
|
438
|
-
// Handle different opcodes
|
|
439
|
-
if (opcode === 0x8) {
|
|
440
|
-
// Close frame
|
|
441
|
-
socket.end();
|
|
442
|
-
break;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if (opcode === 0x1 || opcode === 0x2) {
|
|
446
|
-
// Text or binary frame
|
|
447
|
-
let payload = buffer.slice(offset, totalLength);
|
|
448
|
-
|
|
449
|
-
if (masked) {
|
|
450
|
-
const maskKey = buffer.slice(maskKeyOffset, maskKeyOffset + 4);
|
|
451
|
-
payload = Buffer.from(payload);
|
|
452
|
-
for (let i = 0; i < payload.length; i++) {
|
|
453
|
-
payload[i] ^= maskKey[i % 4];
|
|
454
|
-
}
|
|
443
|
+
ws.on('message', (data) => {
|
|
444
|
+
const message = data.toString('utf8');
|
|
445
|
+
|
|
446
|
+
// Check for resize message
|
|
447
|
+
if (message.startsWith('{')) {
|
|
448
|
+
try {
|
|
449
|
+
const msg = JSON.parse(message);
|
|
450
|
+
if (msg.type === 'resize') {
|
|
451
|
+
ptyProcess.resize(msg.cols, msg.rows);
|
|
452
|
+
return;
|
|
455
453
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
// Check for resize message
|
|
460
|
-
if (data.startsWith('{')) {
|
|
461
|
-
try {
|
|
462
|
-
const msg = JSON.parse(data);
|
|
463
|
-
if (msg.type === 'resize') {
|
|
464
|
-
ptyProcess.resize(msg.cols, msg.rows);
|
|
465
|
-
buffer = buffer.slice(totalLength);
|
|
466
|
-
continue;
|
|
467
|
-
}
|
|
468
|
-
} catch (e) {
|
|
469
|
-
// Not JSON, treat as input
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Send to PTY
|
|
474
|
-
ptyProcess.write(data);
|
|
454
|
+
} catch (e) {
|
|
455
|
+
// Not JSON, treat as input
|
|
475
456
|
}
|
|
476
|
-
|
|
477
|
-
buffer = buffer.slice(totalLength);
|
|
478
457
|
}
|
|
458
|
+
|
|
459
|
+
// Send to PTY
|
|
460
|
+
ptyProcess.write(message);
|
|
479
461
|
});
|
|
480
462
|
|
|
481
|
-
|
|
482
|
-
const session = sessions.get(
|
|
463
|
+
ws.on('close', () => {
|
|
464
|
+
const session = sessions.get(ws);
|
|
483
465
|
if (session) {
|
|
484
466
|
session.pty.kill();
|
|
485
|
-
sessions.delete(
|
|
467
|
+
sessions.delete(ws);
|
|
486
468
|
}
|
|
487
469
|
});
|
|
488
470
|
|
|
489
|
-
|
|
471
|
+
ws.on('error', () => {
|
|
490
472
|
// Ignore socket errors (connection reset, etc.)
|
|
491
473
|
});
|
|
492
474
|
|
|
493
475
|
// Send welcome message
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
socket,
|
|
509
|
-
`${C}║${R} ${C}║${R}\r\n`
|
|
510
|
-
);
|
|
511
|
-
sendWebSocketFrame(
|
|
512
|
-
socket,
|
|
513
|
-
`${C}║${R} You have a real shell session with full PTY support. ${C}║${R}\r\n`
|
|
514
|
-
);
|
|
515
|
-
sendWebSocketFrame(
|
|
516
|
-
socket,
|
|
517
|
-
`${C}║${R} Try: ${Y}ls${R}, ${Y}cd${R}, ${Y}top${R}, ${Y}vim${R}, or any command! ${C}║${R}\r\n`
|
|
518
|
-
);
|
|
519
|
-
sendWebSocketFrame(
|
|
520
|
-
socket,
|
|
521
|
-
`${C}╚══════════════════════════════════════════════════════════════╝${R}\r\n\r\n`
|
|
522
|
-
);
|
|
523
|
-
}, 100);
|
|
476
|
+
const C = '\x1b[1;36m'; // Cyan
|
|
477
|
+
const G = '\x1b[1;32m'; // Green
|
|
478
|
+
const Y = '\x1b[1;33m'; // Yellow
|
|
479
|
+
const R = '\x1b[0m'; // Reset
|
|
480
|
+
ws.send(`${C}╔══════════════════════════════════════════════════════════════╗${R}\r\n`);
|
|
481
|
+
ws.send(
|
|
482
|
+
`${C}║${R} ${G}Welcome to ghostty-web!${R} ${C}║${R}\r\n`
|
|
483
|
+
);
|
|
484
|
+
ws.send(`${C}║${R} ${C}║${R}\r\n`);
|
|
485
|
+
ws.send(`${C}║${R} You have a real shell session with full PTY support. ${C}║${R}\r\n`);
|
|
486
|
+
ws.send(
|
|
487
|
+
`${C}║${R} Try: ${Y}ls${R}, ${Y}cd${R}, ${Y}top${R}, ${Y}vim${R}, or any command! ${C}║${R}\r\n`
|
|
488
|
+
);
|
|
489
|
+
ws.send(`${C}╚══════════════════════════════════════════════════════════════╝${R}\r\n\r\n`);
|
|
524
490
|
});
|
|
525
491
|
|
|
526
|
-
function sendWebSocketFrame(socket, data) {
|
|
527
|
-
const payload = Buffer.from(data, 'utf8');
|
|
528
|
-
let header;
|
|
529
|
-
|
|
530
|
-
if (payload.length < 126) {
|
|
531
|
-
header = Buffer.alloc(2);
|
|
532
|
-
header[0] = 0x81; // FIN + text frame
|
|
533
|
-
header[1] = payload.length;
|
|
534
|
-
} else if (payload.length < 65536) {
|
|
535
|
-
header = Buffer.alloc(4);
|
|
536
|
-
header[0] = 0x81;
|
|
537
|
-
header[1] = 126;
|
|
538
|
-
header.writeUInt16BE(payload.length, 2);
|
|
539
|
-
} else {
|
|
540
|
-
header = Buffer.alloc(10);
|
|
541
|
-
header[0] = 0x81;
|
|
542
|
-
header[1] = 127;
|
|
543
|
-
header.writeBigUInt64BE(BigInt(payload.length), 2);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
socket.write(Buffer.concat([header, payload]));
|
|
547
|
-
}
|
|
548
|
-
|
|
549
492
|
// ============================================================================
|
|
550
493
|
// Startup
|
|
551
494
|
// ============================================================================
|
|
552
495
|
|
|
553
|
-
|
|
496
|
+
function printBanner(url) {
|
|
554
497
|
console.log('\n' + '═'.repeat(60));
|
|
555
|
-
console.log(' 🚀 ghostty-web demo server' + (
|
|
498
|
+
console.log(' 🚀 ghostty-web demo server' + (DEV_MODE ? ' (dev mode)' : ''));
|
|
556
499
|
console.log('═'.repeat(60));
|
|
557
|
-
console.log(`\n 📺 Open:
|
|
500
|
+
console.log(`\n 📺 Open: ${url}`);
|
|
558
501
|
console.log(` 📡 WebSocket PTY: ws://localhost:${WS_PORT}/ws`);
|
|
559
502
|
console.log(` 🐚 Shell: ${getShell()}`);
|
|
560
503
|
console.log(` 📁 Home: ${homedir()}`);
|
|
561
|
-
if (
|
|
504
|
+
if (DEV_MODE) {
|
|
505
|
+
console.log(` 🔥 Hot reload enabled via Vite`);
|
|
506
|
+
} else if (repoRoot) {
|
|
562
507
|
console.log(` 📦 Using local build: ${distPath}`);
|
|
563
508
|
}
|
|
564
509
|
console.log('\n ⚠️ This server provides shell access.');
|
|
565
510
|
console.log(' Only use for local development.\n');
|
|
566
511
|
console.log('═'.repeat(60));
|
|
567
512
|
console.log(' Press Ctrl+C to stop.\n');
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
wsServer.listen(WS_PORT);
|
|
513
|
+
}
|
|
571
514
|
|
|
572
515
|
// Graceful shutdown
|
|
573
516
|
process.on('SIGINT', () => {
|
|
574
517
|
console.log('\n\nShutting down...');
|
|
575
|
-
for (const [
|
|
518
|
+
for (const [ws, session] of sessions.entries()) {
|
|
576
519
|
session.pty.kill();
|
|
577
|
-
|
|
520
|
+
ws.close();
|
|
578
521
|
}
|
|
522
|
+
wss.close();
|
|
579
523
|
process.exit(0);
|
|
580
524
|
});
|
|
525
|
+
|
|
526
|
+
// Start HTTP/Vite server
|
|
527
|
+
if (DEV_MODE) {
|
|
528
|
+
// Dev mode: use Vite for hot reload
|
|
529
|
+
const { createServer } = await import('vite');
|
|
530
|
+
const vite = await createServer({
|
|
531
|
+
root: repoRoot,
|
|
532
|
+
server: {
|
|
533
|
+
port: HTTP_PORT,
|
|
534
|
+
strictPort: true,
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
await vite.listen();
|
|
538
|
+
printBanner(`http://localhost:${HTTP_PORT}/demo/`);
|
|
539
|
+
} else {
|
|
540
|
+
// Production mode: static file server
|
|
541
|
+
httpServer.listen(HTTP_PORT, () => {
|
|
542
|
+
printBanner(`http://localhost:${HTTP_PORT}`);
|
|
543
|
+
});
|
|
544
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghostty-web/demo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-next.10.g90c1178",
|
|
4
4
|
"description": "Cross-platform demo server for ghostty-web terminal emulator",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/demo.js",
|
|
11
|
-
"dev": "
|
|
11
|
+
"dev": "node bin/demo.js --dev"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@lydell/node-pty": "^1.0.1",
|
|
15
|
-
"ghostty-web": "
|
|
15
|
+
"ghostty-web": "0.3.0-next.10.g90c1178",
|
|
16
|
+
"ws": "^8.18.0"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|
|
18
19
|
"bin",
|