remotosh 1.0.2 → 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 +318 -175
- package/package.json +1 -1
package/bin/remoto.js
CHANGED
|
@@ -1,227 +1,365 @@
|
|
|
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');
|
|
17
|
+
|
|
18
|
+
// Sensitive environment variable patterns to filter out
|
|
19
|
+
const SENSITIVE_ENV_PATTERNS = [
|
|
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,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Create sanitized environment for PTY
|
|
31
|
+
function getSanitizedEnv() {
|
|
32
|
+
const env = { ...process.env };
|
|
33
|
+
for (const key of Object.keys(env)) {
|
|
34
|
+
if (SENSITIVE_ENV_PATTERNS.some(pattern => pattern.test(key))) {
|
|
35
|
+
delete env[key];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return env;
|
|
39
|
+
}
|
|
13
40
|
|
|
14
|
-
//
|
|
15
|
-
|
|
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
|
+
}
|
|
16
52
|
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
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
|
+
}
|
|
63
|
+
}
|
|
20
64
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
let sessionToken = null;
|
|
35
|
-
let isAnonymous = true;
|
|
36
|
-
let outputBuffer = '';
|
|
37
|
-
let flushTimeout = null;
|
|
38
|
-
|
|
39
|
-
ws.on('open', () => {
|
|
40
|
-
// Connection established, wait for session_created message
|
|
41
|
-
});
|
|
65
|
+
// Open URL in browser
|
|
66
|
+
async function openBrowser(url) {
|
|
67
|
+
const { exec } = await import('child_process');
|
|
68
|
+
const platform = os.platform();
|
|
69
|
+
|
|
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
|
+
}
|
|
42
78
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
+
}
|
|
86
|
+
|
|
87
|
+
// Poll for auth completion
|
|
88
|
+
async function pollForAuth(deviceCode) {
|
|
89
|
+
const pollUrl = `${WEB_APP_URL}/api/cli-auth/poll?code=${deviceCode}`;
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < 60; i++) { // Poll for 5 minutes max
|
|
92
|
+
await new Promise(r => setTimeout(r, 5000)); // Wait 5 seconds
|
|
93
|
+
|
|
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
|
+
}
|
|
52
107
|
}
|
|
53
|
-
});
|
|
54
108
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
cleanup();
|
|
58
|
-
});
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
59
111
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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;
|
|
64
119
|
}
|
|
65
|
-
process.exit(1);
|
|
66
|
-
});
|
|
67
120
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
case 'session_created':
|
|
71
|
-
sessionId = message.sessionId;
|
|
72
|
-
sessionToken = message.sessionToken;
|
|
73
|
-
isAnonymous = message.isAnonymous;
|
|
74
|
-
showQRCode();
|
|
75
|
-
startPTY();
|
|
76
|
-
break;
|
|
77
|
-
|
|
78
|
-
case 'phone_connected':
|
|
79
|
-
console.log(chalk.green(`\n phone connected`));
|
|
80
|
-
if (message.phoneCount > 1) {
|
|
81
|
-
console.log(chalk.dim(` ${message.phoneCount} devices connected`));
|
|
82
|
-
}
|
|
83
|
-
console.log('');
|
|
84
|
-
break;
|
|
85
|
-
|
|
86
|
-
case 'phone_disconnected':
|
|
87
|
-
if (message.phoneCount > 0) {
|
|
88
|
-
console.log(chalk.yellow(`\n phone disconnected (${message.phoneCount} remaining)\n`));
|
|
89
|
-
} else {
|
|
90
|
-
console.log(chalk.yellow(`\n phone disconnected\n`));
|
|
91
|
-
}
|
|
92
|
-
break;
|
|
121
|
+
// No token - need to authenticate via browser
|
|
122
|
+
console.log(chalk.yellow(' login required\n'));
|
|
93
123
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (ptyProcess) {
|
|
98
|
-
console.log(chalk.dim(` [debug] writing to pty: "${message.data}"`));
|
|
99
|
-
ptyProcess.write(message.data);
|
|
100
|
-
} else {
|
|
101
|
-
console.log(chalk.red(' [debug] ptyProcess is null!'));
|
|
102
|
-
}
|
|
103
|
-
break;
|
|
124
|
+
// Generate device code
|
|
125
|
+
const deviceCode = randomBytes(16).toString('hex');
|
|
126
|
+
const authUrl = `${WEB_APP_URL}/cli-auth?code=${deviceCode}`;
|
|
104
127
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
ptyProcess.resize(message.cols, message.rows);
|
|
109
|
-
}
|
|
110
|
-
break;
|
|
128
|
+
console.log(chalk.white(' opening browser to login...\n'));
|
|
129
|
+
|
|
130
|
+
await openBrowser(authUrl);
|
|
111
131
|
|
|
112
|
-
|
|
113
|
-
|
|
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...'));
|
|
135
|
+
|
|
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);
|
|
114
142
|
}
|
|
143
|
+
|
|
144
|
+
// Save token
|
|
145
|
+
saveConfig({ token });
|
|
146
|
+
console.log(chalk.green('\n logged in successfully!\n'));
|
|
147
|
+
|
|
148
|
+
return token;
|
|
115
149
|
}
|
|
116
150
|
|
|
117
|
-
|
|
118
|
-
|
|
151
|
+
// Detect shell
|
|
152
|
+
const shell = process.env.SHELL || (os.platform() === 'win32' ? 'powershell.exe' : 'zsh');
|
|
119
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() {
|
|
120
160
|
console.clear();
|
|
121
161
|
console.log(chalk.bold.white('\n remoto'));
|
|
122
162
|
console.log(chalk.dim(' control your terminal from your phone\n'));
|
|
123
163
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
console.log(
|
|
128
|
-
|
|
129
|
-
console.log(chalk.dim(' scan the qr code or open the link on your phone'));
|
|
130
|
-
console.log(chalk.dim(' waiting for connection...\n'));
|
|
131
|
-
console.log(chalk.dim('─'.repeat(Math.min(cols, 60))));
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function startPTY() {
|
|
136
|
-
// Initialize PTY
|
|
137
|
-
console.log(chalk.dim(` [debug] spawning shell: ${shell}`));
|
|
138
|
-
try {
|
|
139
|
-
ptyProcess = pty.spawn(shell, [], {
|
|
140
|
-
name: 'xterm-256color',
|
|
141
|
-
cols,
|
|
142
|
-
rows,
|
|
143
|
-
cwd: process.cwd(),
|
|
144
|
-
env: process.env,
|
|
145
|
-
});
|
|
146
|
-
console.log(chalk.dim(` [debug] pty spawned successfully, pid: ${ptyProcess.pid}`));
|
|
147
|
-
} catch (err) {
|
|
148
|
-
console.error(chalk.red(` [debug] pty spawn failed: ${err.message}`));
|
|
149
|
-
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);
|
|
150
169
|
}
|
|
151
170
|
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
// Write to local terminal
|
|
155
|
-
process.stdout.write(data);
|
|
156
|
-
|
|
157
|
-
// Buffer and send to server
|
|
158
|
-
outputBuffer += data;
|
|
159
|
-
|
|
160
|
-
if (flushTimeout) clearTimeout(flushTimeout);
|
|
161
|
-
flushTimeout = setTimeout(() => {
|
|
162
|
-
if (outputBuffer && ws.readyState === WebSocket.OPEN) {
|
|
163
|
-
// Chunk large outputs
|
|
164
|
-
const chunks = chunkString(outputBuffer, 16000);
|
|
165
|
-
for (const chunk of chunks) {
|
|
166
|
-
ws.send(JSON.stringify({ type: 'output', data: chunk }));
|
|
167
|
-
}
|
|
168
|
-
outputBuffer = '';
|
|
169
|
-
}
|
|
170
|
-
}, 30);
|
|
171
|
-
});
|
|
171
|
+
// Authenticate
|
|
172
|
+
const token = await authenticate();
|
|
172
173
|
|
|
173
|
-
|
|
174
|
-
ptyProcess.onExit(({ exitCode }) => {
|
|
175
|
-
console.log(chalk.dim(`\n session ended`));
|
|
174
|
+
console.log(chalk.dim(' connecting...'));
|
|
176
175
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
console.log(chalk.white('\n create an account to save session history'));
|
|
181
|
-
console.log(chalk.dim(` ${WEB_APP_URL}/signup\n`));
|
|
182
|
-
}
|
|
176
|
+
// Connect to WebSocket server
|
|
177
|
+
const wsUrl = `${WS_SERVER_URL}/cli/?token=${encodeURIComponent(token)}`;
|
|
178
|
+
const ws = new WebSocket(wsUrl);
|
|
183
179
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
187
197
|
}
|
|
188
|
-
process.exit(exitCode);
|
|
189
198
|
});
|
|
190
199
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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`));
|
|
197
217
|
}
|
|
218
|
+
cleanup();
|
|
198
219
|
});
|
|
199
220
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (ptyProcess) {
|
|
205
|
-
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'));
|
|
206
225
|
}
|
|
226
|
+
process.exit(1);
|
|
207
227
|
});
|
|
208
|
-
}
|
|
209
228
|
|
|
210
|
-
function
|
|
211
|
-
|
|
212
|
-
|
|
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
|
+
}
|
|
213
271
|
}
|
|
214
|
-
|
|
215
|
-
|
|
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
|
+
});
|
|
216
290
|
}
|
|
217
|
-
process.exit();
|
|
218
|
-
}
|
|
219
291
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
+
}
|
|
223
362
|
|
|
224
|
-
// Helper to chunk strings
|
|
225
363
|
function chunkString(str, size) {
|
|
226
364
|
const chunks = [];
|
|
227
365
|
for (let i = 0; i < str.length; i += size) {
|
|
@@ -229,3 +367,8 @@ function chunkString(str, size) {
|
|
|
229
367
|
}
|
|
230
368
|
return chunks.length ? chunks : [''];
|
|
231
369
|
}
|
|
370
|
+
|
|
371
|
+
main().catch(err => {
|
|
372
|
+
console.error(chalk.red(` error: ${err.message}`));
|
|
373
|
+
process.exit(1);
|
|
374
|
+
});
|