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.
Files changed (2) hide show
  1. package/bin/remoto.js +299 -236
  2. 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 API_KEY = process.env.REMOTO_API_KEY;
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
- /^AZURE_/i,
18
- /^GCP_/i,
19
- /^GOOGLE_/i,
20
- /^GITHUB_TOKEN$/i,
21
- /^GH_TOKEN$/i,
22
- /^GITLAB_/i,
23
- /^NPM_TOKEN$/i,
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
- // Detect shell
66
- const shell = process.env.SHELL || (os.platform() === 'win32' ? 'powershell.exe' : 'zsh');
67
-
68
- // Terminal dimensions
69
- let cols = process.stdout.columns || 80;
70
- let rows = process.stdout.rows || 24;
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
- console.clear();
73
- console.log(chalk.bold.white('\n remoto'));
74
- console.log(chalk.dim(' control your terminal from your phone\n'));
75
-
76
- // API key is required
77
- if (!API_KEY) {
78
- console.log(chalk.yellow(' authentication required\n'));
79
- console.log(chalk.white(' to use remoto, you need an API key:\n'));
80
- console.log(chalk.dim(' 1. create a free account at ') + chalk.cyan('https://remoto.sh/signup'));
81
- console.log(chalk.dim(' 2. go to ') + chalk.cyan('https://remoto.sh/dashboard/api-keys'));
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
- console.log(chalk.dim(' connecting...'));
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
- // Connect to WebSocket server
95
- const wsUrl = `${WS_SERVER_URL}/cli/?apiKey=${encodeURIComponent(API_KEY)}`;
96
- const ws = new WebSocket(wsUrl);
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
- let ptyProcess = null;
99
- let sessionId = null;
100
- let sessionToken = null;
101
- let maxDuration = null;
102
- let outputBuffer = '';
103
- let flushTimeout = null;
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
- ws.on('open', () => {
106
- // Connection established, wait for session_created message
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
- ws.on('message', (data) => {
110
- const raw = data.toString();
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
- ws.on('close', (code, reason) => {
122
- const reasonStr = reason?.toString() || '';
123
-
124
- if (code === 4001) {
125
- console.log(chalk.red('\n error: API key required'));
126
- console.log(chalk.dim(' set REMOTO_API_KEY environment variable'));
127
- } else if (code === 4002) {
128
- console.log(chalk.red('\n error: invalid API key'));
129
- console.log(chalk.dim(' check your API key at https://remoto.sh/dashboard/api-keys'));
130
- } else if (code === 4004) {
131
- console.log(chalk.yellow('\n session limit reached'));
132
- console.log(chalk.dim(' free plan allows 2 concurrent sessions'));
133
- console.log(chalk.dim(' close an existing session and try again'));
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
- ws.on('error', (err) => {
145
- console.error(chalk.red(`\n connection error: ${err.message}`));
146
- if (err.message.includes('ECONNREFUSED')) {
147
- console.log(chalk.dim('\n make sure you have internet access'));
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
- function handleServerMessage(message) {
153
- switch (message.type) {
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
- case 'session_expiring':
179
- console.log(chalk.yellow(`\n session expiring in ${message.minutesRemaining} minutes\n`));
180
- break;
124
+ // Generate device code
125
+ const deviceCode = randomBytes(16).toString('hex');
126
+ const authUrl = `${WEB_APP_URL}/cli-auth?code=${deviceCode}`;
181
127
 
182
- case 'input':
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
- case 'resize':
190
- // Resize from phone
191
- if (ptyProcess && message.cols && message.rows) {
192
- ptyProcess.resize(message.cols, message.rows);
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
- default:
197
- // Ignore unknown messages
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
- function showQRCode() {
202
- const connectionUrl = `${WEB_APP_URL}/session/${sessionId}?token=${sessionToken}`;
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
- qrcode.generate(connectionUrl, { small: true }, (qr) => {
210
- // Indent the QR code
211
- const indentedQr = qr.split('\n').map(line => ' ' + line).join('\n');
212
- console.log(indentedQr);
213
- console.log(chalk.dim(`\n ${connectionUrl}\n`));
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
- // Handle PTY output
240
- ptyProcess.onData((data) => {
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
- // Handle PTY exit
261
- ptyProcess.onExit(({ exitCode }) => {
262
- console.log(chalk.dim(`\n session ended`));
174
+ console.log(chalk.dim(' connecting...'));
263
175
 
264
- if (ws.readyState === WebSocket.OPEN) {
265
- ws.send(JSON.stringify({ type: 'exit', code: exitCode }));
266
- ws.close();
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
- // Handle local terminal resize
272
- process.stdout.on('resize', () => {
273
- cols = process.stdout.columns;
274
- rows = process.stdout.rows;
275
- if (ptyProcess) {
276
- ptyProcess.resize(cols, rows);
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
- // Handle local input
281
- process.stdin.setRawMode(true);
282
- process.stdin.resume();
283
- process.stdin.on('data', (data) => {
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 cleanup() {
291
- if (ptyProcess) {
292
- ptyProcess.kill();
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
- if (ws.readyState === WebSocket.OPEN) {
295
- ws.close();
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
- // Cleanup on exit
301
- process.on('SIGINT', cleanup);
302
- process.on('SIGTERM', cleanup);
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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remotosh",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Control your terminal from your phone",
5
5
  "type": "module",
6
6
  "bin": {