cli-tunnel 1.3.1-beta.2 → 1.3.1-beta.3

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 (2) hide show
  1. package/dist/index.js +32 -17
  2. package/package.json +3 -1
package/dist/index.js CHANGED
@@ -23,6 +23,10 @@ import http from 'node:http';
23
23
  import readline from 'node:readline';
24
24
  import { WebSocketServer, WebSocket } from 'ws';
25
25
  import os from 'node:os';
26
+ import headlessPkg from '@xterm/headless';
27
+ const HeadlessTerminal = headlessPkg.Terminal;
28
+ import serializePkg from '@xterm/addon-serialize';
29
+ const SerializeAddon = serializePkg.SerializeAddon;
26
30
  import { redactSecrets } from './redact.js';
27
31
  // F-15: Global error handlers to prevent unclean crashes
28
32
  process.on('uncaughtException', (err) => {
@@ -58,7 +62,7 @@ ${BOLD}Options:${RESET}
58
62
  --local Disable devtunnel (localhost only)
59
63
  --port <n> Bridge port (default: random)
60
64
  --name <name> Session name (shown in dashboard)
61
- --no-replay Disable replay buffer (on by default)
65
+ --replay (deprecated, screen buffer is always on)
62
66
  --help, -h Show this help
63
67
 
64
68
  ${BOLD}Examples:${RESET}
@@ -525,10 +529,10 @@ wss.on('connection', (ws, req) => {
525
529
  // F-10: WS ping/pong heartbeat
526
530
  ws._isAlive = true;
527
531
  ws.on('pong', () => { ws._isAlive = true; });
528
- // Replay: send accumulated PTY output as one bulk write
529
- // xterm.js processes the full ANSI stream so cursor codes work correctly
530
- if (hasReplay && replayBuffer.length > 0) {
531
- ws.send(JSON.stringify({ type: 'pty', data: replayBuffer }));
532
+ // Send current screen state so new clients see what's on the terminal now
533
+ const screenState = getScreenState();
534
+ if (screenState) {
535
+ ws.send(JSON.stringify({ type: 'pty', data: screenState }));
532
536
  }
533
537
  ws.on('message', (data) => {
534
538
  // F-13: Enforce WS message rate limit (100 msg/sec)
@@ -562,6 +566,8 @@ wss.on('connection', (ws, req) => {
562
566
  const localRecentlyResized = Date.now() - localResizeAt < 2000;
563
567
  if (Number.isFinite(cols) && Number.isFinite(rows) && ptyProcess && !localRecentlyResized) {
564
568
  ptyProcess.resize(Math.max(1, Math.min(500, cols)), Math.max(1, Math.min(200, rows)));
569
+ if (screenTerminal)
570
+ screenTerminal.resize(Math.max(1, Math.min(500, cols)), Math.max(1, Math.min(200, rows)));
565
571
  }
566
572
  }
567
573
  }
@@ -584,20 +590,26 @@ setInterval(() => {
584
590
  ws.ping();
585
591
  }
586
592
  }, 30000);
587
- // Replay buffer: store raw PTY bytes in a single rolling buffer
588
- // xterm.js processes the full stream on connect, so ANSI cursor codes work correctly
589
- const MAX_REPLAY_BYTES = 256 * 1024; // 256KB
590
- let replayBuffer = '';
593
+ // Screen buffer: headless xterm.js terminal that tracks the current screen state
594
+ // On client connect, serialize the screen and send it renders correctly regardless of dimensions
595
+ let screenTerminal = null;
596
+ let serializeAddon = null;
597
+ function initScreenBuffer(cols, rows) {
598
+ screenTerminal = new HeadlessTerminal({ cols, rows, scrollback: 1000, allowProposedApi: true });
599
+ serializeAddon = new SerializeAddon();
600
+ screenTerminal.loadAddon(serializeAddon);
601
+ }
602
+ function getScreenState() {
603
+ if (!serializeAddon)
604
+ return '';
605
+ return serializeAddon.serialize();
606
+ }
591
607
  function broadcast(data) {
592
608
  const redacted = redactSecrets(data);
609
+ // Feed into headless terminal to track screen state
610
+ if (screenTerminal)
611
+ screenTerminal.write(redacted);
593
612
  const msg = JSON.stringify({ type: 'pty', data: redacted });
594
- if (hasReplay) {
595
- replayBuffer += redacted;
596
- // Trim from the front if too large
597
- if (replayBuffer.length > MAX_REPLAY_BYTES) {
598
- replayBuffer = replayBuffer.slice(replayBuffer.length - MAX_REPLAY_BYTES);
599
- }
600
- }
601
613
  for (const [, ws] of connections) {
602
614
  if (ws.readyState === WebSocket.OPEN)
603
615
  ws.send(msg);
@@ -866,6 +878,8 @@ async function main() {
866
878
  cols, rows, cwd,
867
879
  env: safeEnv,
868
880
  });
881
+ // Initialize headless terminal for screen state tracking
882
+ initScreenBuffer(cols, rows);
869
883
  // Detect CSPRNG crash (rare Node.js + PTY issue) and show helpful message
870
884
  let earlyExitCode = null;
871
885
  const earlyExitCheck = new Promise((resolve) => {
@@ -903,6 +917,7 @@ async function main() {
903
917
  process.stdin.setRawMode(true);
904
918
  process.stdin.resume();
905
919
  process.stdin.on('data', (data) => ptyProcess.write(data.toString()));
906
- process.stdout.on('resize', () => { localResizeAt = Date.now(); ptyProcess.resize(process.stdout.columns || 120, process.stdout.rows || 30); });
920
+ process.stdout.on('resize', () => { localResizeAt = Date.now(); const c = process.stdout.columns || 120; const r = process.stdout.rows || 30; ptyProcess.resize(c, r); if (screenTerminal)
921
+ screenTerminal.resize(c, r); });
907
922
  }
908
923
  main().catch((err) => { console.error(err); process.exit(1); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli-tunnel",
3
- "version": "1.3.1-beta.2",
3
+ "version": "1.3.1-beta.3",
4
4
  "description": "Tunnel any CLI app to your phone — PTY + devtunnel + xterm.js",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -36,6 +36,8 @@
36
36
  "node": ">=22.0.0"
37
37
  },
38
38
  "dependencies": {
39
+ "@xterm/addon-serialize": "^0.14.0",
40
+ "@xterm/headless": "^6.0.0",
39
41
  "node-pty": "1.1.0",
40
42
  "qrcode-terminal": "0.12.0",
41
43
  "ws": "8.19.0"