remotosh 1.1.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/remoto.js +299 -236
- package/package.json +1 -1
package/bin/remoto.js
CHANGED
|
@@ -1,54 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import os from 'os';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
4
6
|
import pty from 'node-pty';
|
|
5
7
|
import qrcode from 'qrcode-terminal';
|
|
6
8
|
import WebSocket from 'ws';
|
|
7
9
|
import chalk from 'chalk';
|
|
10
|
+
import { randomBytes } from 'crypto';
|
|
8
11
|
|
|
9
12
|
// Configuration
|
|
10
13
|
const WS_SERVER_URL = process.env.REMOTO_WS_URL || 'wss://remoto-ws.fly.dev';
|
|
11
14
|
const WEB_APP_URL = process.env.REMOTO_WEB_URL || 'https://remoto.sh';
|
|
12
|
-
const
|
|
15
|
+
const CONFIG_DIR = path.join(os.homedir(), '.remoto');
|
|
16
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
13
17
|
|
|
14
18
|
// Sensitive environment variable patterns to filter out
|
|
15
19
|
const SENSITIVE_ENV_PATTERNS = [
|
|
16
|
-
/^AWS_/i,
|
|
17
|
-
/^
|
|
18
|
-
/^
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
/^NUGET_/i,
|
|
25
|
-
/^DOCKER_/i,
|
|
26
|
-
/^KUBERNETES_/i,
|
|
27
|
-
/^K8S_/i,
|
|
28
|
-
/SECRET/i,
|
|
29
|
-
/PASSWORD/i,
|
|
30
|
-
/PRIVATE_KEY/i,
|
|
31
|
-
/API_KEY/i,
|
|
32
|
-
/AUTH_TOKEN/i,
|
|
33
|
-
/ACCESS_TOKEN/i,
|
|
34
|
-
/REFRESH_TOKEN/i,
|
|
35
|
-
/DATABASE_URL/i,
|
|
36
|
-
/DB_PASSWORD/i,
|
|
37
|
-
/POSTGRES_/i,
|
|
38
|
-
/MYSQL_/i,
|
|
39
|
-
/MONGO_/i,
|
|
40
|
-
/REDIS_/i,
|
|
41
|
-
/STRIPE_/i,
|
|
42
|
-
/TWILIO_/i,
|
|
43
|
-
/SENDGRID_/i,
|
|
44
|
-
/MAILGUN_/i,
|
|
45
|
-
/SUPABASE_/i,
|
|
46
|
-
/FIREBASE_/i,
|
|
47
|
-
/OPENAI_/i,
|
|
48
|
-
/ANTHROPIC_/i,
|
|
49
|
-
/SLACK_/i,
|
|
50
|
-
/DISCORD_/i,
|
|
51
|
-
/^REMOTO_API_KEY$/i, // Our own API key
|
|
20
|
+
/^AWS_/i, /^AZURE_/i, /^GCP_/i, /^GOOGLE_/i,
|
|
21
|
+
/^GITHUB_TOKEN$/i, /^GH_TOKEN$/i, /^GITLAB_/i,
|
|
22
|
+
/^NPM_TOKEN$/i, /^DOCKER_/i, /^KUBERNETES_/i, /^K8S_/i,
|
|
23
|
+
/SECRET/i, /PASSWORD/i, /PRIVATE_KEY/i, /API_KEY/i,
|
|
24
|
+
/AUTH_TOKEN/i, /ACCESS_TOKEN/i, /REFRESH_TOKEN/i,
|
|
25
|
+
/DATABASE_URL/i, /DB_PASSWORD/i, /POSTGRES_/i, /MYSQL_/i, /MONGO_/i, /REDIS_/i,
|
|
26
|
+
/STRIPE_/i, /TWILIO_/i, /SENDGRID_/i, /MAILGUN_/i,
|
|
27
|
+
/SUPABASE_/i, /FIREBASE_/i, /OPENAI_/i, /ANTHROPIC_/i, /SLACK_/i, /DISCORD_/i,
|
|
52
28
|
];
|
|
53
29
|
|
|
54
30
|
// Create sanitized environment for PTY
|
|
@@ -62,246 +38,328 @@ function getSanitizedEnv() {
|
|
|
62
38
|
return env;
|
|
63
39
|
}
|
|
64
40
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
41
|
+
// Load saved config
|
|
42
|
+
function loadConfig() {
|
|
43
|
+
try {
|
|
44
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
45
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
46
|
+
}
|
|
47
|
+
} catch (e) {
|
|
48
|
+
// Ignore errors
|
|
49
|
+
}
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
71
52
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
console.log(chalk.dim(' 3. create an API key and set it as an environment variable:\n'));
|
|
83
|
-
console.log(chalk.green(' export REMOTO_API_KEY="your_key_here"'));
|
|
84
|
-
console.log(chalk.dim('\n free plan includes:'));
|
|
85
|
-
console.log(chalk.dim(' • 2 concurrent sessions'));
|
|
86
|
-
console.log(chalk.dim(' • 1 hour session duration'));
|
|
87
|
-
console.log(chalk.dim(' • unlimited sessions per month'));
|
|
88
|
-
console.log(chalk.dim('\n pro plans with higher limits coming soon!\n'));
|
|
89
|
-
process.exit(1);
|
|
53
|
+
// Save config
|
|
54
|
+
function saveConfig(config) {
|
|
55
|
+
try {
|
|
56
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
57
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
58
|
+
}
|
|
59
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
60
|
+
} catch (e) {
|
|
61
|
+
// Ignore errors
|
|
62
|
+
}
|
|
90
63
|
}
|
|
91
64
|
|
|
92
|
-
|
|
65
|
+
// Open URL in browser
|
|
66
|
+
async function openBrowser(url) {
|
|
67
|
+
const { exec } = await import('child_process');
|
|
68
|
+
const platform = os.platform();
|
|
93
69
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
70
|
+
let command;
|
|
71
|
+
if (platform === 'darwin') {
|
|
72
|
+
command = `open "${url}"`;
|
|
73
|
+
} else if (platform === 'win32') {
|
|
74
|
+
command = `start "" "${url}"`;
|
|
75
|
+
} else {
|
|
76
|
+
command = `xdg-open "${url}"`;
|
|
77
|
+
}
|
|
97
78
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
79
|
+
exec(command, (err) => {
|
|
80
|
+
if (err) {
|
|
81
|
+
console.log(chalk.dim(` couldn't open browser automatically`));
|
|
82
|
+
console.log(chalk.dim(` open this URL manually: ${url}`));
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
104
86
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
87
|
+
// Poll for auth completion
|
|
88
|
+
async function pollForAuth(deviceCode) {
|
|
89
|
+
const pollUrl = `${WEB_APP_URL}/api/cli-auth/poll?code=${deviceCode}`;
|
|
108
90
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
console.log(chalk.dim(` [debug] received: ${raw.substring(0, 100)}`));
|
|
112
|
-
try {
|
|
113
|
-
const message = JSON.parse(raw);
|
|
114
|
-
handleServerMessage(message);
|
|
115
|
-
} catch (err) {
|
|
116
|
-
console.error(chalk.red(` invalid message from server: ${err.message}`));
|
|
117
|
-
console.error(chalk.red(` raw data: ${raw.substring(0, 50)}`));
|
|
118
|
-
}
|
|
119
|
-
});
|
|
91
|
+
for (let i = 0; i < 60; i++) { // Poll for 5 minutes max
|
|
92
|
+
await new Promise(r => setTimeout(r, 5000)); // Wait 5 seconds
|
|
120
93
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
console.log(chalk.dim('\n pro plans with higher limits coming soon!'));
|
|
135
|
-
} else if (reasonStr.includes('expired')) {
|
|
136
|
-
console.log(chalk.yellow('\n session expired (1 hour limit)'));
|
|
137
|
-
console.log(chalk.dim(' start a new session with: npx remotosh'));
|
|
138
|
-
} else {
|
|
139
|
-
console.log(chalk.dim(`\n disconnected (${code})`));
|
|
94
|
+
try {
|
|
95
|
+
const response = await fetch(pollUrl);
|
|
96
|
+
const data = await response.json();
|
|
97
|
+
|
|
98
|
+
if (data.status === 'authorized' && data.token) {
|
|
99
|
+
return data.token;
|
|
100
|
+
} else if (data.status === 'expired') {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
// status === 'pending' - keep polling
|
|
104
|
+
} catch (e) {
|
|
105
|
+
// Network error, keep trying
|
|
106
|
+
}
|
|
140
107
|
}
|
|
141
|
-
cleanup();
|
|
142
|
-
});
|
|
143
108
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Main auth flow
|
|
113
|
+
async function authenticate() {
|
|
114
|
+
const config = loadConfig();
|
|
115
|
+
|
|
116
|
+
// Check if we have a saved token
|
|
117
|
+
if (config.token) {
|
|
118
|
+
return config.token;
|
|
148
119
|
}
|
|
149
|
-
process.exit(1);
|
|
150
|
-
});
|
|
151
120
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
case 'session_created':
|
|
155
|
-
sessionId = message.sessionId;
|
|
156
|
-
sessionToken = message.sessionToken;
|
|
157
|
-
maxDuration = message.maxDuration;
|
|
158
|
-
showQRCode();
|
|
159
|
-
startPTY();
|
|
160
|
-
break;
|
|
161
|
-
|
|
162
|
-
case 'phone_connected':
|
|
163
|
-
console.log(chalk.green(`\n phone connected`));
|
|
164
|
-
if (message.phoneCount > 1) {
|
|
165
|
-
console.log(chalk.dim(` ${message.phoneCount} devices connected`));
|
|
166
|
-
}
|
|
167
|
-
console.log('');
|
|
168
|
-
break;
|
|
169
|
-
|
|
170
|
-
case 'phone_disconnected':
|
|
171
|
-
if (message.phoneCount > 0) {
|
|
172
|
-
console.log(chalk.yellow(`\n phone disconnected (${message.phoneCount} remaining)\n`));
|
|
173
|
-
} else {
|
|
174
|
-
console.log(chalk.yellow(`\n phone disconnected\n`));
|
|
175
|
-
}
|
|
176
|
-
break;
|
|
121
|
+
// No token - need to authenticate via browser
|
|
122
|
+
console.log(chalk.yellow(' login required\n'));
|
|
177
123
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
124
|
+
// Generate device code
|
|
125
|
+
const deviceCode = randomBytes(16).toString('hex');
|
|
126
|
+
const authUrl = `${WEB_APP_URL}/cli-auth?code=${deviceCode}`;
|
|
181
127
|
|
|
182
|
-
|
|
183
|
-
// Input from phone
|
|
184
|
-
if (ptyProcess) {
|
|
185
|
-
ptyProcess.write(message.data);
|
|
186
|
-
}
|
|
187
|
-
break;
|
|
128
|
+
console.log(chalk.white(' opening browser to login...\n'));
|
|
188
129
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
break;
|
|
130
|
+
await openBrowser(authUrl);
|
|
131
|
+
|
|
132
|
+
console.log(chalk.dim(` if browser didn't open, go to:`));
|
|
133
|
+
console.log(chalk.cyan(` ${authUrl}\n`));
|
|
134
|
+
console.log(chalk.dim(' waiting for login...'));
|
|
195
135
|
|
|
196
|
-
|
|
197
|
-
|
|
136
|
+
const token = await pollForAuth(deviceCode);
|
|
137
|
+
|
|
138
|
+
if (!token) {
|
|
139
|
+
console.log(chalk.red('\n login timed out or was cancelled'));
|
|
140
|
+
console.log(chalk.dim(' run `npx remotosh` to try again\n'));
|
|
141
|
+
process.exit(1);
|
|
198
142
|
}
|
|
143
|
+
|
|
144
|
+
// Save token
|
|
145
|
+
saveConfig({ token });
|
|
146
|
+
console.log(chalk.green('\n logged in successfully!\n'));
|
|
147
|
+
|
|
148
|
+
return token;
|
|
199
149
|
}
|
|
200
150
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const durationMinutes = maxDuration ? Math.floor(maxDuration / 60000) : 60;
|
|
151
|
+
// Detect shell
|
|
152
|
+
const shell = process.env.SHELL || (os.platform() === 'win32' ? 'powershell.exe' : 'zsh');
|
|
204
153
|
|
|
154
|
+
// Terminal dimensions
|
|
155
|
+
let cols = process.stdout.columns || 80;
|
|
156
|
+
let rows = process.stdout.rows || 24;
|
|
157
|
+
|
|
158
|
+
// Main
|
|
159
|
+
async function main() {
|
|
205
160
|
console.clear();
|
|
206
161
|
console.log(chalk.bold.white('\n remoto'));
|
|
207
162
|
console.log(chalk.dim(' control your terminal from your phone\n'));
|
|
208
163
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
console.log(
|
|
213
|
-
|
|
214
|
-
console.log(chalk.dim(' scan the qr code or open the link on your phone'));
|
|
215
|
-
console.log(chalk.dim(` session expires in ${durationMinutes} minutes`));
|
|
216
|
-
console.log(chalk.dim(' waiting for connection...\n'));
|
|
217
|
-
console.log(chalk.dim('─'.repeat(Math.min(cols, 60))));
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function startPTY() {
|
|
222
|
-
// Initialize PTY with sanitized environment (removes secrets)
|
|
223
|
-
console.log(chalk.dim(` [debug] spawning shell: ${shell}`));
|
|
224
|
-
try {
|
|
225
|
-
const sanitizedEnv = getSanitizedEnv();
|
|
226
|
-
ptyProcess = pty.spawn(shell, [], {
|
|
227
|
-
name: 'xterm-256color',
|
|
228
|
-
cols,
|
|
229
|
-
rows,
|
|
230
|
-
cwd: process.cwd(),
|
|
231
|
-
env: sanitizedEnv,
|
|
232
|
-
});
|
|
233
|
-
console.log(chalk.dim(` [debug] pty spawned successfully, pid: ${ptyProcess.pid}`));
|
|
234
|
-
} catch (err) {
|
|
235
|
-
console.error(chalk.red(` [debug] pty spawn failed: ${err.message}`));
|
|
236
|
-
return;
|
|
164
|
+
// Handle logout command
|
|
165
|
+
if (process.argv[2] === 'logout') {
|
|
166
|
+
saveConfig({});
|
|
167
|
+
console.log(chalk.green(' logged out successfully\n'));
|
|
168
|
+
process.exit(0);
|
|
237
169
|
}
|
|
238
170
|
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
// Write to local terminal
|
|
242
|
-
process.stdout.write(data);
|
|
243
|
-
|
|
244
|
-
// Buffer and send to server
|
|
245
|
-
outputBuffer += data;
|
|
246
|
-
|
|
247
|
-
if (flushTimeout) clearTimeout(flushTimeout);
|
|
248
|
-
flushTimeout = setTimeout(() => {
|
|
249
|
-
if (outputBuffer && ws.readyState === WebSocket.OPEN) {
|
|
250
|
-
// Chunk large outputs
|
|
251
|
-
const chunks = chunkString(outputBuffer, 16000);
|
|
252
|
-
for (const chunk of chunks) {
|
|
253
|
-
ws.send(JSON.stringify({ type: 'output', data: chunk }));
|
|
254
|
-
}
|
|
255
|
-
outputBuffer = '';
|
|
256
|
-
}
|
|
257
|
-
}, 30);
|
|
258
|
-
});
|
|
171
|
+
// Authenticate
|
|
172
|
+
const token = await authenticate();
|
|
259
173
|
|
|
260
|
-
|
|
261
|
-
ptyProcess.onExit(({ exitCode }) => {
|
|
262
|
-
console.log(chalk.dim(`\n session ended`));
|
|
174
|
+
console.log(chalk.dim(' connecting...'));
|
|
263
175
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
176
|
+
// Connect to WebSocket server
|
|
177
|
+
const wsUrl = `${WS_SERVER_URL}/cli/?token=${encodeURIComponent(token)}`;
|
|
178
|
+
const ws = new WebSocket(wsUrl);
|
|
179
|
+
|
|
180
|
+
let ptyProcess = null;
|
|
181
|
+
let sessionId = null;
|
|
182
|
+
let sessionToken = null;
|
|
183
|
+
let maxDuration = null;
|
|
184
|
+
let outputBuffer = '';
|
|
185
|
+
let flushTimeout = null;
|
|
186
|
+
|
|
187
|
+
ws.on('open', () => {
|
|
188
|
+
// Connection established, wait for session_created message
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
ws.on('message', (data) => {
|
|
192
|
+
try {
|
|
193
|
+
const message = JSON.parse(data.toString());
|
|
194
|
+
handleServerMessage(message);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
// Ignore invalid messages
|
|
267
197
|
}
|
|
268
|
-
process.exit(exitCode);
|
|
269
198
|
});
|
|
270
199
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
200
|
+
ws.on('close', (code, reason) => {
|
|
201
|
+
const reasonStr = reason?.toString() || '';
|
|
202
|
+
|
|
203
|
+
if (code === 4001 || code === 4002) {
|
|
204
|
+
// Invalid/expired token - clear it and re-auth
|
|
205
|
+
saveConfig({});
|
|
206
|
+
console.log(chalk.yellow('\n session expired, please login again'));
|
|
207
|
+
console.log(chalk.dim(' run `npx remotosh` to login\n'));
|
|
208
|
+
} else if (code === 4004) {
|
|
209
|
+
console.log(chalk.yellow('\n session limit reached'));
|
|
210
|
+
console.log(chalk.dim(' free plan allows 2 concurrent sessions'));
|
|
211
|
+
console.log(chalk.dim(' close an existing session and try again\n'));
|
|
212
|
+
} else if (reasonStr.includes('expired')) {
|
|
213
|
+
console.log(chalk.yellow('\n session expired (1 hour limit)'));
|
|
214
|
+
console.log(chalk.dim(' run `npx remotosh` to start a new session\n'));
|
|
215
|
+
} else {
|
|
216
|
+
console.log(chalk.dim(`\n disconnected\n`));
|
|
277
217
|
}
|
|
218
|
+
cleanup();
|
|
278
219
|
});
|
|
279
220
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (ptyProcess) {
|
|
285
|
-
ptyProcess.write(data.toString());
|
|
221
|
+
ws.on('error', (err) => {
|
|
222
|
+
console.error(chalk.red(`\n connection error: ${err.message}`));
|
|
223
|
+
if (err.message.includes('ECONNREFUSED')) {
|
|
224
|
+
console.log(chalk.dim('\n make sure you have internet access'));
|
|
286
225
|
}
|
|
226
|
+
process.exit(1);
|
|
287
227
|
});
|
|
288
|
-
}
|
|
289
228
|
|
|
290
|
-
function
|
|
291
|
-
|
|
292
|
-
|
|
229
|
+
function handleServerMessage(message) {
|
|
230
|
+
switch (message.type) {
|
|
231
|
+
case 'session_created':
|
|
232
|
+
sessionId = message.sessionId;
|
|
233
|
+
sessionToken = message.sessionToken;
|
|
234
|
+
maxDuration = message.maxDuration;
|
|
235
|
+
showQRCode();
|
|
236
|
+
startPTY();
|
|
237
|
+
break;
|
|
238
|
+
|
|
239
|
+
case 'phone_connected':
|
|
240
|
+
console.log(chalk.green(`\n phone connected`));
|
|
241
|
+
if (message.phoneCount > 1) {
|
|
242
|
+
console.log(chalk.dim(` ${message.phoneCount} devices connected`));
|
|
243
|
+
}
|
|
244
|
+
console.log('');
|
|
245
|
+
break;
|
|
246
|
+
|
|
247
|
+
case 'phone_disconnected':
|
|
248
|
+
if (message.phoneCount > 0) {
|
|
249
|
+
console.log(chalk.yellow(`\n phone disconnected (${message.phoneCount} remaining)\n`));
|
|
250
|
+
} else {
|
|
251
|
+
console.log(chalk.yellow(`\n phone disconnected\n`));
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
|
|
255
|
+
case 'session_expiring':
|
|
256
|
+
console.log(chalk.yellow(`\n session expiring in ${message.minutesRemaining} minutes\n`));
|
|
257
|
+
break;
|
|
258
|
+
|
|
259
|
+
case 'input':
|
|
260
|
+
if (ptyProcess) {
|
|
261
|
+
ptyProcess.write(message.data);
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
|
|
265
|
+
case 'resize':
|
|
266
|
+
if (ptyProcess && message.cols && message.rows) {
|
|
267
|
+
ptyProcess.resize(message.cols, message.rows);
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
293
271
|
}
|
|
294
|
-
|
|
295
|
-
|
|
272
|
+
|
|
273
|
+
function showQRCode() {
|
|
274
|
+
const connectionUrl = `${WEB_APP_URL}/session/${sessionId}?token=${sessionToken}`;
|
|
275
|
+
const durationMinutes = maxDuration ? Math.floor(maxDuration / 60000) : 60;
|
|
276
|
+
|
|
277
|
+
console.clear();
|
|
278
|
+
console.log(chalk.bold.white('\n remoto'));
|
|
279
|
+
console.log(chalk.dim(' control your terminal from your phone\n'));
|
|
280
|
+
|
|
281
|
+
qrcode.generate(connectionUrl, { small: true }, (qr) => {
|
|
282
|
+
const indentedQr = qr.split('\n').map(line => ' ' + line).join('\n');
|
|
283
|
+
console.log(indentedQr);
|
|
284
|
+
console.log(chalk.dim(`\n ${connectionUrl}\n`));
|
|
285
|
+
console.log(chalk.dim(' scan the qr code or open the link on your phone'));
|
|
286
|
+
console.log(chalk.dim(` session expires in ${durationMinutes} minutes`));
|
|
287
|
+
console.log(chalk.dim(' waiting for connection...\n'));
|
|
288
|
+
console.log(chalk.dim('─'.repeat(Math.min(cols, 60))));
|
|
289
|
+
});
|
|
296
290
|
}
|
|
297
|
-
process.exit();
|
|
298
|
-
}
|
|
299
291
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
292
|
+
function startPTY() {
|
|
293
|
+
try {
|
|
294
|
+
const sanitizedEnv = getSanitizedEnv();
|
|
295
|
+
ptyProcess = pty.spawn(shell, [], {
|
|
296
|
+
name: 'xterm-256color',
|
|
297
|
+
cols,
|
|
298
|
+
rows,
|
|
299
|
+
cwd: process.cwd(),
|
|
300
|
+
env: sanitizedEnv,
|
|
301
|
+
});
|
|
302
|
+
} catch (err) {
|
|
303
|
+
console.error(chalk.red(` failed to start shell: ${err.message}`));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
ptyProcess.onData((data) => {
|
|
308
|
+
process.stdout.write(data);
|
|
309
|
+
outputBuffer += data;
|
|
310
|
+
|
|
311
|
+
if (flushTimeout) clearTimeout(flushTimeout);
|
|
312
|
+
flushTimeout = setTimeout(() => {
|
|
313
|
+
if (outputBuffer && ws.readyState === WebSocket.OPEN) {
|
|
314
|
+
const chunks = chunkString(outputBuffer, 16000);
|
|
315
|
+
for (const chunk of chunks) {
|
|
316
|
+
ws.send(JSON.stringify({ type: 'output', data: chunk }));
|
|
317
|
+
}
|
|
318
|
+
outputBuffer = '';
|
|
319
|
+
}
|
|
320
|
+
}, 30);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
324
|
+
console.log(chalk.dim(`\n session ended`));
|
|
325
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
326
|
+
ws.send(JSON.stringify({ type: 'exit', code: exitCode }));
|
|
327
|
+
ws.close();
|
|
328
|
+
}
|
|
329
|
+
process.exit(exitCode);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
process.stdout.on('resize', () => {
|
|
333
|
+
cols = process.stdout.columns;
|
|
334
|
+
rows = process.stdout.rows;
|
|
335
|
+
if (ptyProcess) {
|
|
336
|
+
ptyProcess.resize(cols, rows);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
process.stdin.setRawMode(true);
|
|
341
|
+
process.stdin.resume();
|
|
342
|
+
process.stdin.on('data', (data) => {
|
|
343
|
+
if (ptyProcess) {
|
|
344
|
+
ptyProcess.write(data.toString());
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function cleanup() {
|
|
350
|
+
if (ptyProcess) {
|
|
351
|
+
ptyProcess.kill();
|
|
352
|
+
}
|
|
353
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
354
|
+
ws.close();
|
|
355
|
+
}
|
|
356
|
+
process.exit();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
process.on('SIGINT', cleanup);
|
|
360
|
+
process.on('SIGTERM', cleanup);
|
|
361
|
+
}
|
|
303
362
|
|
|
304
|
-
// Helper to chunk strings
|
|
305
363
|
function chunkString(str, size) {
|
|
306
364
|
const chunks = [];
|
|
307
365
|
for (let i = 0; i < str.length; i += size) {
|
|
@@ -309,3 +367,8 @@ function chunkString(str, size) {
|
|
|
309
367
|
}
|
|
310
368
|
return chunks.length ? chunks : [''];
|
|
311
369
|
}
|
|
370
|
+
|
|
371
|
+
main().catch(err => {
|
|
372
|
+
console.error(chalk.red(` error: ${err.message}`));
|
|
373
|
+
process.exit(1);
|
|
374
|
+
});
|