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.
@@ -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
+ }