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.

224 lines (186 loc) • 6.46 kB
const { spawn, exec } = require('child_process'); const fs = require('fs').promises; const path = require('path'); const os = require('os'); class ServerManager { constructor() { this.pidFile = path.join(os.homedir(), '.terminal-mirror', 'server.pid'); this.logFile = path.join(os.homedir(), '.terminal-mirror', 'server.log'); this.serverScript = path.join(__dirname, '..', 'server.js'); } async start(options = {}) { // Check if server is already running if (await this.isRunning()) { console.log('āš ļø Terminal Mirror server is already running'); await this.status(); return; } // Load configuration require('dotenv').config(); const port = options.port || process.env.PORT || 3000; const host = options.host || process.env.HOST || '0.0.0.0'; const baseUrl = process.env.BASE_URL || `http://localhost:${port}`; // Starting Terminal Mirror server // Ensure log directory exists await fs.mkdir(path.dirname(this.logFile), { recursive: true }); if (options.daemon) { // Start as daemon await this.startDaemon(port, host); } else { // Start in foreground await this.startForeground(port, host, baseUrl); } } async startForeground(port, host, baseUrl) { console.log(`šŸ“ Server URL: ${baseUrl}`); console.log(`šŸ”§ Host: ${host}:${port}`); console.log(''); console.log('Press Ctrl+C to stop the server'); console.log('─'.repeat(50)); console.log(''); // Start server process const serverProcess = spawn('node', [this.serverScript], { stdio: 'inherit', env: { ...process.env, PORT: port, HOST: host } }); // Handle graceful shutdown const shutdown = () => { console.log('\nšŸ›‘ Shutting down server...'); serverProcess.kill('SIGTERM'); process.exit(0); }; process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); serverProcess.on('error', (error) => { console.error('āŒ Failed to start server:', error.message); process.exit(1); }); serverProcess.on('exit', (code) => { if (code !== 0) { console.error(`āŒ Server exited with code ${code}`); process.exit(code); } }); } async startDaemon(port, host) { console.log('šŸ”§ Starting server as daemon...'); const serverProcess = spawn('node', [this.serverScript], { detached: true, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, PORT: port, HOST: host } }); // Save PID await fs.writeFile(this.pidFile, serverProcess.pid.toString()); // Redirect output to log file const logStream = await fs.open(this.logFile, 'a'); serverProcess.stdout.pipe(logStream.createWriteStream()); serverProcess.stderr.pipe(logStream.createWriteStream()); serverProcess.unref(); console.log(`āœ… Server started as daemon (PID: ${serverProcess.pid})`); console.log(`šŸ“ Logs: ${this.logFile}`); // Wait a moment to check if it started successfully await new Promise(resolve => setTimeout(resolve, 1000)); if (await this.isRunning()) { console.log('🌐 Server is running and accessible'); await this.status(); } else { console.log('āŒ Server failed to start'); process.exit(1); } } async stop() { console.log('šŸ›‘ Stopping Terminal Mirror server...'); if (!await this.isRunning()) { console.log('āš ļø Server is not running'); return; } try { const pid = await fs.readFile(this.pidFile, 'utf8'); process.kill(parseInt(pid), 'SIGTERM'); // Wait for process to stop await new Promise(resolve => setTimeout(resolve, 2000)); if (!await this.isRunning()) { console.log('āœ… Server stopped successfully'); // Clean up PID file try { await fs.unlink(this.pidFile); } catch (error) { // Ignore if file doesn't exist } } else { console.log('āš ļø Server may still be running'); } } catch (error) { console.error('āŒ Failed to stop server:', error.message); throw error; } } async status() { console.log('šŸ“Š Terminal Mirror Status'); console.log('─'.repeat(30)); const isRunning = await this.isRunning(); console.log(`Status: ${isRunning ? '🟢 Running' : 'šŸ”“ Stopped'}`); if (isRunning) { try { const pid = await fs.readFile(this.pidFile, 'utf8'); console.log(`PID: ${pid.trim()}`); // Get process info const processInfo = await this.getProcessInfo(parseInt(pid)); if (processInfo) { console.log(`Memory: ${processInfo.memory} MB`); console.log(`CPU: ${processInfo.cpu}%`); console.log(`Uptime: ${processInfo.uptime}`); } } catch (error) { console.log('PID: Unknown'); } } // Show configuration require('dotenv').config(); console.log(`URL: ${process.env.BASE_URL || 'Not configured'}`); console.log(`Port: ${process.env.PORT || 'Not configured'}`); // Check log file try { const stats = await fs.stat(this.logFile); console.log(`Log file: ${this.logFile} (${Math.round(stats.size / 1024)} KB)`); } catch (error) { console.log('Log file: Not found'); } console.log(''); if (isRunning) { console.log('🌐 Access your terminal at: ' + (process.env.BASE_URL || 'http://localhost:3000')); } else { console.log('šŸ’” Run "terminal-mirror start" to start the server'); } } async isRunning() { try { const pid = await fs.readFile(this.pidFile, 'utf8'); process.kill(parseInt(pid), 0); // Check if process exists return true; } catch (error) { return false; } } async getProcessInfo(pid) { return new Promise((resolve) => { exec(`ps -p ${pid} -o pid,pcpu,pmem,etime --no-headers`, (error, stdout) => { if (error) { resolve(null); return; } const parts = stdout.trim().split(/\s+/); if (parts.length >= 4) { resolve({ cpu: parseFloat(parts[1]), memory: Math.round(parseFloat(parts[2]) * 100) / 100, uptime: parts[3] }); } else { resolve(null); } }); }); } } module.exports = new ServerManager();