@ghostty-web/demo 0.2.1-next.12.g5d6bd7b → 0.2.1-next.13.g7b2dd99
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/bin/demo.js +42 -154
- package/package.json +3 -2
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,6 +15,8 @@ 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);
|
|
@@ -349,7 +350,7 @@ function serveFile(filePath, res) {
|
|
|
349
350
|
}
|
|
350
351
|
|
|
351
352
|
// ============================================================================
|
|
352
|
-
// WebSocket Server (using
|
|
353
|
+
// WebSocket Server (using ws package)
|
|
353
354
|
// ============================================================================
|
|
354
355
|
|
|
355
356
|
const sessions = new Map();
|
|
@@ -380,196 +381,85 @@ function createPtySession(cols, rows) {
|
|
|
380
381
|
return ptyProcess;
|
|
381
382
|
}
|
|
382
383
|
|
|
383
|
-
// WebSocket server
|
|
384
|
-
const
|
|
384
|
+
// WebSocket server using ws package
|
|
385
|
+
const wss = new WebSocketServer({ port: WS_PORT, path: '/ws' });
|
|
385
386
|
|
|
386
|
-
|
|
387
|
+
wss.on('connection', (ws, req) => {
|
|
387
388
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
388
|
-
|
|
389
|
-
if (url.pathname !== '/ws') {
|
|
390
|
-
socket.destroy();
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
389
|
const cols = Number.parseInt(url.searchParams.get('cols') || '80');
|
|
395
390
|
const rows = Number.parseInt(url.searchParams.get('rows') || '24');
|
|
396
391
|
|
|
397
|
-
// Parse WebSocket key and create accept key
|
|
398
|
-
const key = req.headers['sec-websocket-key'];
|
|
399
|
-
const acceptKey = crypto
|
|
400
|
-
.createHash('sha1')
|
|
401
|
-
.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
|
|
402
|
-
.digest('base64');
|
|
403
|
-
|
|
404
|
-
// Send WebSocket handshake response
|
|
405
|
-
socket.write(
|
|
406
|
-
'HTTP/1.1 101 Switching Protocols\r\n' +
|
|
407
|
-
'Upgrade: websocket\r\n' +
|
|
408
|
-
'Connection: Upgrade\r\n' +
|
|
409
|
-
'Sec-WebSocket-Accept: ' +
|
|
410
|
-
acceptKey +
|
|
411
|
-
'\r\n\r\n'
|
|
412
|
-
);
|
|
413
|
-
|
|
414
|
-
const sessionId = crypto.randomUUID().slice(0, 8);
|
|
415
|
-
|
|
416
392
|
// Create PTY
|
|
417
393
|
const ptyProcess = createPtySession(cols, rows);
|
|
418
|
-
sessions.set(
|
|
394
|
+
sessions.set(ws, { pty: ptyProcess });
|
|
419
395
|
|
|
420
396
|
// PTY -> WebSocket
|
|
421
397
|
ptyProcess.onData((data) => {
|
|
422
|
-
if (
|
|
423
|
-
|
|
398
|
+
if (ws.readyState === ws.OPEN) {
|
|
399
|
+
ws.send(data);
|
|
424
400
|
}
|
|
425
401
|
});
|
|
426
402
|
|
|
427
403
|
ptyProcess.onExit(({ exitCode }) => {
|
|
428
|
-
|
|
429
|
-
|
|
404
|
+
if (ws.readyState === ws.OPEN) {
|
|
405
|
+
ws.send(`\r\n\x1b[33mShell exited (code: ${exitCode})\x1b[0m\r\n`);
|
|
406
|
+
ws.close();
|
|
407
|
+
}
|
|
430
408
|
});
|
|
431
409
|
|
|
432
410
|
// WebSocket -> PTY
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
let offset = 2;
|
|
445
|
-
|
|
446
|
-
if (payloadLength === 126) {
|
|
447
|
-
if (buffer.length < 4) break;
|
|
448
|
-
payloadLength = buffer.readUInt16BE(2);
|
|
449
|
-
offset = 4;
|
|
450
|
-
} else if (payloadLength === 127) {
|
|
451
|
-
if (buffer.length < 10) break;
|
|
452
|
-
payloadLength = Number(buffer.readBigUInt64BE(2));
|
|
453
|
-
offset = 10;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const maskKeyOffset = offset;
|
|
457
|
-
if (masked) offset += 4;
|
|
458
|
-
|
|
459
|
-
const totalLength = offset + payloadLength;
|
|
460
|
-
if (buffer.length < totalLength) break;
|
|
461
|
-
|
|
462
|
-
// Handle different opcodes
|
|
463
|
-
if (opcode === 0x8) {
|
|
464
|
-
// Close frame
|
|
465
|
-
socket.end();
|
|
466
|
-
break;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
if (opcode === 0x1 || opcode === 0x2) {
|
|
470
|
-
// Text or binary frame
|
|
471
|
-
let payload = buffer.slice(offset, totalLength);
|
|
472
|
-
|
|
473
|
-
if (masked) {
|
|
474
|
-
const maskKey = buffer.slice(maskKeyOffset, maskKeyOffset + 4);
|
|
475
|
-
payload = Buffer.from(payload);
|
|
476
|
-
for (let i = 0; i < payload.length; i++) {
|
|
477
|
-
payload[i] ^= maskKey[i % 4];
|
|
478
|
-
}
|
|
411
|
+
ws.on('message', (data) => {
|
|
412
|
+
const message = data.toString('utf8');
|
|
413
|
+
|
|
414
|
+
// Check for resize message
|
|
415
|
+
if (message.startsWith('{')) {
|
|
416
|
+
try {
|
|
417
|
+
const msg = JSON.parse(message);
|
|
418
|
+
if (msg.type === 'resize') {
|
|
419
|
+
ptyProcess.resize(msg.cols, msg.rows);
|
|
420
|
+
return;
|
|
479
421
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
// Check for resize message
|
|
484
|
-
if (data.startsWith('{')) {
|
|
485
|
-
try {
|
|
486
|
-
const msg = JSON.parse(data);
|
|
487
|
-
if (msg.type === 'resize') {
|
|
488
|
-
ptyProcess.resize(msg.cols, msg.rows);
|
|
489
|
-
buffer = buffer.slice(totalLength);
|
|
490
|
-
continue;
|
|
491
|
-
}
|
|
492
|
-
} catch (e) {
|
|
493
|
-
// Not JSON, treat as input
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Send to PTY
|
|
498
|
-
ptyProcess.write(data);
|
|
422
|
+
} catch (e) {
|
|
423
|
+
// Not JSON, treat as input
|
|
499
424
|
}
|
|
500
|
-
|
|
501
|
-
buffer = buffer.slice(totalLength);
|
|
502
425
|
}
|
|
426
|
+
|
|
427
|
+
// Send to PTY
|
|
428
|
+
ptyProcess.write(message);
|
|
503
429
|
});
|
|
504
430
|
|
|
505
|
-
|
|
506
|
-
const session = sessions.get(
|
|
431
|
+
ws.on('close', () => {
|
|
432
|
+
const session = sessions.get(ws);
|
|
507
433
|
if (session) {
|
|
508
434
|
session.pty.kill();
|
|
509
|
-
sessions.delete(
|
|
435
|
+
sessions.delete(ws);
|
|
510
436
|
}
|
|
511
437
|
});
|
|
512
438
|
|
|
513
|
-
|
|
439
|
+
ws.on('error', () => {
|
|
514
440
|
// Ignore socket errors (connection reset, etc.)
|
|
515
441
|
});
|
|
516
442
|
|
|
517
443
|
// Send welcome message
|
|
518
444
|
setTimeout(() => {
|
|
445
|
+
if (ws.readyState !== ws.OPEN) return;
|
|
519
446
|
const C = '\x1b[1;36m'; // Cyan
|
|
520
447
|
const G = '\x1b[1;32m'; // Green
|
|
521
448
|
const Y = '\x1b[1;33m'; // Yellow
|
|
522
449
|
const R = '\x1b[0m'; // Reset
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
`${C}╔══════════════════════════════════════════════════════════════╗${R}\r\n`
|
|
526
|
-
);
|
|
527
|
-
sendWebSocketFrame(
|
|
528
|
-
socket,
|
|
450
|
+
ws.send(`${C}╔══════════════════════════════════════════════════════════════╗${R}\r\n`);
|
|
451
|
+
ws.send(
|
|
529
452
|
`${C}║${R} ${G}Welcome to ghostty-web!${R} ${C}║${R}\r\n`
|
|
530
453
|
);
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
);
|
|
535
|
-
sendWebSocketFrame(
|
|
536
|
-
socket,
|
|
537
|
-
`${C}║${R} You have a real shell session with full PTY support. ${C}║${R}\r\n`
|
|
538
|
-
);
|
|
539
|
-
sendWebSocketFrame(
|
|
540
|
-
socket,
|
|
454
|
+
ws.send(`${C}║${R} ${C}║${R}\r\n`);
|
|
455
|
+
ws.send(`${C}║${R} You have a real shell session with full PTY support. ${C}║${R}\r\n`);
|
|
456
|
+
ws.send(
|
|
541
457
|
`${C}║${R} Try: ${Y}ls${R}, ${Y}cd${R}, ${Y}top${R}, ${Y}vim${R}, or any command! ${C}║${R}\r\n`
|
|
542
458
|
);
|
|
543
|
-
|
|
544
|
-
socket,
|
|
545
|
-
`${C}╚══════════════════════════════════════════════════════════════╝${R}\r\n\r\n`
|
|
546
|
-
);
|
|
459
|
+
ws.send(`${C}╚══════════════════════════════════════════════════════════════╝${R}\r\n\r\n`);
|
|
547
460
|
}, 100);
|
|
548
461
|
});
|
|
549
462
|
|
|
550
|
-
function sendWebSocketFrame(socket, data) {
|
|
551
|
-
const payload = Buffer.from(data, 'utf8');
|
|
552
|
-
let header;
|
|
553
|
-
|
|
554
|
-
if (payload.length < 126) {
|
|
555
|
-
header = Buffer.alloc(2);
|
|
556
|
-
header[0] = 0x81; // FIN + text frame
|
|
557
|
-
header[1] = payload.length;
|
|
558
|
-
} else if (payload.length < 65536) {
|
|
559
|
-
header = Buffer.alloc(4);
|
|
560
|
-
header[0] = 0x81;
|
|
561
|
-
header[1] = 126;
|
|
562
|
-
header.writeUInt16BE(payload.length, 2);
|
|
563
|
-
} else {
|
|
564
|
-
header = Buffer.alloc(10);
|
|
565
|
-
header[0] = 0x81;
|
|
566
|
-
header[1] = 127;
|
|
567
|
-
header.writeBigUInt64BE(BigInt(payload.length), 2);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
socket.write(Buffer.concat([header, payload]));
|
|
571
|
-
}
|
|
572
|
-
|
|
573
463
|
// ============================================================================
|
|
574
464
|
// Startup
|
|
575
465
|
// ============================================================================
|
|
@@ -596,16 +486,14 @@ function printBanner(url) {
|
|
|
596
486
|
// Graceful shutdown
|
|
597
487
|
process.on('SIGINT', () => {
|
|
598
488
|
console.log('\n\nShutting down...');
|
|
599
|
-
for (const [
|
|
489
|
+
for (const [ws, session] of sessions.entries()) {
|
|
600
490
|
session.pty.kill();
|
|
601
|
-
|
|
491
|
+
ws.close();
|
|
602
492
|
}
|
|
493
|
+
wss.close();
|
|
603
494
|
process.exit(0);
|
|
604
495
|
});
|
|
605
496
|
|
|
606
|
-
// Start WebSocket PTY server (runs in both modes)
|
|
607
|
-
wsServer.listen(WS_PORT);
|
|
608
|
-
|
|
609
497
|
// Start HTTP/Vite server
|
|
610
498
|
if (DEV_MODE) {
|
|
611
499
|
// Dev mode: use Vite for hot reload
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghostty-web/demo",
|
|
3
|
-
"version": "0.2.1-next.
|
|
3
|
+
"version": "0.2.1-next.13.g7b2dd99",
|
|
4
4
|
"description": "Cross-platform demo server for ghostty-web terminal emulator",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@lydell/node-pty": "^1.0.1",
|
|
15
|
-
"ghostty-web": "next"
|
|
15
|
+
"ghostty-web": "next",
|
|
16
|
+
"ws": "^8.18.0"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|
|
18
19
|
"bin",
|