remotosh 1.0.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/.env.example +10 -0
- package/bin/remoto.js +217 -0
- package/package.json +19 -0
package/.env.example
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Remoto CLI Configuration
|
|
2
|
+
|
|
3
|
+
# Your API key (get from https://remoto.sh/dashboard/api-keys)
|
|
4
|
+
REMOTO_API_KEY=rmt_your-api-key-here
|
|
5
|
+
|
|
6
|
+
# WebSocket server URL
|
|
7
|
+
REMOTO_WS_URL=wss://remoto-ws.fly.dev
|
|
8
|
+
|
|
9
|
+
# Web app URL (for QR code)
|
|
10
|
+
REMOTO_WEB_URL=https://remoto.sh
|
package/bin/remoto.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import pty from 'node-pty';
|
|
5
|
+
import qrcode from 'qrcode-terminal';
|
|
6
|
+
import WebSocket from 'ws';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
|
|
9
|
+
// Configuration
|
|
10
|
+
const WS_SERVER_URL = process.env.REMOTO_WS_URL || 'wss://remoto-ws.fly.dev';
|
|
11
|
+
const WEB_APP_URL = process.env.REMOTO_WEB_URL || 'https://remoto.sh';
|
|
12
|
+
const API_KEY = process.env.REMOTO_API_KEY;
|
|
13
|
+
|
|
14
|
+
// Detect shell
|
|
15
|
+
const shell = process.env.SHELL || (os.platform() === 'win32' ? 'powershell.exe' : 'zsh');
|
|
16
|
+
|
|
17
|
+
// Terminal dimensions
|
|
18
|
+
let cols = process.stdout.columns || 80;
|
|
19
|
+
let rows = process.stdout.rows || 24;
|
|
20
|
+
|
|
21
|
+
console.clear();
|
|
22
|
+
console.log(chalk.bold.white('\n remoto'));
|
|
23
|
+
console.log(chalk.dim(' control your terminal from your phone\n'));
|
|
24
|
+
console.log(chalk.dim(' connecting...'));
|
|
25
|
+
|
|
26
|
+
// Connect to WebSocket server (API key is optional)
|
|
27
|
+
const wsUrl = API_KEY
|
|
28
|
+
? `${WS_SERVER_URL}/cli/?apiKey=${encodeURIComponent(API_KEY)}`
|
|
29
|
+
: `${WS_SERVER_URL}/cli/`;
|
|
30
|
+
const ws = new WebSocket(wsUrl);
|
|
31
|
+
|
|
32
|
+
let ptyProcess = null;
|
|
33
|
+
let sessionId = null;
|
|
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
|
+
});
|
|
42
|
+
|
|
43
|
+
ws.on('message', (data) => {
|
|
44
|
+
try {
|
|
45
|
+
const message = JSON.parse(data.toString());
|
|
46
|
+
handleServerMessage(message);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error(chalk.red(' invalid message from server'));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
ws.on('close', (code, reason) => {
|
|
53
|
+
console.log(chalk.dim(`\n disconnected (${code})`));
|
|
54
|
+
cleanup();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
ws.on('error', (err) => {
|
|
58
|
+
console.error(chalk.red(`\n connection error: ${err.message}`));
|
|
59
|
+
if (err.message.includes('ECONNREFUSED')) {
|
|
60
|
+
console.log(chalk.dim('\n make sure you have internet access'));
|
|
61
|
+
}
|
|
62
|
+
process.exit(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
function handleServerMessage(message) {
|
|
66
|
+
switch (message.type) {
|
|
67
|
+
case 'session_created':
|
|
68
|
+
sessionId = message.sessionId;
|
|
69
|
+
sessionToken = message.sessionToken;
|
|
70
|
+
isAnonymous = message.isAnonymous;
|
|
71
|
+
showQRCode();
|
|
72
|
+
startPTY();
|
|
73
|
+
break;
|
|
74
|
+
|
|
75
|
+
case 'phone_connected':
|
|
76
|
+
console.log(chalk.green(`\n phone connected`));
|
|
77
|
+
if (message.phoneCount > 1) {
|
|
78
|
+
console.log(chalk.dim(` ${message.phoneCount} devices connected`));
|
|
79
|
+
}
|
|
80
|
+
console.log('');
|
|
81
|
+
break;
|
|
82
|
+
|
|
83
|
+
case 'phone_disconnected':
|
|
84
|
+
if (message.phoneCount > 0) {
|
|
85
|
+
console.log(chalk.yellow(`\n phone disconnected (${message.phoneCount} remaining)\n`));
|
|
86
|
+
} else {
|
|
87
|
+
console.log(chalk.yellow(`\n phone disconnected\n`));
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
|
|
91
|
+
case 'input':
|
|
92
|
+
// Input from phone
|
|
93
|
+
if (ptyProcess) {
|
|
94
|
+
ptyProcess.write(message.data);
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
case 'resize':
|
|
99
|
+
// Resize from phone
|
|
100
|
+
if (ptyProcess && message.cols && message.rows) {
|
|
101
|
+
ptyProcess.resize(message.cols, message.rows);
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
|
|
105
|
+
default:
|
|
106
|
+
// Ignore unknown messages
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function showQRCode() {
|
|
111
|
+
const connectionUrl = `${WEB_APP_URL}/session/${sessionId}?token=${sessionToken}`;
|
|
112
|
+
|
|
113
|
+
console.clear();
|
|
114
|
+
console.log(chalk.bold.white('\n remoto'));
|
|
115
|
+
console.log(chalk.dim(' control your terminal from your phone\n'));
|
|
116
|
+
|
|
117
|
+
qrcode.generate(connectionUrl, { small: true }, (qr) => {
|
|
118
|
+
// Indent the QR code
|
|
119
|
+
const indentedQr = qr.split('\n').map(line => ' ' + line).join('\n');
|
|
120
|
+
console.log(indentedQr);
|
|
121
|
+
console.log(chalk.dim(`\n ${connectionUrl}\n`));
|
|
122
|
+
console.log(chalk.dim(' scan the qr code or open the link on your phone'));
|
|
123
|
+
console.log(chalk.dim(' waiting for connection...\n'));
|
|
124
|
+
console.log(chalk.dim('─'.repeat(Math.min(cols, 60))));
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function startPTY() {
|
|
129
|
+
// Initialize PTY
|
|
130
|
+
ptyProcess = pty.spawn(shell, [], {
|
|
131
|
+
name: 'xterm-256color',
|
|
132
|
+
cols,
|
|
133
|
+
rows,
|
|
134
|
+
cwd: process.cwd(),
|
|
135
|
+
env: process.env,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Handle PTY output
|
|
139
|
+
ptyProcess.onData((data) => {
|
|
140
|
+
// Write to local terminal
|
|
141
|
+
process.stdout.write(data);
|
|
142
|
+
|
|
143
|
+
// Buffer and send to server
|
|
144
|
+
outputBuffer += data;
|
|
145
|
+
|
|
146
|
+
if (flushTimeout) clearTimeout(flushTimeout);
|
|
147
|
+
flushTimeout = setTimeout(() => {
|
|
148
|
+
if (outputBuffer && ws.readyState === WebSocket.OPEN) {
|
|
149
|
+
// Chunk large outputs
|
|
150
|
+
const chunks = chunkString(outputBuffer, 16000);
|
|
151
|
+
for (const chunk of chunks) {
|
|
152
|
+
ws.send(JSON.stringify({ type: 'output', data: chunk }));
|
|
153
|
+
}
|
|
154
|
+
outputBuffer = '';
|
|
155
|
+
}
|
|
156
|
+
}, 30);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Handle PTY exit
|
|
160
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
161
|
+
console.log(chalk.dim(`\n session ended`));
|
|
162
|
+
|
|
163
|
+
// Show account nudge for anonymous users
|
|
164
|
+
if (isAnonymous) {
|
|
165
|
+
console.log(chalk.dim('\n ─────────────────────────────────────────'));
|
|
166
|
+
console.log(chalk.white('\n create an account to save session history'));
|
|
167
|
+
console.log(chalk.dim(` ${WEB_APP_URL}/signup\n`));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
171
|
+
ws.send(JSON.stringify({ type: 'exit', code: exitCode }));
|
|
172
|
+
ws.close();
|
|
173
|
+
}
|
|
174
|
+
process.exit(exitCode);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Handle local terminal resize
|
|
178
|
+
process.stdout.on('resize', () => {
|
|
179
|
+
cols = process.stdout.columns;
|
|
180
|
+
rows = process.stdout.rows;
|
|
181
|
+
if (ptyProcess) {
|
|
182
|
+
ptyProcess.resize(cols, rows);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Handle local input
|
|
187
|
+
process.stdin.setRawMode(true);
|
|
188
|
+
process.stdin.resume();
|
|
189
|
+
process.stdin.on('data', (data) => {
|
|
190
|
+
if (ptyProcess) {
|
|
191
|
+
ptyProcess.write(data.toString());
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function cleanup() {
|
|
197
|
+
if (ptyProcess) {
|
|
198
|
+
ptyProcess.kill();
|
|
199
|
+
}
|
|
200
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
201
|
+
ws.close();
|
|
202
|
+
}
|
|
203
|
+
process.exit();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Cleanup on exit
|
|
207
|
+
process.on('SIGINT', cleanup);
|
|
208
|
+
process.on('SIGTERM', cleanup);
|
|
209
|
+
|
|
210
|
+
// Helper to chunk strings
|
|
211
|
+
function chunkString(str, size) {
|
|
212
|
+
const chunks = [];
|
|
213
|
+
for (let i = 0; i < str.length; i += size) {
|
|
214
|
+
chunks.push(str.slice(i, i + size));
|
|
215
|
+
}
|
|
216
|
+
return chunks.length ? chunks : [''];
|
|
217
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "remotosh",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Control your terminal from your phone",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"remoto": "./bin/remoto.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "node bin/remoto.js",
|
|
11
|
+
"build": "echo 'No build step needed'"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"node-pty": "^1.0.0",
|
|
15
|
+
"qrcode-terminal": "^0.12.0",
|
|
16
|
+
"ws": "^8.18.0",
|
|
17
|
+
"chalk": "^5.3.0"
|
|
18
|
+
}
|
|
19
|
+
}
|