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

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 CHANGED
@@ -163,12 +163,13 @@ cli-tunnel uses a layered security model:
163
163
 
164
164
  ## Terminal Size Behavior
165
165
 
166
- cli-tunnel uses a single PTY shared between your local terminal and all remote viewers. When a phone connects, the PTY resizes to match the remote device's screen dimensions — the CLI app renders correctly on the device you're actively using.
166
+ cli-tunnel uses a single PTY shared between your local terminal and all remote viewers. The PTY stays at your local terminal's dimensions — remote devices view the terminal through their own xterm.js viewport.
167
167
 
168
168
  **Tips for the best experience:**
169
169
  - Rotate your phone to landscape for a wider terminal
170
170
  - Use the key bar (↑↓←→ Tab Enter Esc Ctrl+C) at the bottom for navigation
171
- - If multiple devices connect, the last one to resize wins
171
+ - To select text on the local terminal, hold **Shift** while clicking/dragging (raw mode captures all input otherwise)
172
+ - Use `--no-wait` flag to skip the press-any-key prompt (useful for scripting)
172
173
 
173
174
  ## FAQ
174
175
 
package/dist/index.js CHANGED
@@ -564,7 +564,8 @@ wss.on('connection', (ws, req) => {
564
564
  });
565
565
  ws.on('close', () => connections.delete(id));
566
566
  });
567
- // F-10: WS heartbeat — ping every 30s, close unresponsive after 10s
567
+ // F-10: WS heartbeat — ping every 2 minutes, close unresponsive connections
568
+ // Longer interval prevents killing phone connections that go to background briefly
568
569
  setInterval(() => {
569
570
  for (const [id, ws] of connections) {
570
571
  if (ws._isAlive === false) {
@@ -575,7 +576,7 @@ setInterval(() => {
575
576
  ws._isAlive = false;
576
577
  ws.ping();
577
578
  }
578
- }, 30000);
579
+ }, 120000);
579
580
  function broadcast(data) {
580
581
  const redacted = redactSecrets(data);
581
582
  const msg = JSON.stringify({ type: 'pty', data: redacted });
@@ -785,13 +786,9 @@ async function main() {
785
786
  if (process.stdin.isTTY)
786
787
  process.stdin.setRawMode(true);
787
788
  process.stdin.resume();
788
- process.stdin.once('data', () => {
789
- if (process.stdin.isTTY)
790
- process.stdin.setRawMode(false);
791
- process.stdin.pause();
792
- resolve();
793
- });
789
+ process.stdin.once('data', () => resolve());
794
790
  });
791
+ // Don't pause or reset raw mode — we'll set it up properly for PTY below
795
792
  }
796
793
  console.log(` ${DIM}Starting ${command}...${RESET}\n`);
797
794
  // Clear screen before PTY takes over — prevents overlap with banner/QR output
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli-tunnel",
3
- "version": "1.3.1-beta.5",
3
+ "version": "1.3.1-beta.7",
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
@@ -259,55 +259,94 @@
259
259
  var resp = await fetch('/api/sessions', { headers: headers });
260
260
  if (!resp.ok) throw new Error('Status ' + resp.status);
261
261
  var data = await resp.json();
262
- renderDashboard(data.sessions || []);
262
+ if (!isHubMode) {
263
+ renderNonHubSessions(data.sessions || []);
264
+ } else {
265
+ renderDashboard(data.sessions || []);
266
+ }
263
267
  } catch (err) {
264
268
  dashboard.innerHTML = '<div style="padding:20px 12px;color:var(--text-dim);text-align:center">' +
265
269
  escapeHtml('Sessions unavailable. Use Hub mode (cli-tunnel with no command) to see all sessions.') + '</div>';
266
270
  }
267
271
  }
268
272
 
273
+ function renderNonHubSessions(sessions) {
274
+ var currentName = document.title || 'this session';
275
+ var html = '<div class="non-hub-view">' +
276
+ '<div class="non-hub-current">You\'re connected to: <strong>' + escapeHtml(currentName) + '</strong></div>' +
277
+ '<div class="non-hub-back"><a href="#" data-action="back-to-terminal">← Back to terminal</a></div>' +
278
+ '<div class="non-hub-hint">Start a Hub to see all sessions: <code>cli-tunnel</code> (no command)</div>' +
279
+ '</div>';
280
+ dashboard.innerHTML = html;
281
+ var backLink = dashboard.querySelector('[data-action="back-to-terminal"]');
282
+ if (backLink) {
283
+ backLink.addEventListener('click', function(e) { e.preventDefault(); toggleView(); });
284
+ }
285
+ }
286
+
269
287
  function renderDashboard(sessions) {
270
- const filtered = showOffline ? sessions : sessions.filter(s => s.online);
271
- const offlineCount = sessions.filter(s => !s.online).length;
272
- const onlineCount = sessions.filter(s => s.online).length;
273
- const connectable = filtered.filter(s => s.online && s.token);
274
-
275
- let html = `<div style="padding:8px 4px;display:flex;align-items:center;gap:8px">
276
- <span style="color:var(--text-dim);font-size:12px">${onlineCount} online${offlineCount > 0 ? ', ' + offlineCount + ' offline' : ''}</span>
277
- <span style="flex:1"></span>
278
- ${connectable.length > 1 ? '<button data-action="grid-view" style="background:none;border:1px solid var(--blue);color:var(--blue);font-family:var(--font);font-size:11px;padding:3px 8px;border-radius:4px;cursor:pointer">⊞ Grid</button>' : ''}
279
- <button data-action="toggle-offline" style="background:none;border:1px solid var(--border);color:var(--text-dim);font-family:var(--font);font-size:11px;padding:3px 8px;border-radius:4px;cursor:pointer">${showOffline ? 'Hide offline' : 'Show offline'}</button>
280
- ${offlineCount > 0 ? '<button data-action="clean-offline" style="background:none;border:1px solid var(--red);color:var(--red);font-family:var(--font);font-size:11px;padding:3px 8px;border-radius:4px;cursor:pointer">Clean offline</button>' : ''}
281
- <button data-action="refresh" style="background:none;border:1px solid var(--border);color:var(--text-dim);font-family:var(--font);font-size:11px;padding:3px 8px;border-radius:4px;cursor:pointer">↻</button>
282
- </div>`;
288
+ var filtered = showOffline ? sessions : sessions.filter(function(s) { return s.online; });
289
+ var offlineCount = sessions.filter(function(s) { return !s.online; }).length;
290
+ var connectable = filtered.filter(function(s) { return s.online && s.hasToken; });
291
+ var remoteCount = filtered.length - connectable.length;
292
+
293
+ // Hub header
294
+ var html = '<div class="hub-header">' +
295
+ '<h2 class="hub-title">cli-tunnel Hub</h2>' +
296
+ '<div class="hub-stats">' + connectable.length + ' connectable · ' + remoteCount + ' remote' +
297
+ (offlineCount > 0 ? ' · ' + offlineCount + ' offline' : '') +
298
+ ' <span class="hub-refresh-indicator" title="Auto-refreshes every 10s">↻</span>' +
299
+ '</div>' +
300
+ '</div>';
301
+
302
+ // Toolbar actions
303
+ html += '<div class="hub-toolbar">' +
304
+ '<button data-action="toggle-offline" class="hub-toolbar-btn">' + (showOffline ? 'Hide offline' : 'Show offline') + '</button>' +
305
+ (offlineCount > 0 ? '<button data-action="clean-offline" class="hub-toolbar-btn hub-toolbar-btn-danger">Clean offline</button>' : '') +
306
+ '<button data-action="refresh" class="hub-toolbar-btn">↻ Refresh</button>' +
307
+ '</div>';
308
+
309
+ // Grid banner when 2+ connectable
310
+ if (connectable.length >= 2) {
311
+ html += '<div class="grid-banner" data-action="grid-view">' +
312
+ '<span>Monitor all sessions live</span>' +
313
+ '<span class="grid-banner-btn">⊞ Open Grid View</span>' +
314
+ '</div>';
315
+ }
283
316
 
284
317
  if (filtered.length === 0) {
285
318
  html += '<div style="padding:20px 12px;color:var(--text-dim);text-align:center">' +
286
319
  (sessions.length === 0 ? 'No cli-tunnel sessions found.' : 'No online sessions. Tap "Show offline" to see stale ones.') +
287
320
  '</div>';
288
321
  } else {
289
- html += filtered.map(s => {
290
- const hasAccess = s.hasToken;
291
- return `
292
- <div class="session-card" ${s.online && hasAccess ? 'data-session-port="' + s.port + '" data-session-base-url="' + escapeHtml(s.url) + '"' : ''}>
293
- <span class="status-dot ${s.online ? 'online' : 'offline'}"></span>
294
- <div class="info">
295
- <div class="session-name">${escapeHtml(s.name)}</div>
296
- <div class="repo">📦 ${escapeHtml(s.repo)}</div>
297
- <div class="branch">🌿 ${escapeHtml(s.branch)}</div>
298
- <div class="machine">💻 ${escapeHtml(s.machine)}${!hasAccess && s.online ? ' 🔒' : ''}</div>
299
- </div>
300
- ${s.online && hasAccess ? '<span class="arrow">→</span>' :
301
- !s.online ? '<button data-delete-id="' + escapeHtml(s.id) + '" style="background:none;border:none;color:var(--red);cursor:pointer;font-size:14px" title="Remove">✕</button>'
302
- : '<span style="color:var(--text-dim);font-size:11px">remote</span>'}
303
- </div>`;
304
- }).join('');
322
+ filtered.forEach(function(s) {
323
+ var canConnect = s.online && s.hasToken;
324
+ html += '<div class="session-card-v2' + (canConnect ? ' connectable' : '') + '"' +
325
+ (canConnect ? ' data-session-port="' + s.port + '" data-session-base-url="' + escapeHtml(s.url) + '"' : '') + '>' +
326
+ '<div class="card-header">' +
327
+ '<span class="card-status ' + (s.online ? 'online' : 'offline') + '"></span>' +
328
+ '<span class="card-name">' + escapeHtml(s.name) + '</span>' +
329
+ (canConnect ? '<span class="card-connect">Connect →</span>' :
330
+ s.online ? '<span class="card-remote">Remote 🔒</span>' :
331
+ '<span class="card-offline">Offline</span>') +
332
+ '</div>' +
333
+ '<div class="card-details">' +
334
+ '<span>💻 ' + escapeHtml(s.machine) + '</span>' +
335
+ '<span>📦 ' + escapeHtml(s.repo) + '</span>' +
336
+ '<span>🌿 ' + escapeHtml(s.branch) + '</span>' +
337
+ '</div>' +
338
+ (!s.online ? '<button data-delete-id="' + escapeHtml(s.id) + '" class="card-delete" title="Remove">✕</button>' : '') +
339
+ '</div>';
340
+ });
305
341
  }
342
+
306
343
  dashboard.innerHTML = html;
307
344
  cachedSessions = sessions;
345
+
308
346
  // Event delegation
309
- dashboard.querySelectorAll('.session-card[data-session-port]').forEach(function(card) {
310
- card.addEventListener('click', function() {
347
+ dashboard.querySelectorAll('.session-card-v2[data-session-port]').forEach(function(card) {
348
+ card.addEventListener('click', function(e) {
349
+ if (e.target.closest('[data-delete-id]')) return;
311
350
  var port = card.dataset.sessionPort;
312
351
  var baseUrl = card.dataset.sessionBaseUrl;
313
352
  var tokenParam = new URLSearchParams(window.location.search).get('token');
@@ -365,7 +404,7 @@
365
404
 
366
405
  // ─── Grid View (multi-terminal with layout modes) ───────────
367
406
  function showGridView(sessions) {
368
- var connectable = sessions.filter(function(s) { return s.online && s.token; });
407
+ var connectable = sessions.filter(function(s) { return s.online && s.hasToken; });
369
408
  if (connectable.length === 0) return;
370
409
 
371
410
  // Clean up previous grid
@@ -985,7 +1024,7 @@
985
1024
  ws.onclose = () => {
986
1025
  if (isRecording) { stopRecording(); var btn = document.getElementById('btn-record'); if (btn) { btn.classList.remove('recording'); btn.textContent = '⏺'; btn.title = 'Record terminal'; } }
987
1026
  connected = false; acpReady = false; sessionId = null;
988
- setStatus('offline', 'Disconnected');
1027
+ setStatus('offline', 'Reconnecting...');
989
1028
  const delay = Math.min(30000, 1000 * Math.pow(2, reconnectAttempt)) + Math.random() * 1000;
990
1029
  reconnectAttempt++;
991
1030
  setTimeout(connect, delay);
@@ -999,6 +1038,14 @@
999
1038
  };
1000
1039
  }
1001
1040
 
1041
+ // Reconnect immediately when phone comes back from background
1042
+ document.addEventListener('visibilitychange', function() {
1043
+ if (!document.hidden && !connected && !isHubMode) {
1044
+ reconnectAttempt = 0;
1045
+ connect();
1046
+ }
1047
+ });
1048
+
1002
1049
  // ─── Message Handler ─────────────────────────────────────
1003
1050
  function handleMessage(msg) {
1004
1051
  // Replay events from bridge recording
@@ -42,8 +42,6 @@
42
42
  <button data-key="\x03">Ctrl+C</button>
43
43
  <button data-key=" ">Space</button>
44
44
  <button data-key="\x7f">⌫</button>
45
- <button id="btn-record" data-action="toggle-record" title="Record terminal" aria-label="Record terminal">⏺</button>
46
- <button id="btn-screenshot" data-action="take-screenshot" title="Screenshot terminal" aria-label="Screenshot terminal">📷</button>
47
45
  </div>
48
46
  <form id="input-form">
49
47
  <span class="prompt">&gt;</span>
@@ -250,6 +250,79 @@ header {
250
250
  .session-card .machine { color: var(--text-dim); font-size: 11px; }
251
251
  .session-card .arrow { color: var(--text-dim); }
252
252
 
253
+ /* Hub Dashboard Header */
254
+ .hub-header { padding: 16px 12px 8px; }
255
+ .hub-title { color: var(--text-bright); font-size: 18px; font-weight: bold; margin: 0 0 4px; font-family: var(--font); }
256
+ .hub-stats { color: var(--text-dim); font-size: 12px; }
257
+ .hub-refresh-indicator { display: inline-block; animation: spin 2s linear infinite; font-size: 10px; }
258
+ @keyframes spin { 100% { transform: rotate(360deg); } }
259
+
260
+ /* Hub Toolbar */
261
+ .hub-toolbar { display: flex; gap: 6px; padding: 4px 12px 8px; flex-wrap: wrap; }
262
+ .hub-toolbar-btn {
263
+ background: none; border: 1px solid var(--border); color: var(--text-dim);
264
+ font-family: var(--font); font-size: 11px; padding: 3px 8px;
265
+ border-radius: 4px; cursor: pointer;
266
+ }
267
+ .hub-toolbar-btn:hover { border-color: var(--blue); color: var(--blue); }
268
+ .hub-toolbar-btn-danger { border-color: var(--red); color: var(--red); }
269
+ .hub-toolbar-btn-danger:hover { background: var(--red); color: #fff; }
270
+
271
+ /* Grid Banner */
272
+ .grid-banner {
273
+ margin: 8px; padding: 12px 16px; background: var(--bg-tool);
274
+ border: 1px solid var(--blue); border-radius: 8px;
275
+ display: flex; justify-content: space-between; align-items: center;
276
+ cursor: pointer;
277
+ }
278
+ .grid-banner:hover { background: var(--border); }
279
+ .grid-banner span:first-child { color: var(--text); font-size: 13px; }
280
+ .grid-banner-btn { color: var(--blue); font-weight: bold; font-size: 13px; }
281
+
282
+ /* Session Card v2 */
283
+ .session-card-v2 {
284
+ background: var(--bg-tool); border: 1px solid var(--border); border-radius: 8px;
285
+ padding: 12px; margin: 6px 8px; position: relative;
286
+ }
287
+ .session-card-v2.connectable { cursor: pointer; }
288
+ .session-card-v2.connectable:hover { border-color: var(--green); }
289
+ .card-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
290
+ .card-status { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
291
+ .card-status.online { background: var(--green); }
292
+ .card-status.offline { background: var(--text-dim); }
293
+ .card-name { color: var(--text-bright); font-weight: bold; font-size: 15px; flex: 1; }
294
+ .card-connect { color: var(--green); font-size: 13px; font-weight: bold; }
295
+ .card-remote { color: var(--text-dim); font-size: 11px; }
296
+ .card-offline { color: var(--red); font-size: 11px; }
297
+ .card-details { display: flex; gap: 12px; flex-wrap: wrap; color: var(--text-dim); font-size: 11px; }
298
+ .card-delete {
299
+ background: none; border: none; color: var(--red); cursor: pointer;
300
+ font-size: 14px; position: absolute; top: 8px; right: 8px;
301
+ }
302
+ .card-delete:hover { color: var(--text-bright); }
303
+
304
+ /* Non-Hub Sessions View */
305
+ .non-hub-view { padding: 24px 16px; text-align: center; }
306
+ .non-hub-current {
307
+ color: var(--text-bright); font-size: 15px; margin-bottom: 16px;
308
+ padding: 12px; background: var(--bg-tool); border: 1px solid var(--border);
309
+ border-radius: 8px;
310
+ }
311
+ .non-hub-current strong { color: var(--green); }
312
+ .non-hub-back { margin-bottom: 16px; }
313
+ .non-hub-back a {
314
+ color: var(--blue); text-decoration: none; font-size: 13px;
315
+ }
316
+ .non-hub-back a:hover { text-decoration: underline; }
317
+ .non-hub-hint {
318
+ color: var(--text-dim); font-size: 12px;
319
+ padding: 10px; background: var(--bg-tool); border-radius: 6px;
320
+ }
321
+ .non-hub-hint code {
322
+ color: var(--cyan); background: var(--bg); padding: 2px 6px;
323
+ border-radius: 3px; font-family: var(--font);
324
+ }
325
+
253
326
  /* Grid View (multi-terminal with layout modes) */
254
327
  #grid-view {
255
328
  flex: 1;