shell-mirror 1.5.8 → 1.5.10
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 +1 -1
- package/public/app/terminal.html +41 -3
- package/public/app/terminal.js +515 -41
package/package.json
CHANGED
package/public/app/terminal.html
CHANGED
|
@@ -6,12 +6,50 @@
|
|
|
6
6
|
<script src="https://cdn.jsdelivr.net/npm/xterm@4.15.0/lib/xterm.js"></script>
|
|
7
7
|
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.js"></script>
|
|
8
8
|
<style>
|
|
9
|
-
body, html {
|
|
10
|
-
|
|
9
|
+
body, html {
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
height: 100%;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
background-color: #1e1e1e;
|
|
15
|
+
color: #ccc;
|
|
16
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
17
|
+
}
|
|
18
|
+
#terminal-container {
|
|
19
|
+
display: none;
|
|
20
|
+
height: 100%;
|
|
21
|
+
width: 100%;
|
|
22
|
+
background-color: #000000;
|
|
23
|
+
}
|
|
24
|
+
#terminal {
|
|
25
|
+
padding: 8px; /* Mac Terminal.app padding */
|
|
26
|
+
background-color: #000000;
|
|
27
|
+
height: calc(100% - 16px);
|
|
28
|
+
width: calc(100% - 16px);
|
|
29
|
+
}
|
|
30
|
+
#connect-container { padding: 2em; text-align: center; }
|
|
31
|
+
#agent-id-input { font-size: 1.2em; padding: 8px; width: 400px; margin-bottom: 1em; }
|
|
32
|
+
#connect-btn { font-size: 1.2em; padding: 10px 20px; }
|
|
11
33
|
</style>
|
|
12
34
|
</head>
|
|
13
35
|
<body>
|
|
14
|
-
<div id="
|
|
36
|
+
<div id="connect-container">
|
|
37
|
+
<h2>Terminal Mirror</h2>
|
|
38
|
+
<div id="agent-discovery">
|
|
39
|
+
<p>Discovering available Mac agents...</p>
|
|
40
|
+
<div id="agent-list"></div>
|
|
41
|
+
</div>
|
|
42
|
+
<div id="manual-connect" style="display: none; margin-top: 20px;">
|
|
43
|
+
<p>Or manually enter Agent ID:</p>
|
|
44
|
+
<input type="text" id="agent-id-input" placeholder="e.g., agent-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx">
|
|
45
|
+
<br>
|
|
46
|
+
<button id="connect-btn">Connect</button>
|
|
47
|
+
</div>
|
|
48
|
+
<button id="show-manual" style="margin-top: 10px;">Manual Connect</button>
|
|
49
|
+
</div>
|
|
50
|
+
<div id="terminal-container">
|
|
51
|
+
<div id="terminal"></div>
|
|
52
|
+
</div>
|
|
15
53
|
<script src="/app/terminal.js"></script>
|
|
16
54
|
</body>
|
|
17
55
|
</html>
|
package/public/app/terminal.js
CHANGED
|
@@ -2,91 +2,546 @@ const term = new Terminal({
|
|
|
2
2
|
cursorBlink: true,
|
|
3
3
|
macOptionIsMeta: true,
|
|
4
4
|
scrollback: 1000,
|
|
5
|
+
// Mac Terminal.app appearance settings
|
|
6
|
+
theme: {
|
|
7
|
+
background: '#000000', // Pure black like Mac Terminal
|
|
8
|
+
foreground: '#ffffff', // White text
|
|
9
|
+
cursor: '#ffffff', // White cursor
|
|
10
|
+
cursorAccent: '#000000', // Black cursor accent
|
|
11
|
+
selection: '#5c5c5c', // Mac selection color
|
|
12
|
+
// Mac Terminal color palette
|
|
13
|
+
black: '#000000',
|
|
14
|
+
red: '#c23621',
|
|
15
|
+
green: '#25bc24',
|
|
16
|
+
yellow: '#adad27',
|
|
17
|
+
blue: '#492ee1',
|
|
18
|
+
magenta: '#d338d3',
|
|
19
|
+
cyan: '#33bbc8',
|
|
20
|
+
white: '#cbcccd',
|
|
21
|
+
brightBlack: '#818383',
|
|
22
|
+
brightRed: '#fc391f',
|
|
23
|
+
brightGreen: '#31e722',
|
|
24
|
+
brightYellow: '#eaec23',
|
|
25
|
+
brightBlue: '#5833ff',
|
|
26
|
+
brightMagenta: '#f935f8',
|
|
27
|
+
brightCyan: '#14f0f0',
|
|
28
|
+
brightWhite: '#e9ebeb'
|
|
29
|
+
},
|
|
30
|
+
fontFamily: '"SF Mono", Monaco, Menlo, "Ubuntu Mono", monospace', // Mac system fonts
|
|
31
|
+
fontSize: 11, // Mac Terminal default size
|
|
32
|
+
lineHeight: 1.2, // Mac Terminal line spacing
|
|
33
|
+
letterSpacing: 0, // Tight character spacing like Mac
|
|
34
|
+
allowTransparency: false, // Solid background
|
|
35
|
+
convertEol: true, // Convert line endings properly
|
|
36
|
+
cols: 120, // Match agent terminal width
|
|
37
|
+
rows: 30 // Match agent terminal height
|
|
5
38
|
});
|
|
6
39
|
const fitAddon = new FitAddon.FitAddon();
|
|
7
40
|
term.loadAddon(fitAddon);
|
|
8
|
-
|
|
9
|
-
|
|
41
|
+
|
|
42
|
+
const connectContainer = document.getElementById('connect-container');
|
|
43
|
+
const terminalContainer = document.getElementById('terminal-container');
|
|
44
|
+
const agentIdInput = document.getElementById('agent-id-input');
|
|
45
|
+
const connectBtn = document.getElementById('connect-btn');
|
|
46
|
+
const showManualBtn = document.getElementById('show-manual');
|
|
47
|
+
const manualConnect = document.getElementById('manual-connect');
|
|
48
|
+
const agentDiscovery = document.getElementById('agent-discovery');
|
|
49
|
+
const agentList = document.getElementById('agent-list');
|
|
10
50
|
|
|
11
51
|
let ws;
|
|
12
52
|
let peerConnection;
|
|
13
53
|
let dataChannel;
|
|
14
54
|
let user;
|
|
55
|
+
let AGENT_ID;
|
|
56
|
+
let CLIENT_ID;
|
|
57
|
+
|
|
58
|
+
// Auto-discover agents on page load
|
|
59
|
+
window.addEventListener('load', () => {
|
|
60
|
+
discoverAgents();
|
|
61
|
+
});
|
|
15
62
|
|
|
16
|
-
|
|
63
|
+
showManualBtn.onclick = () => {
|
|
64
|
+
agentDiscovery.style.display = 'none';
|
|
65
|
+
manualConnect.style.display = 'block';
|
|
66
|
+
showManualBtn.style.display = 'none';
|
|
67
|
+
};
|
|
17
68
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (!
|
|
21
|
-
|
|
69
|
+
connectBtn.onclick = () => {
|
|
70
|
+
AGENT_ID = agentIdInput.value.trim();
|
|
71
|
+
if (!AGENT_ID) {
|
|
72
|
+
alert('Please enter a valid Agent ID.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
startConnection();
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function connectToAgent(agentId) {
|
|
79
|
+
AGENT_ID = agentId;
|
|
80
|
+
startConnection();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function startConnection() {
|
|
84
|
+
connectContainer.style.display = 'none';
|
|
85
|
+
terminalContainer.style.display = 'block';
|
|
86
|
+
term.open(document.getElementById('terminal'));
|
|
87
|
+
// Delay fit to ensure proper dimensions after CSS transitions
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
fitAddon.fit();
|
|
90
|
+
}, 100);
|
|
91
|
+
initialize();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function discoverAgents() {
|
|
95
|
+
console.log('[DISCOVERY] 🔍 Starting agent discovery...');
|
|
96
|
+
agentList.innerHTML = '<p style="color: #ccc;">Searching for Mac agents...</p>';
|
|
97
|
+
|
|
98
|
+
// Production WebSocket server configuration
|
|
99
|
+
let signalingUrl;
|
|
100
|
+
if (window.location.hostname === 'shellmirror.app') {
|
|
101
|
+
// Production: Use separate WebSocket server (to be deployed)
|
|
102
|
+
signalingUrl = 'wss://ws.shellmirror.app';
|
|
103
|
+
} else {
|
|
104
|
+
// Development: Use local server
|
|
105
|
+
signalingUrl = (window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host;
|
|
106
|
+
}
|
|
107
|
+
const discoveryWs = new WebSocket(`${signalingUrl}?role=discovery`);
|
|
108
|
+
|
|
109
|
+
discoveryWs.onopen = () => {
|
|
110
|
+
console.log('[DISCOVERY] ✅ Connected to signaling server for agent discovery');
|
|
111
|
+
discoveryWs.send(JSON.stringify({ type: 'list-agents' }));
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
discoveryWs.onmessage = (message) => {
|
|
115
|
+
const data = JSON.parse(message.data);
|
|
116
|
+
console.log('[DISCOVERY] 📨 Received:', data);
|
|
117
|
+
if (data.type === 'agent-list') {
|
|
118
|
+
displayAvailableAgents(data.agents);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
discoveryWs.onclose = () => {
|
|
123
|
+
console.log('[DISCOVERY] 🔌 Discovery connection closed');
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
discoveryWs.onerror = (error) => {
|
|
127
|
+
console.error('[DISCOVERY] ❌ WebSocket error:', error);
|
|
128
|
+
agentList.innerHTML = '<p style="color: #f44336;">Discovery failed. Check server connection.</p>';
|
|
129
|
+
showManualBtn.style.display = 'block';
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Timeout after 8 seconds (increased from 5)
|
|
133
|
+
setTimeout(() => {
|
|
134
|
+
if (discoveryWs.readyState === WebSocket.OPEN) {
|
|
135
|
+
discoveryWs.close();
|
|
136
|
+
}
|
|
137
|
+
if (agentList.children.length === 0 || agentList.textContent.includes('Searching')) {
|
|
138
|
+
console.log('[DISCOVERY] ⏰ Discovery timeout - no agents found');
|
|
139
|
+
agentList.innerHTML = '<p style="color: #ff9800;">⚠️ No Mac agents found.<br><small>Make sure your Mac agent is running with: <code>cd mac-agent && npm start</code></small></p>';
|
|
140
|
+
showManualBtn.style.display = 'block';
|
|
141
|
+
}
|
|
142
|
+
}, 8000);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function displayAvailableAgents(agents) {
|
|
146
|
+
console.log('[DISCOVERY] 🖥️ Displaying agents:', agents);
|
|
147
|
+
agentList.innerHTML = '';
|
|
148
|
+
|
|
149
|
+
if (agents.length === 0) {
|
|
150
|
+
agentList.innerHTML = '<p style="color: #ff9800;">❌ No Mac agents currently running.</p>';
|
|
151
|
+
showManualBtn.style.display = 'block';
|
|
22
152
|
return;
|
|
23
153
|
}
|
|
24
|
-
|
|
154
|
+
|
|
155
|
+
console.log(`[DISCOVERY] ✅ Found ${agents.length} agent(s)`);
|
|
156
|
+
|
|
157
|
+
agents.forEach(agent => {
|
|
158
|
+
const agentDiv = document.createElement('div');
|
|
159
|
+
agentDiv.style.cssText = 'margin: 10px 0; padding: 15px; background: #333; border-radius: 8px; cursor: pointer; border: 2px solid #555; transition: all 0.3s ease;';
|
|
160
|
+
agentDiv.innerHTML = `
|
|
161
|
+
<div style="display: flex; align-items: center; gap: 10px;">
|
|
162
|
+
<div style="width: 12px; height: 12px; background: #4CAF50; border-radius: 50%; animation: pulse 2s infinite;"></div>
|
|
163
|
+
<div>
|
|
164
|
+
<strong style="color: #fff;">${agent.id}</strong><br>
|
|
165
|
+
<small style="color: #aaa;">🖱️ Click to connect to Mac terminal</small>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
`;
|
|
169
|
+
|
|
170
|
+
agentDiv.onmouseover = () => {
|
|
171
|
+
agentDiv.style.borderColor = '#4CAF50';
|
|
172
|
+
agentDiv.style.background = '#444';
|
|
173
|
+
};
|
|
174
|
+
agentDiv.onmouseout = () => {
|
|
175
|
+
agentDiv.style.borderColor = '#555';
|
|
176
|
+
agentDiv.style.background = '#333';
|
|
177
|
+
};
|
|
178
|
+
agentDiv.onclick = () => {
|
|
179
|
+
console.log(`[DISCOVERY] 🖱️ User clicked on agent: ${agent.id}`);
|
|
180
|
+
connectToAgent(agent.id);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
agentList.appendChild(agentDiv);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
showManualBtn.style.display = 'block';
|
|
187
|
+
|
|
188
|
+
// Add CSS animation for pulse effect
|
|
189
|
+
const style = document.createElement('style');
|
|
190
|
+
style.textContent = `
|
|
191
|
+
@keyframes pulse {
|
|
192
|
+
0% { opacity: 1; }
|
|
193
|
+
50% { opacity: 0.5; }
|
|
194
|
+
100% { opacity: 1; }
|
|
195
|
+
}
|
|
196
|
+
`;
|
|
197
|
+
document.head.appendChild(style);
|
|
198
|
+
}
|
|
25
199
|
|
|
26
|
-
|
|
200
|
+
async function initialize() {
|
|
201
|
+
console.log('[CLIENT] 🚀 Initializing WebRTC connection to agent:', AGENT_ID);
|
|
202
|
+
// Production WebSocket server configuration
|
|
203
|
+
let signalingUrl;
|
|
204
|
+
if (window.location.hostname === 'shellmirror.app') {
|
|
205
|
+
// Production: Use separate WebSocket server (to be deployed)
|
|
206
|
+
signalingUrl = 'wss://ws.shellmirror.app';
|
|
207
|
+
} else {
|
|
208
|
+
// Development: Use local server
|
|
209
|
+
signalingUrl = (window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host;
|
|
210
|
+
}
|
|
27
211
|
ws = new WebSocket(`${signalingUrl}?role=client`);
|
|
28
212
|
|
|
29
213
|
ws.onopen = () => {
|
|
30
|
-
console.log('
|
|
31
|
-
// Let the agent know we want to connect
|
|
32
|
-
sendMessage({ type: 'client-hello', from: user.id, to: AGENT_ID });
|
|
214
|
+
console.log('[CLIENT] ✅ WebSocket connection to signaling server opened.');
|
|
33
215
|
};
|
|
34
216
|
|
|
35
217
|
ws.onmessage = async (message) => {
|
|
36
218
|
const data = JSON.parse(message.data);
|
|
37
|
-
console.log(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
219
|
+
console.log(`[CLIENT] Received message of type: ${data.type}`);
|
|
220
|
+
|
|
221
|
+
switch (data.type) {
|
|
222
|
+
case 'server-hello':
|
|
223
|
+
CLIENT_ID = data.id;
|
|
224
|
+
console.log(`[CLIENT] Assigned Client ID: ${CLIENT_ID}`);
|
|
225
|
+
|
|
226
|
+
// First send a test message to verify communication
|
|
227
|
+
console.log(`[CLIENT] 🧪 Sending test ping message first...`);
|
|
228
|
+
const testSent = sendMessage({ type: 'ping', from: CLIENT_ID, to: AGENT_ID, timestamp: Date.now() });
|
|
229
|
+
|
|
230
|
+
if (!testSent) {
|
|
231
|
+
console.error(`[CLIENT] ❌ Failed to send test message - WebSocket connection broken`);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Start polling to connect to the agent
|
|
236
|
+
const intervalId = setInterval(() => {
|
|
237
|
+
console.log(`[CLIENT] 📞 Sending client-hello to Agent: ${AGENT_ID}`);
|
|
238
|
+
const sent = sendMessage({ type: 'client-hello', from: CLIENT_ID, to: AGENT_ID });
|
|
239
|
+
if (!sent) {
|
|
240
|
+
console.error(`[CLIENT] ❌ Failed to send client-hello - stopping attempts`);
|
|
241
|
+
clearInterval(intervalId);
|
|
242
|
+
}
|
|
243
|
+
}, 1000);
|
|
244
|
+
|
|
245
|
+
// This is a bit of a hack for the message handler.
|
|
246
|
+
// We redefine it to handle the next phase of messages.
|
|
247
|
+
ws.onmessage = async (nextMessage) => {
|
|
248
|
+
let messageData = nextMessage.data;
|
|
249
|
+
|
|
250
|
+
// Handle Blob messages by converting to text first
|
|
251
|
+
if (messageData instanceof Blob) {
|
|
252
|
+
console.log(`[CLIENT] 📄 Received Blob message, converting to text...`);
|
|
253
|
+
messageData = await messageData.text();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const nextData = JSON.parse(messageData);
|
|
258
|
+
console.log(`[CLIENT] 📨 Received message of type: ${nextData.type}`);
|
|
259
|
+
|
|
260
|
+
if (nextData.type === 'offer') {
|
|
261
|
+
console.log('[CLIENT] Received offer from agent. Stopping client-hello retries.');
|
|
262
|
+
clearInterval(intervalId);
|
|
263
|
+
|
|
264
|
+
console.log('[CLIENT] Received WebRTC offer from agent.');
|
|
265
|
+
await createPeerConnection();
|
|
266
|
+
await peerConnection.setRemoteDescription(new RTCSessionDescription(nextData));
|
|
267
|
+
const answer = await peerConnection.createAnswer();
|
|
268
|
+
await peerConnection.setLocalDescription(answer);
|
|
269
|
+
console.log('[CLIENT] Sending WebRTC answer to agent.');
|
|
270
|
+
sendMessage({ type: 'answer', sdp: answer.sdp, to: AGENT_ID, from: CLIENT_ID });
|
|
271
|
+
|
|
272
|
+
// Force ICE gathering if it hasn't started within 2 seconds
|
|
273
|
+
console.log('[CLIENT] 🔧 Setting up ICE gathering fallback timer...');
|
|
274
|
+
setTimeout(() => {
|
|
275
|
+
if (peerConnection.iceGatheringState === 'new') {
|
|
276
|
+
console.log('[CLIENT] ⚠️ ICE gathering hasn\'t started - triggering restart');
|
|
277
|
+
try {
|
|
278
|
+
peerConnection.restartIce();
|
|
279
|
+
} catch (error) {
|
|
280
|
+
console.error('[CLIENT] ❌ Failed to restart ICE:', error);
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
console.log('[CLIENT] ✅ ICE gathering is active:', peerConnection.iceGatheringState);
|
|
284
|
+
}
|
|
285
|
+
}, 2000);
|
|
286
|
+
} else if (nextData.type === 'candidate') {
|
|
287
|
+
console.log('[CLIENT] 🧊 Received ICE candidate from agent:', {
|
|
288
|
+
candidate: nextData.candidate.candidate,
|
|
289
|
+
sdpMid: nextData.candidate.sdpMid,
|
|
290
|
+
sdpMLineIndex: nextData.candidate.sdpMLineIndex
|
|
291
|
+
});
|
|
292
|
+
if (peerConnection) {
|
|
293
|
+
try {
|
|
294
|
+
await peerConnection.addIceCandidate(new RTCIceCandidate(nextData.candidate));
|
|
295
|
+
console.log('[CLIENT] ✅ ICE candidate added successfully');
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.error('[CLIENT] ❌ Error adding ICE candidate:', error);
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
console.error('[CLIENT] ❌ Cannot add ICE candidate - no peer connection');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.error(`[CLIENT] ❌ Error processing WebRTC message:`, error);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
break;
|
|
47
308
|
}
|
|
48
309
|
};
|
|
49
310
|
|
|
50
|
-
ws.onclose = () => {
|
|
51
|
-
console.log(
|
|
311
|
+
ws.onclose = (event) => {
|
|
312
|
+
console.log(`[CLIENT] 🔌 Disconnected from signaling server. Code: ${event.code}, Reason: ${event.reason}`);
|
|
52
313
|
term.write('\r\n\r\nConnection to server lost. Please refresh.\r\n');
|
|
53
314
|
};
|
|
315
|
+
|
|
316
|
+
ws.onerror = (error) => {
|
|
317
|
+
console.error('[CLIENT] ❌ WebSocket error:', error);
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function testSTUNConnectivity() {
|
|
322
|
+
console.log('[CLIENT] 🧪 Testing STUN server connectivity...');
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
// Create a test peer connection to check STUN server access
|
|
326
|
+
const testPC = new RTCPeerConnection({
|
|
327
|
+
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
let candidateReceived = false;
|
|
331
|
+
|
|
332
|
+
return new Promise((resolve) => {
|
|
333
|
+
const timeout = setTimeout(() => {
|
|
334
|
+
console.log('[CLIENT] ⚠️ STUN connectivity test timed out - may indicate network restrictions');
|
|
335
|
+
testPC.close();
|
|
336
|
+
resolve(false);
|
|
337
|
+
}, 5000);
|
|
338
|
+
|
|
339
|
+
testPC.onicecandidate = (event) => {
|
|
340
|
+
if (event.candidate && !candidateReceived) {
|
|
341
|
+
candidateReceived = true;
|
|
342
|
+
console.log('[CLIENT] ✅ STUN server connectivity confirmed');
|
|
343
|
+
clearTimeout(timeout);
|
|
344
|
+
testPC.close();
|
|
345
|
+
resolve(true);
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// Create a dummy data channel to trigger ICE gathering
|
|
350
|
+
testPC.createDataChannel('test');
|
|
351
|
+
testPC.createOffer().then(offer => testPC.setLocalDescription(offer));
|
|
352
|
+
});
|
|
353
|
+
} catch (error) {
|
|
354
|
+
console.error('[CLIENT] ❌ STUN connectivity test failed:', error);
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
54
357
|
}
|
|
55
358
|
|
|
56
359
|
async function createPeerConnection() {
|
|
57
|
-
|
|
360
|
+
console.log('[CLIENT] Creating PeerConnection.');
|
|
361
|
+
|
|
362
|
+
// Test STUN connectivity first
|
|
363
|
+
const stunWorking = await testSTUNConnectivity();
|
|
364
|
+
if (!stunWorking) {
|
|
365
|
+
console.log('[CLIENT] ⚠️ STUN servers may be blocked - using TURN servers for connectivity');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Test STUN server connectivity with multiple backup servers
|
|
369
|
+
console.log('[CLIENT] 🌐 Configuring ICE servers with multiple STUN/TURN options...');
|
|
370
|
+
const iceServers = [
|
|
371
|
+
// Google STUN servers (primary)
|
|
372
|
+
{ urls: 'stun:stun.l.google.com:19302' },
|
|
373
|
+
{ urls: 'stun:stun1.l.google.com:19302' },
|
|
374
|
+
// Cloudflare STUN servers (backup)
|
|
375
|
+
{ urls: 'stun:stun.cloudflare.com:3478' },
|
|
376
|
+
// Mozilla STUN servers (backup)
|
|
377
|
+
{ urls: 'stun:stun.services.mozilla.com:3478' },
|
|
378
|
+
// OpenRelay free TURN server (for NAT traversal)
|
|
379
|
+
{
|
|
380
|
+
urls: 'turn:openrelay.metered.ca:80',
|
|
381
|
+
username: 'openrelayproject',
|
|
382
|
+
credential: 'openrelayproject'
|
|
383
|
+
},
|
|
384
|
+
// Alternative TURN server
|
|
385
|
+
{
|
|
386
|
+
urls: 'turn:openrelay.metered.ca:443',
|
|
387
|
+
username: 'openrelayproject',
|
|
388
|
+
credential: 'openrelayproject'
|
|
389
|
+
}
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
console.log('[CLIENT] 📋 Configured ICE servers:', iceServers.map(server => server.urls));
|
|
393
|
+
|
|
394
|
+
// Enhanced WebRTC configuration for better ICE candidate generation
|
|
395
|
+
const rtcConfig = {
|
|
396
|
+
iceServers: iceServers,
|
|
397
|
+
iceCandidatePoolSize: 10, // Generate more ICE candidates
|
|
398
|
+
iceTransportPolicy: 'all', // Use both STUN and TURN
|
|
399
|
+
bundlePolicy: 'balanced' // Optimize for connection establishment
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
console.log('[CLIENT] ⚙️ WebRTC config:', rtcConfig);
|
|
403
|
+
peerConnection = new RTCPeerConnection(rtcConfig);
|
|
58
404
|
|
|
405
|
+
// Debug: Verify event handler is being attached
|
|
406
|
+
console.log('[CLIENT] 🔧 Attaching ICE candidate event handler...');
|
|
407
|
+
|
|
59
408
|
peerConnection.onicecandidate = (event) => {
|
|
409
|
+
console.log('[CLIENT] 🧊 ICE candidate event fired:', event.candidate ? 'candidate found' : 'gathering complete');
|
|
60
410
|
if (event.candidate) {
|
|
61
|
-
|
|
411
|
+
console.log('[CLIENT] 📤 ICE candidate details:', {
|
|
412
|
+
candidate: event.candidate.candidate,
|
|
413
|
+
sdpMid: event.candidate.sdpMid,
|
|
414
|
+
sdpMLineIndex: event.candidate.sdpMLineIndex
|
|
415
|
+
});
|
|
416
|
+
console.log('[CLIENT] 📤 Sending ICE candidate to agent...');
|
|
417
|
+
const sent = sendMessage({ type: 'candidate', candidate: event.candidate, to: AGENT_ID, from: CLIENT_ID });
|
|
418
|
+
if (sent) {
|
|
419
|
+
console.log('[CLIENT] ✅ ICE candidate sent successfully');
|
|
420
|
+
} else {
|
|
421
|
+
console.log('[CLIENT] ❌ Failed to send ICE candidate');
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
console.log('[CLIENT] 🏁 ICE candidate gathering complete.');
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
peerConnection.oniceconnectionstatechange = () => {
|
|
429
|
+
console.log(`[CLIENT] 📊 ICE connection state changed: ${peerConnection.iceConnectionState}`);
|
|
430
|
+
console.log(`[CLIENT] 📊 ICE gathering state: ${peerConnection.iceGatheringState}`);
|
|
431
|
+
|
|
432
|
+
switch (peerConnection.iceConnectionState) {
|
|
433
|
+
case 'new':
|
|
434
|
+
console.log('[CLIENT] 🆕 ICE connection starting...');
|
|
435
|
+
break;
|
|
436
|
+
case 'checking':
|
|
437
|
+
console.log('[CLIENT] 🔍 ICE connection checking candidates...');
|
|
438
|
+
break;
|
|
439
|
+
case 'connected':
|
|
440
|
+
console.log('[CLIENT] ✅ WebRTC connection established!');
|
|
441
|
+
break;
|
|
442
|
+
case 'completed':
|
|
443
|
+
console.log('[CLIENT] ✅ ICE connection completed successfully!');
|
|
444
|
+
break;
|
|
445
|
+
case 'failed':
|
|
446
|
+
console.log('[CLIENT] ❌ ICE connection failed - no viable candidates');
|
|
447
|
+
console.log('[CLIENT] 💡 Troubleshooting: This may be due to firewall/NAT issues or blocked STUN servers');
|
|
448
|
+
term.write('\r\n\r\n❌ Connection failed: Network connectivity issues\r\n');
|
|
449
|
+
term.write('💡 This may be due to:\r\n');
|
|
450
|
+
term.write(' • Firewall blocking WebRTC traffic\r\n');
|
|
451
|
+
term.write(' • Corporate network restrictions\r\n');
|
|
452
|
+
term.write(' • STUN/TURN servers unreachable\r\n');
|
|
453
|
+
term.write('\r\n🔄 Please refresh the page to retry...\r\n');
|
|
454
|
+
break;
|
|
455
|
+
case 'disconnected':
|
|
456
|
+
console.log('[CLIENT] ⚠️ ICE connection disconnected');
|
|
457
|
+
break;
|
|
458
|
+
case 'closed':
|
|
459
|
+
console.log('[CLIENT] 🔐 ICE connection closed');
|
|
460
|
+
break;
|
|
62
461
|
}
|
|
63
462
|
};
|
|
64
463
|
|
|
65
|
-
|
|
66
|
-
|
|
464
|
+
peerConnection.onconnectionstatechange = () => {
|
|
465
|
+
console.log(`[CLIENT] 📡 Connection state changed: ${peerConnection.connectionState}`);
|
|
466
|
+
|
|
467
|
+
switch (peerConnection.connectionState) {
|
|
468
|
+
case 'new':
|
|
469
|
+
console.log('[CLIENT] 🆕 Connection starting...');
|
|
470
|
+
break;
|
|
471
|
+
case 'connecting':
|
|
472
|
+
console.log('[CLIENT] 🔄 Connection in progress...');
|
|
473
|
+
break;
|
|
474
|
+
case 'connected':
|
|
475
|
+
console.log('[CLIENT] ✅ Peer connection fully established!');
|
|
476
|
+
break;
|
|
477
|
+
case 'disconnected':
|
|
478
|
+
console.log('[CLIENT] ⚠️ Peer connection disconnected');
|
|
479
|
+
break;
|
|
480
|
+
case 'failed':
|
|
481
|
+
console.log('[CLIENT] ❌ Peer connection failed completely');
|
|
482
|
+
break;
|
|
483
|
+
case 'closed':
|
|
484
|
+
console.log('[CLIENT] 🔐 Peer connection closed');
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
peerConnection.onicegatheringstatechange = () => {
|
|
490
|
+
console.log(`[CLIENT] 🔍 ICE gathering state changed: ${peerConnection.iceGatheringState}`);
|
|
491
|
+
|
|
492
|
+
switch (peerConnection.iceGatheringState) {
|
|
493
|
+
case 'new':
|
|
494
|
+
console.log('[CLIENT] 🆕 ICE gathering not started');
|
|
495
|
+
break;
|
|
496
|
+
case 'gathering':
|
|
497
|
+
console.log('[CLIENT] 🔍 ICE gathering in progress...');
|
|
498
|
+
break;
|
|
499
|
+
case 'complete':
|
|
500
|
+
console.log('[CLIENT] ✅ ICE gathering completed');
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// Client waits for data channel from agent
|
|
506
|
+
peerConnection.ondatachannel = (event) => {
|
|
507
|
+
console.log('[CLIENT] 📨 Data channel received from agent!');
|
|
508
|
+
dataChannel = event.channel;
|
|
509
|
+
setupDataChannel();
|
|
510
|
+
};
|
|
67
511
|
}
|
|
68
512
|
|
|
69
513
|
function setupDataChannel() {
|
|
70
514
|
dataChannel.onopen = () => {
|
|
71
|
-
console.log('Data channel is open!');
|
|
515
|
+
console.log('[CLIENT] ✅ Data channel is open!');
|
|
72
516
|
term.focus();
|
|
73
517
|
fitAddon.fit();
|
|
518
|
+
// Mac-style connection message with proper colors
|
|
519
|
+
term.write('\r\n\x1b[32mConnected to Mac Terminal\x1b[0m\r\n');
|
|
74
520
|
};
|
|
75
521
|
|
|
76
522
|
dataChannel.onmessage = (event) => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
523
|
+
try {
|
|
524
|
+
const message = JSON.parse(event.data);
|
|
525
|
+
if (message.type === 'output') {
|
|
526
|
+
term.write(message.data);
|
|
527
|
+
}
|
|
528
|
+
} catch (err) {
|
|
529
|
+
console.error('[CLIENT] Error parsing data channel message:', err);
|
|
80
530
|
}
|
|
81
531
|
};
|
|
82
532
|
|
|
83
533
|
dataChannel.onclose = () => {
|
|
84
|
-
console.log('Data channel closed.');
|
|
85
|
-
term.write('\r\n\r\
|
|
534
|
+
console.log('[CLIENT] Data channel closed.');
|
|
535
|
+
term.write('\r\n\r\n\x1b[31m❌ Terminal session ended.\x1b[0m\r\n');
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
dataChannel.onerror = (error) => {
|
|
539
|
+
console.error('[CLIENT] Data channel error:', error);
|
|
540
|
+
term.write('\r\n\r\n\x1b[31m❌ Data channel error occurred.\x1b[0m\r\n');
|
|
86
541
|
};
|
|
87
542
|
|
|
88
543
|
term.onData((data) => {
|
|
89
|
-
if (dataChannel.readyState === 'open') {
|
|
544
|
+
if (dataChannel && dataChannel.readyState === 'open') {
|
|
90
545
|
dataChannel.send(JSON.stringify({ type: 'input', data }));
|
|
91
546
|
}
|
|
92
547
|
});
|
|
@@ -96,16 +551,35 @@ function setupDataChannel() {
|
|
|
96
551
|
});
|
|
97
552
|
|
|
98
553
|
term.onResize(({ cols, rows }) => {
|
|
99
|
-
if (dataChannel.readyState === 'open') {
|
|
554
|
+
if (dataChannel && dataChannel.readyState === 'open') {
|
|
100
555
|
dataChannel.send(JSON.stringify({ type: 'resize', cols, rows }));
|
|
101
556
|
}
|
|
102
557
|
});
|
|
103
558
|
}
|
|
104
559
|
|
|
105
560
|
function sendMessage(message) {
|
|
106
|
-
|
|
107
|
-
|
|
561
|
+
console.log(`[CLIENT] 📤 Attempting to send message:`, message);
|
|
562
|
+
console.log(`[CLIENT] 🔍 WebSocket state: ${ws ? ws.readyState : 'null'} (OPEN=1)`);
|
|
563
|
+
|
|
564
|
+
if (!ws) {
|
|
565
|
+
console.error('[CLIENT] ❌ WebSocket is null - cannot send message');
|
|
566
|
+
return false;
|
|
108
567
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
568
|
+
|
|
569
|
+
if (ws.readyState !== 1) { // WebSocket.OPEN = 1
|
|
570
|
+
const states = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
|
|
571
|
+
console.error(`[CLIENT] ❌ WebSocket not open (state: ${ws.readyState} = ${states[ws.readyState] || 'UNKNOWN'}) - cannot send message`);
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
try {
|
|
576
|
+
const messageStr = JSON.stringify(message);
|
|
577
|
+
console.log(`[CLIENT] 📨 Sending message: ${messageStr}`);
|
|
578
|
+
ws.send(messageStr);
|
|
579
|
+
console.log(`[CLIENT] ✅ Message sent successfully`);
|
|
580
|
+
return true;
|
|
581
|
+
} catch (error) {
|
|
582
|
+
console.error(`[CLIENT] ❌ Error sending message:`, error);
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
}
|