termbeam 0.0.1

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.
@@ -0,0 +1,490 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
8
+ />
9
+ <meta name="apple-mobile-web-app-capable" content="yes" />
10
+ <meta name="mobile-web-app-capable" content="yes" />
11
+ <meta name="theme-color" content="#1a1a2e" />
12
+ <title>TermBeam — Terminal</title>
13
+ <link
14
+ rel="stylesheet"
15
+ href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css"
16
+ />
17
+ <style>
18
+ @font-face {
19
+ font-family: 'NerdFont';
20
+ src: url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/JetBrainsMono/Ligatures/Regular/JetBrainsMonoNerdFont-Regular.ttf')
21
+ format('truetype');
22
+ font-weight: normal;
23
+ font-style: normal;
24
+ font-display: swap;
25
+ }
26
+ @font-face {
27
+ font-family: 'NerdFont';
28
+ src: url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/JetBrainsMono/Ligatures/Bold/JetBrainsMonoNerdFont-Bold.ttf')
29
+ format('truetype');
30
+ font-weight: bold;
31
+ font-style: normal;
32
+ font-display: swap;
33
+ }
34
+
35
+ * {
36
+ margin: 0;
37
+ padding: 0;
38
+ box-sizing: border-box;
39
+ }
40
+ html,
41
+ body {
42
+ height: 100%;
43
+ width: 100%;
44
+ background: #1a1a2e;
45
+ color: #e0e0e0;
46
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
47
+ overflow: hidden;
48
+ touch-action: manipulation;
49
+ /* Use dvh to account for mobile browser chrome + keyboard */
50
+ height: 100dvh;
51
+ }
52
+
53
+ #status-bar {
54
+ height: 36px;
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: space-between;
58
+ padding: 0 12px;
59
+ background: #16213e;
60
+ border-bottom: 1px solid #0f3460;
61
+ font-size: 13px;
62
+ }
63
+ #status-bar .left {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 8px;
67
+ }
68
+ #back-btn {
69
+ background: none;
70
+ border: none;
71
+ color: #888;
72
+ font-size: 18px;
73
+ cursor: pointer;
74
+ padding: 0 4px;
75
+ }
76
+ #back-btn:active {
77
+ color: #e0e0e0;
78
+ }
79
+ #status-dot {
80
+ width: 8px;
81
+ height: 8px;
82
+ border-radius: 50%;
83
+ background: #e74c3c;
84
+ display: inline-block;
85
+ }
86
+ #status-dot.connected {
87
+ background: #2ecc71;
88
+ }
89
+ #status-text {
90
+ color: #aaa;
91
+ }
92
+ #session-name {
93
+ font-weight: 600;
94
+ }
95
+
96
+ #terminal-container {
97
+ position: absolute;
98
+ top: 36px;
99
+ left: 0;
100
+ right: 0;
101
+ bottom: 44px;
102
+ padding: 2px;
103
+ overflow: hidden;
104
+ }
105
+
106
+ #key-bar {
107
+ position: fixed;
108
+ bottom: 0;
109
+ left: 0;
110
+ right: 0;
111
+ height: 44px;
112
+ display: flex;
113
+ align-items: center;
114
+ background: #16213e;
115
+ border-top: 1px solid #0f3460;
116
+ padding: 0 3px;
117
+ padding-bottom: env(safe-area-inset-bottom, 0);
118
+ gap: 3px;
119
+ overflow-x: auto;
120
+ z-index: 50;
121
+ }
122
+ .key-btn {
123
+ min-width: 40px;
124
+ height: 32px;
125
+ background: #0f3460;
126
+ color: #e0e0e0;
127
+ border: 1px solid #1a1a5e;
128
+ border-radius: 6px;
129
+ font-size: 11px;
130
+ font-weight: 600;
131
+ cursor: pointer;
132
+ display: flex;
133
+ align-items: center;
134
+ justify-content: center;
135
+ -webkit-tap-highlight-color: transparent;
136
+ user-select: none;
137
+ white-space: nowrap;
138
+ padding: 0 6px;
139
+ flex-shrink: 0;
140
+ }
141
+ .key-btn:active {
142
+ background: #533483;
143
+ }
144
+ .key-btn.wide {
145
+ min-width: 52px;
146
+ }
147
+ .key-sep {
148
+ width: 1px;
149
+ height: 20px;
150
+ background: #0f3460;
151
+ flex-shrink: 0;
152
+ }
153
+
154
+ .xterm {
155
+ height: 100% !important;
156
+ }
157
+ .xterm-viewport {
158
+ overflow-y: hidden !important;
159
+ }
160
+
161
+ #reconnect-overlay {
162
+ display: none;
163
+ position: fixed;
164
+ top: 0;
165
+ left: 0;
166
+ right: 0;
167
+ bottom: 0;
168
+ background: rgba(0, 0, 0, 0.85);
169
+ z-index: 100;
170
+ flex-direction: column;
171
+ align-items: center;
172
+ justify-content: center;
173
+ gap: 16px;
174
+ }
175
+ #reconnect-overlay.visible {
176
+ display: flex;
177
+ }
178
+ #reconnect-overlay .msg {
179
+ font-size: 17px;
180
+ }
181
+ .overlay-actions {
182
+ display: flex;
183
+ gap: 12px;
184
+ }
185
+ .overlay-actions button {
186
+ padding: 10px 24px;
187
+ border: none;
188
+ border-radius: 8px;
189
+ font-size: 15px;
190
+ font-weight: 600;
191
+ cursor: pointer;
192
+ }
193
+ #reconnect-btn {
194
+ background: #533483;
195
+ color: white;
196
+ }
197
+ #back-to-sessions {
198
+ background: #0f3460;
199
+ color: #e0e0e0;
200
+ }
201
+ </style>
202
+ </head>
203
+ <body>
204
+ <div id="status-bar">
205
+ <div class="left">
206
+ <button id="back-btn" onclick="location.href = '/'">‹</button>
207
+ <span id="status-dot"></span>
208
+ <span id="session-name">…</span>
209
+ </div>
210
+ <span id="status-text">Connecting…</span>
211
+ <span id="version-text" style="font-size: 11px; color: #555; margin-left: 8px"></span>
212
+ </div>
213
+
214
+ <div id="terminal-container"></div>
215
+
216
+ <div id="key-bar">
217
+ <button class="key-btn" data-key="&#x1b;[A">↑</button>
218
+ <button class="key-btn" data-key="&#x1b;[B">↓</button>
219
+ <button class="key-btn" data-key="&#x1b;[D">←</button>
220
+ <button class="key-btn" data-key="&#x1b;[C">→</button>
221
+ <div class="key-sep"></div>
222
+ <button class="key-btn wide" data-key="&#x09;">Tab</button>
223
+ <button class="key-btn wide" data-key="&#x0d;">Enter</button>
224
+ <button class="key-btn" data-key="&#x1b;">Esc</button>
225
+ <div class="key-sep"></div>
226
+ <button class="key-btn" data-key="&#x03;">^C</button>
227
+ <button class="key-btn" data-key="&#x04;">^D</button>
228
+ <button class="key-btn" data-key="&#x1a;">^Z</button>
229
+ <button class="key-btn" data-key="&#x0c;">^L</button>
230
+ <div class="key-sep"></div>
231
+ <button class="key-btn" id="zoom-out">A-</button>
232
+ <button class="key-btn" id="zoom-in">A+</button>
233
+ </div>
234
+
235
+ <div id="reconnect-overlay">
236
+ <div class="msg">Session disconnected</div>
237
+ <div class="overlay-actions">
238
+ <button id="back-to-sessions" onclick="location.href = '/'">Sessions</button>
239
+ <button id="reconnect-btn">Reconnect</button>
240
+ </div>
241
+ </div>
242
+
243
+ <script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
244
+ <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
245
+ <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
246
+ <script>
247
+ const sessionId = new URLSearchParams(location.search).get('id');
248
+ if (!sessionId) {
249
+ location.href = '/';
250
+ }
251
+
252
+ const statusDot = document.getElementById('status-dot');
253
+ const statusText = document.getElementById('status-text');
254
+ const sessionName = document.getElementById('session-name');
255
+ const reconnectOverlay = document.getElementById('reconnect-overlay');
256
+
257
+ // Load Nerd Font, then init terminal
258
+ const nerdFont = new FontFace(
259
+ 'NerdFont',
260
+ "url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/JetBrainsMono/Ligatures/Regular/JetBrainsMonoNerdFont-Regular.ttf')",
261
+ );
262
+
263
+ nerdFont
264
+ .load()
265
+ .then((font) => {
266
+ document.fonts.add(font);
267
+ initTerminal();
268
+ })
269
+ .catch(() => {
270
+ // Fallback: init without Nerd Font
271
+ console.warn('Nerd Font failed to load, using fallback');
272
+ initTerminal();
273
+ });
274
+
275
+ function initTerminal() {
276
+ const savedFontSize = parseInt(localStorage.getItem('termbeam-fontsize') || '8', 10);
277
+ const term = new window.Terminal({
278
+ cursorBlink: true,
279
+ fontSize: savedFontSize,
280
+ fontFamily:
281
+ "'NerdFont', 'JetBrains Mono', 'MesloLGS NF', 'Hack Nerd Font', 'Fira Code', Menlo, monospace",
282
+ fontWeight: 'normal',
283
+ fontWeightBold: 'bold',
284
+ letterSpacing: 0,
285
+ lineHeight: 1.1,
286
+ theme: {
287
+ background: '#1a1a2e',
288
+ foreground: '#e0e0e0',
289
+ cursor: '#533483',
290
+ cursorAccent: '#1a1a2e',
291
+ selectionBackground: 'rgba(83, 52, 131, 0.4)',
292
+ black: '#1a1a2e',
293
+ red: '#e74c3c',
294
+ green: '#2ecc71',
295
+ yellow: '#f1c40f',
296
+ blue: '#3498db',
297
+ magenta: '#9b59b6',
298
+ cyan: '#1abc9c',
299
+ white: '#ecf0f1',
300
+ brightBlack: '#636e72',
301
+ brightRed: '#ff6b6b',
302
+ brightGreen: '#55efc4',
303
+ brightYellow: '#ffeaa7',
304
+ brightBlue: '#74b9ff',
305
+ brightMagenta: '#a29bfe',
306
+ brightCyan: '#81ecec',
307
+ brightWhite: '#ffffff',
308
+ },
309
+ allowProposedApi: true,
310
+ scrollback: 10000,
311
+ });
312
+
313
+ const fitAddon = new window.FitAddon.FitAddon();
314
+ const webLinksAddon = new window.WebLinksAddon.WebLinksAddon();
315
+ term.loadAddon(fitAddon);
316
+ term.loadAddon(webLinksAddon);
317
+
318
+ const container = document.getElementById('terminal-container');
319
+ term.open(container);
320
+ fitAddon.fit();
321
+
322
+ let ws = null;
323
+
324
+ function connect() {
325
+ const proto = location.protocol === 'https:' ? 'wss' : 'ws';
326
+ ws = new WebSocket(`${proto}://${location.host}/ws`);
327
+
328
+ ws.onopen = () => {
329
+ statusDot.className = 'connected';
330
+ statusText.textContent = 'Connected';
331
+ reconnectOverlay.classList.remove('visible');
332
+ // Attach to session
333
+ ws.send(JSON.stringify({ type: 'attach', sessionId }));
334
+ };
335
+
336
+ ws.onmessage = (event) => {
337
+ try {
338
+ const msg = JSON.parse(event.data);
339
+ if (msg.type === 'output') {
340
+ term.write(msg.data);
341
+ } else if (msg.type === 'attached') {
342
+ // Send terminal size after attach
343
+ const dims = fitAddon.proposeDimensions();
344
+ if (dims) {
345
+ ws.send(JSON.stringify({ type: 'resize', cols: dims.cols, rows: dims.rows }));
346
+ }
347
+ } else if (msg.type === 'exit') {
348
+ statusText.textContent = `Exited (code ${msg.code})`;
349
+ statusDot.className = '';
350
+ reconnectOverlay.querySelector('.msg').textContent =
351
+ `Session exited (code ${msg.code})`;
352
+ reconnectOverlay.classList.add('visible');
353
+ } else if (msg.type === 'error') {
354
+ statusText.textContent = msg.message;
355
+ reconnectOverlay.querySelector('.msg').textContent = msg.message;
356
+ reconnectOverlay.classList.add('visible');
357
+ }
358
+ } catch {
359
+ term.write(event.data);
360
+ }
361
+ };
362
+
363
+ ws.onclose = () => {
364
+ statusDot.className = '';
365
+ statusText.textContent = 'Disconnected';
366
+ reconnectOverlay.classList.add('visible');
367
+ };
368
+
369
+ ws.onerror = () => {
370
+ statusText.textContent = 'Connection error';
371
+ };
372
+ }
373
+
374
+ // Terminal input → WebSocket
375
+ term.onData((data) => {
376
+ if (ws && ws.readyState === 1) {
377
+ ws.send(JSON.stringify({ type: 'input', data }));
378
+ }
379
+ });
380
+
381
+ // Resize
382
+ function doResize() {
383
+ fitAddon.fit();
384
+ if (ws && ws.readyState === 1) {
385
+ const dims = fitAddon.proposeDimensions();
386
+ if (dims) {
387
+ ws.send(JSON.stringify({ type: 'resize', cols: dims.cols, rows: dims.rows }));
388
+ }
389
+ }
390
+ }
391
+ window.addEventListener('resize', doResize);
392
+ screen.orientation?.addEventListener('change', () => setTimeout(doResize, 150));
393
+
394
+ // Key bar (skip zoom buttons — handled separately)
395
+ // Use mousedown + preventDefault to stop buttons from stealing focus/opening keyboard
396
+ document.getElementById('key-bar').addEventListener('mousedown', (e) => {
397
+ // Only prevent default on buttons, not the scrollable bar itself
398
+ if (e.target.closest('.key-btn')) {
399
+ e.preventDefault();
400
+ }
401
+ });
402
+ // touchstart must be passive to allow native horizontal scrolling
403
+ document
404
+ .getElementById('key-bar')
405
+ .addEventListener('touchstart', () => {}, { passive: true });
406
+ document.getElementById('key-bar').addEventListener('click', (e) => {
407
+ const btn = e.target.closest('.key-btn');
408
+ if (!btn || btn.id === 'zoom-in' || btn.id === 'zoom-out') return;
409
+ if (ws && ws.readyState === 1) {
410
+ ws.send(JSON.stringify({ type: 'input', data: btn.dataset.key }));
411
+ }
412
+ // Don't call term.focus() here — it opens the soft keyboard
413
+ });
414
+
415
+ // Zoom
416
+ const MIN_FONT = 2,
417
+ MAX_FONT = 28;
418
+ let fontSize = parseInt(localStorage.getItem('termbeam-fontsize') || '8', 10);
419
+
420
+ function applyZoom(size) {
421
+ fontSize = Math.max(MIN_FONT, Math.min(MAX_FONT, size));
422
+ term.options.fontSize = fontSize;
423
+ localStorage.setItem('termbeam-fontsize', fontSize);
424
+ doResize();
425
+ }
426
+
427
+ document.getElementById('zoom-in').addEventListener('click', () => {
428
+ applyZoom(fontSize + 2);
429
+ });
430
+ document.getElementById('zoom-out').addEventListener('click', () => {
431
+ applyZoom(fontSize - 2);
432
+ });
433
+
434
+ // Reconnect
435
+ document.getElementById('reconnect-btn').addEventListener('click', () => {
436
+ term.clear();
437
+ connect();
438
+ });
439
+
440
+ // Tap terminal area to toggle keyboard (intentional user action)
441
+ container.addEventListener('click', () => term.focus());
442
+
443
+ // Handle mobile soft keyboard via visualViewport
444
+ // When keyboard opens, the viewport shrinks — reposition key bar and resize terminal
445
+ if (window.visualViewport) {
446
+ const keyBar = document.getElementById('key-bar');
447
+ const statusBar = document.getElementById('status-bar');
448
+
449
+ function onViewportResize() {
450
+ const vv = window.visualViewport;
451
+ const keyboardHeight = window.innerHeight - vv.height;
452
+
453
+ if (keyboardHeight > 50) {
454
+ // Keyboard is open — move key bar above it
455
+ keyBar.style.bottom = keyboardHeight + 'px';
456
+ container.style.bottom = 44 + keyboardHeight + 'px';
457
+ } else {
458
+ // Keyboard closed
459
+ keyBar.style.bottom = '0px';
460
+ container.style.bottom = '44px';
461
+ }
462
+ // Refit terminal to new available space
463
+ setTimeout(() => doResize(), 50);
464
+ }
465
+
466
+ window.visualViewport.addEventListener('resize', onViewportResize);
467
+ window.visualViewport.addEventListener('scroll', onViewportResize);
468
+ }
469
+
470
+ // Fetch session name
471
+ fetch('/api/sessions')
472
+ .then((r) => r.json())
473
+ .then((sessions) => {
474
+ const s = sessions.find((s) => s.id === sessionId);
475
+ if (s) sessionName.textContent = s.name;
476
+ });
477
+
478
+ // Fetch version
479
+ fetch('/api/version')
480
+ .then((r) => r.json())
481
+ .then((d) => {
482
+ document.getElementById('version-text').textContent = 'v' + d.version;
483
+ })
484
+ .catch(() => {});
485
+
486
+ connect();
487
+ }
488
+ </script>
489
+ </body>
490
+ </html>
package/src/auth.js ADDED
@@ -0,0 +1,124 @@
1
+ const crypto = require('crypto');
2
+
3
+ const LOGIN_HTML = `<!DOCTYPE html>
4
+ <html lang="en">
5
+ <head>
6
+ <meta charset="UTF-8" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
8
+ <meta name="theme-color" content="#1a1a2e" />
9
+ <title>TermBeam — Login</title>
10
+ <style>
11
+ * { margin: 0; padding: 0; box-sizing: border-box; }
12
+ html, body { height: 100%; background: #1a1a2e; color: #e0e0e0;
13
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
14
+ display: flex; align-items: center; justify-content: center; }
15
+ .card { background: #16213e; border: 1px solid #0f3460; border-radius: 16px;
16
+ padding: 32px 24px; width: 320px; text-align: center; }
17
+ h1 { font-size: 20px; margin-bottom: 8px; }
18
+ h1 span { color: #533483; }
19
+ p { font-size: 13px; color: #888; margin-bottom: 24px; }
20
+ input { width: 100%; padding: 12px; background: #1a1a2e; border: 1px solid #0f3460;
21
+ border-radius: 8px; color: #e0e0e0; font-size: 16px; outline: none;
22
+ text-align: center; letter-spacing: 2px; }
23
+ input:focus { border-color: #533483; }
24
+ button { width: 100%; padding: 12px; margin-top: 16px; background: #533483;
25
+ color: white; border: none; border-radius: 8px; font-size: 16px;
26
+ font-weight: 600; cursor: pointer; }
27
+ button:active { background: #6a42a8; }
28
+ .error { color: #e74c3c; font-size: 13px; margin-top: 12px; display: none; }
29
+ </style>
30
+ </head>
31
+ <body>
32
+ <div class="card">
33
+ <h1>📡 Term<span>Beam</span></h1>
34
+ <p>Enter the access password</p>
35
+ <form id="form">
36
+ <input type="password" id="pw" placeholder="Password" autocomplete="off" autofocus />
37
+ <button type="submit">Unlock</button>
38
+ </form>
39
+ <div class="error" id="err">Incorrect password</div>
40
+ </div>
41
+ <script>
42
+ document.getElementById('form').addEventListener('submit', async (e) => {
43
+ e.preventDefault();
44
+ const pw = document.getElementById('pw').value;
45
+ const res = await fetch('/api/auth', {
46
+ method: 'POST',
47
+ headers: { 'Content-Type': 'application/json' },
48
+ body: JSON.stringify({ password: pw }),
49
+ });
50
+ if (res.ok) { location.href = '/'; }
51
+ else {
52
+ document.getElementById('err').style.display = 'block';
53
+ document.getElementById('pw').value = '';
54
+ }
55
+ });
56
+ </script>
57
+ </body>
58
+ </html>`;
59
+
60
+ function createAuth(password) {
61
+ const tokens = new Map();
62
+ const authAttempts = new Map();
63
+
64
+ function generateToken() {
65
+ const token = crypto.randomBytes(32).toString('hex');
66
+ tokens.set(token, Date.now() + 24 * 60 * 60 * 1000);
67
+ return token;
68
+ }
69
+
70
+ function validateToken(token) {
71
+ const expiry = tokens.get(token);
72
+ if (!expiry) return false;
73
+ if (Date.now() > expiry) {
74
+ tokens.delete(token);
75
+ return false;
76
+ }
77
+ return true;
78
+ }
79
+
80
+ function middleware(req, res, next) {
81
+ if (!password) return next();
82
+ if (req.cookies.pty_token && validateToken(req.cookies.pty_token)) return next();
83
+ const authHeader = req.headers.authorization;
84
+ if (authHeader === `Bearer ${password}`) return next();
85
+ if (req.accepts('html')) return res.redirect('/login');
86
+ res.status(401).json({ error: 'unauthorized' });
87
+ }
88
+
89
+ function rateLimit(req, res, next) {
90
+ const ip = req.ip || req.socket.remoteAddress;
91
+ const now = Date.now();
92
+ const window = 60 * 1000;
93
+ const maxAttempts = 5;
94
+ const attempts = authAttempts.get(ip) || [];
95
+ const recent = attempts.filter((t) => now - t < window);
96
+ if (recent.length >= maxAttempts) {
97
+ return res.status(429).json({ error: 'Too many attempts. Try again later.' });
98
+ }
99
+ recent.push(now);
100
+ authAttempts.set(ip, recent);
101
+ next();
102
+ }
103
+
104
+ function parseCookies(str) {
105
+ const cookies = {};
106
+ str.split(';').forEach((c) => {
107
+ const [k, ...v] = c.trim().split('=');
108
+ if (k) cookies[k] = v.join('=');
109
+ });
110
+ return cookies;
111
+ }
112
+
113
+ return {
114
+ password,
115
+ generateToken,
116
+ validateToken,
117
+ middleware,
118
+ rateLimit,
119
+ parseCookies,
120
+ loginHTML: LOGIN_HTML,
121
+ };
122
+ }
123
+
124
+ module.exports = { createAuth };
package/src/cli.js ADDED
@@ -0,0 +1,81 @@
1
+ const os = require('os');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+
5
+ function printHelp() {
6
+ console.log(`
7
+ termbeam — Beam your terminal to any device
8
+
9
+ Usage:
10
+ termbeam [options] [shell] [args...]
11
+
12
+ Options:
13
+ --password <pw> Set access password (or TERMBEAM_PASSWORD env var)
14
+ --generate-password Auto-generate a secure password
15
+ --tunnel Create a public devtunnel URL
16
+ --port <port> Set port (default: 3456, or PORT env var)
17
+ --host <addr> Bind address (default: 0.0.0.0)
18
+ -h, --help Show this help
19
+ -v, --version Show version
20
+
21
+ Examples:
22
+ termbeam Start with default shell
23
+ termbeam --password secret Start with password auth
24
+ termbeam --generate-password Start with auto-generated password
25
+ termbeam --tunnel --password pw Start with public tunnel
26
+ termbeam /bin/bash Use bash instead of default shell
27
+
28
+ Environment:
29
+ PORT Server port (default: 3456)
30
+ TERMBEAM_PASSWORD Access password
31
+ TERMBEAM_CWD Working directory
32
+ `);
33
+ }
34
+
35
+ function parseArgs() {
36
+ let port = parseInt(process.env.PORT || '3456', 10);
37
+ let host = '0.0.0.0';
38
+ const defaultShell = process.env.SHELL || '/bin/zsh';
39
+ const cwd = process.env.TERMBEAM_CWD || process.env.PTY_CWD || process.cwd();
40
+ let password = process.env.TERMBEAM_PASSWORD || process.env.PTY_PASSWORD || null;
41
+ let useTunnel = false;
42
+
43
+ const args = process.argv.slice(2);
44
+ const filteredArgs = [];
45
+
46
+ for (let i = 0; i < args.length; i++) {
47
+ if (args[i] === '--password' && args[i + 1]) {
48
+ password = args[++i];
49
+ } else if (args[i] === '--tunnel') {
50
+ useTunnel = true;
51
+ } else if (args[i].startsWith('--password=')) {
52
+ password = args[i].split('=')[1];
53
+ } else if (args[i] === '--help' || args[i] === '-h') {
54
+ printHelp();
55
+ process.exit(0);
56
+ } else if (args[i] === '--version' || args[i] === '-v') {
57
+ const { getVersion } = require('./version');
58
+ console.log(`termbeam v${getVersion()}`);
59
+ process.exit(0);
60
+ } else if (args[i] === '--generate-password') {
61
+ password = crypto.randomBytes(16).toString('base64url');
62
+ console.log(`Generated password: ${password}`);
63
+ } else if (args[i] === '--port' && args[i + 1]) {
64
+ port = parseInt(args[++i], 10);
65
+ } else if (args[i] === '--host' && args[i + 1]) {
66
+ host = args[++i];
67
+ } else {
68
+ filteredArgs.push(args[i]);
69
+ }
70
+ }
71
+
72
+ const shell = filteredArgs[0] || defaultShell;
73
+ const shellArgs = filteredArgs.slice(1);
74
+
75
+ const { getVersion } = require('./version');
76
+ const version = getVersion();
77
+
78
+ return { port, host, password, useTunnel, shell, shellArgs, cwd, defaultShell, version };
79
+ }
80
+
81
+ module.exports = { parseArgs, printHelp };