@ghostty-web/demo 0.2.1 → 0.3.0-next.10.g90c1178

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 +5 -18
  2. package/bin/demo.js +173 -209
  3. 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**, **macOS**, and **Windows**.
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, cmd.exe, or PowerShell)
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 HTTP_PORT = process.env.PORT || 8080;
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
- const possiblePaths = [
32
- // Development: running from repo root (demo/bin/demo.js -> ../../dist)
33
- path.join(__dirname, '..', '..', 'dist'),
34
- // When installed as dependency (demo/node_modules/ghostty-web/dist)
35
- path.join(__dirname, '..', 'node_modules', 'ghostty-web', 'dist'),
36
- // When in a monorepo or hoisted
37
- path.join(__dirname, '..', '..', 'node_modules', 'ghostty-web', 'dist'),
38
- path.join(__dirname, '..', '..', '..', 'node_modules', 'ghostty-web', 'dist'),
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
- padding: 0;
161
- min-height: 400px;
162
- height: 60vh;
174
+ height: 600px;
175
+ padding: 16px;
176
+ background: #1e1e1e;
177
+ position: relative;
178
+ overflow: hidden;
163
179
  }
164
180
 
165
- #terminal {
166
- width: 100%;
167
- height: 100%;
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 — shell</span>
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,44 @@ const HTML_TEMPLATE = `<!doctype html>
249
275
  }
250
276
  });
251
277
 
252
- // Handle resize
253
- window.addEventListener('resize', () => {
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: term.cols, rows: term.rows }));
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
+ });
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
+ }
259
316
  </script>
260
317
  </body>
261
318
  </html>`;
@@ -325,7 +382,7 @@ function serveFile(filePath, res) {
325
382
  }
326
383
 
327
384
  // ============================================================================
328
- // WebSocket Server (using native WebSocket upgrade)
385
+ // WebSocket Server (using ws package)
329
386
  // ============================================================================
330
387
 
331
388
  const sessions = new Map();
@@ -356,225 +413,132 @@ function createPtySession(cols, rows) {
356
413
  return ptyProcess;
357
414
  }
358
415
 
359
- // WebSocket server
360
- const wsServer = http.createServer();
416
+ // WebSocket server using ws package
417
+ const wss = new WebSocketServer({ port: WS_PORT, path: '/ws' });
361
418
 
362
- wsServer.on('upgrade', (req, socket, head) => {
419
+ wss.on('connection', (ws, req) => {
363
420
  const url = new URL(req.url, `http://${req.headers.host}`);
364
-
365
- if (url.pathname !== '/ws') {
366
- socket.destroy();
367
- return;
368
- }
369
-
370
421
  const cols = Number.parseInt(url.searchParams.get('cols') || '80');
371
422
  const rows = Number.parseInt(url.searchParams.get('rows') || '24');
372
423
 
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
424
  // Create PTY
393
425
  const ptyProcess = createPtySession(cols, rows);
394
- sessions.set(socket, { id: sessionId, pty: ptyProcess });
426
+ sessions.set(ws, { pty: ptyProcess });
395
427
 
396
428
  // PTY -> WebSocket
397
429
  ptyProcess.onData((data) => {
398
- if (socket.writable) {
399
- sendWebSocketFrame(socket, data);
430
+ if (ws.readyState === ws.OPEN) {
431
+ ws.send(data);
400
432
  }
401
433
  });
402
434
 
403
435
  ptyProcess.onExit(({ exitCode }) => {
404
- sendWebSocketFrame(socket, `\r\n\x1b[33mShell exited (code: ${exitCode})\x1b[0m\r\n`);
405
- socket.end();
436
+ if (ws.readyState === ws.OPEN) {
437
+ ws.send(`\r\n\x1b[33mShell exited (code: ${exitCode})\x1b[0m\r\n`);
438
+ ws.close();
439
+ }
406
440
  });
407
441
 
408
442
  // WebSocket -> PTY
409
- let buffer = Buffer.alloc(0);
410
-
411
- socket.on('data', (chunk) => {
412
- buffer = Buffer.concat([buffer, chunk]);
413
-
414
- while (buffer.length >= 2) {
415
- const fin = (buffer[0] & 0x80) !== 0;
416
- const opcode = buffer[0] & 0x0f;
417
- const masked = (buffer[1] & 0x80) !== 0;
418
- let payloadLength = buffer[1] & 0x7f;
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
- }
443
+ ws.on('message', (data) => {
444
+ const message = data.toString('utf8');
445
+
446
+ // Check for resize message
447
+ if (message.startsWith('{')) {
448
+ try {
449
+ const msg = JSON.parse(message);
450
+ if (msg.type === 'resize') {
451
+ ptyProcess.resize(msg.cols, msg.rows);
452
+ return;
455
453
  }
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
- }
471
- }
472
-
473
- // Send to PTY
474
- ptyProcess.write(data);
454
+ } catch (e) {
455
+ // Not JSON, treat as input
475
456
  }
476
-
477
- buffer = buffer.slice(totalLength);
478
457
  }
458
+
459
+ // Send to PTY
460
+ ptyProcess.write(message);
479
461
  });
480
462
 
481
- socket.on('close', () => {
482
- const session = sessions.get(socket);
463
+ ws.on('close', () => {
464
+ const session = sessions.get(ws);
483
465
  if (session) {
484
466
  session.pty.kill();
485
- sessions.delete(socket);
467
+ sessions.delete(ws);
486
468
  }
487
469
  });
488
470
 
489
- socket.on('error', () => {
471
+ ws.on('error', () => {
490
472
  // Ignore socket errors (connection reset, etc.)
491
473
  });
492
474
 
493
475
  // Send welcome message
494
- setTimeout(() => {
495
- const C = '\x1b[1;36m'; // Cyan
496
- const G = '\x1b[1;32m'; // Green
497
- const Y = '\x1b[1;33m'; // Yellow
498
- const R = '\x1b[0m'; // Reset
499
- sendWebSocketFrame(
500
- socket,
501
- `${C}╔══════════════════════════════════════════════════════════════╗${R}\r\n`
502
- );
503
- sendWebSocketFrame(
504
- socket,
505
- `${C}║${R} ${G}Welcome to ghostty-web!${R} ${C}║${R}\r\n`
506
- );
507
- sendWebSocketFrame(
508
- socket,
509
- `${C}║${R} ${C}║${R}\r\n`
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,
517
- `${C}║${R} Try: ${Y}ls${R}, ${Y}cd${R}, ${Y}top${R}, ${Y}vim${R}, or any command! ${C}║${R}\r\n`
518
- );
519
- sendWebSocketFrame(
520
- socket,
521
- `${C}╚══════════════════════════════════════════════════════════════╝${R}\r\n\r\n`
522
- );
523
- }, 100);
476
+ const C = '\x1b[1;36m'; // Cyan
477
+ const G = '\x1b[1;32m'; // Green
478
+ const Y = '\x1b[1;33m'; // Yellow
479
+ const R = '\x1b[0m'; // Reset
480
+ ws.send(`${C}╔══════════════════════════════════════════════════════════════╗${R}\r\n`);
481
+ ws.send(
482
+ `${C}║${R} ${G}Welcome to ghostty-web!${R} ${C}║${R}\r\n`
483
+ );
484
+ ws.send(`${C}║${R} ${C}║${R}\r\n`);
485
+ ws.send(`${C}║${R} You have a real shell session with full PTY support. ${C}║${R}\r\n`);
486
+ ws.send(
487
+ `${C}║${R} Try: ${Y}ls${R}, ${Y}cd${R}, ${Y}top${R}, ${Y}vim${R}, or any command! ${C}║${R}\r\n`
488
+ );
489
+ ws.send(`${C}╚══════════════════════════════════════════════════════════════╝${R}\r\n\r\n`);
524
490
  });
525
491
 
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
492
  // ============================================================================
550
493
  // Startup
551
494
  // ============================================================================
552
495
 
553
- httpServer.listen(HTTP_PORT, () => {
496
+ function printBanner(url) {
554
497
  console.log('\n' + '═'.repeat(60));
555
- console.log(' 🚀 ghostty-web demo server' + (isDev ? ' (dev mode)' : ''));
498
+ console.log(' 🚀 ghostty-web demo server' + (DEV_MODE ? ' (dev mode)' : ''));
556
499
  console.log('═'.repeat(60));
557
- console.log(`\n 📺 Open: http://localhost:${HTTP_PORT}`);
500
+ console.log(`\n 📺 Open: ${url}`);
558
501
  console.log(` 📡 WebSocket PTY: ws://localhost:${WS_PORT}/ws`);
559
502
  console.log(` 🐚 Shell: ${getShell()}`);
560
503
  console.log(` 📁 Home: ${homedir()}`);
561
- if (isDev) {
504
+ if (DEV_MODE) {
505
+ console.log(` 🔥 Hot reload enabled via Vite`);
506
+ } else if (repoRoot) {
562
507
  console.log(` 📦 Using local build: ${distPath}`);
563
508
  }
564
509
  console.log('\n ⚠️ This server provides shell access.');
565
510
  console.log(' Only use for local development.\n');
566
511
  console.log('═'.repeat(60));
567
512
  console.log(' Press Ctrl+C to stop.\n');
568
- });
569
-
570
- wsServer.listen(WS_PORT);
513
+ }
571
514
 
572
515
  // Graceful shutdown
573
516
  process.on('SIGINT', () => {
574
517
  console.log('\n\nShutting down...');
575
- for (const [socket, session] of sessions.entries()) {
518
+ for (const [ws, session] of sessions.entries()) {
576
519
  session.pty.kill();
577
- socket.destroy();
520
+ ws.close();
578
521
  }
522
+ wss.close();
579
523
  process.exit(0);
580
524
  });
525
+
526
+ // Start HTTP/Vite server
527
+ if (DEV_MODE) {
528
+ // Dev mode: use Vite for hot reload
529
+ const { createServer } = await import('vite');
530
+ const vite = await createServer({
531
+ root: repoRoot,
532
+ server: {
533
+ port: HTTP_PORT,
534
+ strictPort: true,
535
+ },
536
+ });
537
+ await vite.listen();
538
+ printBanner(`http://localhost:${HTTP_PORT}/demo/`);
539
+ } else {
540
+ // Production mode: static file server
541
+ httpServer.listen(HTTP_PORT, () => {
542
+ printBanner(`http://localhost:${HTTP_PORT}`);
543
+ });
544
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostty-web/demo",
3
- "version": "0.2.1",
3
+ "version": "0.3.0-next.10.g90c1178",
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": "bun run server/pty-server.ts"
11
+ "dev": "node bin/demo.js --dev"
12
12
  },
13
13
  "dependencies": {
14
14
  "@lydell/node-pty": "^1.0.1",
15
- "ghostty-web": "^0.2.1"
15
+ "ghostty-web": "0.3.0-next.10.g90c1178",
16
+ "ws": "^8.18.0"
16
17
  },
17
18
  "files": [
18
19
  "bin",