shell-mirror 1.5.16 → 1.5.18

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/lib/auto-start.js CHANGED
@@ -5,6 +5,7 @@ const crypto = require('crypto');
5
5
  const { spawn } = require('child_process');
6
6
  const https = require('https');
7
7
  const querystring = require('querystring');
8
+ const NetworkUtils = require('./network-utils');
8
9
 
9
10
  class AutoStart {
10
11
  constructor() {
@@ -400,13 +401,16 @@ NODE_ENV=development
400
401
  });
401
402
  }
402
403
 
403
- async createEnvFile(authInfo) {
404
+ async createEnvFile(authInfo, wsConfig = null) {
405
+ const port = wsConfig ? wsConfig.port : 8080;
406
+ const host = wsConfig ? wsConfig.host : '0.0.0.0';
407
+
404
408
  const envContent = `# Shell Mirror Configuration
405
409
  # Auto-generated on ${new Date().toISOString()}
406
410
 
407
- BASE_URL=http://localhost:8080
408
- PORT=8080
409
- HOST=0.0.0.0
411
+ BASE_URL=http://localhost:${port}
412
+ PORT=${port}
413
+ HOST=${host}
410
414
  GOOGLE_CLIENT_ID=${this.oauthConfig.clientId}
411
415
  GOOGLE_CLIENT_SECRET=${this.oauthConfig.clientSecret}
412
416
  SESSION_SECRET=${crypto.randomBytes(32).toString('hex')}
@@ -416,62 +420,93 @@ NODE_ENV=development
416
420
  USER_EMAIL=${authInfo.email}
417
421
  USER_NAME=${authInfo.name}
418
422
  ACCESS_TOKEN=${authInfo.accessToken}
423
+
424
+ # WebSocket server configuration
425
+ ${wsConfig ? `WS_PORT=${wsConfig.port}
426
+ WS_HOST=${wsConfig.host}
427
+ WS_LOCAL_IP=${wsConfig.localIP}
428
+ WS_URL=${wsConfig.wsUrl}` : ''}
419
429
  `;
420
430
 
421
431
  await fs.writeFile(this.envFile, envContent);
422
432
  }
423
433
 
424
- displayStatus(authInfo) {
434
+ displayStatus(authInfo, wsConfig = null) {
425
435
  const pkg = require('../package.json');
436
+ console.log('');
426
437
  console.log(`✅ Shell Mirror v${pkg.version} - Running (${authInfo.email})`);
438
+ if (wsConfig) {
439
+ console.log(`🌐 WebSocket Server: ${wsConfig.wsUrl}`);
440
+ console.log(`📱 Access from browser: https://shellmirror.app`);
441
+ }
427
442
  console.log('Press Ctrl+C to stop');
443
+ console.log('');
428
444
  }
429
445
 
430
446
  async startAuthenticatedServer(authInfo) {
431
447
  console.log('');
432
- console.log('🚀 Starting authenticated Shell Mirror server...');
433
-
434
- // Create .env file with authentication
435
- await this.createEnvFile(authInfo);
436
-
437
- // Register this server as a Mac agent
438
- await this.registerAsMacAgent(authInfo);
439
-
440
- // Find the package root directory (where server.js is located)
441
- const packageRoot = path.resolve(__dirname, '..');
442
- const serverPath = path.join(packageRoot, 'server.js');
448
+ console.log('🚀 Starting authenticated Shell Mirror with local WebSocket server...');
443
449
 
444
- // Start the main server process
445
- const serverProcess = spawn('node', [serverPath], {
446
- stdio: 'inherit',
447
- cwd: path.dirname(this.envFile),
448
- env: { ...process.env }
449
- });
450
+ try {
451
+ // Set up local WebSocket server configuration
452
+ const wsPort = await NetworkUtils.findAvailablePort(8080, 10);
453
+ const wsConfig = NetworkUtils.generateWebSocketConfig(wsPort);
454
+
455
+ // Display network information
456
+ NetworkUtils.displayNetworkInfo(wsConfig);
457
+
458
+ // Create .env file with authentication and WebSocket config
459
+ await this.createEnvFile(authInfo, wsConfig);
460
+
461
+ // Register this server as a Mac agent with WebSocket endpoint
462
+ await this.registerAsMacAgent(authInfo, wsConfig);
463
+
464
+ // Start the local WebSocket server process
465
+ const packageRoot = path.resolve(__dirname, '..');
466
+ const serverPath = path.join(packageRoot, 'server.js');
467
+
468
+ console.log('🔄 Starting local WebSocket server...');
469
+ const serverProcess = spawn('node', [serverPath], {
470
+ stdio: 'inherit',
471
+ cwd: path.dirname(this.envFile),
472
+ env: {
473
+ ...process.env,
474
+ WS_PORT: wsConfig.port.toString(),
475
+ WS_HOST: wsConfig.host,
476
+ LOCAL_IP: wsConfig.localIP
477
+ }
478
+ });
450
479
 
451
- // Display status information
452
- this.displayStatus(authInfo);
480
+ // Display status information
481
+ this.displayStatus(authInfo, wsConfig);
453
482
 
454
- // Handle Ctrl+C gracefully
455
- process.on('SIGINT', () => {
456
- console.log('');
457
- console.log('🛑 Stopping Shell Mirror...');
458
- serverProcess.kill('SIGINT');
459
- process.exit(0);
460
- });
483
+ // Handle Ctrl+C gracefully
484
+ process.on('SIGINT', () => {
485
+ console.log('');
486
+ console.log('🛑 Stopping Shell Mirror and WebSocket server...');
487
+ serverProcess.kill('SIGINT');
488
+ process.exit(0);
489
+ });
461
490
 
462
- // Wait for server process
463
- serverProcess.on('close', (code) => {
464
- if (code !== 0) {
465
- console.error(`❌ Server exited with code ${code}`);
466
- process.exit(code);
467
- }
468
- });
491
+ // Wait for server process
492
+ serverProcess.on('close', (code) => {
493
+ if (code !== 0) {
494
+ console.error(`❌ Server exited with code ${code}`);
495
+ process.exit(code);
496
+ }
497
+ });
498
+
499
+ } catch (error) {
500
+ console.error('❌ Failed to start WebSocket server:', error.message);
501
+ process.exit(1);
502
+ }
469
503
  }
470
504
 
471
- async registerAsMacAgent(authInfo) {
472
- console.log('🔗 Registering as Mac agent...');
505
+ async registerAsMacAgent(authInfo, wsConfig) {
506
+ console.log('🔗 Registering as Mac agent with WebSocket endpoint...');
473
507
  console.log(` User: ${authInfo.email}`);
474
508
  console.log(` Machine: ${os.hostname()}`);
509
+ console.log(` WebSocket: ${wsConfig.wsUrl}`);
475
510
 
476
511
  try {
477
512
  const agentId = `local-${os.hostname()}-${Date.now()}`;
@@ -481,9 +516,11 @@ ACCESS_TOKEN=${authInfo.accessToken}
481
516
  ownerName: authInfo.name,
482
517
  ownerToken: authInfo.accessToken,
483
518
  machineName: os.hostname(),
484
- agentVersion: '1.3.0',
519
+ agentVersion: '1.5.17',
485
520
  capabilities: ['terminal', 'websocket'],
486
- serverPort: 8080
521
+ serverPort: wsConfig.port,
522
+ websocketUrl: wsConfig.wsUrl,
523
+ localIP: wsConfig.localIP
487
524
  };
488
525
 
489
526
  console.log(` Agent ID: ${agentId}`);
@@ -0,0 +1,145 @@
1
+ const os = require('os');
2
+
3
+ /**
4
+ * Network utilities for local WebSocket server setup
5
+ */
6
+ class NetworkUtils {
7
+ /**
8
+ * Get the local IP address for WebSocket server binding
9
+ * Prioritizes Wi-Fi and Ethernet interfaces over others
10
+ */
11
+ static getLocalIP() {
12
+ const interfaces = os.networkInterfaces();
13
+
14
+ // Priority order for interface selection
15
+ const priorityOrder = [
16
+ 'Wi-Fi', // macOS Wi-Fi
17
+ 'en0', // macOS primary interface
18
+ 'en1', // macOS secondary interface
19
+ 'eth0', // Linux Ethernet
20
+ 'wlan0', // Linux Wi-Fi
21
+ 'Ethernet', // Windows Ethernet
22
+ 'Wi-Fi' // Windows Wi-Fi (might be different name)
23
+ ];
24
+
25
+ // First, try priority interfaces
26
+ for (const interfaceName of priorityOrder) {
27
+ const addresses = interfaces[interfaceName];
28
+ if (addresses) {
29
+ for (const addr of addresses) {
30
+ if (addr.family === 'IPv4' && !addr.internal) {
31
+ console.log(`🌐 Selected network interface: ${interfaceName} (${addr.address})`);
32
+ return addr.address;
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ // Fallback: find any non-internal IPv4 address
39
+ for (const interfaceName in interfaces) {
40
+ const addresses = interfaces[interfaceName];
41
+ for (const addr of addresses) {
42
+ if (addr.family === 'IPv4' && !addr.internal) {
43
+ console.log(`🌐 Fallback network interface: ${interfaceName} (${addr.address})`);
44
+ return addr.address;
45
+ }
46
+ }
47
+ }
48
+
49
+ // Last resort: localhost (won't work for remote clients)
50
+ console.warn('⚠️ No external network interface found, using localhost');
51
+ return '127.0.0.1';
52
+ }
53
+
54
+ /**
55
+ * Find an available port for the WebSocket server
56
+ * @param {number} startPort - Starting port to check
57
+ * @param {number} maxAttempts - Maximum number of ports to try
58
+ */
59
+ static async findAvailablePort(startPort = 8080, maxAttempts = 10) {
60
+ const net = require('net');
61
+
62
+ for (let i = 0; i < maxAttempts; i++) {
63
+ const port = startPort + i;
64
+ const isAvailable = await this.checkPortAvailable(port);
65
+ if (isAvailable) {
66
+ console.log(`🔌 Found available port: ${port}`);
67
+ return port;
68
+ }
69
+ }
70
+
71
+ throw new Error(`No available ports found in range ${startPort}-${startPort + maxAttempts - 1}`);
72
+ }
73
+
74
+ /**
75
+ * Check if a specific port is available
76
+ * @param {number} port - Port to check
77
+ */
78
+ static checkPortAvailable(port) {
79
+ return new Promise((resolve) => {
80
+ const net = require('net');
81
+ const server = net.createServer();
82
+
83
+ server.listen(port, (err) => {
84
+ if (err) {
85
+ resolve(false);
86
+ } else {
87
+ server.once('close', () => resolve(true));
88
+ server.close();
89
+ }
90
+ });
91
+
92
+ server.on('error', () => resolve(false));
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Generate WebSocket server configuration
98
+ * @param {number} port - Port for WebSocket server
99
+ */
100
+ static generateWebSocketConfig(port) {
101
+ const localIP = this.getLocalIP();
102
+
103
+ return {
104
+ host: '0.0.0.0', // Bind to all interfaces
105
+ port: port,
106
+ localIP: localIP,
107
+ wsUrl: `ws://${localIP}:${port}`,
108
+ httpsWsUrl: `wss://${localIP}:${port}` // For future HTTPS support
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Display network connectivity information
114
+ * @param {object} config - WebSocket configuration
115
+ */
116
+ static displayNetworkInfo(config) {
117
+ console.log('');
118
+ console.log('🌐 Network Configuration:');
119
+ console.log(` Local IP: ${config.localIP}`);
120
+ console.log(` WebSocket URL: ${config.wsUrl}`);
121
+ console.log(` Binding: ${config.host}:${config.port}`);
122
+ console.log('');
123
+ console.log('📱 Access from other devices:');
124
+ console.log(` Use this URL in browser: https://shellmirror.app`);
125
+ console.log(` Mac agent WebSocket: ${config.wsUrl}`);
126
+ console.log('');
127
+ }
128
+
129
+ /**
130
+ * Validate WebSocket URL format
131
+ * @param {string} wsUrl - WebSocket URL to validate
132
+ */
133
+ static validateWebSocketURL(wsUrl) {
134
+ try {
135
+ const url = new URL(wsUrl);
136
+ return (url.protocol === 'ws:' || url.protocol === 'wss:') &&
137
+ url.hostname &&
138
+ url.port;
139
+ } catch (error) {
140
+ return false;
141
+ }
142
+ }
143
+ }
144
+
145
+ module.exports = NetworkUtils;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shell-mirror",
3
- "version": "1.5.16",
3
+ "version": "1.5.18",
4
4
  "description": "Access your Mac shell from any device securely. Perfect for mobile coding with Claude Code CLI, Gemini CLI, and any shell tool.",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -54,7 +54,7 @@
54
54
  <!-- Version Footer -->
55
55
  <footer style="background: #ff6b35; color: white; text-align: center; padding: 10px 0; font-size: 0.8rem; position: fixed; bottom: 0; left: 0; right: 0; z-index: 1000;">
56
56
  <div style="max-width: 1200px; margin: 0 auto;">
57
- <p id="terminal-version-info">Terminal Mirror v1.5.14 • Loading...</p>
57
+ <p id="terminal-version-info">Terminal Mirror • Loading version...</p>
58
58
  </div>
59
59
  </footer>
60
60
 
@@ -54,6 +54,7 @@ let dataChannel;
54
54
  let user;
55
55
  let AGENT_ID;
56
56
  let CLIENT_ID;
57
+ let SELECTED_AGENT; // Store full agent data including WebSocket URL
57
58
 
58
59
  // Auto-discover agents on page load
59
60
  window.addEventListener('load', () => {
@@ -67,13 +68,21 @@ async function loadVersionInfo() {
67
68
  const response = await fetch('/build-info.json');
68
69
  const buildInfo = await response.json();
69
70
  const versionElement = document.getElementById('terminal-version-info');
71
+ const footerElement = versionElement?.parentElement?.parentElement; // Get the footer element
72
+
70
73
  if (versionElement && buildInfo) {
71
74
  const buildDate = new Date(buildInfo.buildTime).toLocaleDateString();
72
75
  versionElement.textContent = `Terminal Mirror v${buildInfo.version} • Built ${buildDate}`;
76
+
77
+ // Apply random footer color from build info
78
+ if (footerElement && buildInfo.footerColor) {
79
+ footerElement.style.background = buildInfo.footerColor;
80
+ console.log(`🎨 Applied footer color: ${buildInfo.footerColor}`);
81
+ }
73
82
  }
74
83
  } catch (error) {
75
84
  console.log('Could not load build info for terminal:', error);
76
- // Keep default version if build-info.json not available
85
+ // Keep default version and color if build-info.json not available
77
86
  }
78
87
  }
79
88
 
@@ -92,8 +101,17 @@ connectBtn.onclick = () => {
92
101
  startConnection();
93
102
  };
94
103
 
95
- function connectToAgent(agentId) {
96
- AGENT_ID = agentId;
104
+ function connectToAgent(agent) {
105
+ if (typeof agent === 'string') {
106
+ // Fallback for manual connection - agent is just the ID
107
+ AGENT_ID = agent;
108
+ SELECTED_AGENT = { id: agent, websocketUrl: null };
109
+ } else {
110
+ // Full agent object from discovery
111
+ AGENT_ID = agent.id;
112
+ SELECTED_AGENT = agent;
113
+ }
114
+ console.log('[CLIENT] 🔗 Connecting to agent:', SELECTED_AGENT);
97
115
  startConnection();
98
116
  }
99
117
 
@@ -185,8 +203,8 @@ function displayAvailableAgents(agents) {
185
203
  agentDiv.style.background = '#333';
186
204
  };
187
205
  agentDiv.onclick = () => {
188
- console.log(`[DISCOVERY] 🖱️ User clicked on agent: ${agent.id}`);
189
- connectToAgent(agent.id);
206
+ console.log(`[DISCOVERY] 🖱️ User clicked on agent: ${agent.id}`, agent);
207
+ connectToAgent(agent);
190
208
  };
191
209
 
192
210
  agentList.appendChild(agentDiv);
@@ -208,8 +226,19 @@ function displayAvailableAgents(agents) {
208
226
 
209
227
  async function initialize() {
210
228
  console.log('[CLIENT] 🚀 Initializing WebRTC connection to agent:', AGENT_ID);
211
- // WebSocket server on same domain as web app
212
- const signalingUrl = (window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host;
229
+ console.log('[CLIENT] 📋 Selected agent data:', SELECTED_AGENT);
230
+
231
+ // Determine WebSocket URL - use agent's local WebSocket server if available
232
+ let signalingUrl;
233
+ if (SELECTED_AGENT && SELECTED_AGENT.websocketUrl) {
234
+ signalingUrl = SELECTED_AGENT.websocketUrl;
235
+ console.log('[CLIENT] 🌐 Using agent\'s local WebSocket server:', signalingUrl);
236
+ } else {
237
+ // Fallback to web app domain (old behavior)
238
+ signalingUrl = (window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host;
239
+ console.log('[CLIENT] 🌐 Using fallback WebSocket server:', signalingUrl);
240
+ }
241
+
213
242
  ws = new WebSocket(`${signalingUrl}?role=client`);
214
243
 
215
244
  ws.onopen = () => {
package/public/index.html CHANGED
@@ -655,7 +655,7 @@
655
655
  <!-- Version Footer -->
656
656
  <footer style="background: #ff6b35; color: white; text-align: center; padding: 20px 0; font-size: 0.8rem;">
657
657
  <div class="container">
658
- <p id="version-info">Shell Mirror v1.5.11 • Loading build info...</p>
658
+ <p id="version-info">Shell Mirror • Loading version...</p>
659
659
  </div>
660
660
  </footer>
661
661
  </main>
@@ -695,9 +695,17 @@
695
695
  const response = await fetch('/build-info.json');
696
696
  const buildInfo = await response.json();
697
697
  const versionElement = document.getElementById('version-info');
698
+ const footerElement = versionElement?.parentElement?.parentElement; // Get the footer element
699
+
698
700
  if (versionElement && buildInfo) {
699
701
  const buildDate = new Date(buildInfo.buildTime).toLocaleDateString();
700
702
  versionElement.textContent = `Shell Mirror v${buildInfo.version} • Built ${buildDate}`;
703
+
704
+ // Apply random footer color from build info
705
+ if (footerElement && buildInfo.footerColor) {
706
+ footerElement.style.background = buildInfo.footerColor;
707
+ console.log(`🎨 Applied footer color: ${buildInfo.footerColor}`);
708
+ }
701
709
  }
702
710
  } catch (error) {
703
711
  console.log('Could not load build info:', error);
package/server.js CHANGED
@@ -255,10 +255,15 @@ function handleSignalingMessage(ws, rawMessage, senderRole) {
255
255
  }
256
256
 
257
257
  // --- Server Startup ---
258
- const PORT = process.env.PORT || 3000;
259
- const HOST = process.env.HOST || '0.0.0.0';
258
+ const PORT = process.env.WS_PORT || process.env.PORT || 8080;
259
+ const HOST = process.env.WS_HOST || process.env.HOST || '0.0.0.0';
260
+ const LOCAL_IP = process.env.WS_LOCAL_IP || process.env.LOCAL_IP || 'localhost';
261
+
260
262
  server.listen(PORT, HOST, () => {
261
- logToFile(`✅ Signaling server is running on ${process.env.BASE_URL}`);
263
+ const baseUrl = process.env.BASE_URL || `http://${LOCAL_IP}:${PORT}`;
264
+ logToFile(`✅ Local WebSocket server is running on ${baseUrl}`);
265
+ logToFile(`🌐 WebSocket URL: ws://${LOCAL_IP}:${PORT}`);
266
+ logToFile(`🔧 Binding to: ${HOST}:${PORT}`);
262
267
  });
263
268
 
264
269
  // Graceful shutdown