shell-mirror 1.0.0 → 1.2.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/bin/shell-mirror CHANGED
@@ -15,7 +15,17 @@ const program = new Command();
15
15
  program
16
16
  .name('shell-mirror')
17
17
  .description('Access your Mac shell from any device securely')
18
- .version(package.version);
18
+ .version(package.version)
19
+ .action(async () => {
20
+ // Default action: auto-start with authentication
21
+ try {
22
+ const autoStart = require('../lib/auto-start');
23
+ await autoStart.run();
24
+ } catch (error) {
25
+ console.error('Failed to start Shell Mirror:', error.message);
26
+ process.exit(1);
27
+ }
28
+ });
19
29
 
20
30
  // Setup command - Interactive OAuth configuration wizard
21
31
  program
@@ -0,0 +1,500 @@
1
+ const fs = require('fs').promises;
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const crypto = require('crypto');
5
+ const { spawn } = require('child_process');
6
+ const https = require('https');
7
+ const querystring = require('querystring');
8
+
9
+ class AutoStart {
10
+ constructor() {
11
+ this.configDir = path.join(os.homedir(), '.shell-mirror');
12
+ this.authFile = path.join(this.configDir, 'auth.json');
13
+ this.configFile = path.join(this.configDir, 'config.json');
14
+ this.envFile = path.join(process.cwd(), '.env');
15
+
16
+ // Shell Mirror OAuth App credentials (production)
17
+ this.oauthConfig = {
18
+ clientId: '804759223392-i5nv5csn1o6siqr760c99l2a9k4sammp.apps.googleusercontent.com',
19
+ clientSecret: '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',
20
+ redirectUri: 'http://localhost:3000/auth/google/callback'
21
+ };
22
+ }
23
+
24
+ async run() {
25
+ console.log('🚀 Starting Shell Mirror...');
26
+ console.log('');
27
+
28
+ try {
29
+ // Ensure config directory exists
30
+ await fs.mkdir(this.configDir, { recursive: true });
31
+
32
+ // Check authentication status
33
+ const authInfo = await this.checkAuth();
34
+
35
+ if (!authInfo) {
36
+ console.log('🔐 Not logged in. Opening browser for Google authentication...');
37
+ await this.initiateLogin();
38
+ return;
39
+ }
40
+
41
+ // Start the server
42
+ await this.startServer(authInfo);
43
+
44
+ } catch (error) {
45
+ console.error('❌ Failed to start Shell Mirror:', error.message);
46
+ process.exit(1);
47
+ }
48
+ }
49
+
50
+ async checkAuth() {
51
+ try {
52
+ const authData = await fs.readFile(this.authFile, 'utf8');
53
+ const auth = JSON.parse(authData);
54
+
55
+ // Check if token is still valid (basic check)
56
+ if (auth.accessToken && auth.email && auth.expiresAt) {
57
+ const now = Date.now();
58
+ if (now < auth.expiresAt) {
59
+ return auth;
60
+ }
61
+ }
62
+
63
+ return null;
64
+ } catch (error) {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ async initiateLogin() {
70
+ console.log('');
71
+ console.log('🔐 Opening browser for Google authentication...');
72
+ console.log('');
73
+
74
+ try {
75
+ // Start OAuth callback server
76
+ console.log('🔄 Starting OAuth callback server...');
77
+ const server = await this.createOAuthCallbackServer();
78
+
79
+ // Open browser for OAuth
80
+ const authUrl = this.buildAuthUrl();
81
+ console.log('🌐 Opening browser for login...');
82
+ await this.openBrowser(authUrl);
83
+
84
+ console.log('');
85
+ console.log('👤 Please complete the Google login in your browser');
86
+ console.log(' (If browser didn\'t open, visit: ' + authUrl + ')');
87
+ console.log('');
88
+
89
+ } catch (error) {
90
+ console.error('❌ Failed to start OAuth flow:', error.message);
91
+ process.exit(1);
92
+ }
93
+ }
94
+
95
+ buildAuthUrl() {
96
+ const params = new URLSearchParams({
97
+ client_id: this.oauthConfig.clientId,
98
+ redirect_uri: this.oauthConfig.redirectUri,
99
+ response_type: 'code',
100
+ scope: 'openid email profile',
101
+ access_type: 'offline',
102
+ prompt: 'consent'
103
+ });
104
+
105
+ return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
106
+ }
107
+
108
+ async createOAuthCallbackServer() {
109
+ const express = require('express');
110
+ const app = express();
111
+
112
+ return new Promise((resolve, reject) => {
113
+ let serverInstance;
114
+
115
+ app.get('/auth/google/callback', async (req, res) => {
116
+ try {
117
+ const code = req.query.code;
118
+ const error = req.query.error;
119
+
120
+ if (error) {
121
+ throw new Error(`OAuth error: ${error}`);
122
+ }
123
+
124
+ if (code) {
125
+ console.log('📝 Received authorization code, exchanging for tokens...');
126
+
127
+ // Exchange code for tokens
128
+ const tokens = await this.exchangeCodeForTokens(code);
129
+ await this.saveAuth(tokens);
130
+
131
+ res.send(`
132
+ <html>
133
+ <head>
134
+ <title>Shell Mirror - Login Successful</title>
135
+ <style>
136
+ body { font-family: Arial, sans-serif; text-align: center; padding: 50px; background: #f5f5f5; }
137
+ .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; }
138
+ h2 { color: #4CAF50; margin-bottom: 20px; }
139
+ p { color: #666; margin-bottom: 15px; }
140
+ .email { color: #333; font-weight: bold; }
141
+ </style>
142
+ </head>
143
+ <body>
144
+ <div class="container">
145
+ <h2>✅ Login Successful!</h2>
146
+ <p>Welcome, <span class="email">${tokens.email}</span></p>
147
+ <p>Shell Mirror is now starting with your account.</p>
148
+ <p>You can close this window and return to your terminal.</p>
149
+ <p><small>Or visit <strong>https://shellmirror.app</strong> on your phone to access your terminal.</small></p>
150
+ </div>
151
+ <script>
152
+ setTimeout(() => {
153
+ window.close();
154
+ }, 3000);
155
+ </script>
156
+ </body>
157
+ </html>
158
+ `);
159
+
160
+ // Close the temporary server and restart with authenticated state
161
+ setTimeout(() => {
162
+ console.log(`✅ Login successful! Welcome ${tokens.email}`);
163
+ serverInstance.close(() => {
164
+ this.startAuthenticatedServer(tokens);
165
+ });
166
+ }, 1000);
167
+
168
+ } else {
169
+ throw new Error('No authorization code received');
170
+ }
171
+ } catch (error) {
172
+ console.error('❌ OAuth callback error:', error.message);
173
+ res.send(`
174
+ <html>
175
+ <head>
176
+ <title>Shell Mirror - Login Failed</title>
177
+ <style>
178
+ body { font-family: Arial, sans-serif; text-align: center; padding: 50px; background: #f5f5f5; }
179
+ .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; }
180
+ h2 { color: #f44336; margin-bottom: 20px; }
181
+ p { color: #666; margin-bottom: 15px; }
182
+ </style>
183
+ </head>
184
+ <body>
185
+ <div class="container">
186
+ <h2>❌ Login Failed</h2>
187
+ <p>Error: ${error.message}</p>
188
+ <p>Please try running 'shell-mirror' again.</p>
189
+ </div>
190
+ </body>
191
+ </html>
192
+ `);
193
+ }
194
+ });
195
+
196
+ serverInstance = app.listen(3000, (err) => {
197
+ if (err) {
198
+ reject(err);
199
+ } else {
200
+ resolve(serverInstance);
201
+ }
202
+ });
203
+ });
204
+ }
205
+
206
+ async createProductionConfig() {
207
+ // Create production environment file with real OAuth credentials
208
+ const envContent = `# Shell Mirror Configuration
209
+ # Auto-generated on ${new Date().toISOString()}
210
+
211
+ BASE_URL=http://localhost:3000
212
+ PORT=3000
213
+ HOST=0.0.0.0
214
+ GOOGLE_CLIENT_ID=${this.oauthConfig.clientId}
215
+ GOOGLE_CLIENT_SECRET=${this.oauthConfig.clientSecret}
216
+ SESSION_SECRET=${crypto.randomBytes(32).toString('hex')}
217
+ NODE_ENV=development
218
+ `;
219
+
220
+ await fs.writeFile(this.envFile, envContent);
221
+ }
222
+
223
+ async startServerForLogin() {
224
+ console.log('🔄 Starting Shell Mirror server...');
225
+ console.log('');
226
+
227
+ // Start the server in background
228
+ const serverProcess = spawn('node', ['server.js'], {
229
+ stdio: 'pipe',
230
+ cwd: path.dirname(this.envFile)
231
+ });
232
+
233
+ // Wait a moment for server to start
234
+ setTimeout(() => {
235
+ this.displayLoginInstructions();
236
+ this.openBrowser('http://localhost:3000');
237
+ }, 2000);
238
+
239
+ // Handle Ctrl+C gracefully
240
+ process.on('SIGINT', () => {
241
+ console.log('');
242
+ console.log('🛑 Stopping Shell Mirror...');
243
+ serverProcess.kill('SIGINT');
244
+ process.exit(0);
245
+ });
246
+
247
+ // Monitor server
248
+ serverProcess.on('close', (code) => {
249
+ if (code !== 0) {
250
+ console.error(`❌ Server exited with code ${code}`);
251
+ process.exit(code);
252
+ }
253
+ });
254
+ }
255
+
256
+ displayLoginInstructions() {
257
+ console.log('✅ Shell Mirror server is running');
258
+ console.log('🌐 Local server: http://localhost:3000');
259
+ console.log('📱 Access from phone: https://shellmirror.app');
260
+ console.log('');
261
+ console.log('🔐 First-time setup:');
262
+ console.log(' 1. A browser window should open automatically');
263
+ console.log(' 2. Click "Login with Google" to authenticate');
264
+ console.log(' 3. After login, your Mac terminal will be accessible from your phone');
265
+ console.log('');
266
+ console.log('â„šī¸ To use from your phone:');
267
+ console.log(' 1. Open https://shellmirror.app on your phone');
268
+ console.log(' 2. Login with the same Google account');
269
+ console.log(' 3. Access your Mac terminal instantly');
270
+ console.log('');
271
+ console.log('Press Ctrl+C to stop');
272
+ console.log('');
273
+ }
274
+
275
+ async exchangeCodeForTokens(code) {
276
+ return new Promise((resolve, reject) => {
277
+ const postData = querystring.stringify({
278
+ code: code,
279
+ client_id: this.oauthConfig.clientId,
280
+ client_secret: this.oauthConfig.clientSecret,
281
+ redirect_uri: this.oauthConfig.redirectUri,
282
+ grant_type: 'authorization_code'
283
+ });
284
+
285
+ const options = {
286
+ hostname: 'oauth2.googleapis.com',
287
+ port: 443,
288
+ path: '/token',
289
+ method: 'POST',
290
+ headers: {
291
+ 'Content-Type': 'application/x-www-form-urlencoded',
292
+ 'Content-Length': Buffer.byteLength(postData)
293
+ }
294
+ };
295
+
296
+ const req = https.request(options, (res) => {
297
+ let data = '';
298
+ res.on('data', (chunk) => {
299
+ data += chunk;
300
+ });
301
+ res.on('end', async () => {
302
+ try {
303
+ const tokens = JSON.parse(data);
304
+ if (tokens.access_token) {
305
+ // Get user info
306
+ const userInfo = await this.getUserInfo(tokens.access_token);
307
+ resolve({
308
+ accessToken: tokens.access_token,
309
+ refreshToken: tokens.refresh_token,
310
+ email: userInfo.email,
311
+ name: userInfo.name,
312
+ expiresAt: Date.now() + (tokens.expires_in * 1000)
313
+ });
314
+ } else {
315
+ reject(new Error('Failed to get access token: ' + data));
316
+ }
317
+ } catch (error) {
318
+ reject(error);
319
+ }
320
+ });
321
+ });
322
+
323
+ req.on('error', (error) => {
324
+ reject(error);
325
+ });
326
+
327
+ req.write(postData);
328
+ req.end();
329
+ });
330
+ }
331
+
332
+ async getUserInfo(accessToken) {
333
+ return new Promise((resolve, reject) => {
334
+ const options = {
335
+ hostname: 'www.googleapis.com',
336
+ port: 443,
337
+ path: '/oauth2/v2/userinfo',
338
+ method: 'GET',
339
+ headers: {
340
+ 'Authorization': `Bearer ${accessToken}`
341
+ }
342
+ };
343
+
344
+ const req = https.request(options, (res) => {
345
+ let data = '';
346
+ res.on('data', (chunk) => {
347
+ data += chunk;
348
+ });
349
+ res.on('end', () => {
350
+ try {
351
+ const userInfo = JSON.parse(data);
352
+ resolve(userInfo);
353
+ } catch (error) {
354
+ reject(error);
355
+ }
356
+ });
357
+ });
358
+
359
+ req.on('error', (error) => {
360
+ reject(error);
361
+ });
362
+
363
+ req.end();
364
+ });
365
+ }
366
+
367
+ async saveAuth(authData) {
368
+ await fs.writeFile(this.authFile, JSON.stringify(authData, null, 2));
369
+ console.log('🔐 Authentication saved');
370
+ }
371
+
372
+ async startServer(authInfo) {
373
+ // Create .env file with configuration
374
+ await this.createEnvFile(authInfo);
375
+
376
+ // Start the Express server
377
+ console.log('🔄 Starting server...');
378
+ console.log('');
379
+
380
+ // Start the main server process
381
+ const serverProcess = spawn('node', ['server.js'], {
382
+ stdio: 'inherit',
383
+ cwd: path.dirname(this.envFile)
384
+ });
385
+
386
+ // Display status information
387
+ this.displayStatus(authInfo);
388
+
389
+ // Handle Ctrl+C gracefully
390
+ process.on('SIGINT', () => {
391
+ console.log('');
392
+ console.log('🛑 Stopping Shell Mirror...');
393
+ serverProcess.kill('SIGINT');
394
+ process.exit(0);
395
+ });
396
+
397
+ // Wait for server process
398
+ serverProcess.on('close', (code) => {
399
+ if (code !== 0) {
400
+ console.error(`❌ Server exited with code ${code}`);
401
+ process.exit(code);
402
+ }
403
+ });
404
+ }
405
+
406
+ async createEnvFile(authInfo) {
407
+ const envContent = `# Shell Mirror Configuration
408
+ # Auto-generated on ${new Date().toISOString()}
409
+
410
+ BASE_URL=http://localhost:3000
411
+ PORT=3000
412
+ HOST=0.0.0.0
413
+ GOOGLE_CLIENT_ID=${this.oauthConfig.clientId}
414
+ GOOGLE_CLIENT_SECRET=${this.oauthConfig.clientSecret}
415
+ SESSION_SECRET=${crypto.randomBytes(32).toString('hex')}
416
+ NODE_ENV=development
417
+
418
+ # User authentication
419
+ USER_EMAIL=${authInfo.email}
420
+ USER_NAME=${authInfo.name}
421
+ ACCESS_TOKEN=${authInfo.accessToken}
422
+ `;
423
+
424
+ await fs.writeFile(this.envFile, envContent);
425
+ }
426
+
427
+ displayStatus(authInfo) {
428
+ console.log('✅ Shell Mirror is running');
429
+ console.log(`👤 Logged in as: ${authInfo.email}`);
430
+ console.log('🌐 Local server: http://localhost:3000');
431
+ console.log('📱 Access from phone: https://shellmirror.app');
432
+ console.log('');
433
+ console.log('â„šī¸ To use from your phone:');
434
+ console.log(' 1. Open https://shellmirror.app on your phone');
435
+ console.log(` 2. Login with the same Google account (${authInfo.email})`);
436
+ console.log(' 3. Access your Mac terminal instantly');
437
+ console.log('');
438
+ console.log('Press Ctrl+C to stop');
439
+ console.log('');
440
+ }
441
+
442
+ async startAuthenticatedServer(authInfo) {
443
+ console.log('');
444
+ console.log('🚀 Starting authenticated Shell Mirror server...');
445
+
446
+ // Create .env file with authentication
447
+ await this.createEnvFile(authInfo);
448
+
449
+ // Start the main server process
450
+ const serverProcess = spawn('node', ['server.js'], {
451
+ stdio: 'inherit',
452
+ cwd: path.dirname(this.envFile)
453
+ });
454
+
455
+ // Display status information
456
+ this.displayStatus(authInfo);
457
+
458
+ // Handle Ctrl+C gracefully
459
+ process.on('SIGINT', () => {
460
+ console.log('');
461
+ console.log('🛑 Stopping Shell Mirror...');
462
+ serverProcess.kill('SIGINT');
463
+ process.exit(0);
464
+ });
465
+
466
+ // Wait for server process
467
+ serverProcess.on('close', (code) => {
468
+ if (code !== 0) {
469
+ console.error(`❌ Server exited with code ${code}`);
470
+ process.exit(code);
471
+ }
472
+ });
473
+ }
474
+
475
+ async openBrowser(url) {
476
+ const platform = os.platform();
477
+ let command;
478
+
479
+ switch (platform) {
480
+ case 'darwin': // macOS
481
+ command = 'open';
482
+ break;
483
+ case 'win32': // Windows
484
+ command = 'start';
485
+ break;
486
+ default: // Linux and others
487
+ command = 'xdg-open';
488
+ break;
489
+ }
490
+
491
+ try {
492
+ spawn(command, [url], { detached: true, stdio: 'ignore' });
493
+ } catch (error) {
494
+ console.log('âš ī¸ Could not open browser automatically.');
495
+ console.log(' Please visit: ' + url);
496
+ }
497
+ }
498
+ }
499
+
500
+ module.exports = new AutoStart();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shell-mirror",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
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
5
  "main": "server.js",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -692,9 +692,9 @@
692
692
  <p><strong>Next steps:</strong></p>
693
693
  <ol style="text-align: left; margin: 20px 0;">
694
694
  <li>Install Shell Mirror using the command above</li>
695
- <li>Run <code>shell-mirror setup</code> to configure</li>
696
- <li>Run <code>shell-mirror start</code> to start the server</li>
697
- <li>Come back and click "Login with Google" again</li>
695
+ <li>Run <code>shell-mirror</code> (it will auto-start and open your browser)</li>
696
+ <li>Login with Google when prompted</li>
697
+ <li>Access your terminal from your phone at shellmirror.app</li>
698
698
  </ol>
699
699
 
700
700
  <button class="btn-primary" onclick="closeInstallModal()" style="margin-top: 20px;">Got it!</button>
package/server.js CHANGED
@@ -96,11 +96,11 @@ if (missingEnvVars.length > 0) {
96
96
  missingEnvVars.forEach(varName => {
97
97
  console.error(` - ${varName}`);
98
98
  });
99
- console.error('\nPlease create a .env file based on .env.example and fill in the required values.');
100
- console.error('See README.md for instructions on getting Google OAuth credentials.');
99
+ console.error('\nPlease run "shell-mirror" to set up authentication.');
101
100
  process.exit(1);
102
101
  }
103
102
 
103
+
104
104
  // 4. WebSocket Connection Handler with Authentication
105
105
  wss.on('connection', function connection(ws, request) {
106
106
  console.log('New WebSocket connection attempt');