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 +11 -1
- package/lib/auto-start.js +325 -0
- package/package.json +1 -1
- package/public/index.html +3 -3
- package/server.js +11 -2
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
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
|
|
696
|
-
<li>
|
|
697
|
-
<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
|
|
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');
|