@ghostty-web/demo 0.3.0 → 0.4.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/README.md +45 -1
- package/bin/demo.js +89 -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() {
|
|
@@ -286,6 +286,33 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
286
286
|
window.addEventListener('resize', () => {
|
|
287
287
|
fitAddon.fit();
|
|
288
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
|
+
}
|
|
289
316
|
</script>
|
|
290
317
|
</body>
|
|
291
318
|
</html>`;
|
|
@@ -386,8 +413,23 @@ function createPtySession(cols, rows) {
|
|
|
386
413
|
return ptyProcess;
|
|
387
414
|
}
|
|
388
415
|
|
|
389
|
-
// WebSocket server
|
|
390
|
-
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
|
+
});
|
|
391
433
|
|
|
392
434
|
wss.on('connection', (ws, req) => {
|
|
393
435
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
@@ -446,23 +488,20 @@ wss.on('connection', (ws, req) => {
|
|
|
446
488
|
});
|
|
447
489
|
|
|
448
490
|
// Send welcome message
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
);
|
|
464
|
-
ws.send(`${C}╚══════════════════════════════════════════════════════════════╝${R}\r\n\r\n`);
|
|
465
|
-
}, 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`);
|
|
466
505
|
});
|
|
467
506
|
|
|
468
507
|
// ============================================================================
|
|
@@ -474,7 +513,7 @@ function printBanner(url) {
|
|
|
474
513
|
console.log(' 🚀 ghostty-web demo server' + (DEV_MODE ? ' (dev mode)' : ''));
|
|
475
514
|
console.log('═'.repeat(60));
|
|
476
515
|
console.log(`\n 📺 Open: ${url}`);
|
|
477
|
-
console.log(` 📡 WebSocket PTY:
|
|
516
|
+
console.log(` 📡 WebSocket PTY: same endpoint /ws`);
|
|
478
517
|
console.log(` 🐚 Shell: ${getShell()}`);
|
|
479
518
|
console.log(` 📁 Home: ${homedir()}`);
|
|
480
519
|
if (DEV_MODE) {
|
|
@@ -510,7 +549,34 @@ if (DEV_MODE) {
|
|
|
510
549
|
strictPort: true,
|
|
511
550
|
},
|
|
512
551
|
});
|
|
552
|
+
|
|
513
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
|
+
|
|
514
580
|
printBanner(`http://localhost:${HTTP_PORT}/demo/`);
|
|
515
581
|
} else {
|
|
516
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",
|
|
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",
|
|
16
16
|
"ws": "^8.18.0"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|