shell-mirror 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +272 -0
- package/auth.js +22 -0
- package/bin/shell-mirror +127 -0
- package/lib/config-manager.js +203 -0
- package/lib/health-checker.js +192 -0
- package/lib/server-manager.js +225 -0
- package/lib/setup-wizard.js +222 -0
- package/package.json +70 -0
- package/public/app/terminal.html +867 -0
- package/public/index.html +809 -0
- package/server.js +171 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { exec } = require('child_process');
|
|
5
|
+
const { promisify } = require('util');
|
|
6
|
+
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
|
|
9
|
+
class HealthChecker {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.configDir = path.join(os.homedir(), '.terminal-mirror');
|
|
12
|
+
this.envFile = path.join(process.cwd(), '.env');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async run() {
|
|
16
|
+
console.log('🏥 Terminal Mirror Health Check');
|
|
17
|
+
console.log('─'.repeat(40));
|
|
18
|
+
console.log('');
|
|
19
|
+
|
|
20
|
+
const checks = [
|
|
21
|
+
{ name: 'Node.js Version', check: () => this.checkNodeVersion() },
|
|
22
|
+
{ name: 'Configuration Files', check: () => this.checkConfigFiles() },
|
|
23
|
+
{ name: 'Environment Variables', check: () => this.checkEnvironment() },
|
|
24
|
+
{ name: 'Network Connectivity', check: () => this.checkNetwork() },
|
|
25
|
+
{ name: 'Dependencies', check: () => this.checkDependencies() },
|
|
26
|
+
{ name: 'Port Availability', check: () => this.checkPort() },
|
|
27
|
+
{ name: 'Google OAuth Setup', check: () => this.checkOAuth() },
|
|
28
|
+
{ name: 'Permissions', check: () => this.checkPermissions() }
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
let allPassed = true;
|
|
32
|
+
|
|
33
|
+
for (const check of checks) {
|
|
34
|
+
try {
|
|
35
|
+
const result = await check.check();
|
|
36
|
+
console.log(`✅ ${check.name}: ${result}`);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.log(`❌ ${check.name}: ${error.message}`);
|
|
39
|
+
allPassed = false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log('');
|
|
44
|
+
if (allPassed) {
|
|
45
|
+
console.log('🎉 All health checks passed! Terminal Mirror should work correctly.');
|
|
46
|
+
} else {
|
|
47
|
+
console.log('⚠️ Some health checks failed. Please address the issues above.');
|
|
48
|
+
console.log('');
|
|
49
|
+
console.log('Common solutions:');
|
|
50
|
+
console.log('• Run "terminal-mirror setup" to reconfigure');
|
|
51
|
+
console.log('• Check your Google OAuth credentials');
|
|
52
|
+
console.log('• Ensure the port is not in use by another application');
|
|
53
|
+
console.log('• Verify your internet connection');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async checkNodeVersion() {
|
|
58
|
+
const version = process.version;
|
|
59
|
+
const major = parseInt(version.split('.')[0].substring(1));
|
|
60
|
+
|
|
61
|
+
if (major < 14) {
|
|
62
|
+
throw new Error(`Node.js ${major} is too old. Requires Node.js 14 or newer.`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return `${version} (supported)`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async checkConfigFiles() {
|
|
69
|
+
const files = [
|
|
70
|
+
{ path: this.envFile, name: '.env' },
|
|
71
|
+
{ path: path.join(this.configDir, 'config.json'), name: 'user config' }
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
let found = 0;
|
|
75
|
+
for (const file of files) {
|
|
76
|
+
try {
|
|
77
|
+
await fs.access(file.path);
|
|
78
|
+
found++;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
// File doesn't exist
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (found === 0) {
|
|
85
|
+
throw new Error('No configuration files found. Run "terminal-mirror setup".');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return `${found}/${files.length} files found`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async checkEnvironment() {
|
|
92
|
+
require('dotenv').config({ path: this.envFile });
|
|
93
|
+
|
|
94
|
+
const required = [
|
|
95
|
+
'BASE_URL',
|
|
96
|
+
'GOOGLE_CLIENT_ID',
|
|
97
|
+
'GOOGLE_CLIENT_SECRET',
|
|
98
|
+
'SESSION_SECRET'
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const missing = required.filter(key => !process.env[key]);
|
|
102
|
+
|
|
103
|
+
if (missing.length > 0) {
|
|
104
|
+
throw new Error(`Missing variables: ${missing.join(', ')}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return 'All required variables set';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async checkNetwork() {
|
|
111
|
+
try {
|
|
112
|
+
// Test DNS resolution
|
|
113
|
+
await execAsync('nslookup google.com');
|
|
114
|
+
return 'Internet connectivity OK';
|
|
115
|
+
} catch (error) {
|
|
116
|
+
throw new Error('Cannot resolve DNS. Check internet connection.');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async checkDependencies() {
|
|
121
|
+
const packageJsonPath = path.join(__dirname, '..', 'package.json');
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
125
|
+
const dependencies = Object.keys(packageJson.dependencies || {});
|
|
126
|
+
|
|
127
|
+
// Check if node_modules exists
|
|
128
|
+
const nodeModulesPath = path.join(__dirname, '..', 'node_modules');
|
|
129
|
+
await fs.access(nodeModulesPath);
|
|
130
|
+
|
|
131
|
+
return `${dependencies.length} dependencies installed`;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
throw new Error('Dependencies not installed. Run "npm install".');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async checkPort() {
|
|
138
|
+
require('dotenv').config({ path: this.envFile });
|
|
139
|
+
const port = process.env.PORT || 3000;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const { stdout } = await execAsync(`lsof -i :${port}`);
|
|
143
|
+
if (stdout.trim()) {
|
|
144
|
+
throw new Error(`Port ${port} is already in use`);
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error.message.includes('already in use')) {
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
// lsof command failed (probably port is free)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return `Port ${port} is available`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async checkOAuth() {
|
|
157
|
+
require('dotenv').config({ path: this.envFile });
|
|
158
|
+
|
|
159
|
+
const clientId = process.env.GOOGLE_CLIENT_ID;
|
|
160
|
+
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
|
|
161
|
+
|
|
162
|
+
if (!clientId || !clientSecret) {
|
|
163
|
+
throw new Error('OAuth credentials not configured');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Basic format validation
|
|
167
|
+
if (!clientId.includes('.googleusercontent.com')) {
|
|
168
|
+
throw new Error('Client ID format appears invalid');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (clientSecret.length < 20) {
|
|
172
|
+
throw new Error('Client secret appears too short');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return 'OAuth credentials format looks correct';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async checkPermissions() {
|
|
179
|
+
// Check if we can write to config directory
|
|
180
|
+
try {
|
|
181
|
+
await fs.mkdir(this.configDir, { recursive: true });
|
|
182
|
+
const testFile = path.join(this.configDir, 'test-permissions.tmp');
|
|
183
|
+
await fs.writeFile(testFile, 'test');
|
|
184
|
+
await fs.unlink(testFile);
|
|
185
|
+
return 'File system permissions OK';
|
|
186
|
+
} catch (error) {
|
|
187
|
+
throw new Error('Cannot write to config directory: ' + error.message);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = new HealthChecker();
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
const { spawn, exec } = require('child_process');
|
|
2
|
+
const fs = require('fs').promises;
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
|
|
6
|
+
class ServerManager {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.pidFile = path.join(os.homedir(), '.terminal-mirror', 'server.pid');
|
|
9
|
+
this.logFile = path.join(os.homedir(), '.terminal-mirror', 'server.log');
|
|
10
|
+
this.serverScript = path.join(__dirname, '..', 'server.js');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async start(options = {}) {
|
|
14
|
+
// Check if server is already running
|
|
15
|
+
if (await this.isRunning()) {
|
|
16
|
+
console.log('⚠️ Terminal Mirror server is already running');
|
|
17
|
+
await this.status();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Load configuration
|
|
22
|
+
require('dotenv').config();
|
|
23
|
+
|
|
24
|
+
const port = options.port || process.env.PORT || 3000;
|
|
25
|
+
const host = options.host || process.env.HOST || '0.0.0.0';
|
|
26
|
+
const baseUrl = process.env.BASE_URL || `http://localhost:${port}`;
|
|
27
|
+
|
|
28
|
+
console.log('🚀 Starting Terminal Mirror server...');
|
|
29
|
+
console.log('');
|
|
30
|
+
|
|
31
|
+
// Ensure log directory exists
|
|
32
|
+
await fs.mkdir(path.dirname(this.logFile), { recursive: true });
|
|
33
|
+
|
|
34
|
+
if (options.daemon) {
|
|
35
|
+
// Start as daemon
|
|
36
|
+
await this.startDaemon(port, host);
|
|
37
|
+
} else {
|
|
38
|
+
// Start in foreground
|
|
39
|
+
await this.startForeground(port, host, baseUrl);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async startForeground(port, host, baseUrl) {
|
|
44
|
+
console.log(`📍 Server URL: ${baseUrl}`);
|
|
45
|
+
console.log(`🔧 Host: ${host}:${port}`);
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log('Press Ctrl+C to stop the server');
|
|
48
|
+
console.log('─'.repeat(50));
|
|
49
|
+
console.log('');
|
|
50
|
+
|
|
51
|
+
// Start server process
|
|
52
|
+
const serverProcess = spawn('node', [this.serverScript], {
|
|
53
|
+
stdio: 'inherit',
|
|
54
|
+
env: { ...process.env, PORT: port, HOST: host }
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Handle graceful shutdown
|
|
58
|
+
const shutdown = () => {
|
|
59
|
+
console.log('\n🛑 Shutting down server...');
|
|
60
|
+
serverProcess.kill('SIGTERM');
|
|
61
|
+
process.exit(0);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
process.on('SIGINT', shutdown);
|
|
65
|
+
process.on('SIGTERM', shutdown);
|
|
66
|
+
|
|
67
|
+
serverProcess.on('error', (error) => {
|
|
68
|
+
console.error('❌ Failed to start server:', error.message);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
serverProcess.on('exit', (code) => {
|
|
73
|
+
if (code !== 0) {
|
|
74
|
+
console.error(`❌ Server exited with code ${code}`);
|
|
75
|
+
process.exit(code);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async startDaemon(port, host) {
|
|
81
|
+
console.log('🔧 Starting server as daemon...');
|
|
82
|
+
|
|
83
|
+
const serverProcess = spawn('node', [this.serverScript], {
|
|
84
|
+
detached: true,
|
|
85
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
86
|
+
env: { ...process.env, PORT: port, HOST: host }
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Save PID
|
|
90
|
+
await fs.writeFile(this.pidFile, serverProcess.pid.toString());
|
|
91
|
+
|
|
92
|
+
// Redirect output to log file
|
|
93
|
+
const logStream = await fs.open(this.logFile, 'a');
|
|
94
|
+
serverProcess.stdout.pipe(logStream.createWriteStream());
|
|
95
|
+
serverProcess.stderr.pipe(logStream.createWriteStream());
|
|
96
|
+
|
|
97
|
+
serverProcess.unref();
|
|
98
|
+
|
|
99
|
+
console.log(`✅ Server started as daemon (PID: ${serverProcess.pid})`);
|
|
100
|
+
console.log(`📝 Logs: ${this.logFile}`);
|
|
101
|
+
|
|
102
|
+
// Wait a moment to check if it started successfully
|
|
103
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
104
|
+
|
|
105
|
+
if (await this.isRunning()) {
|
|
106
|
+
console.log('🌐 Server is running and accessible');
|
|
107
|
+
await this.status();
|
|
108
|
+
} else {
|
|
109
|
+
console.log('❌ Server failed to start');
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async stop() {
|
|
115
|
+
console.log('🛑 Stopping Terminal Mirror server...');
|
|
116
|
+
|
|
117
|
+
if (!await this.isRunning()) {
|
|
118
|
+
console.log('⚠️ Server is not running');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const pid = await fs.readFile(this.pidFile, 'utf8');
|
|
124
|
+
process.kill(parseInt(pid), 'SIGTERM');
|
|
125
|
+
|
|
126
|
+
// Wait for process to stop
|
|
127
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
128
|
+
|
|
129
|
+
if (!await this.isRunning()) {
|
|
130
|
+
console.log('✅ Server stopped successfully');
|
|
131
|
+
// Clean up PID file
|
|
132
|
+
try {
|
|
133
|
+
await fs.unlink(this.pidFile);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
// Ignore if file doesn't exist
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
console.log('⚠️ Server may still be running');
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error('❌ Failed to stop server:', error.message);
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async status() {
|
|
147
|
+
console.log('📊 Terminal Mirror Status');
|
|
148
|
+
console.log('─'.repeat(30));
|
|
149
|
+
|
|
150
|
+
const isRunning = await this.isRunning();
|
|
151
|
+
console.log(`Status: ${isRunning ? '🟢 Running' : '🔴 Stopped'}`);
|
|
152
|
+
|
|
153
|
+
if (isRunning) {
|
|
154
|
+
try {
|
|
155
|
+
const pid = await fs.readFile(this.pidFile, 'utf8');
|
|
156
|
+
console.log(`PID: ${pid.trim()}`);
|
|
157
|
+
|
|
158
|
+
// Get process info
|
|
159
|
+
const processInfo = await this.getProcessInfo(parseInt(pid));
|
|
160
|
+
if (processInfo) {
|
|
161
|
+
console.log(`Memory: ${processInfo.memory} MB`);
|
|
162
|
+
console.log(`CPU: ${processInfo.cpu}%`);
|
|
163
|
+
console.log(`Uptime: ${processInfo.uptime}`);
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.log('PID: Unknown');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Show configuration
|
|
171
|
+
require('dotenv').config();
|
|
172
|
+
console.log(`URL: ${process.env.BASE_URL || 'Not configured'}`);
|
|
173
|
+
console.log(`Port: ${process.env.PORT || 'Not configured'}`);
|
|
174
|
+
|
|
175
|
+
// Check log file
|
|
176
|
+
try {
|
|
177
|
+
const stats = await fs.stat(this.logFile);
|
|
178
|
+
console.log(`Log file: ${this.logFile} (${Math.round(stats.size / 1024)} KB)`);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.log('Log file: Not found');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log('');
|
|
184
|
+
|
|
185
|
+
if (isRunning) {
|
|
186
|
+
console.log('🌐 Access your terminal at: ' + (process.env.BASE_URL || 'http://localhost:3000'));
|
|
187
|
+
} else {
|
|
188
|
+
console.log('💡 Run "terminal-mirror start" to start the server');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async isRunning() {
|
|
193
|
+
try {
|
|
194
|
+
const pid = await fs.readFile(this.pidFile, 'utf8');
|
|
195
|
+
process.kill(parseInt(pid), 0); // Check if process exists
|
|
196
|
+
return true;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async getProcessInfo(pid) {
|
|
203
|
+
return new Promise((resolve) => {
|
|
204
|
+
exec(`ps -p ${pid} -o pid,pcpu,pmem,etime --no-headers`, (error, stdout) => {
|
|
205
|
+
if (error) {
|
|
206
|
+
resolve(null);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const parts = stdout.trim().split(/\s+/);
|
|
211
|
+
if (parts.length >= 4) {
|
|
212
|
+
resolve({
|
|
213
|
+
cpu: parseFloat(parts[1]),
|
|
214
|
+
memory: Math.round(parseFloat(parts[2]) * 100) / 100,
|
|
215
|
+
uptime: parts[3]
|
|
216
|
+
});
|
|
217
|
+
} else {
|
|
218
|
+
resolve(null);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = new ServerManager();
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
const inquirer = require('inquirer');
|
|
2
|
+
const fs = require('fs').promises;
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
|
|
7
|
+
class SetupWizard {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.configDir = path.join(os.homedir(), '.terminal-mirror');
|
|
10
|
+
this.configFile = path.join(this.configDir, 'config.json');
|
|
11
|
+
this.envFile = path.join(process.cwd(), '.env');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async run(options = {}) {
|
|
15
|
+
console.log('🚀 Welcome to Terminal Mirror Setup!');
|
|
16
|
+
console.log('');
|
|
17
|
+
console.log('This wizard will help you configure Google OAuth and get Terminal Mirror running.');
|
|
18
|
+
console.log('');
|
|
19
|
+
|
|
20
|
+
// Check if already configured
|
|
21
|
+
if (!options.force && await this.isConfigured()) {
|
|
22
|
+
const { proceed } = await inquirer.prompt([{
|
|
23
|
+
type: 'confirm',
|
|
24
|
+
name: 'proceed',
|
|
25
|
+
message: 'Terminal Mirror is already configured. Reconfigure?',
|
|
26
|
+
default: false
|
|
27
|
+
}]);
|
|
28
|
+
|
|
29
|
+
if (!proceed) {
|
|
30
|
+
console.log('Setup cancelled.');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log('📋 First, you\'ll need Google OAuth credentials.');
|
|
36
|
+
console.log('');
|
|
37
|
+
console.log('1. Go to: https://console.cloud.google.com/');
|
|
38
|
+
console.log('2. Create a new project or select existing one');
|
|
39
|
+
console.log('3. Enable the Google People API');
|
|
40
|
+
console.log('4. Create OAuth 2.0 credentials (Web application)');
|
|
41
|
+
console.log('5. Add authorized origins and redirect URIs');
|
|
42
|
+
console.log('');
|
|
43
|
+
|
|
44
|
+
const { ready } = await inquirer.prompt([{
|
|
45
|
+
type: 'confirm',
|
|
46
|
+
name: 'ready',
|
|
47
|
+
message: 'Have you completed the Google Cloud Console setup?',
|
|
48
|
+
default: false
|
|
49
|
+
}]);
|
|
50
|
+
|
|
51
|
+
if (!ready) {
|
|
52
|
+
console.log('');
|
|
53
|
+
console.log('Please complete the Google Cloud Console setup first.');
|
|
54
|
+
console.log('Detailed instructions: https://github.com/yourusername/terminal-mirror#setup');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Collect configuration
|
|
59
|
+
const config = await this.collectConfig();
|
|
60
|
+
|
|
61
|
+
// Save configuration
|
|
62
|
+
await this.saveConfig(config);
|
|
63
|
+
|
|
64
|
+
// Run verification
|
|
65
|
+
await this.verifySetup(config);
|
|
66
|
+
|
|
67
|
+
console.log('');
|
|
68
|
+
console.log('✅ Setup completed successfully!');
|
|
69
|
+
console.log('');
|
|
70
|
+
console.log('Next steps:');
|
|
71
|
+
console.log('1. Run: terminal-mirror start');
|
|
72
|
+
console.log(`2. Open: ${config.baseUrl}`);
|
|
73
|
+
console.log('3. Log in with your Google account');
|
|
74
|
+
console.log('');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async collectConfig() {
|
|
78
|
+
const questions = [
|
|
79
|
+
{
|
|
80
|
+
type: 'input',
|
|
81
|
+
name: 'clientId',
|
|
82
|
+
message: 'Google Client ID:',
|
|
83
|
+
validate: (input) => input.length > 0 || 'Client ID is required'
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: 'password',
|
|
87
|
+
name: 'clientSecret',
|
|
88
|
+
message: 'Google Client Secret:',
|
|
89
|
+
validate: (input) => input.length > 0 || 'Client Secret is required'
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: 'list',
|
|
93
|
+
name: 'environment',
|
|
94
|
+
message: 'Environment:',
|
|
95
|
+
choices: ['Development (localhost)', 'Production (custom domain)'],
|
|
96
|
+
default: 'Development (localhost)'
|
|
97
|
+
}
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const answers = await inquirer.prompt(questions);
|
|
101
|
+
|
|
102
|
+
let baseUrl, port = '3000';
|
|
103
|
+
|
|
104
|
+
if (answers.environment === 'Development (localhost)') {
|
|
105
|
+
const portQuestion = await inquirer.prompt([{
|
|
106
|
+
type: 'input',
|
|
107
|
+
name: 'port',
|
|
108
|
+
message: 'Port number:',
|
|
109
|
+
default: '3000',
|
|
110
|
+
validate: (input) => {
|
|
111
|
+
const port = parseInt(input);
|
|
112
|
+
return (port > 0 && port < 65536) || 'Please enter a valid port number';
|
|
113
|
+
}
|
|
114
|
+
}]);
|
|
115
|
+
port = portQuestion.port;
|
|
116
|
+
baseUrl = `http://localhost:${port}`;
|
|
117
|
+
} else {
|
|
118
|
+
const domainQuestion = await inquirer.prompt([{
|
|
119
|
+
type: 'input',
|
|
120
|
+
name: 'domain',
|
|
121
|
+
message: 'Your domain (e.g., example.com):',
|
|
122
|
+
validate: (input) => input.length > 0 || 'Domain is required'
|
|
123
|
+
}]);
|
|
124
|
+
baseUrl = `https://${domainQuestion.domain}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Generate secure session secret
|
|
128
|
+
const sessionSecret = crypto.randomBytes(32).toString('hex');
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
clientId: answers.clientId,
|
|
132
|
+
clientSecret: answers.clientSecret,
|
|
133
|
+
baseUrl,
|
|
134
|
+
port,
|
|
135
|
+
sessionSecret,
|
|
136
|
+
environment: answers.environment
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async saveConfig(config) {
|
|
141
|
+
// Ensure config directory exists
|
|
142
|
+
await fs.mkdir(this.configDir, { recursive: true });
|
|
143
|
+
|
|
144
|
+
// Save to user config
|
|
145
|
+
const userConfig = {
|
|
146
|
+
baseUrl: config.baseUrl,
|
|
147
|
+
port: config.port,
|
|
148
|
+
environment: config.environment,
|
|
149
|
+
setupDate: new Date().toISOString()
|
|
150
|
+
};
|
|
151
|
+
await fs.writeFile(this.configFile, JSON.stringify(userConfig, null, 2));
|
|
152
|
+
|
|
153
|
+
// Create .env file
|
|
154
|
+
const envContent = `# Terminal Mirror Configuration
|
|
155
|
+
# Generated by setup wizard on ${new Date().toISOString()}
|
|
156
|
+
|
|
157
|
+
BASE_URL=${config.baseUrl}
|
|
158
|
+
PORT=${config.port}
|
|
159
|
+
HOST=0.0.0.0
|
|
160
|
+
GOOGLE_CLIENT_ID=${config.clientId}
|
|
161
|
+
GOOGLE_CLIENT_SECRET=${config.clientSecret}
|
|
162
|
+
SESSION_SECRET=${config.sessionSecret}
|
|
163
|
+
NODE_ENV=${config.environment === 'Production (custom domain)' ? 'production' : 'development'}
|
|
164
|
+
`;
|
|
165
|
+
|
|
166
|
+
await fs.writeFile(this.envFile, envContent);
|
|
167
|
+
|
|
168
|
+
console.log(`✅ Configuration saved to ${this.envFile}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async verifySetup(config) {
|
|
172
|
+
console.log('');
|
|
173
|
+
console.log('🔍 Verifying setup...');
|
|
174
|
+
|
|
175
|
+
// Check if all required files exist
|
|
176
|
+
const checks = [
|
|
177
|
+
{ name: 'Configuration file', path: this.configFile },
|
|
178
|
+
{ name: 'Environment file', path: this.envFile }
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
for (const check of checks) {
|
|
182
|
+
try {
|
|
183
|
+
await fs.access(check.path);
|
|
184
|
+
console.log(`✅ ${check.name} exists`);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.log(`❌ ${check.name} missing`);
|
|
187
|
+
throw new Error(`Setup verification failed: ${check.name} not found`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Validate environment variables
|
|
192
|
+
require('dotenv').config({ path: this.envFile });
|
|
193
|
+
const required = ['BASE_URL', 'GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET', 'SESSION_SECRET'];
|
|
194
|
+
const missing = required.filter(key => !process.env[key]);
|
|
195
|
+
|
|
196
|
+
if (missing.length > 0) {
|
|
197
|
+
console.log(`❌ Missing environment variables: ${missing.join(', ')}`);
|
|
198
|
+
throw new Error('Environment validation failed');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log('✅ Environment variables configured');
|
|
202
|
+
|
|
203
|
+
// Test OAuth configuration format
|
|
204
|
+
if (!config.clientId.includes('.googleusercontent.com')) {
|
|
205
|
+
console.log('⚠️ Warning: Client ID format looks unusual');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log('✅ Setup verification completed');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async isConfigured() {
|
|
212
|
+
try {
|
|
213
|
+
await fs.access(this.configFile);
|
|
214
|
+
await fs.access(this.envFile);
|
|
215
|
+
return true;
|
|
216
|
+
} catch {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = new SetupWizard();
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shell-mirror",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Access your Mac shell from any device securely. Perfect for mobile coding with Claude Code CLI, Gemini CLI, and any shell tool.",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"shell-mirror": "./bin/shell-mirror"
|
|
8
|
+
},
|
|
9
|
+
"preferGlobal": true,
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=14.0.0"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "node server.js",
|
|
15
|
+
"dev": "node server.js",
|
|
16
|
+
"pm2:start": "pm2 start ecosystem.config.js --env production",
|
|
17
|
+
"pm2:stop": "pm2 stop shell-mirror",
|
|
18
|
+
"pm2:restart": "pm2 restart shell-mirror",
|
|
19
|
+
"pm2:delete": "pm2 delete shell-mirror",
|
|
20
|
+
"pm2:logs": "pm2 logs shell-mirror",
|
|
21
|
+
"deploy": "node deploy-php.cjs",
|
|
22
|
+
"deploy:nodejs": "node deploy.cjs",
|
|
23
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
24
|
+
"postinstall": "node bin/shell-mirror install"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"shell",
|
|
28
|
+
"remote",
|
|
29
|
+
"mobile coding",
|
|
30
|
+
"claude code",
|
|
31
|
+
"gemini cli",
|
|
32
|
+
"ssh",
|
|
33
|
+
"web shell",
|
|
34
|
+
"mac shell",
|
|
35
|
+
"mobile shell",
|
|
36
|
+
"ai coding",
|
|
37
|
+
"google oauth"
|
|
38
|
+
],
|
|
39
|
+
"author": "Shell Mirror Contributors",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"type": "commonjs",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/karmalsky/shell-mirror.git"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://shellmirror.app",
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/karmalsky/shell-mirror/issues"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"commander": "^11.1.0",
|
|
52
|
+
"dotenv": "^16.4.5",
|
|
53
|
+
"express": "^5.1.0",
|
|
54
|
+
"express-session": "^1.18.1",
|
|
55
|
+
"inquirer": "^9.2.12",
|
|
56
|
+
"node-pty": "^1.0.0",
|
|
57
|
+
"passport": "^0.7.0",
|
|
58
|
+
"passport-google-oauth20": "^2.0.0",
|
|
59
|
+
"ws": "^8.18.2"
|
|
60
|
+
},
|
|
61
|
+
"files": [
|
|
62
|
+
"bin/",
|
|
63
|
+
"lib/",
|
|
64
|
+
"public/",
|
|
65
|
+
"server.js",
|
|
66
|
+
"auth.js",
|
|
67
|
+
"README.md",
|
|
68
|
+
"LICENSE"
|
|
69
|
+
]
|
|
70
|
+
}
|