shell-mirror 1.5.17 → 1.5.19

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.17",
3
+ "version": "1.5.19",
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,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', () => {
@@ -100,8 +101,17 @@ connectBtn.onclick = () => {
100
101
  startConnection();
101
102
  };
102
103
 
103
- function connectToAgent(agentId) {
104
- 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);
105
115
  startConnection();
106
116
  }
107
117
 
@@ -193,8 +203,8 @@ function displayAvailableAgents(agents) {
193
203
  agentDiv.style.background = '#333';
194
204
  };
195
205
  agentDiv.onclick = () => {
196
- console.log(`[DISCOVERY] 🖱️ User clicked on agent: ${agent.id}`);
197
- connectToAgent(agent.id);
206
+ console.log(`[DISCOVERY] 🖱️ User clicked on agent: ${agent.id}`, agent);
207
+ connectToAgent(agent);
198
208
  };
199
209
 
200
210
  agentList.appendChild(agentDiv);
@@ -216,8 +226,19 @@ function displayAvailableAgents(agents) {
216
226
 
217
227
  async function initialize() {
218
228
  console.log('[CLIENT] 🚀 Initializing WebRTC connection to agent:', AGENT_ID);
219
- // WebSocket server on same domain as web app
220
- 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
+
221
242
  ws = new WebSocket(`${signalingUrl}?role=client`);
222
243
 
223
244
  ws.onopen = () => {
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