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.
Files changed (3) hide show
  1. package/.env.example +10 -0
  2. package/bin/remoto.js +217 -0
  3. 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
+ }