@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.
Files changed (3) hide show
  1. package/README.md +45 -1
  2. package/bin/demo.js +89 -23
  3. 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
- - Starts a WebSocket server on port 3001 for PTY communication
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 wsUrl = 'ws://' + window.location.hostname + ':${WS_PORT}/ws?cols=' + term.cols + '&rows=' + term.rows;
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 using ws package
390
- const wss = new WebSocketServer({ port: WS_PORT, path: '/ws' });
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
- setTimeout(() => {
450
- if (ws.readyState !== ws.OPEN) return;
451
- const C = '\x1b[1;36m'; // Cyan
452
- const G = '\x1b[1;32m'; // Green
453
- const Y = '\x1b[1;33m'; // Yellow
454
- const R = '\x1b[0m'; // Reset
455
- ws.send(`${C}╔══════════════════════════════════════════════════════════════╗${R}\r\n`);
456
- ws.send(
457
- `${C}║${R} ${G}Welcome to ghostty-web!${R} ${C}║${R}\r\n`
458
- );
459
- ws.send(`${C}║${R} ${C}║${R}\r\n`);
460
- ws.send(`${C}║${R} You have a real shell session with full PTY support. ${C}║${R}\r\n`);
461
- ws.send(
462
- `${C}║${R} Try: ${Y}ls${R}, ${Y}cd${R}, ${Y}top${R}, ${Y}vim${R}, or any command! ${C}║${R}\r\n`
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: ws://localhost:${WS_PORT}/ws`);
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.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.3.0",
15
+ "ghostty-web": "0.4.0",
16
16
  "ws": "^8.18.0"
17
17
  },
18
18
  "files": [