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

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/dist/index.js CHANGED
@@ -23,10 +23,6 @@ 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;
30
26
  import { redactSecrets } from './redact.js';
31
27
  // F-15: Global error handlers to prevent unclean crashes
32
28
  process.on('uncaughtException', (err) => {
@@ -83,12 +79,13 @@ pass through to the underlying app. cli-tunnel's own flags
83
79
  const hasLocal = args.includes('--local');
84
80
  const hasTunnel = !hasLocal;
85
81
  const hasReplay = !args.includes('--no-replay');
82
+ const noWait = args.includes('--no-wait');
86
83
  const portIdx = args.indexOf('--port');
87
84
  const port = (portIdx !== -1 && args[portIdx + 1]) ? parseInt(args[portIdx + 1], 10) : 0;
88
85
  const nameIdx = args.indexOf('--name');
89
86
  const sessionName = (nameIdx !== -1 && args[nameIdx + 1]) ? args[nameIdx + 1] : '';
90
87
  // Everything that's not our flags is the command
91
- const ourFlags = new Set(['--local', '--tunnel', '--port', '--name', '--no-replay']);
88
+ const ourFlags = new Set(['--local', '--tunnel', '--port', '--name', '--no-replay', '--no-wait']);
92
89
  const cmdArgs = [];
93
90
  let skip = false;
94
91
  for (let i = 0; i < args.length; i++) {
@@ -100,7 +97,7 @@ for (let i = 0; i < args.length; i++) {
100
97
  skip = true;
101
98
  continue;
102
99
  }
103
- if (args[i] === '--local' || args[i] === '--tunnel' || args[i] === '--no-replay')
100
+ if (args[i] === '--local' || args[i] === '--tunnel' || args[i] === '--no-replay' || args[i] === '--no-wait')
104
101
  continue;
105
102
  cmdArgs.push(args[i]);
106
103
  }
@@ -529,11 +526,6 @@ wss.on('connection', (ws, req) => {
529
526
  // F-10: WS ping/pong heartbeat
530
527
  ws._isAlive = true;
531
528
  ws.on('pong', () => { ws._isAlive = true; });
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 }));
536
- }
537
529
  ws.on('message', (data) => {
538
530
  // F-13: Enforce WS message rate limit (100 msg/sec)
539
531
  const now = Date.now();
@@ -559,16 +551,10 @@ wss.on('connection', (ws, req) => {
559
551
  ptyProcess.write(msg.data);
560
552
  }
561
553
  }
562
- // #7: NaN guard on pty_resizeremote resize only if local hasn't resized recently
554
+ // pty_resize from remote clients is ignored PTY stays at local terminal size
555
+ // The phone's xterm.js handles display via its own viewport/scrolling
563
556
  if (msg.type === 'pty_resize') {
564
- const cols = Number(msg.cols);
565
- const rows = Number(msg.rows);
566
- const localRecentlyResized = Date.now() - localResizeAt < 2000;
567
- if (Number.isFinite(cols) && Number.isFinite(rows) && ptyProcess && !localRecentlyResized) {
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)));
571
- }
557
+ // Only log, don't resize — prevents breaking local terminal layout
572
558
  }
573
559
  }
574
560
  catch {
@@ -590,25 +576,8 @@ setInterval(() => {
590
576
  ws.ping();
591
577
  }
592
578
  }, 30000);
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
- }
607
579
  function broadcast(data) {
608
580
  const redacted = redactSecrets(data);
609
- // Feed into headless terminal to track screen state
610
- if (screenTerminal)
611
- screenTerminal.write(redacted);
612
581
  const msg = JSON.stringify({ type: 'pty', data: redacted });
613
582
  for (const [, ws] of connections) {
614
583
  if (ws.readyState === WebSocket.OPEN)
@@ -810,7 +779,7 @@ async function main() {
810
779
  await new Promise(() => { });
811
780
  }
812
781
  // Wait for user to scan QR / copy URL before starting the CLI tool
813
- if (hasTunnel) {
782
+ if (hasTunnel && !noWait) {
814
783
  console.log(` ${BOLD}Press any key to start ${command}...${RESET}`);
815
784
  await new Promise((resolve) => {
816
785
  if (process.stdin.isTTY)
@@ -878,8 +847,6 @@ async function main() {
878
847
  cols, rows, cwd,
879
848
  env: safeEnv,
880
849
  });
881
- // Initialize headless terminal for screen state tracking
882
- initScreenBuffer(cols, rows);
883
850
  // Detect CSPRNG crash (rare Node.js + PTY issue) and show helpful message
884
851
  let earlyExitCode = null;
885
852
  const earlyExitCheck = new Promise((resolve) => {
@@ -917,7 +884,6 @@ async function main() {
917
884
  process.stdin.setRawMode(true);
918
885
  process.stdin.resume();
919
886
  process.stdin.on('data', (data) => ptyProcess.write(data.toString()));
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); });
887
+ process.stdout.on('resize', () => { localResizeAt = Date.now(); const c = process.stdout.columns || 120; const r = process.stdout.rows || 30; ptyProcess.resize(c, r); });
922
888
  }
923
889
  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.3",
3
+ "version": "1.3.1-beta.5",
4
4
  "description": "Tunnel any CLI app to your phone — PTY + devtunnel + xterm.js",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/remote-ui/app.js CHANGED
@@ -63,9 +63,18 @@
63
63
  if (currentView === 'grid' && gridMode === 'fullscreen' && gridTerminals[focusedIndex]) {
64
64
  canvas = gridTerminals[focusedIndex].panel.querySelector('canvas');
65
65
  } else {
66
- canvas = termContainer.querySelector('canvas');
66
+ var tc = document.getElementById('terminal-container');
67
+ if (tc) canvas = tc.querySelector('canvas');
68
+ }
69
+ if (!canvas) {
70
+ // Fallback: try to find any canvas in the xterm container
71
+ var xtermEl = document.querySelector('.xterm');
72
+ if (xtermEl) canvas = xtermEl.querySelector('canvas');
73
+ }
74
+ if (!canvas || !canvas.captureStream) {
75
+ if (statusText) { var prev = statusText.textContent; statusText.textContent = 'Recording not supported'; setTimeout(function() { statusText.textContent = prev; }, 3000); }
76
+ return false;
67
77
  }
68
- if (!canvas) { console.warn('No terminal canvas found'); return false; }
69
78
  try {
70
79
  var stream = canvas.captureStream(30); // 30 fps
71
80
  var mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9') ? 'video/webm;codecs=vp9'
@@ -140,7 +149,13 @@
140
149
  if (currentView === 'grid' && gridMode === 'fullscreen' && gridTerminals[focusedIndex]) {
141
150
  canvas = gridTerminals[focusedIndex].panel.querySelector('canvas');
142
151
  } else {
143
- canvas = termContainer.querySelector('canvas');
152
+ var tc = document.getElementById('terminal-container');
153
+ if (tc) canvas = tc.querySelector('canvas');
154
+ }
155
+ if (!canvas) {
156
+ // Fallback: try to find any canvas in the xterm container
157
+ var xtermEl = document.querySelector('.xterm');
158
+ if (xtermEl) canvas = xtermEl.querySelector('canvas');
144
159
  }
145
160
  if (!canvas) {
146
161
  if (statusText) { var prev = statusText.textContent; statusText.textContent = 'No terminal to capture'; setTimeout(function() { statusText.textContent = prev; }, 2000); }
@@ -239,13 +254,15 @@
239
254
 
240
255
  async function loadSessions() {
241
256
  try {
242
- const tokenParam = new URLSearchParams(window.location.search).get('token');
243
- const headers = tokenParam ? { 'Authorization': 'Bearer ' + tokenParam } : {};
244
- const resp = await fetch('/api/sessions', { headers });
245
- const data = await resp.json();
257
+ var tokenParam = new URLSearchParams(window.location.search).get('token');
258
+ var headers = tokenParam ? { 'Authorization': 'Bearer ' + tokenParam } : {};
259
+ var resp = await fetch('/api/sessions', { headers: headers });
260
+ if (!resp.ok) throw new Error('Status ' + resp.status);
261
+ var data = await resp.json();
246
262
  renderDashboard(data.sessions || []);
247
263
  } catch (err) {
248
- dashboard.innerHTML = '<div style="padding:12px;color:var(--red)">' + escapeHtml('Failed to load sessions: ' + err.message) + '</div>';
264
+ dashboard.innerHTML = '<div style="padding:20px 12px;color:var(--text-dim);text-align:center">' +
265
+ escapeHtml('Sessions unavailable. Use Hub mode (cli-tunnel with no command) to see all sessions.') + '</div>';
249
266
  }
250
267
  }
251
268