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

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
@@ -785,13 +785,9 @@ async function main() {
785
785
  if (process.stdin.isTTY)
786
786
  process.stdin.setRawMode(true);
787
787
  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
- });
788
+ process.stdin.once('data', () => resolve());
794
789
  });
790
+ // Don't pause or reset raw mode — we'll set it up properly for PTY below
795
791
  }
796
792
  console.log(` ${DIM}Starting ${command}...${RESET}\n`);
797
793
  // 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.6",
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');
@@ -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;