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.
222 lines (189 loc) • 6.64 kB
JavaScript
const inquirer = require('inquirer');
const fs = require('fs').promises;
const path = require('path');
const os = require('os');
const crypto = require('crypto');
class SetupWizard {
constructor() {
this.configDir = path.join(os.homedir(), '.terminal-mirror');
this.configFile = path.join(this.configDir, 'config.json');
this.envFile = path.join(process.cwd(), '.env');
}
async run(options = {}) {
console.log('🚀 Welcome to Terminal Mirror Setup!');
console.log('');
console.log('This wizard will help you configure Google OAuth and get Terminal Mirror running.');
console.log('');
// Check if already configured
if (!options.force && await this.isConfigured()) {
const { proceed } = await inquirer.prompt([{
type: 'confirm',
name: 'proceed',
message: 'Terminal Mirror is already configured. Reconfigure?',
default: false
}]);
if (!proceed) {
console.log('Setup cancelled.');
return;
}
}
console.log('📋 First, you\'ll need Google OAuth credentials.');
console.log('');
console.log('1. Go to: https://console.cloud.google.com/');
console.log('2. Create a new project or select existing one');
console.log('3. Enable the Google People API');
console.log('4. Create OAuth 2.0 credentials (Web application)');
console.log('5. Add authorized origins and redirect URIs');
console.log('');
const { ready } = await inquirer.prompt([{
type: 'confirm',
name: 'ready',
message: 'Have you completed the Google Cloud Console setup?',
default: false
}]);
if (!ready) {
console.log('');
console.log('Please complete the Google Cloud Console setup first.');
console.log('Detailed instructions: https://github.com/yourusername/terminal-mirror#setup');
return;
}
// Collect configuration
const config = await this.collectConfig();
// Save configuration
await this.saveConfig(config);
// Run verification
await this.verifySetup(config);
console.log('');
console.log('✅ Setup completed successfully!');
console.log('');
console.log('Next steps:');
console.log('1. Run: terminal-mirror start');
console.log(`2. Open: ${config.baseUrl}`);
console.log('3. Log in with your Google account');
console.log('');
}
async collectConfig() {
const questions = [
{
type: 'input',
name: 'clientId',
message: 'Google Client ID:',
validate: (input) => input.length > 0 || 'Client ID is required'
},
{
type: 'password',
name: 'clientSecret',
message: 'Google Client Secret:',
validate: (input) => input.length > 0 || 'Client Secret is required'
},
{
type: 'list',
name: 'environment',
message: 'Environment:',
choices: ['Development (localhost)', 'Production (custom domain)'],
default: 'Development (localhost)'
}
];
const answers = await inquirer.prompt(questions);
let baseUrl, port = '3000';
if (answers.environment === 'Development (localhost)') {
const portQuestion = await inquirer.prompt([{
type: 'input',
name: 'port',
message: 'Port number:',
default: '3000',
validate: (input) => {
const port = parseInt(input);
return (port > 0 && port < 65536) || 'Please enter a valid port number';
}
}]);
port = portQuestion.port;
baseUrl = `http://localhost:${port}`;
} else {
const domainQuestion = await inquirer.prompt([{
type: 'input',
name: 'domain',
message: 'Your domain (e.g., example.com):',
validate: (input) => input.length > 0 || 'Domain is required'
}]);
baseUrl = `https://${domainQuestion.domain}`;
}
// Generate secure session secret
const sessionSecret = crypto.randomBytes(32).toString('hex');
return {
clientId: answers.clientId,
clientSecret: answers.clientSecret,
baseUrl,
port,
sessionSecret,
environment: answers.environment
};
}
async saveConfig(config) {
// Ensure config directory exists
await fs.mkdir(this.configDir, { recursive: true });
// Save to user config
const userConfig = {
baseUrl: config.baseUrl,
port: config.port,
environment: config.environment,
setupDate: new Date().toISOString()
};
await fs.writeFile(this.configFile, JSON.stringify(userConfig, null, 2));
// Create .env file
const envContent = `# Terminal Mirror Configuration
# Generated by setup wizard on ${new Date().toISOString()}
BASE_URL=${config.baseUrl}
PORT=${config.port}
HOST=0.0.0.0
GOOGLE_CLIENT_ID=${config.clientId}
GOOGLE_CLIENT_SECRET=${config.clientSecret}
SESSION_SECRET=${config.sessionSecret}
NODE_ENV=${config.environment === 'Production (custom domain)' ? 'production' : 'development'}
`;
await fs.writeFile(this.envFile, envContent);
console.log(`✅ Configuration saved to ${this.envFile}`);
}
async verifySetup(config) {
console.log('');
console.log('🔍 Verifying setup...');
// Check if all required files exist
const checks = [
{ name: 'Configuration file', path: this.configFile },
{ name: 'Environment file', path: this.envFile }
];
for (const check of checks) {
try {
await fs.access(check.path);
console.log(`✅ ${check.name} exists`);
} catch (error) {
console.log(`❌ ${check.name} missing`);
throw new Error(`Setup verification failed: ${check.name} not found`);
}
}
// Validate environment variables
require('dotenv').config({ path: this.envFile });
const required = ['BASE_URL', 'GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET', 'SESSION_SECRET'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
console.log(`❌ Missing environment variables: ${missing.join(', ')}`);
throw new Error('Environment validation failed');
}
console.log('✅ Environment variables configured');
// Test OAuth configuration format
if (!config.clientId.includes('.googleusercontent.com')) {
console.log('⚠️ Warning: Client ID format looks unusual');
}
console.log('✅ Setup verification completed');
}
async isConfigured() {
try {
await fs.access(this.configFile);
await fs.access(this.envFile);
return true;
} catch {
return false;
}
}
}
module.exports = new SetupWizard();