webtty 1.1.1 → 1.3.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.
@@ -53,7 +53,7 @@ __export(exports_bun, {
53
53
  spawn: () => spawn
54
54
  });
55
55
  import { homedir } from "node:os";
56
- function spawn(shell, cols, rows) {
56
+ function spawn(shell, cols, rows, term, colorTerm) {
57
57
  let onDataCb;
58
58
  let onExitCb;
59
59
  const proc = Bun.spawn([shell], {
@@ -65,7 +65,7 @@ function spawn(shell, cols, rows) {
65
65
  }
66
66
  },
67
67
  cwd: homedir(),
68
- env: { ...process.env, TERM: "xterm-256color", COLORTERM: "truecolor" }
68
+ env: { ...process.env, TERM: term, COLORTERM: colorTerm }
69
69
  });
70
70
  proc.exited.then((exitCode) => {
71
71
  onExitCb?.({ exitCode: exitCode ?? 0 });
@@ -97,13 +97,13 @@ __export(exports_node, {
97
97
  });
98
98
  import { homedir as homedir2 } from "node:os";
99
99
  import nodePty from "@lydell/node-pty";
100
- function spawn2(shell, cols, rows) {
100
+ function spawn2(shell, cols, rows, term, colorTerm) {
101
101
  const ptyProc = nodePty.spawn(shell, [], {
102
- name: "xterm-256color",
102
+ name: term,
103
103
  cols,
104
104
  rows,
105
105
  cwd: homedir2(),
106
- env: { ...process.env, TERM: "xterm-256color", COLORTERM: "truecolor" }
106
+ env: { ...process.env, TERM: term, COLORTERM: colorTerm }
107
107
  });
108
108
  return {
109
109
  onData(cb) {
@@ -126,135 +126,116 @@ function spawn2(shell, cols, rows) {
126
126
  var init_node = () => {};
127
127
 
128
128
  // src/server/index.ts
129
+ import fs3 from "node:fs";
129
130
  import http from "node:http";
131
+ import path4 from "node:path";
132
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
133
+
134
+ // src/config.ts
135
+ import fs from "node:fs";
136
+ import os from "node:os";
137
+ import path from "node:path";
138
+ function configDir() {
139
+ return path.join(process.env.HOME ?? os.homedir(), ".config", "webtty");
140
+ }
141
+ function getConfigPath() {
142
+ return path.join(configDir(), "config.json");
143
+ }
144
+ var DEFAULT_THEME = {
145
+ background: "#000000",
146
+ foreground: "#CCCCCC",
147
+ cursor: "#FFFFFF",
148
+ selection: "#FFFFFF",
149
+ black: "#0C0C0C",
150
+ red: "#C50F1F",
151
+ green: "#13A10E",
152
+ yellow: "#C19C00",
153
+ blue: "#0037DA",
154
+ purple: "#881798",
155
+ cyan: "#3A96DD",
156
+ white: "#CCCCCC",
157
+ brightBlack: "#767676",
158
+ brightRed: "#E74856",
159
+ brightGreen: "#16C60C",
160
+ brightYellow: "#F9F1A5",
161
+ brightBlue: "#3B78FF",
162
+ brightPurple: "#B4009E",
163
+ brightCyan: "#61D6D6",
164
+ brightWhite: "#F2F2F2"
165
+ };
166
+ var DEFAULT_CONFIG = {
167
+ port: 2346,
168
+ host: "127.0.0.1",
169
+ shell: process.platform === "win32" ? process.env.COMSPEC ?? "cmd.exe" : process.env.SHELL ?? "/bin/bash",
170
+ term: process.env.TERM ?? "xterm-256color",
171
+ colorTerm: "truecolor",
172
+ scrollback: 256 * 1024,
173
+ cols: 80,
174
+ rows: 24,
175
+ fontSize: 13,
176
+ fontFamily: "Menlo, Consolas, 'DejaVu Sans Mono', monospace",
177
+ cursorBlink: true,
178
+ copyOnSelect: true,
179
+ rightClickBehavior: "default",
180
+ logs: false,
181
+ theme: DEFAULT_THEME
182
+ };
183
+ function loadConfig() {
184
+ if (!fs.existsSync(getConfigPath())) {
185
+ try {
186
+ saveConfig(DEFAULT_CONFIG);
187
+ } catch (err) {
188
+ console.warn(`webtty: failed to write default config to ${getConfigPath()}: ${err.message}`);
189
+ return { ...DEFAULT_CONFIG };
190
+ }
191
+ }
192
+ let raw;
193
+ try {
194
+ raw = fs.readFileSync(getConfigPath(), "utf8");
195
+ } catch (err) {
196
+ throw new Error(`webtty: failed to read config at ${getConfigPath()}: ${err.message}`);
197
+ }
198
+ let parsed;
199
+ try {
200
+ parsed = JSON.parse(raw);
201
+ } catch {
202
+ throw new Error(`webtty: invalid JSON in config file ${getConfigPath()}`);
203
+ }
204
+ const p = parsed;
205
+ return {
206
+ ...DEFAULT_CONFIG,
207
+ ...typeof p.port === "number" && { port: p.port },
208
+ ...typeof p.host === "string" && { host: p.host },
209
+ ...typeof p.shell === "string" && { shell: p.shell },
210
+ ...typeof p.term === "string" && { term: p.term },
211
+ ...typeof p.colorTerm === "string" && { colorTerm: p.colorTerm },
212
+ ...typeof p.scrollback === "number" && { scrollback: p.scrollback },
213
+ ...typeof p.cols === "number" && { cols: p.cols },
214
+ ...typeof p.rows === "number" && { rows: p.rows },
215
+ ...typeof p.fontSize === "number" && { fontSize: p.fontSize },
216
+ ...typeof p.fontFamily === "string" && { fontFamily: p.fontFamily },
217
+ ...typeof p.cursorBlink === "boolean" && { cursorBlink: p.cursorBlink },
218
+ ...typeof p.copyOnSelect === "boolean" && { copyOnSelect: p.copyOnSelect },
219
+ ...typeof p.rightClickBehavior === "string" && {
220
+ rightClickBehavior: p.rightClickBehavior === "copyPaste" ? "copyPaste" : "default"
221
+ },
222
+ ...typeof p.logs === "boolean" && { logs: p.logs },
223
+ ...p.theme && typeof p.theme === "object" && { theme: { ...DEFAULT_THEME, ...p.theme } }
224
+ };
225
+ }
226
+ function saveConfig(_config) {
227
+ fs.mkdirSync(path.dirname(getConfigPath()), { recursive: true });
228
+ const content = JSON.stringify({
229
+ port: DEFAULT_CONFIG.port,
230
+ host: DEFAULT_CONFIG.host
231
+ }, null, 2);
232
+ fs.writeFileSync(getConfigPath(), content, "utf8");
233
+ }
130
234
 
131
235
  // src/server/routes.ts
132
236
  import path3 from "node:path";
133
237
 
134
- // src/server/client.ts
135
- function render(sessionId) {
136
- return `<!doctype html>
137
- <html lang="en">
138
- <head>
139
- <meta charset="UTF-8" />
140
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
141
- <title>webtty — ${sessionId}</title>
142
- <style>
143
- * { margin: 0; padding: 0; box-sizing: border-box; }
144
-
145
- html, body, #terminal { width: 100%; height: 100%; overflow: hidden; background: #282A36; }
146
-
147
- #terminal canvas { display: block; }
148
- </style>
149
- </head>
150
- <body>
151
- <div id="terminal"></div>
152
-
153
- <script type="module">
154
- import { init, Terminal, FitAddon } from '/dist/ghostty-web.js';
155
-
156
- await init();
157
- const term = new Terminal({
158
- cols: 80,
159
- rows: 24,
160
- cursorBlink: true,
161
- fontSize: 14,
162
- fontFamily: "'FiraMono Nerd Font', Menlo, Monaco, 'Courier New', monospace",
163
- scrollback: 10000,
164
- theme: {
165
- background: '#282A36',
166
- foreground: '#F8F8F2',
167
- cursor: '#F8F8F2',
168
- selection: '#44475A',
169
- black: '#21222C',
170
- red: '#FF5555',
171
- green: '#50FA7B',
172
- yellow: '#F1FA8C',
173
- blue: '#BD93F9',
174
- purple: '#FF79C6',
175
- cyan: '#8BE9FD',
176
- white: '#F8F8F2',
177
- brightBlack: '#6272A4',
178
- brightRed: '#FF6E6E',
179
- brightGreen: '#69FF94',
180
- brightYellow: '#FFFFA5',
181
- brightBlue: '#D6ACFF',
182
- brightPurple: '#FF92DF',
183
- brightCyan: '#A4FFFF',
184
- brightWhite: '#FFFFFF',
185
- },
186
- });
187
-
188
- const fitAddon = new FitAddon();
189
- term.loadAddon(fitAddon);
190
-
191
- const container = document.getElementById('terminal');
192
- await term.open(container);
193
- fitAddon.fit();
194
- fitAddon.observeResize();
195
-
196
- const sessionId = ${JSON.stringify(sessionId)};
197
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
198
- let ws;
199
-
200
- function connect() {
201
- const wsUrl = protocol + '//' + window.location.host + '/ws/' + sessionId + '?cols=' + term.cols + '&rows=' + term.rows;
202
- ws = new WebSocket(wsUrl);
203
-
204
- ws.onopen = () => {
205
- console.log('[webtty] connected to session ' + sessionId);
206
- ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
207
- };
208
-
209
- ws.onmessage = (event) => {
210
- term.write(event.data);
211
- };
212
-
213
- ws.onclose = (event) => {
214
- if (event.code === 4001) {
215
- term.write('\\r\\n\\x1b[31mSession removed.\\x1b[0m\\r\\n');
216
- setTimeout(() => window.close(), 500);
217
- return;
218
- }
219
- if (event.code === 1001) {
220
- term.write('\\r\\n\\x1b[33mServer stopped.\\x1b[0m\\r\\n');
221
- setTimeout(() => window.close(), 500);
222
- return;
223
- }
224
- console.log('[webtty] disconnected, reconnecting in 2s...');
225
- term.write('\\r\\n\\x1b[31mConnection closed. Reconnecting in 2s...\\x1b[0m\\r\\n');
226
- setTimeout(connect, 2000);
227
- };
228
-
229
- ws.onerror = () => {
230
- console.error('[webtty] websocket error');
231
- };
232
- }
233
-
234
- connect();
235
-
236
- term.onData((data) => {
237
- if (ws && ws.readyState === WebSocket.OPEN) {
238
- ws.send(data);
239
- }
240
- });
241
-
242
- term.onResize(({ cols, rows }) => {
243
- if (ws && ws.readyState === WebSocket.OPEN) {
244
- ws.send(JSON.stringify({ type: 'resize', cols, rows }));
245
- }
246
- });
247
-
248
- window.addEventListener('resize', () => {
249
- fitAddon.fit();
250
- });
251
- </script>
252
- </body>
253
- </html>`;
254
- }
255
-
256
238
  // src/server/session.ts
257
- var SCROLLBACK_MAX = 256 * 1024;
258
239
  var sessionRegistry = new Map;
259
240
  var lastUsedId = null;
260
241
  function setLastUsedId(id) {
@@ -283,13 +264,13 @@ function sessionToJson(s) {
283
264
  }
284
265
 
285
266
  // src/server/static.ts
286
- import fs from "node:fs";
267
+ import fs2 from "node:fs";
287
268
  import { createRequire as createRequire2 } from "node:module";
288
269
  import path2 from "node:path";
289
270
  import { fileURLToPath } from "node:url";
290
-
291
- // src/utils.ts
292
- import path from "node:path";
271
+ var __filename2 = fileURLToPath(import.meta.url);
272
+ var __dirname2 = path2.dirname(__filename2);
273
+ var require2 = createRequire2(import.meta.url);
293
274
  var MIME_TYPES = {
294
275
  ".html": "text/html",
295
276
  ".js": "application/javascript",
@@ -302,24 +283,19 @@ var MIME_TYPES = {
302
283
  ".ico": "image/x-icon"
303
284
  };
304
285
  function mimeType(filePath) {
305
- const ext = path.extname(filePath);
286
+ const ext = path2.extname(filePath);
306
287
  return MIME_TYPES[ext] ?? "application/octet-stream";
307
288
  }
308
289
  function ghosttyWebRootFromMain(mainPath) {
309
290
  return mainPath.replace(/[/\\]dist[/\\].*$/, "");
310
291
  }
311
-
312
- // src/server/static.ts
313
- var __filename2 = fileURLToPath(import.meta.url);
314
- var __dirname2 = path2.dirname(__filename2);
315
- var require2 = createRequire2(import.meta.url);
316
292
  function findGhosttyWeb() {
317
293
  try {
318
294
  const ghosttyWebMain = require2.resolve("ghostty-web");
319
295
  const ghosttyWebRoot = ghosttyWebRootFromMain(ghosttyWebMain);
320
296
  const distPath = path2.join(ghosttyWebRoot, "dist");
321
297
  const wasmPath = path2.join(ghosttyWebRoot, "ghostty-vt.wasm");
322
- if (fs.existsSync(path2.join(distPath, "ghostty-web.js")) && fs.existsSync(wasmPath)) {
298
+ if (fs2.existsSync(path2.join(distPath, "ghostty-web.js")) && fs2.existsSync(wasmPath)) {
323
299
  return { distPath, wasmPath };
324
300
  }
325
301
  } catch {}
@@ -328,7 +304,7 @@ function findGhosttyWeb() {
328
304
  }
329
305
  function serveFile(filePath, res) {
330
306
  const contentType = mimeType(filePath);
331
- fs.readFile(filePath, (err, data) => {
307
+ fs2.readFile(filePath, (err, data) => {
332
308
  if (err) {
333
309
  res.writeHead(404);
334
310
  res.end("Not Found");
@@ -339,6 +315,178 @@ function serveFile(filePath, res) {
339
315
  });
340
316
  }
341
317
 
318
+ // src/server/websocket.ts
319
+ import { WebSocketServer } from "ws";
320
+
321
+ // src/pty/index.ts
322
+ var isBun = !!process.versions.bun;
323
+ console.log(`pty: ${isBun ? "Bun.Terminal" : "node-pty"}`);
324
+ var { spawn: _spawn } = await (isBun ? Promise.resolve().then(() => (init_bun(), exports_bun)) : Promise.resolve().then(() => (init_node(), exports_node)));
325
+ function spawnForSession(cols, rows, shell, term, colorTerm) {
326
+ return _spawn(shell, cols, rows, term, colorTerm);
327
+ }
328
+
329
+ // src/server/websocket.ts
330
+ var WS_CLOSE = {
331
+ SERVER_STOPPED: 1001,
332
+ BAD_REQUEST: 1008,
333
+ SESSION_GONE: 4001
334
+ };
335
+ function closeClients(session, code, reason) {
336
+ session.pty?.kill();
337
+ for (const client of session.clients)
338
+ client.close(code, reason);
339
+ }
340
+ function closeSession(session) {
341
+ closeClients(session, WS_CLOSE.SESSION_GONE, "session deleted");
342
+ }
343
+ function closeAllSessions() {
344
+ for (const session of sessionRegistry.values()) {
345
+ closeClients(session, WS_CLOSE.SERVER_STOPPED, "server stopped");
346
+ }
347
+ }
348
+ var onLastSessionClosed = null;
349
+ function setLastSessionClosedHandler(handler) {
350
+ onLastSessionClosed = handler;
351
+ }
352
+ var DIM = "\x1B[2m";
353
+ var RESET = "\x1B[0m";
354
+ var BOLD = "\x1B[1m";
355
+ var ITALIC = "\x1B[3m";
356
+ var YELLOW = "\x1B[1;33m";
357
+ var CYAN = "\x1B[1;36m";
358
+ var isBun2 = process.execPath.includes("bun");
359
+ var PKG_RUNNER = isBun2 ? "bunx" : "npx";
360
+ function sessionBanner() {
361
+ const W = 48;
362
+ const pipe = (content) => `${CYAN}║${RESET}${content}${CYAN}║${RESET}`;
363
+ const blank = pipe(" ".repeat(W));
364
+ const titleVis = " [ webtty ] Terminal UI in the browser";
365
+ const titleStr = ` ${DIM}[${RESET} ${BOLD}${YELLOW}webtty${RESET} ${DIM}] Terminal UI in the browser${RESET}`;
366
+ const titleLine = pipe(titleStr + " ".repeat(W - titleVis.length));
367
+ const cmd = `${PKG_RUNNER} webtty help`;
368
+ const helpVis = ` Run \`${cmd}\` for more information.`;
369
+ const helpStr = ` ${DIM}Run \`${RESET}${ITALIC}${cmd}${RESET}${DIM}\` for more information.${RESET}`;
370
+ const helpLine = pipe(helpStr + " ".repeat(W - helpVis.length));
371
+ return [
372
+ `${CYAN}╔${"═".repeat(W)}╗${RESET}`,
373
+ `\r
374
+ `,
375
+ blank,
376
+ `\r
377
+ `,
378
+ titleLine,
379
+ `\r
380
+ `,
381
+ blank,
382
+ `\r
383
+ `,
384
+ helpLine,
385
+ `\r
386
+ `,
387
+ blank,
388
+ `\r
389
+ `,
390
+ `${CYAN}╚${"═".repeat(W)}╝${RESET}`,
391
+ `\r
392
+ `,
393
+ `\r
394
+ `
395
+ ].join("");
396
+ }
397
+ function createWebSocketServer(httpServer) {
398
+ const wss = new WebSocketServer({ noServer: true });
399
+ httpServer.on("upgrade", (req, socket, head) => {
400
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
401
+ if (url.pathname.match(/^\/ws\/([^/]+)$/)) {
402
+ wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, req));
403
+ } else {
404
+ socket.destroy();
405
+ }
406
+ });
407
+ wss.on("connection", (ws, req) => {
408
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
409
+ const wsMatch = url.pathname.match(/^\/ws\/([^/]+)$/);
410
+ if (!wsMatch) {
411
+ ws.close();
412
+ return;
413
+ }
414
+ let id;
415
+ try {
416
+ id = decodeURIComponent(wsMatch[1]);
417
+ } catch {
418
+ ws.close(WS_CLOSE.BAD_REQUEST, "Bad Request");
419
+ return;
420
+ }
421
+ const cols = Math.max(1, Math.min(1000, Number.parseInt(url.searchParams.get("cols") ?? "80", 10) || 80));
422
+ const rows = Math.max(1, Math.min(500, Number.parseInt(url.searchParams.get("rows") ?? "24", 10) || 24));
423
+ if (!sessionRegistry.has(id)) {
424
+ ws.close(WS_CLOSE.SESSION_GONE, "session deleted");
425
+ return;
426
+ }
427
+ const session = sessionRegistry.get(id);
428
+ if (!session) {
429
+ ws.close(WS_CLOSE.SESSION_GONE, "session deleted");
430
+ return;
431
+ }
432
+ session.clients.add(ws);
433
+ setLastUsedId(id);
434
+ if (!session.pty) {
435
+ const config = loadConfig();
436
+ session.pty = spawnForSession(cols, rows, config.shell, config.term, config.colorTerm);
437
+ session.pty.onData((data) => {
438
+ session.scrollback = (session.scrollback + data).slice(-config.scrollback);
439
+ for (const client of session.clients) {
440
+ if (client.readyState === client.OPEN) {
441
+ client.send(data, { binary: false });
442
+ }
443
+ }
444
+ });
445
+ session.pty.onExit(() => {
446
+ sessionRegistry.delete(session.id);
447
+ for (const client of session.clients) {
448
+ if (client.readyState === client.OPEN) {
449
+ client.close(WS_CLOSE.SESSION_GONE, "shell exited");
450
+ }
451
+ }
452
+ session.pty = null;
453
+ if (sessionRegistry.size === 0)
454
+ onLastSessionClosed?.();
455
+ });
456
+ const banner = sessionBanner();
457
+ ws.send(banner);
458
+ session.scrollback = banner;
459
+ session.pty.write(`
460
+ `);
461
+ } else {
462
+ if (session.scrollback) {
463
+ ws.send(session.scrollback, { binary: false });
464
+ }
465
+ session.pty.resize(cols, rows);
466
+ }
467
+ ws.on("message", (data) => {
468
+ const message = data.toString("utf8");
469
+ if (message.startsWith("{")) {
470
+ try {
471
+ const msg = JSON.parse(message);
472
+ if (msg.type === "resize") {
473
+ const c = Math.max(1, Math.min(1000, Math.trunc(msg.cols) || 80));
474
+ const r = Math.max(1, Math.min(500, Math.trunc(msg.rows) || 24));
475
+ session.pty?.resize(c, r);
476
+ return;
477
+ }
478
+ } catch {}
479
+ }
480
+ session.pty?.write(message);
481
+ });
482
+ ws.on("close", () => {
483
+ session.clients.delete(ws);
484
+ });
485
+ ws.on("error", () => {});
486
+ });
487
+ return wss;
488
+ }
489
+
342
490
  // src/server/routes.ts
343
491
  var MAX_BODY = 64 * 1024;
344
492
  function decodeId(raw) {
@@ -368,7 +516,7 @@ function readJson(req) {
368
516
  req.on("error", reject);
369
517
  });
370
518
  }
371
- async function handleRequest(req, res, distPath, wasmPath, onStop) {
519
+ async function handleRequest(req, res, distPath, wasmPath, clientDistPath, onStop) {
372
520
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
373
521
  const pathname = url.pathname;
374
522
  if (req.method === "POST" && pathname === "/api/server/stop") {
@@ -377,6 +525,23 @@ async function handleRequest(req, res, distPath, wasmPath, onStop) {
377
525
  onStop();
378
526
  return;
379
527
  }
528
+ if (req.method === "GET" && pathname === "/api/config") {
529
+ const config = loadConfig();
530
+ const clientConfig = {
531
+ cols: config.cols,
532
+ rows: config.rows,
533
+ fontSize: config.fontSize,
534
+ fontFamily: config.fontFamily,
535
+ cursorBlink: config.cursorBlink,
536
+ scrollback: config.scrollback,
537
+ theme: config.theme,
538
+ copyOnSelect: config.copyOnSelect,
539
+ rightClickBehavior: config.rightClickBehavior
540
+ };
541
+ res.writeHead(200, { "Content-Type": "application/json" });
542
+ res.end(JSON.stringify(clientConfig));
543
+ return;
544
+ }
380
545
  if (pathname === "/api/sessions") {
381
546
  if (req.method === "GET") {
382
547
  const list = [...sessionRegistry.values()].map(sessionToJson);
@@ -481,10 +646,8 @@ async function handleRequest(req, res, distPath, wasmPath, onStop) {
481
646
  sessionRegistry.delete(id);
482
647
  if (lastUsedId === id)
483
648
  setLastUsedId(null);
484
- for (const client of session.clients)
485
- client.close(4001, "session deleted");
486
- session.pty?.kill();
487
- res.writeHead(204);
649
+ closeSession(session);
650
+ res.writeHead(204, { "X-Sessions-Remaining": String(sessionRegistry.size) });
488
651
  res.end();
489
652
  return;
490
653
  }
@@ -508,12 +671,21 @@ async function handleRequest(req, res, distPath, wasmPath, onStop) {
508
671
  res.end("Not Found");
509
672
  return;
510
673
  }
511
- res.writeHead(200, { "Content-Type": "text/html" });
512
- res.end(render(id));
674
+ const clientHtml = path3.resolve(clientDistPath, "client.html");
675
+ serveFile(clientHtml, res);
513
676
  return;
514
677
  }
515
678
  if (pathname.startsWith("/dist/")) {
516
- const filePath = path3.resolve(distPath, pathname.slice(6));
679
+ const relativePath = pathname.slice(6);
680
+ const ownFile = path3.resolve(clientDistPath, relativePath);
681
+ if (ownFile.startsWith(clientDistPath + path3.sep)) {
682
+ const fs3 = await import("node:fs");
683
+ if (fs3.existsSync(ownFile)) {
684
+ serveFile(ownFile, res);
685
+ return;
686
+ }
687
+ }
688
+ const filePath = path3.resolve(distPath, relativePath);
517
689
  if (!filePath.startsWith(path3.resolve(distPath) + path3.sep) && filePath !== path3.resolve(distPath)) {
518
690
  res.writeHead(404);
519
691
  res.end("Not Found");
@@ -530,151 +702,19 @@ async function handleRequest(req, res, distPath, wasmPath, onStop) {
530
702
  res.end("Not Found");
531
703
  }
532
704
 
533
- // src/server/websocket.ts
534
- import { WebSocketServer } from "ws";
535
-
536
- // src/pty/index.ts
537
- var isBun = !!process.versions.bun;
538
- console.log(`pty: ${isBun ? "Bun.Terminal" : "node-pty"}`);
539
- var { spawn: _spawn } = await (isBun ? Promise.resolve().then(() => (init_bun(), exports_bun)) : Promise.resolve().then(() => (init_node(), exports_node)));
540
- function spawnForSession(cols, rows) {
541
- const shell = process.platform === "win32" ? process.env.COMSPEC ?? "cmd.exe" : process.env.SHELL ?? "/bin/bash";
542
- return _spawn(shell, cols, rows);
543
- }
544
-
545
- // src/server/websocket.ts
546
- function createWebSocketServer(httpServer) {
547
- const wss = new WebSocketServer({ noServer: true });
548
- httpServer.on("upgrade", (req, socket, head) => {
549
- const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
550
- if (url.pathname.match(/^\/ws\/([^/]+)$/)) {
551
- wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, req));
552
- } else {
553
- socket.destroy();
554
- }
555
- });
556
- wss.on("connection", (ws, req) => {
557
- const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
558
- const wsMatch = url.pathname.match(/^\/ws\/([^/]+)$/);
559
- if (!wsMatch) {
560
- ws.close();
561
- return;
562
- }
563
- let id;
564
- try {
565
- id = decodeURIComponent(wsMatch[1]);
566
- } catch {
567
- ws.close(1008, "Bad Request");
568
- return;
569
- }
570
- const cols = Math.max(1, Math.min(1000, Number.parseInt(url.searchParams.get("cols") ?? "80", 10) || 80));
571
- const rows = Math.max(1, Math.min(500, Number.parseInt(url.searchParams.get("rows") ?? "24", 10) || 24));
572
- if (!sessionRegistry.has(id)) {
573
- ws.close(4001, "session deleted");
574
- return;
575
- }
576
- const session = sessionRegistry.get(id);
577
- if (!session) {
578
- ws.close(4001, "session deleted");
579
- return;
580
- }
581
- session.clients.add(ws);
582
- setLastUsedId(id);
583
- if (!session.pty) {
584
- session.pty = spawnForSession(cols, rows);
585
- session.pty.onData((data) => {
586
- session.scrollback = (session.scrollback + data).slice(-SCROLLBACK_MAX);
587
- for (const client of session.clients) {
588
- if (client.readyState === client.OPEN) {
589
- client.send(data, { binary: false });
590
- }
591
- }
592
- });
593
- session.pty.onExit(() => {
594
- sessionRegistry.delete(session.id);
595
- for (const client of session.clients) {
596
- if (client.readyState === client.OPEN) {
597
- client.close(4001, "shell exited");
598
- }
599
- }
600
- session.pty = null;
601
- });
602
- const C = "\x1B[1;36m";
603
- const G = "\x1B[1;32m";
604
- const Y = "\x1B[1;33m";
605
- const R = "\x1B[0m";
606
- const banner = [
607
- `${C}╔══════════════════════════════════════════════════════════════╗${R}\r
608
- `,
609
- `${C}║${R} ${G}Welcome to webtty!${R} ${C}║${R}\r
610
- `,
611
- `${C}║${R} ${C}║${R}\r
612
- `,
613
- `${C}║${R} You have a real shell session with full PTY support. ${C}║${R}\r
614
- `,
615
- `${C}║${R} Try: ${Y}ls${R}, ${Y}cd${R}, ${Y}top${R}, ${Y}vim${R}, or any command! ${C}║${R}\r
616
- `,
617
- `${C}╚══════════════════════════════════════════════════════════════╝${R}\r
618
- \r
619
- `
620
- ].join("");
621
- ws.send(banner);
622
- session.scrollback = banner;
623
- session.pty.write(`
624
- `);
625
- } else {
626
- if (session.scrollback) {
627
- ws.send(session.scrollback, { binary: false });
628
- }
629
- session.pty.resize(cols, rows);
630
- }
631
- ws.on("message", (data) => {
632
- const message = data.toString("utf8");
633
- if (message.startsWith("{")) {
634
- try {
635
- const msg = JSON.parse(message);
636
- if (msg.type === "resize") {
637
- const c = Math.max(1, Math.min(1000, Math.trunc(msg.cols) || 80));
638
- const r = Math.max(1, Math.min(500, Math.trunc(msg.rows) || 24));
639
- session.pty?.resize(c, r);
640
- return;
641
- }
642
- } catch {}
643
- }
644
- session.pty?.write(message);
645
- });
646
- ws.on("close", () => {
647
- session.clients.delete(ws);
648
- });
649
- ws.on("error", () => {});
650
- });
651
- return wss;
652
- }
653
-
654
705
  // src/server/index.ts
655
- var HTTP_PORT = Number(process.env.PORT) || 2346;
706
+ var __filename3 = fileURLToPath2(import.meta.url);
707
+ var __dirname3 = path4.dirname(__filename3);
708
+ var config = loadConfig();
709
+ var HTTP_PORT = Number(process.env.PORT) || config.port;
710
+ var HTTP_HOST = config.host;
656
711
  var { distPath, wasmPath } = findGhosttyWeb();
657
- var httpServer = http.createServer((req, res) => {
658
- handleRequest(req, res, distPath, wasmPath, () => {
659
- for (const session of sessionRegistry.values()) {
660
- session.pty?.kill();
661
- for (const client of session.clients)
662
- client.close(1001, "server stopped");
663
- }
664
- wss.close();
665
- httpServer.close(() => process.exit(0));
666
- });
667
- });
668
- var wss = createWebSocketServer(httpServer);
669
- process.on("SIGINT", () => {
670
- console.log(`
671
-
672
- Shutting down...`);
673
- for (const session of sessionRegistry.values()) {
674
- session.pty?.kill();
675
- for (const client of session.clients)
676
- client.close(1001, "server stopped");
677
- }
712
+ var projectRoot = path4.resolve(__dirname3, "..", "..");
713
+ var builtDistPath = path4.join(projectRoot, "dist");
714
+ var srcClientPath = path4.join(projectRoot, "src", "client");
715
+ var clientDistPath = fs3.existsSync(path4.join(builtDistPath, "client.html")) ? builtDistPath : srcClientPath;
716
+ function shutdown() {
717
+ closeAllSessions();
678
718
  wss.close();
679
719
  const exit = () => process.exit(0);
680
720
  const shutdownTimeout = setTimeout(exit, 1000);
@@ -682,9 +722,20 @@ Shutting down...`);
682
722
  clearTimeout(shutdownTimeout);
683
723
  exit();
684
724
  });
725
+ }
726
+ var httpServer = http.createServer((req, res) => {
727
+ handleRequest(req, res, distPath, wasmPath, clientDistPath, shutdown);
728
+ });
729
+ var wss = createWebSocketServer(httpServer);
730
+ setLastSessionClosedHandler(shutdown);
731
+ process.on("SIGINT", () => {
732
+ console.log(`
733
+
734
+ Shutting down...`);
735
+ shutdown();
685
736
  });
686
- httpServer.listen(HTTP_PORT, "127.0.0.1", () => {
687
- console.log(`listening on http://127.0.0.1:${HTTP_PORT}`);
737
+ httpServer.listen(HTTP_PORT, HTTP_HOST, () => {
738
+ console.log(`listening on http://${HTTP_HOST}:${HTTP_PORT}`);
688
739
  });
689
740
 
690
- //# debugId=243D0B6704BD6DB364756E2164756E21
741
+ //# debugId=D4A6F10E2C8CA53E64756E2164756E21