@@ -23,7 +23,6 @@ const __dirname = path.dirname(__filename);
2323
2424const DEV_MODE = process . argv . includes ( '--dev' ) ;
2525const HTTP_PORT = process . env . PORT || ( DEV_MODE ? 8000 : 8080 ) ;
26- const WS_PORT = 3001 ;
2726
2827// ============================================================================
2928// Locate ghostty-web assets
@@ -239,8 +238,9 @@ const HTML_TEMPLATE = `<!doctype html>
239238 statusText.textContent = text;
240239 }
241240
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;
244244 let ws;
245245
246246 function connect() {
@@ -413,8 +413,23 @@ function createPtySession(cols, rows) {
413413 return ptyProcess ;
414414}
415415
416- // WebSocket server using ws package
417- 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+ } ) ;
418433
419434wss . on ( 'connection' , ( ws , req ) => {
420435 const url = new URL ( req . url , `http://${ req . headers . host } ` ) ;
@@ -498,7 +513,7 @@ function printBanner(url) {
498513 console . log ( ' 🚀 ghostty-web demo server' + ( DEV_MODE ? ' (dev mode)' : '' ) ) ;
499514 console . log ( '═' . repeat ( 60 ) ) ;
500515 console . log ( `\n 📺 Open: ${ url } ` ) ;
501- console . log ( ` 📡 WebSocket PTY: ws://localhost: ${ WS_PORT } /ws` ) ;
516+ console . log ( ` 📡 WebSocket PTY: same endpoint /ws` ) ;
502517 console . log ( ` 🐚 Shell: ${ getShell ( ) } ` ) ;
503518 console . log ( ` 📁 Home: ${ homedir ( ) } ` ) ;
504519 if ( DEV_MODE ) {
@@ -534,7 +549,34 @@ if (DEV_MODE) {
534549 strictPort : true ,
535550 } ,
536551 } ) ;
552+
537553 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+
538580 printBanner ( `http://localhost:${ HTTP_PORT } /demo/` ) ;
539581} else {
540582 // Production mode: static file server
0 commit comments