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
JavaScript
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();