@ghostty-web/demo 0.2.1 → 0.3.0-next.2.gfcdee7f
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 +5 -18
- package/bin/demo.js +139 -199
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -5,46 +5,33 @@ Cross-platform demo server for [ghostty-web](https://github.com/coder/ghostty-we
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx @ghostty-web/demo
|
|
8
|
+
npx @ghostty-web/demo@next
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
This starts a local web server with a fully functional terminal connected to your shell.
|
|
12
|
-
Works on **Linux
|
|
12
|
+
Works on **Linux** and **macOS** (no Windows support yet).
|
|
13
13
|
|
|
14
14
|
## What it does
|
|
15
15
|
|
|
16
16
|
- Starts an HTTP server on port 8080 (configurable via `PORT` env var)
|
|
17
17
|
- Starts a WebSocket server on port 3001 for PTY communication
|
|
18
|
-
- Opens a real shell session (bash, zsh,
|
|
18
|
+
- Opens a real shell session (bash, zsh, etc.)
|
|
19
19
|
- Provides full PTY support (colors, cursor positioning, resize, etc.)
|
|
20
20
|
|
|
21
21
|
## Usage
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
24
|
# Default (port 8080)
|
|
25
|
-
npx @ghostty-web/demo
|
|
25
|
+
npx @ghostty-web/demo@next
|
|
26
26
|
|
|
27
27
|
# Custom port
|
|
28
|
-
PORT=3000 npx @ghostty-web/demo
|
|
28
|
+
PORT=3000 npx @ghostty-web/demo@next
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
Then open http://localhost:8080 in your browser.
|
|
32
32
|
|
|
33
|
-
## Features
|
|
34
|
-
|
|
35
|
-
- 🖥️ Real shell sessions with full PTY support
|
|
36
|
-
- 🎨 True color (24-bit) and 256 color support
|
|
37
|
-
- ⌨️ Full keyboard support including special keys
|
|
38
|
-
- 📐 Dynamic terminal resizing
|
|
39
|
-
- 🔄 Auto-reconnection on disconnect
|
|
40
|
-
- 🌐 Cross-platform (Linux, macOS, Windows)
|
|
41
|
-
|
|
42
33
|
## Security Warning
|
|
43
34
|
|
|
44
35
|
⚠️ **This server provides full shell access.**
|
|
45
36
|
|
|
46
37
|
Only use for local development and demos. Do not expose to untrusted networks.
|
|
47
|
-
|
|
48
|
-
## License
|
|
49
|
-
|
|
50
|
-
MIT
|
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,40 +15,58 @@ 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);
|
|
22
23
|
|
|
23
|
-
const
|
|
24
|
+
const DEV_MODE = process.argv.includes('--dev');
|
|
25
|
+
const HTTP_PORT = process.env.PORT || (DEV_MODE ? 8000 : 8080);
|
|
24
26
|
const WS_PORT = 3001;
|
|
25
27
|
|
|
26
28
|
// ============================================================================
|
|
27
29
|
// Locate ghostty-web assets
|
|
28
30
|
// ============================================================================
|
|
29
31
|
|
|
32
|
+
import { createRequire } from 'module';
|
|
33
|
+
const require = createRequire(import.meta.url);
|
|
34
|
+
|
|
30
35
|
function findGhosttyWeb() {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
path.join(__dirname, '..', '..'
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
for (const p of possiblePaths) {
|
|
42
|
-
const jsPath = path.join(p, 'ghostty-web.js');
|
|
43
|
-
if (fs.existsSync(jsPath)) {
|
|
44
|
-
// Find WASM file - check both dist/ and parent directory
|
|
45
|
-
let wasmPath = path.join(p, 'ghostty-vt.wasm');
|
|
46
|
-
if (!fs.existsSync(wasmPath)) {
|
|
47
|
-
wasmPath = path.join(path.dirname(p), 'ghostty-vt.wasm');
|
|
48
|
-
}
|
|
49
|
-
if (fs.existsSync(wasmPath)) {
|
|
50
|
-
return { distPath: p, wasmPath };
|
|
51
|
-
}
|
|
36
|
+
// In dev mode, we use Vite - no need to find built assets
|
|
37
|
+
if (DEV_MODE) {
|
|
38
|
+
const repoRoot = path.join(__dirname, '..', '..');
|
|
39
|
+
const wasmPath = path.join(repoRoot, 'ghostty-vt.wasm');
|
|
40
|
+
if (!fs.existsSync(wasmPath)) {
|
|
41
|
+
console.error('Error: ghostty-vt.wasm not found.');
|
|
42
|
+
console.error('Run: bun run build:wasm');
|
|
43
|
+
process.exit(1);
|
|
52
44
|
}
|
|
45
|
+
return { distPath: null, wasmPath, repoRoot };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// First, check for local development (repo root dist/)
|
|
49
|
+
const localDist = path.join(__dirname, '..', '..', 'dist');
|
|
50
|
+
const localJs = path.join(localDist, 'ghostty-web.js');
|
|
51
|
+
const localWasm = path.join(__dirname, '..', '..', 'ghostty-vt.wasm');
|
|
52
|
+
|
|
53
|
+
if (fs.existsSync(localJs) && fs.existsSync(localWasm)) {
|
|
54
|
+
return { distPath: localDist, wasmPath: localWasm, repoRoot: path.join(__dirname, '..', '..') };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Use require.resolve to find the installed ghostty-web package
|
|
58
|
+
try {
|
|
59
|
+
const ghosttyWebMain = require.resolve('ghostty-web');
|
|
60
|
+
// Strip dist/... from path to get package root (regex already gives us the root)
|
|
61
|
+
const ghosttyWebRoot = ghosttyWebMain.replace(/[/\\]dist[/\\].*$/, '');
|
|
62
|
+
const distPath = path.join(ghosttyWebRoot, 'dist');
|
|
63
|
+
const wasmPath = path.join(ghosttyWebRoot, 'ghostty-vt.wasm');
|
|
64
|
+
|
|
65
|
+
if (fs.existsSync(path.join(distPath, 'ghostty-web.js')) && fs.existsSync(wasmPath)) {
|
|
66
|
+
return { distPath, wasmPath, repoRoot: null };
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// require.resolve failed, package not found
|
|
53
70
|
}
|
|
54
71
|
|
|
55
72
|
console.error('Error: Could not find ghostty-web package.');
|
|
@@ -59,10 +76,7 @@ function findGhosttyWeb() {
|
|
|
59
76
|
process.exit(1);
|
|
60
77
|
}
|
|
61
78
|
|
|
62
|
-
const { distPath, wasmPath } = findGhosttyWeb();
|
|
63
|
-
const isDev =
|
|
64
|
-
distPath.includes(path.join('demo', '..', 'dist')) ||
|
|
65
|
-
distPath === path.join(__dirname, '..', '..', 'dist');
|
|
79
|
+
const { distPath, wasmPath, repoRoot } = findGhosttyWeb();
|
|
66
80
|
|
|
67
81
|
// ============================================================================
|
|
68
82
|
// HTML Template
|
|
@@ -157,14 +171,22 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
157
171
|
}
|
|
158
172
|
|
|
159
173
|
.terminal-content {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
174
|
+
height: 600px;
|
|
175
|
+
padding: 16px;
|
|
176
|
+
background: #1e1e1e;
|
|
177
|
+
position: relative;
|
|
178
|
+
overflow: hidden;
|
|
163
179
|
}
|
|
164
180
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
181
|
+
/* Ensure terminal canvas can handle scrolling */
|
|
182
|
+
.terminal-content canvas {
|
|
183
|
+
display: block;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@media (max-width: 768px) {
|
|
187
|
+
.terminal-content {
|
|
188
|
+
height: 500px;
|
|
189
|
+
}
|
|
168
190
|
}
|
|
169
191
|
</style>
|
|
170
192
|
</head>
|
|
@@ -176,25 +198,28 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
176
198
|
<div class="light yellow"></div>
|
|
177
199
|
<div class="light green"></div>
|
|
178
200
|
</div>
|
|
179
|
-
<span class="title">ghostty-web
|
|
201
|
+
<span class="title">ghostty-web</span>
|
|
180
202
|
<div class="connection-status">
|
|
181
203
|
<div class="status-dot connecting" id="status-dot"></div>
|
|
182
204
|
<span id="status-text">Connecting...</span>
|
|
183
205
|
</div>
|
|
184
206
|
</div>
|
|
185
|
-
<div class="terminal-content">
|
|
186
|
-
<div id="terminal"></div>
|
|
187
|
-
</div>
|
|
207
|
+
<div class="terminal-content" id="terminal"></div>
|
|
188
208
|
</div>
|
|
189
209
|
|
|
190
210
|
<script type="module">
|
|
191
|
-
import { Terminal, FitAddon } from '/dist/ghostty-web.js';
|
|
211
|
+
import { init, Terminal, FitAddon } from '/dist/ghostty-web.js';
|
|
192
212
|
|
|
213
|
+
await init();
|
|
193
214
|
const term = new Terminal({
|
|
194
215
|
cols: 80,
|
|
195
216
|
rows: 24,
|
|
196
217
|
fontFamily: 'JetBrains Mono, Menlo, Monaco, monospace',
|
|
197
218
|
fontSize: 14,
|
|
219
|
+
theme: {
|
|
220
|
+
background: '#1e1e1e',
|
|
221
|
+
foreground: '#d4d4d4',
|
|
222
|
+
},
|
|
198
223
|
});
|
|
199
224
|
|
|
200
225
|
const fitAddon = new FitAddon();
|
|
@@ -203,6 +228,7 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
203
228
|
const container = document.getElementById('terminal');
|
|
204
229
|
await term.open(container);
|
|
205
230
|
fitAddon.fit();
|
|
231
|
+
fitAddon.observeResize(); // Auto-fit when container resizes
|
|
206
232
|
|
|
207
233
|
// Status elements
|
|
208
234
|
const statusDot = document.getElementById('status-dot');
|
|
@@ -249,13 +275,17 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
249
275
|
}
|
|
250
276
|
});
|
|
251
277
|
|
|
252
|
-
// Handle resize
|
|
253
|
-
|
|
254
|
-
fitAddon.fit();
|
|
278
|
+
// Handle resize - notify PTY when terminal dimensions change
|
|
279
|
+
term.onResize(({ cols, rows }) => {
|
|
255
280
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
256
|
-
ws.send(JSON.stringify({ type: 'resize', cols
|
|
281
|
+
ws.send(JSON.stringify({ type: 'resize', cols, rows }));
|
|
257
282
|
}
|
|
258
283
|
});
|
|
284
|
+
|
|
285
|
+
// Also handle window resize (for browsers that don't trigger ResizeObserver on window resize)
|
|
286
|
+
window.addEventListener('resize', () => {
|
|
287
|
+
fitAddon.fit();
|
|
288
|
+
});
|
|
259
289
|
</script>
|
|
260
290
|
</body>
|
|
261
291
|
</html>`;
|
|
@@ -325,7 +355,7 @@ function serveFile(filePath, res) {
|
|
|
325
355
|
}
|
|
326
356
|
|
|
327
357
|
// ============================================================================
|
|
328
|
-
// WebSocket Server (using
|
|
358
|
+
// WebSocket Server (using ws package)
|
|
329
359
|
// ============================================================================
|
|
330
360
|
|
|
331
361
|
const sessions = new Map();
|
|
@@ -356,225 +386,135 @@ function createPtySession(cols, rows) {
|
|
|
356
386
|
return ptyProcess;
|
|
357
387
|
}
|
|
358
388
|
|
|
359
|
-
// WebSocket server
|
|
360
|
-
const
|
|
389
|
+
// WebSocket server using ws package
|
|
390
|
+
const wss = new WebSocketServer({ port: WS_PORT, path: '/ws' });
|
|
361
391
|
|
|
362
|
-
|
|
392
|
+
wss.on('connection', (ws, req) => {
|
|
363
393
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
364
|
-
|
|
365
|
-
if (url.pathname !== '/ws') {
|
|
366
|
-
socket.destroy();
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
394
|
const cols = Number.parseInt(url.searchParams.get('cols') || '80');
|
|
371
395
|
const rows = Number.parseInt(url.searchParams.get('rows') || '24');
|
|
372
396
|
|
|
373
|
-
// Parse WebSocket key and create accept key
|
|
374
|
-
const key = req.headers['sec-websocket-key'];
|
|
375
|
-
const acceptKey = crypto
|
|
376
|
-
.createHash('sha1')
|
|
377
|
-
.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
|
|
378
|
-
.digest('base64');
|
|
379
|
-
|
|
380
|
-
// Send WebSocket handshake response
|
|
381
|
-
socket.write(
|
|
382
|
-
'HTTP/1.1 101 Switching Protocols\r\n' +
|
|
383
|
-
'Upgrade: websocket\r\n' +
|
|
384
|
-
'Connection: Upgrade\r\n' +
|
|
385
|
-
'Sec-WebSocket-Accept: ' +
|
|
386
|
-
acceptKey +
|
|
387
|
-
'\r\n\r\n'
|
|
388
|
-
);
|
|
389
|
-
|
|
390
|
-
const sessionId = crypto.randomUUID().slice(0, 8);
|
|
391
|
-
|
|
392
397
|
// Create PTY
|
|
393
398
|
const ptyProcess = createPtySession(cols, rows);
|
|
394
|
-
sessions.set(
|
|
399
|
+
sessions.set(ws, { pty: ptyProcess });
|
|
395
400
|
|
|
396
401
|
// PTY -> WebSocket
|
|
397
402
|
ptyProcess.onData((data) => {
|
|
398
|
-
if (
|
|
399
|
-
|
|
403
|
+
if (ws.readyState === ws.OPEN) {
|
|
404
|
+
ws.send(data);
|
|
400
405
|
}
|
|
401
406
|
});
|
|
402
407
|
|
|
403
408
|
ptyProcess.onExit(({ exitCode }) => {
|
|
404
|
-
|
|
405
|
-
|
|
409
|
+
if (ws.readyState === ws.OPEN) {
|
|
410
|
+
ws.send(`\r\n\x1b[33mShell exited (code: ${exitCode})\x1b[0m\r\n`);
|
|
411
|
+
ws.close();
|
|
412
|
+
}
|
|
406
413
|
});
|
|
407
414
|
|
|
408
415
|
// WebSocket -> PTY
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
let offset = 2;
|
|
421
|
-
|
|
422
|
-
if (payloadLength === 126) {
|
|
423
|
-
if (buffer.length < 4) break;
|
|
424
|
-
payloadLength = buffer.readUInt16BE(2);
|
|
425
|
-
offset = 4;
|
|
426
|
-
} else if (payloadLength === 127) {
|
|
427
|
-
if (buffer.length < 10) break;
|
|
428
|
-
payloadLength = Number(buffer.readBigUInt64BE(2));
|
|
429
|
-
offset = 10;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const maskKeyOffset = offset;
|
|
433
|
-
if (masked) offset += 4;
|
|
434
|
-
|
|
435
|
-
const totalLength = offset + payloadLength;
|
|
436
|
-
if (buffer.length < totalLength) break;
|
|
437
|
-
|
|
438
|
-
// Handle different opcodes
|
|
439
|
-
if (opcode === 0x8) {
|
|
440
|
-
// Close frame
|
|
441
|
-
socket.end();
|
|
442
|
-
break;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if (opcode === 0x1 || opcode === 0x2) {
|
|
446
|
-
// Text or binary frame
|
|
447
|
-
let payload = buffer.slice(offset, totalLength);
|
|
448
|
-
|
|
449
|
-
if (masked) {
|
|
450
|
-
const maskKey = buffer.slice(maskKeyOffset, maskKeyOffset + 4);
|
|
451
|
-
payload = Buffer.from(payload);
|
|
452
|
-
for (let i = 0; i < payload.length; i++) {
|
|
453
|
-
payload[i] ^= maskKey[i % 4];
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
const data = payload.toString('utf8');
|
|
458
|
-
|
|
459
|
-
// Check for resize message
|
|
460
|
-
if (data.startsWith('{')) {
|
|
461
|
-
try {
|
|
462
|
-
const msg = JSON.parse(data);
|
|
463
|
-
if (msg.type === 'resize') {
|
|
464
|
-
ptyProcess.resize(msg.cols, msg.rows);
|
|
465
|
-
buffer = buffer.slice(totalLength);
|
|
466
|
-
continue;
|
|
467
|
-
}
|
|
468
|
-
} catch (e) {
|
|
469
|
-
// Not JSON, treat as input
|
|
470
|
-
}
|
|
416
|
+
ws.on('message', (data) => {
|
|
417
|
+
const message = data.toString('utf8');
|
|
418
|
+
|
|
419
|
+
// Check for resize message
|
|
420
|
+
if (message.startsWith('{')) {
|
|
421
|
+
try {
|
|
422
|
+
const msg = JSON.parse(message);
|
|
423
|
+
if (msg.type === 'resize') {
|
|
424
|
+
ptyProcess.resize(msg.cols, msg.rows);
|
|
425
|
+
return;
|
|
471
426
|
}
|
|
472
|
-
|
|
473
|
-
//
|
|
474
|
-
ptyProcess.write(data);
|
|
427
|
+
} catch (e) {
|
|
428
|
+
// Not JSON, treat as input
|
|
475
429
|
}
|
|
476
|
-
|
|
477
|
-
buffer = buffer.slice(totalLength);
|
|
478
430
|
}
|
|
431
|
+
|
|
432
|
+
// Send to PTY
|
|
433
|
+
ptyProcess.write(message);
|
|
479
434
|
});
|
|
480
435
|
|
|
481
|
-
|
|
482
|
-
const session = sessions.get(
|
|
436
|
+
ws.on('close', () => {
|
|
437
|
+
const session = sessions.get(ws);
|
|
483
438
|
if (session) {
|
|
484
439
|
session.pty.kill();
|
|
485
|
-
sessions.delete(
|
|
440
|
+
sessions.delete(ws);
|
|
486
441
|
}
|
|
487
442
|
});
|
|
488
443
|
|
|
489
|
-
|
|
444
|
+
ws.on('error', () => {
|
|
490
445
|
// Ignore socket errors (connection reset, etc.)
|
|
491
446
|
});
|
|
492
447
|
|
|
493
448
|
// Send welcome message
|
|
494
449
|
setTimeout(() => {
|
|
450
|
+
if (ws.readyState !== ws.OPEN) return;
|
|
495
451
|
const C = '\x1b[1;36m'; // Cyan
|
|
496
452
|
const G = '\x1b[1;32m'; // Green
|
|
497
453
|
const Y = '\x1b[1;33m'; // Yellow
|
|
498
454
|
const R = '\x1b[0m'; // Reset
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
`${C}╔══════════════════════════════════════════════════════════════╗${R}\r\n`
|
|
502
|
-
);
|
|
503
|
-
sendWebSocketFrame(
|
|
504
|
-
socket,
|
|
455
|
+
ws.send(`${C}╔══════════════════════════════════════════════════════════════╗${R}\r\n`);
|
|
456
|
+
ws.send(
|
|
505
457
|
`${C}║${R} ${G}Welcome to ghostty-web!${R} ${C}║${R}\r\n`
|
|
506
458
|
);
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
);
|
|
511
|
-
sendWebSocketFrame(
|
|
512
|
-
socket,
|
|
513
|
-
`${C}║${R} You have a real shell session with full PTY support. ${C}║${R}\r\n`
|
|
514
|
-
);
|
|
515
|
-
sendWebSocketFrame(
|
|
516
|
-
socket,
|
|
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(
|
|
517
462
|
`${C}║${R} Try: ${Y}ls${R}, ${Y}cd${R}, ${Y}top${R}, ${Y}vim${R}, or any command! ${C}║${R}\r\n`
|
|
518
463
|
);
|
|
519
|
-
|
|
520
|
-
socket,
|
|
521
|
-
`${C}╚══════════════════════════════════════════════════════════════╝${R}\r\n\r\n`
|
|
522
|
-
);
|
|
464
|
+
ws.send(`${C}╚══════════════════════════════════════════════════════════════╝${R}\r\n\r\n`);
|
|
523
465
|
}, 100);
|
|
524
466
|
});
|
|
525
467
|
|
|
526
|
-
function sendWebSocketFrame(socket, data) {
|
|
527
|
-
const payload = Buffer.from(data, 'utf8');
|
|
528
|
-
let header;
|
|
529
|
-
|
|
530
|
-
if (payload.length < 126) {
|
|
531
|
-
header = Buffer.alloc(2);
|
|
532
|
-
header[0] = 0x81; // FIN + text frame
|
|
533
|
-
header[1] = payload.length;
|
|
534
|
-
} else if (payload.length < 65536) {
|
|
535
|
-
header = Buffer.alloc(4);
|
|
536
|
-
header[0] = 0x81;
|
|
537
|
-
header[1] = 126;
|
|
538
|
-
header.writeUInt16BE(payload.length, 2);
|
|
539
|
-
} else {
|
|
540
|
-
header = Buffer.alloc(10);
|
|
541
|
-
header[0] = 0x81;
|
|
542
|
-
header[1] = 127;
|
|
543
|
-
header.writeBigUInt64BE(BigInt(payload.length), 2);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
socket.write(Buffer.concat([header, payload]));
|
|
547
|
-
}
|
|
548
|
-
|
|
549
468
|
// ============================================================================
|
|
550
469
|
// Startup
|
|
551
470
|
// ============================================================================
|
|
552
471
|
|
|
553
|
-
|
|
472
|
+
function printBanner(url) {
|
|
554
473
|
console.log('\n' + '═'.repeat(60));
|
|
555
|
-
console.log(' 🚀 ghostty-web demo server' + (
|
|
474
|
+
console.log(' 🚀 ghostty-web demo server' + (DEV_MODE ? ' (dev mode)' : ''));
|
|
556
475
|
console.log('═'.repeat(60));
|
|
557
|
-
console.log(`\n 📺 Open:
|
|
476
|
+
console.log(`\n 📺 Open: ${url}`);
|
|
558
477
|
console.log(` 📡 WebSocket PTY: ws://localhost:${WS_PORT}/ws`);
|
|
559
478
|
console.log(` 🐚 Shell: ${getShell()}`);
|
|
560
479
|
console.log(` 📁 Home: ${homedir()}`);
|
|
561
|
-
if (
|
|
480
|
+
if (DEV_MODE) {
|
|
481
|
+
console.log(` 🔥 Hot reload enabled via Vite`);
|
|
482
|
+
} else if (repoRoot) {
|
|
562
483
|
console.log(` 📦 Using local build: ${distPath}`);
|
|
563
484
|
}
|
|
564
485
|
console.log('\n ⚠️ This server provides shell access.');
|
|
565
486
|
console.log(' Only use for local development.\n');
|
|
566
487
|
console.log('═'.repeat(60));
|
|
567
488
|
console.log(' Press Ctrl+C to stop.\n');
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
wsServer.listen(WS_PORT);
|
|
489
|
+
}
|
|
571
490
|
|
|
572
491
|
// Graceful shutdown
|
|
573
492
|
process.on('SIGINT', () => {
|
|
574
493
|
console.log('\n\nShutting down...');
|
|
575
|
-
for (const [
|
|
494
|
+
for (const [ws, session] of sessions.entries()) {
|
|
576
495
|
session.pty.kill();
|
|
577
|
-
|
|
496
|
+
ws.close();
|
|
578
497
|
}
|
|
498
|
+
wss.close();
|
|
579
499
|
process.exit(0);
|
|
580
500
|
});
|
|
501
|
+
|
|
502
|
+
// Start HTTP/Vite server
|
|
503
|
+
if (DEV_MODE) {
|
|
504
|
+
// Dev mode: use Vite for hot reload
|
|
505
|
+
const { createServer } = await import('vite');
|
|
506
|
+
const vite = await createServer({
|
|
507
|
+
root: repoRoot,
|
|
508
|
+
server: {
|
|
509
|
+
port: HTTP_PORT,
|
|
510
|
+
strictPort: true,
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
await vite.listen();
|
|
514
|
+
printBanner(`http://localhost:${HTTP_PORT}/demo/`);
|
|
515
|
+
} else {
|
|
516
|
+
// Production mode: static file server
|
|
517
|
+
httpServer.listen(HTTP_PORT, () => {
|
|
518
|
+
printBanner(`http://localhost:${HTTP_PORT}`);
|
|
519
|
+
});
|
|
520
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghostty-web/demo",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.3.0-next.2.gfcdee7f",
|
|
4
4
|
"description": "Cross-platform demo server for ghostty-web terminal emulator",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/demo.js",
|
|
11
|
-
"dev": "
|
|
11
|
+
"dev": "node bin/demo.js --dev"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@lydell/node-pty": "^1.0.1",
|
|
15
|
-
"ghostty-web": "
|
|
15
|
+
"ghostty-web": "0.3.0-next.2.gfcdee7f",
|
|
16
|
+
"ws": "^8.18.0"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|
|
18
19
|
"bin",
|