@ghostty-web/demo 0.3.0-next.9.g59bb045 → 0.4.0-next.0.g9e4e126
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 +45 -1
- package/bin/demo.js +62 -23
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,9 +14,10 @@ Works on **Linux** and **macOS** (no Windows support yet).
|
|
|
14
14
|
## What it does
|
|
15
15
|
|
|
16
16
|
- Starts an HTTP server on port 8080 (configurable via `PORT` env var)
|
|
17
|
-
-
|
|
17
|
+
- Serves WebSocket PTY on the same port at `/ws` endpoint
|
|
18
18
|
- Opens a real shell session (bash, zsh, etc.)
|
|
19
19
|
- Provides full PTY support (colors, cursor positioning, resize, etc.)
|
|
20
|
+
- Supports reverse proxies (ngrok, nginx, etc.) via X-Forwarded-\* headers
|
|
20
21
|
|
|
21
22
|
## Usage
|
|
22
23
|
|
|
@@ -30,6 +31,49 @@ PORT=3000 npx @ghostty-web/demo@next
|
|
|
30
31
|
|
|
31
32
|
Then open http://localhost:8080 in your browser.
|
|
32
33
|
|
|
34
|
+
## Reverse Proxy Support
|
|
35
|
+
|
|
36
|
+
The server now supports reverse proxies like ngrok, nginx, and others by:
|
|
37
|
+
|
|
38
|
+
- Serving WebSocket on the same HTTP port (no separate port needed)
|
|
39
|
+
- Using relative WebSocket URLs on the client side
|
|
40
|
+
- Automatic protocol detection (HTTP/HTTPS, WS/WSS)
|
|
41
|
+
|
|
42
|
+
This means the WebSocket connection automatically adapts to use the same protocol and host as the HTTP connection, making it work seamlessly through any reverse proxy.
|
|
43
|
+
|
|
44
|
+
### Example with ngrok
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Start the demo server
|
|
48
|
+
npx @ghostty-web/demo@next
|
|
49
|
+
|
|
50
|
+
# In another terminal, expose it via ngrok
|
|
51
|
+
ngrok http 8080
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The terminal will work seamlessly through the ngrok URL! Both HTTP and WebSocket traffic will be properly proxied.
|
|
55
|
+
|
|
56
|
+
### Example with nginx
|
|
57
|
+
|
|
58
|
+
```nginx
|
|
59
|
+
server {
|
|
60
|
+
listen 80;
|
|
61
|
+
server_name example.com;
|
|
62
|
+
|
|
63
|
+
location / {
|
|
64
|
+
proxy_pass http://localhost:8080;
|
|
65
|
+
proxy_http_version 1.1;
|
|
66
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
67
|
+
proxy_set_header Connection "upgrade";
|
|
68
|
+
proxy_set_header Host $host;
|
|
69
|
+
proxy_set_header X-Forwarded-Host $host;
|
|
70
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
71
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
72
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
33
77
|
## Security Warning
|
|
34
78
|
|
|
35
79
|
⚠️ **This server provides full shell access.**
|
package/bin/demo.js
CHANGED
|
@@ -23,7 +23,6 @@ const __dirname = path.dirname(__filename);
|
|
|
23
23
|
|
|
24
24
|
const DEV_MODE = process.argv.includes('--dev');
|
|
25
25
|
const HTTP_PORT = process.env.PORT || (DEV_MODE ? 8000 : 8080);
|
|
26
|
-
const WS_PORT = 3001;
|
|
27
26
|
|
|
28
27
|
// ============================================================================
|
|
29
28
|
// Locate ghostty-web assets
|
|
@@ -239,8 +238,9 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
239
238
|
statusText.textContent = text;
|
|
240
239
|
}
|
|
241
240
|
|
|
242
|
-
// Connect to WebSocket PTY server
|
|
243
|
-
const
|
|
241
|
+
// Connect to WebSocket PTY server (use same origin as HTTP server)
|
|
242
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
243
|
+
const wsUrl = protocol + '//' + window.location.host + '/ws?cols=' + term.cols + '&rows=' + term.rows;
|
|
244
244
|
let ws;
|
|
245
245
|
|
|
246
246
|
function connect() {
|
|
@@ -413,8 +413,23 @@ function createPtySession(cols, rows) {
|
|
|
413
413
|
return ptyProcess;
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
-
// WebSocket server
|
|
417
|
-
const wss = new WebSocketServer({
|
|
416
|
+
// WebSocket server attached to HTTP server (same port)
|
|
417
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
418
|
+
|
|
419
|
+
// Handle HTTP upgrade for WebSocket connections
|
|
420
|
+
httpServer.on('upgrade', (req, socket, head) => {
|
|
421
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
422
|
+
|
|
423
|
+
if (url.pathname === '/ws') {
|
|
424
|
+
// In production, consider validating req.headers.origin to prevent CSRF
|
|
425
|
+
// For development/demo purposes, we allow all origins
|
|
426
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
427
|
+
wss.emit('connection', ws, req);
|
|
428
|
+
});
|
|
429
|
+
} else {
|
|
430
|
+
socket.destroy();
|
|
431
|
+
}
|
|
432
|
+
});
|
|
418
433
|
|
|
419
434
|
wss.on('connection', (ws, req) => {
|
|
420
435
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
@@ -473,23 +488,20 @@ wss.on('connection', (ws, req) => {
|
|
|
473
488
|
});
|
|
474
489
|
|
|
475
490
|
// Send welcome message
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
);
|
|
491
|
-
ws.send(`${C}╚══════════════════════════════════════════════════════════════╝${R}\r\n\r\n`);
|
|
492
|
-
}, 100);
|
|
491
|
+
const C = '\x1b[1;36m'; // Cyan
|
|
492
|
+
const G = '\x1b[1;32m'; // Green
|
|
493
|
+
const Y = '\x1b[1;33m'; // Yellow
|
|
494
|
+
const R = '\x1b[0m'; // Reset
|
|
495
|
+
ws.send(`${C}╔══════════════════════════════════════════════════════════════╗${R}\r\n`);
|
|
496
|
+
ws.send(
|
|
497
|
+
`${C}║${R} ${G}Welcome to ghostty-web!${R} ${C}║${R}\r\n`
|
|
498
|
+
);
|
|
499
|
+
ws.send(`${C}║${R} ${C}║${R}\r\n`);
|
|
500
|
+
ws.send(`${C}║${R} You have a real shell session with full PTY support. ${C}║${R}\r\n`);
|
|
501
|
+
ws.send(
|
|
502
|
+
`${C}║${R} Try: ${Y}ls${R}, ${Y}cd${R}, ${Y}top${R}, ${Y}vim${R}, or any command! ${C}║${R}\r\n`
|
|
503
|
+
);
|
|
504
|
+
ws.send(`${C}╚══════════════════════════════════════════════════════════════╝${R}\r\n\r\n`);
|
|
493
505
|
});
|
|
494
506
|
|
|
495
507
|
// ============================================================================
|
|
@@ -501,7 +513,7 @@ function printBanner(url) {
|
|
|
501
513
|
console.log(' 🚀 ghostty-web demo server' + (DEV_MODE ? ' (dev mode)' : ''));
|
|
502
514
|
console.log('═'.repeat(60));
|
|
503
515
|
console.log(`\n 📺 Open: ${url}`);
|
|
504
|
-
console.log(` 📡 WebSocket PTY:
|
|
516
|
+
console.log(` 📡 WebSocket PTY: same endpoint /ws`);
|
|
505
517
|
console.log(` 🐚 Shell: ${getShell()}`);
|
|
506
518
|
console.log(` 📁 Home: ${homedir()}`);
|
|
507
519
|
if (DEV_MODE) {
|
|
@@ -537,7 +549,34 @@ if (DEV_MODE) {
|
|
|
537
549
|
strictPort: true,
|
|
538
550
|
},
|
|
539
551
|
});
|
|
552
|
+
|
|
540
553
|
await vite.listen();
|
|
554
|
+
|
|
555
|
+
// Attach WebSocket handler AFTER Vite has fully initialized
|
|
556
|
+
// Use prependListener (not prependOnceListener) so it runs for every request
|
|
557
|
+
// This ensures our handler runs BEFORE Vite's handlers
|
|
558
|
+
if (vite.httpServer) {
|
|
559
|
+
vite.httpServer.prependListener('upgrade', (req, socket, head) => {
|
|
560
|
+
const pathname = req.url?.split('?')[0] || req.url || '';
|
|
561
|
+
|
|
562
|
+
// ONLY handle /ws - everything else passes through unchanged to Vite
|
|
563
|
+
if (pathname === '/ws') {
|
|
564
|
+
if (!socket.destroyed && !socket.readableEnded) {
|
|
565
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
566
|
+
wss.emit('connection', ws, req);
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
// Stop here - we handled it, socket is consumed
|
|
570
|
+
// Don't call other listeners
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// For non-/ws paths, explicitly do nothing and let the event propagate
|
|
575
|
+
// The key is: don't return, don't touch the socket, just let it pass through
|
|
576
|
+
// Vite's handlers (which were added before ours via prependListener) will process it
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
541
580
|
printBanner(`http://localhost:${HTTP_PORT}/demo/`);
|
|
542
581
|
} else {
|
|
543
582
|
// Production mode: static file server
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghostty-web/demo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-next.0.g9e4e126",
|
|
4
4
|
"description": "Cross-platform demo server for ghostty-web terminal emulator",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@lydell/node-pty": "^1.0.1",
|
|
15
|
-
"ghostty-web": "0.
|
|
15
|
+
"ghostty-web": "0.4.0-next.0.g9e4e126",
|
|
16
16
|
"ws": "^8.18.0"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|