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