UNPKG

shell-mirror

Version:

Access your Mac shell from any device securely. Perfect for mobile coding with Claude Code CLI, Gemini CLI, and any shell tool.

595 lines (512 loc) 18.3 kB
const fs = require('fs').promises; const path = require('path'); const os = require('os'); const crypto = require('crypto'); const { spawn } = require('child_process'); const https = require('https'); const querystring = require('querystring'); class AutoStart { constructor() { this.configDir = path.join(os.homedir(), '.shell-mirror'); this.authFile = path.join(this.configDir, 'auth.json'); this.configFile = path.join(this.configDir, 'config.json'); this.envFile = path.join(process.cwd(), '.env'); // Shell Mirror OAuth App credentials (production) this.oauthConfig = { clientId: '804759223392-i5nv5csn1o6siqr760c99l2a9k4sammp.apps.googleusercontent.com', clientSecret: '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', redirectUri: 'http://localhost:8080/auth/google/callback' }; } async run() { console.log('🚀 Starting Shell Mirror...'); console.log(''); try { // Ensure config directory exists await fs.mkdir(this.configDir, { recursive: true }); // Check authentication status const authInfo = await this.checkAuth(); if (!authInfo) { console.log('🔐 Not logged in. Opening browser for Google authentication...'); await this.initiateLogin(); return; } // Start the server await this.startServer(authInfo); } catch (error) { console.error('❌ Failed to start Shell Mirror:', error.message); process.exit(1); } } async checkAuth() { try { const authData = await fs.readFile(this.authFile, 'utf8'); const auth = JSON.parse(authData); // Check if token is still valid (basic check) if (auth.accessToken && auth.email && auth.expiresAt) { const now = Date.now(); if (now < auth.expiresAt) { return auth; } } return null; } catch (error) { return null; } } async initiateLogin() { console.log(''); console.log('🔐 Opening browser for Google authentication...'); console.log(''); try { // Start OAuth callback server console.log('🔄 Starting OAuth callback server...'); const server = await this.createOAuthCallbackServer(); // Open browser for OAuth const authUrl = this.buildAuthUrl(); console.log('🌐 Opening browser for login...'); await this.openBrowser(authUrl); console.log(''); console.log('👤 Please complete the Google login in your browser'); console.log(' (If browser didn\'t open, visit: ' + authUrl + ')'); console.log(''); } catch (error) { console.error('❌ Failed to start OAuth flow:', error.message); process.exit(1); } } buildAuthUrl() { const params = new URLSearchParams({ client_id: this.oauthConfig.clientId, redirect_uri: this.oauthConfig.redirectUri, response_type: 'code', scope: 'openid email profile', access_type: 'offline', prompt: 'consent' }); return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`; } async createOAuthCallbackServer() { const express = require('express'); const app = express(); return new Promise((resolve, reject) => { let serverInstance; app.get('/auth/google/callback', async (req, res) => { try { const code = req.query.code; const error = req.query.error; if (error) { throw new Error(`OAuth error: ${error}`); } if (code) { console.log('📝 Received authorization code, exchanging for tokens...'); // Exchange code for tokens const tokens = await this.exchangeCodeForTokens(code); await this.saveAuth(tokens); res.send(` <html> <head> <title>Shell Mirror - Login Successful</title> <style> body { font-family: Arial, sans-serif; text-align: center; padding: 50px; background: #f5f5f5; } .container { background: white; padding: 40px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); max-width: 500px; margin: 0 auto; } h2 { color: #4CAF50; margin-bottom: 20px; } p { color: #666; margin-bottom: 15px; } .email { color: #333; font-weight: bold; } </style> </head> <body> <div class="container"> <h2>✅ Login Successful!</h2> <p>Welcome, <span class="email">${tokens.email}</span></p> <p>Shell Mirror is now starting with your account.</p> <p>You can close this window and return to your terminal.</p> <p><small>Or visit <strong>https://shellmirror.app</strong> on your phone to access your terminal.</small></p> </div> <script> setTimeout(() => { window.close(); }, 3000); </script> </body> </html> `); // Close the temporary server and restart with authenticated state setTimeout(() => { console.log(`✅ Login successful! Welcome ${tokens.email}`); serverInstance.close(() => { this.startAuthenticatedServer(tokens); }); }, 1000); } else { throw new Error('No authorization code received'); } } catch (error) { console.error('❌ OAuth callback error:', error.message); res.send(` <html> <head> <title>Shell Mirror - Login Failed</title> <style> body { font-family: Arial, sans-serif; text-align: center; padding: 50px; background: #f5f5f5; } .container { background: white; padding: 40px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); max-width: 500px; margin: 0 auto; } h2 { color: #f44336; margin-bottom: 20px; } p { color: #666; margin-bottom: 15px; } </style> </head> <body> <div class="container"> <h2>❌ Login Failed</h2> <p>Error: ${error.message}</p> <p>Please try running 'shell-mirror' again.</p> </div> </body> </html> `); } }); serverInstance = app.listen(8080, (err) => { if (err) { reject(err); } else { resolve(serverInstance); } }); }); } async createProductionConfig() { // Create production environment file with real OAuth credentials const envContent = `# Shell Mirror Configuration # Auto-generated on ${new Date().toISOString()} BASE_URL=http://localhost:8080 PORT=8080 HOST=0.0.0.0 GOOGLE_CLIENT_ID=${this.oauthConfig.clientId} GOOGLE_CLIENT_SECRET=${this.oauthConfig.clientSecret} SESSION_SECRET=${crypto.randomBytes(32).toString('hex')} NODE_ENV=development `; await fs.writeFile(this.envFile, envContent); } async startServerForLogin() { console.log('🔄 Starting Shell Mirror server...'); console.log(''); // Find the package root directory (where server.js is located) const packageRoot = path.resolve(__dirname, '..'); const serverPath = path.join(packageRoot, 'server.js'); // Start the server in background const serverProcess = spawn('node', [serverPath], { stdio: 'pipe', cwd: path.dirname(this.envFile), env: { ...process.env } }); // Wait a moment for server to start setTimeout(() => { this.displayLoginInstructions(); this.openBrowser('http://localhost:8080'); }, 2000); // Handle Ctrl+C gracefully process.on('SIGINT', () => { console.log(''); console.log('🛑 Stopping Shell Mirror...'); serverProcess.kill('SIGINT'); process.exit(0); }); // Monitor server serverProcess.on('close', (code) => { if (code !== 0) { console.error(`❌ Server exited with code ${code}`); process.exit(code); } }); } displayLoginInstructions() { const pkg = require('../package.json'); console.log(`✅ Shell Mirror v${pkg.version} - First-time setup required`); console.log('Press Ctrl+C to stop'); } async exchangeCodeForTokens(code) { return new Promise((resolve, reject) => { const postData = querystring.stringify({ code: code, client_id: this.oauthConfig.clientId, client_secret: this.oauthConfig.clientSecret, redirect_uri: this.oauthConfig.redirectUri, grant_type: 'authorization_code' }); const options = { hostname: 'oauth2.googleapis.com', port: 443, path: '/token', method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(postData) } }; const req = https.request(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', async () => { try { const tokens = JSON.parse(data); if (tokens.access_token) { // Get user info const userInfo = await this.getUserInfo(tokens.access_token); resolve({ accessToken: tokens.access_token, refreshToken: tokens.refresh_token, email: userInfo.email, name: userInfo.name, expiresAt: Date.now() + (tokens.expires_in * 1000) }); } else { reject(new Error('Failed to get access token: ' + data)); } } catch (error) { reject(error); } }); }); req.on('error', (error) => { reject(error); }); req.write(postData); req.end(); }); } async getUserInfo(accessToken) { return new Promise((resolve, reject) => { const options = { hostname: 'www.googleapis.com', port: 443, path: '/oauth2/v2/userinfo', method: 'GET', headers: { 'Authorization': `Bearer ${accessToken}` } }; const req = https.request(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { const userInfo = JSON.parse(data); resolve(userInfo); } catch (error) { reject(error); } }); }); req.on('error', (error) => { reject(error); }); req.end(); }); } async saveAuth(authData) { await fs.writeFile(this.authFile, JSON.stringify(authData, null, 2)); console.log('🔐 Authentication saved'); } async startServer(authInfo) { // Create .env file with configuration await this.createEnvFile(authInfo); // Start the Express server console.log('🔄 Starting server...'); console.log(''); // Find the package root directory (where server.js is located) const packageRoot = path.resolve(__dirname, '..'); const serverPath = path.join(packageRoot, 'server.js'); // Start the main server process const serverProcess = spawn('node', [serverPath], { stdio: 'inherit', cwd: path.dirname(this.envFile), env: { ...process.env } }); // Display status information this.displayStatus(authInfo); // Handle Ctrl+C gracefully process.on('SIGINT', () => { console.log(''); console.log('🛑 Stopping Shell Mirror...'); serverProcess.kill('SIGINT'); process.exit(0); }); // Wait for server process serverProcess.on('close', (code) => { if (code !== 0) { console.error(`❌ Server exited with code ${code}`); process.exit(code); } }); } async createEnvFile(authInfo) { const envContent = `# Shell Mirror Configuration # Auto-generated on ${new Date().toISOString()} BASE_URL=http://localhost:8080 PORT=8080 HOST=0.0.0.0 GOOGLE_CLIENT_ID=${this.oauthConfig.clientId} GOOGLE_CLIENT_SECRET=${this.oauthConfig.clientSecret} SESSION_SECRET=${crypto.randomBytes(32).toString('hex')} NODE_ENV=development # User authentication USER_EMAIL=${authInfo.email} USER_NAME=${authInfo.name} ACCESS_TOKEN=${authInfo.accessToken} `; await fs.writeFile(this.envFile, envContent); } displayStatus(authInfo) { const pkg = require('../package.json'); console.log(`✅ Shell Mirror v${pkg.version} - Running (${authInfo.email})`); console.log('Press Ctrl+C to stop'); } async startAuthenticatedServer(authInfo) { console.log(''); console.log('🚀 Starting authenticated Shell Mirror server...'); // Create .env file with authentication await this.createEnvFile(authInfo); // Register this server as a Mac agent await this.registerAsMacAgent(authInfo); // Find the package root directory (where server.js is located) const packageRoot = path.resolve(__dirname, '..'); const serverPath = path.join(packageRoot, 'server.js'); // Start the main server process const serverProcess = spawn('node', [serverPath], { stdio: 'inherit', cwd: path.dirname(this.envFile), env: { ...process.env } }); // Display status information this.displayStatus(authInfo); // Handle Ctrl+C gracefully process.on('SIGINT', () => { console.log(''); console.log('🛑 Stopping Shell Mirror...'); serverProcess.kill('SIGINT'); process.exit(0); }); // Wait for server process serverProcess.on('close', (code) => { if (code !== 0) { console.error(`❌ Server exited with code ${code}`); process.exit(code); } }); } async registerAsMacAgent(authInfo) { console.log('🔗 Registering as Mac agent...'); console.log(` User: ${authInfo.email}`); console.log(` Machine: ${os.hostname()}`); try { const agentId = `local-${os.hostname()}-${Date.now()}`; const registrationData = { agentId: agentId, ownerEmail: authInfo.email, ownerName: authInfo.name, ownerToken: authInfo.accessToken, machineName: os.hostname(), agentVersion: '1.3.0', capabilities: ['terminal', 'websocket'], serverPort: 8080 }; console.log(` Agent ID: ${agentId}`); console.log(` Registering with: https://shellmirror.app/php-backend/api/agent-register.php`); const postData = JSON.stringify(registrationData); const options = { hostname: 'shellmirror.app', port: 443, path: '/php-backend/api/agent-register.php', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) } }; console.log(' Sending registration request...'); const response = await this.makeHttpsRequest(options, postData); console.log(` Response status: ${response.statusCode}`); console.log(` Response body: ${response.body}`); if (response.statusCode === 200) { try { const result = JSON.parse(response.body); if (result.success) { console.log(`✅ Registered as Mac agent: ${agentId}`); console.log(`📱 Your Mac is now accessible from https://shellmirror.app`); // Store agent ID for later use await fs.writeFile(path.join(this.configDir, 'agent.json'), JSON.stringify({ agentId: agentId, registeredAt: new Date().toISOString(), ownerEmail: authInfo.email })); console.log(` Agent info saved to: ${path.join(this.configDir, 'agent.json')}`); } else { console.log(`⚠️ Registration failed: ${result.message || 'Unknown error'}`); } } catch (parseError) { console.log(`⚠️ Failed to parse registration response: ${parseError.message}`); console.log(` Raw response: ${response.body}`); } } else { console.log(`⚠️ Registration failed with HTTP ${response.statusCode}`); console.log(` Response: ${response.body}`); console.log(' Server will still run locally'); } } catch (error) { console.log('⚠️ Mac agent registration failed:', error.message); console.log(` Error details: ${error.stack}`); console.log(' Server will run locally only'); } } async makeHttpsRequest(options, postData = null) { return new Promise((resolve, reject) => { const req = https.request(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { resolve({ statusCode: res.statusCode, headers: res.headers, body: data }); }); }); req.on('error', (error) => { reject(error); }); if (postData) { req.write(postData); } req.end(); }); } async openBrowser(url) { const platform = os.platform(); let command; switch (platform) { case 'darwin': // macOS command = 'open'; break; case 'win32': // Windows command = 'start'; break; default: // Linux and others command = 'xdg-open'; break; } try { spawn(command, [url], { detached: true, stdio: 'ignore' }); } catch (error) { console.log('⚠️ Could not open browser automatically.'); console.log(' Please visit: ' + url); } } } module.exports = new AutoStart();