shell-mirror 1.0.0 → 1.1.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,325 @@
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
+
7
+ class AutoStart {
8
+ constructor() {
9
+ this.configDir = path.join(os.homedir(), '.shell-mirror');
10
+ this.authFile = path.join(this.configDir, 'auth.json');
11
+ this.configFile = path.join(this.configDir, 'config.json');
12
+ this.envFile = path.join(process.cwd(), '.env');
13
+
14
+ // Shell Mirror OAuth App credentials (shared for all users)
15
+ this.oauthConfig = {
16
+ clientId: 'your-shell-mirror-oauth-client-id', // TODO: Replace with actual
17
+ clientSecret: 'your-shell-mirror-oauth-client-secret', // TODO: Replace with actual
18
+ redirectUri: 'http://localhost:3000/auth/google/callback'
19
+ };
20
+ }
21
+
22
+ async run() {
23
+ console.log('🚀 Starting Shell Mirror...');
24
+ console.log('');
25
+
26
+ try {
27
+ // Ensure config directory exists
28
+ await fs.mkdir(this.configDir, { recursive: true });
29
+
30
+ // Check authentication status
31
+ const authInfo = await this.checkAuth();
32
+
33
+ if (!authInfo) {
34
+ console.log('🔐 Not logged in. Opening browser for Google authentication...');
35
+ await this.initiateLogin();
36
+ return;
37
+ }
38
+
39
+ // Start the server
40
+ await this.startServer(authInfo);
41
+
42
+ } catch (error) {
43
+ console.error('❌ Failed to start Shell Mirror:', error.message);
44
+ process.exit(1);
45
+ }
46
+ }
47
+
48
+ async checkAuth() {
49
+ try {
50
+ const authData = await fs.readFile(this.authFile, 'utf8');
51
+ const auth = JSON.parse(authData);
52
+
53
+ // Check if token is still valid (basic check)
54
+ if (auth.accessToken && auth.email && auth.expiresAt) {
55
+ const now = Date.now();
56
+ if (now < auth.expiresAt) {
57
+ return auth;
58
+ }
59
+ }
60
+
61
+ return null;
62
+ } catch (error) {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ async initiateLogin() {
68
+ console.log('');
69
+ console.log('📱 Setting up Google authentication...');
70
+ console.log('');
71
+
72
+ // For now, we'll use a simplified setup that creates minimal config
73
+ // and directs user to use the web interface
74
+ await this.createMinimalConfig();
75
+ await this.startServerForLogin();
76
+ }
77
+
78
+ buildAuthUrl() {
79
+ const params = new URLSearchParams({
80
+ client_id: this.oauthConfig.clientId,
81
+ redirect_uri: this.oauthConfig.redirectUri,
82
+ response_type: 'code',
83
+ scope: 'openid email profile',
84
+ access_type: 'offline',
85
+ prompt: 'consent'
86
+ });
87
+
88
+ return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
89
+ }
90
+
91
+ async createTempOAuthServer() {
92
+ // This would create a temporary Express server to handle OAuth callback
93
+ // For now, we'll use a simplified approach
94
+ const express = require('express');
95
+ const app = express();
96
+
97
+ return new Promise((resolve, reject) => {
98
+ app.get('/auth/google/callback', async (req, res) => {
99
+ try {
100
+ const code = req.query.code;
101
+ if (code) {
102
+ // Exchange code for tokens
103
+ const tokens = await this.exchangeCodeForTokens(code);
104
+ await this.saveAuth(tokens);
105
+
106
+ res.send(`
107
+ <html>
108
+ <body style="font-family: Arial; text-align: center; padding: 50px;">
109
+ <h2>✅ Login Successful!</h2>
110
+ <p>You can close this window and return to your terminal.</p>
111
+ <script>window.close();</script>
112
+ </body>
113
+ </html>
114
+ `);
115
+
116
+ // Restart the main process
117
+ setTimeout(() => {
118
+ console.log('✅ Login successful! Starting Shell Mirror...');
119
+ this.run();
120
+ }, 1000);
121
+
122
+ } else {
123
+ throw new Error('No authorization code received');
124
+ }
125
+ } catch (error) {
126
+ res.send(`
127
+ <html>
128
+ <body style="font-family: Arial; text-align: center; padding: 50px;">
129
+ <h2>❌ Login Failed</h2>
130
+ <p>Error: ${error.message}</p>
131
+ <p>Please try running 'shell-mirror' again.</p>
132
+ </body>
133
+ </html>
134
+ `);
135
+ }
136
+ });
137
+
138
+ const server = app.listen(3000, () => {
139
+ resolve(server);
140
+ });
141
+ });
142
+ }
143
+
144
+ async createMinimalConfig() {
145
+ // Create a basic environment file that allows the server to start
146
+ const envContent = `# Shell Mirror Configuration
147
+ # Auto-generated on ${new Date().toISOString()}
148
+
149
+ BASE_URL=http://localhost:3000
150
+ PORT=3000
151
+ HOST=0.0.0.0
152
+ GOOGLE_CLIENT_ID=placeholder-will-be-set-after-login
153
+ GOOGLE_CLIENT_SECRET=placeholder-will-be-set-after-login
154
+ SESSION_SECRET=${crypto.randomBytes(32).toString('hex')}
155
+ NODE_ENV=development
156
+ `;
157
+
158
+ await fs.writeFile(this.envFile, envContent);
159
+ }
160
+
161
+ async startServerForLogin() {
162
+ console.log('🔄 Starting Shell Mirror server...');
163
+ console.log('');
164
+
165
+ // Start the server in background
166
+ const serverProcess = spawn('node', ['server.js'], {
167
+ stdio: 'pipe',
168
+ cwd: path.dirname(this.envFile)
169
+ });
170
+
171
+ // Wait a moment for server to start
172
+ setTimeout(() => {
173
+ this.displayLoginInstructions();
174
+ this.openBrowser('http://localhost:3000');
175
+ }, 2000);
176
+
177
+ // Handle Ctrl+C gracefully
178
+ process.on('SIGINT', () => {
179
+ console.log('');
180
+ console.log('🛑 Stopping Shell Mirror...');
181
+ serverProcess.kill('SIGINT');
182
+ process.exit(0);
183
+ });
184
+
185
+ // Monitor server
186
+ serverProcess.on('close', (code) => {
187
+ if (code !== 0) {
188
+ console.error(`❌ Server exited with code ${code}`);
189
+ process.exit(code);
190
+ }
191
+ });
192
+ }
193
+
194
+ displayLoginInstructions() {
195
+ console.log('✅ Shell Mirror server is running');
196
+ console.log('🌐 Local server: http://localhost:3000');
197
+ console.log('📱 Access from phone: https://shellmirror.app');
198
+ console.log('');
199
+ console.log('🔐 First-time setup:');
200
+ console.log(' 1. A browser window should open automatically');
201
+ console.log(' 2. Click "Login with Google" to authenticate');
202
+ console.log(' 3. After login, your Mac terminal will be accessible from your phone');
203
+ console.log('');
204
+ console.log('â„šī¸ To use from your phone:');
205
+ console.log(' 1. Open https://shellmirror.app on your phone');
206
+ console.log(' 2. Login with the same Google account');
207
+ console.log(' 3. Access your Mac terminal instantly');
208
+ console.log('');
209
+ console.log('Press Ctrl+C to stop');
210
+ console.log('');
211
+ }
212
+
213
+ async exchangeCodeForTokens(code) {
214
+ // This would exchange the OAuth code for access tokens
215
+ // For now, return mock data (this needs real OAuth implementation)
216
+ return {
217
+ accessToken: 'mock-access-token',
218
+ refreshToken: 'mock-refresh-token',
219
+ email: 'user@example.com',
220
+ name: 'User Name',
221
+ expiresAt: Date.now() + (3600 * 1000) // 1 hour
222
+ };
223
+ }
224
+
225
+ async saveAuth(authData) {
226
+ await fs.writeFile(this.authFile, JSON.stringify(authData, null, 2));
227
+ console.log('🔐 Authentication saved');
228
+ }
229
+
230
+ async startServer(authInfo) {
231
+ // Create .env file with configuration
232
+ await this.createEnvFile(authInfo);
233
+
234
+ // Start the Express server
235
+ console.log('🔄 Starting server...');
236
+ console.log('');
237
+
238
+ // Start the main server process
239
+ const serverProcess = spawn('node', ['server.js'], {
240
+ stdio: 'inherit',
241
+ cwd: path.dirname(this.envFile)
242
+ });
243
+
244
+ // Display status information
245
+ this.displayStatus(authInfo);
246
+
247
+ // Handle Ctrl+C gracefully
248
+ process.on('SIGINT', () => {
249
+ console.log('');
250
+ console.log('🛑 Stopping Shell Mirror...');
251
+ serverProcess.kill('SIGINT');
252
+ process.exit(0);
253
+ });
254
+
255
+ // Wait for server process
256
+ serverProcess.on('close', (code) => {
257
+ if (code !== 0) {
258
+ console.error(`❌ Server exited with code ${code}`);
259
+ process.exit(code);
260
+ }
261
+ });
262
+ }
263
+
264
+ async createEnvFile(authInfo) {
265
+ const envContent = `# Shell Mirror Configuration
266
+ # Auto-generated on ${new Date().toISOString()}
267
+
268
+ BASE_URL=http://localhost:3000
269
+ PORT=3000
270
+ HOST=0.0.0.0
271
+ GOOGLE_CLIENT_ID=${this.oauthConfig.clientId}
272
+ GOOGLE_CLIENT_SECRET=${this.oauthConfig.clientSecret}
273
+ SESSION_SECRET=${crypto.randomBytes(32).toString('hex')}
274
+ NODE_ENV=development
275
+
276
+ # User authentication
277
+ USER_EMAIL=${authInfo.email}
278
+ USER_NAME=${authInfo.name}
279
+ ACCESS_TOKEN=${authInfo.accessToken}
280
+ `;
281
+
282
+ await fs.writeFile(this.envFile, envContent);
283
+ }
284
+
285
+ displayStatus(authInfo) {
286
+ console.log('✅ Shell Mirror is running');
287
+ console.log(`👤 Logged in as: ${authInfo.email}`);
288
+ console.log('🌐 Local server: http://localhost:3000');
289
+ console.log('📱 Access from phone: https://shellmirror.app');
290
+ console.log('');
291
+ console.log('â„šī¸ To use from your phone:');
292
+ console.log(' 1. Open https://shellmirror.app on your phone');
293
+ console.log(` 2. Login with the same Google account (${authInfo.email})`);
294
+ console.log(' 3. Access your Mac terminal instantly');
295
+ console.log('');
296
+ console.log('Press Ctrl+C to stop');
297
+ console.log('');
298
+ }
299
+
300
+ async openBrowser(url) {
301
+ const platform = os.platform();
302
+ let command;
303
+
304
+ switch (platform) {
305
+ case 'darwin': // macOS
306
+ command = 'open';
307
+ break;
308
+ case 'win32': // Windows
309
+ command = 'start';
310
+ break;
311
+ default: // Linux and others
312
+ command = 'xdg-open';
313
+ break;
314
+ }
315
+
316
+ try {
317
+ spawn(command, [url], { detached: true, stdio: 'ignore' });
318
+ } catch (error) {
319
+ console.log('âš ī¸ Could not open browser automatically.');
320
+ console.log(' Please visit: ' + url);
321
+ }
322
+ }
323
+ }
324
+
325
+ 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.1.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,20 @@ 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
+ // Check if OAuth credentials are placeholders (first-time setup)
104
+ const isFirstTimeSetup = process.env.GOOGLE_CLIENT_ID === 'placeholder-will-be-set-after-login';
105
+
106
+ if (isFirstTimeSetup) {
107
+ console.log('â„šī¸ First-time setup detected. OAuth authentication is not yet configured.');
108
+ console.log(' The server will start, but Google login will not work until configured.');
109
+ console.log(' Please visit the web interface to complete setup.');
110
+ console.log('');
111
+ }
112
+
104
113
  // 4. WebSocket Connection Handler with Authentication
105
114
  wss.on('connection', function connection(ws, request) {
106
115
  console.log('New WebSocket connection attempt');