cli-tunnel 1.2.2 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli-tunnel",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
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
@@ -52,6 +52,89 @@
52
52
  var focusedIndex = 0;
53
53
  var tmuxPreset = 'equal';
54
54
 
55
+ // ─── Terminal Recording (MediaRecorder API) ───────────────
56
+ var mediaRecorder = null;
57
+ var recordedChunks = [];
58
+ var isRecording = false;
59
+ var recordTimer = null;
60
+
61
+ function startRecording() {
62
+ var canvas = null;
63
+ if (currentView === 'grid' && gridMode === 'fullscreen' && gridTerminals[focusedIndex]) {
64
+ canvas = gridTerminals[focusedIndex].panel.querySelector('canvas');
65
+ } else {
66
+ canvas = termContainer.querySelector('canvas');
67
+ }
68
+ if (!canvas) { console.warn('No terminal canvas found'); return false; }
69
+ try {
70
+ var stream = canvas.captureStream(30); // 30 fps
71
+ var mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9') ? 'video/webm;codecs=vp9'
72
+ : MediaRecorder.isTypeSupported('video/webm;codecs=vp8') ? 'video/webm;codecs=vp8'
73
+ : 'video/webm';
74
+ mediaRecorder = new MediaRecorder(stream, { mimeType: mimeType, videoBitsPerSecond: 2500000 });
75
+ recordedChunks = [];
76
+ mediaRecorder.ondataavailable = function(e) {
77
+ if (e.data && e.data.size > 0) recordedChunks.push(e.data);
78
+ };
79
+ mediaRecorder.onstop = function() {
80
+ var blob = new Blob(recordedChunks, { type: mimeType });
81
+ var url = URL.createObjectURL(blob);
82
+ var a = document.createElement('a');
83
+ var timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
84
+ a.href = url;
85
+ a.download = 'cli-tunnel-' + timestamp + '.webm';
86
+ document.body.appendChild(a);
87
+ a.click();
88
+ document.body.removeChild(a);
89
+ setTimeout(function() { URL.revokeObjectURL(url); }, 5000);
90
+ };
91
+ mediaRecorder.start(1000); // collect data every 1s
92
+ isRecording = true;
93
+ // Auto-stop after 10 minutes to prevent memory issues
94
+ setTimeout(function() {
95
+ if (isRecording) { toggleRecording(); }
96
+ }, 10 * 60 * 1000);
97
+ return true;
98
+ } catch (e) {
99
+ console.error('Recording failed:', e);
100
+ return false;
101
+ }
102
+ }
103
+
104
+ function stopRecording() {
105
+ if (recordTimer) { clearInterval(recordTimer); recordTimer = null; }
106
+ if (mediaRecorder && mediaRecorder.state !== 'inactive') {
107
+ mediaRecorder.stop();
108
+ }
109
+ isRecording = false;
110
+ mediaRecorder = null;
111
+ }
112
+
113
+ function toggleRecording() {
114
+ var btn = document.getElementById('btn-record');
115
+ if (isRecording) {
116
+ stopRecording();
117
+ if (btn) { btn.classList.remove('recording'); btn.textContent = '⏺'; btn.title = 'Record terminal'; }
118
+ } else {
119
+ if (startRecording()) {
120
+ if (btn) { btn.classList.add('recording'); btn.textContent = '⏹'; btn.title = 'Stop recording & download'; btn.setAttribute('aria-label', 'Stop recording'); }
121
+ var recordStartTime = Date.now();
122
+ recordTimer = setInterval(function() {
123
+ if (!isRecording) { clearInterval(recordTimer); recordTimer = null; return; }
124
+ var elapsed = Math.floor((Date.now() - recordStartTime) / 1000);
125
+ var min = Math.floor(elapsed / 60);
126
+ var sec = elapsed % 60;
127
+ if (btn) btn.textContent = '⏹ ' + min + ':' + (sec < 10 ? '0' : '') + sec;
128
+ }, 1000);
129
+ } else {
130
+ // Show error to user
131
+ var prevText = statusText ? statusText.textContent : '';
132
+ if (statusText) { statusText.textContent = 'Recording not available'; }
133
+ setTimeout(function() { if (statusText && statusText.textContent === 'Recording not available') statusText.textContent = prevText; }, 3000);
134
+ }
135
+ }
136
+ }
137
+
55
138
  // ─── xterm.js Terminal ───────────────────────────────────
56
139
  let xterm = null;
57
140
  let fitAddon = null;
@@ -598,6 +681,7 @@
598
681
  }
599
682
 
600
683
  function destroyGrid() {
684
+ if (isRecording) { stopRecording(); var btn = document.getElementById('btn-record'); if (btn) { btn.classList.remove('recording'); btn.textContent = '⏺'; btn.title = 'Record terminal'; } }
601
685
  gridTerminals.forEach(function(gt) {
602
686
  if (gt.ws) { try { gt.ws.close(); } catch(e) {} }
603
687
  if (gt.xterm) { try { gt.xterm.dispose(); } catch(e) {} }
@@ -623,6 +707,7 @@
623
707
  return;
624
708
  }
625
709
  if (currentView === 'terminal') {
710
+ if (isRecording) { stopRecording(); var btn = document.getElementById('btn-record'); if (btn) { btn.classList.remove('recording'); btn.textContent = '⏺'; btn.title = 'Record terminal'; } }
626
711
  currentView = 'dashboard';
627
712
  terminal.classList.add('hidden');
628
713
  termContainer.classList.add('hidden');
@@ -853,6 +938,7 @@
853
938
  if (xterm) { lastCols = 0; lastRows = 0; sendResize(); }
854
939
  };
855
940
  ws.onclose = () => {
941
+ if (isRecording) { stopRecording(); var btn = document.getElementById('btn-record'); if (btn) { btn.classList.remove('recording'); btn.textContent = '⏺'; btn.title = 'Record terminal'; } }
856
942
  connected = false; acpReady = false; sessionId = null;
857
943
  setStatus('offline', 'Disconnected');
858
944
  const delay = Math.min(30000, 1000 * Math.pow(2, reconnectAttempt)) + Math.random() * 1000;
@@ -1007,6 +1093,10 @@
1007
1093
  };
1008
1094
  keyBar.addEventListener('click', function(e) {
1009
1095
  var btn = e.target;
1096
+ if (btn && btn.tagName === 'BUTTON' && btn.dataset.action === 'toggle-record') {
1097
+ toggleRecording();
1098
+ return;
1099
+ }
1010
1100
  if (btn && btn.tagName === 'BUTTON' && btn.dataset.key) {
1011
1101
  var key = keyMap[btn.dataset.key] || btn.dataset.key;
1012
1102
  if (currentView === 'grid' && gridMode === 'fullscreen' && gridTerminals[focusedIndex]) {
@@ -42,6 +42,7 @@
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>
45
46
  </div>
46
47
  <form id="input-form">
47
48
  <span class="prompt">&gt;</span>
@@ -197,6 +197,9 @@ header {
197
197
  -webkit-tap-highlight-color: transparent;
198
198
  }
199
199
  #key-bar button:active { background: var(--blue); color: #000; }
200
+ #btn-record { color: var(--text-dim); }
201
+ #btn-record.recording { color: var(--red); animation: pulse-record 1s infinite; }
202
+ @keyframes pulse-record { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
200
203
  #input-form {
201
204
  display: flex;
202
205
  align-items: center;