termbeam 0.1.0 → 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/README.md +30 -20
- package/package.json +14 -5
- package/public/index.html +35 -13
- package/public/sw.js +16 -2
- package/public/terminal.html +310 -33
- package/src/auth.js +2 -0
- package/src/cli.js +58 -18
- package/src/logger.js +32 -0
- package/src/routes.js +101 -5
- package/src/server.js +150 -112
- package/src/sessions.js +5 -3
- package/src/tunnel.js +27 -25
- package/src/websocket.js +26 -4
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ I built this because I kept needing to run quick commands on my dev machine whil
|
|
|
13
13
|
|
|
14
14
|
[Full documentation](https://dorlugasigal.github.io/TermBeam/)
|
|
15
15
|
|
|
16
|
-
https://github.com/user-attachments/assets/
|
|
16
|
+
https://github.com/user-attachments/assets/9dd4f3d7-f017-4314-9b3a-f6a5688e3671
|
|
17
17
|
|
|
18
18
|
## Quick Start
|
|
19
19
|
|
|
@@ -30,18 +30,22 @@ termbeam
|
|
|
30
30
|
|
|
31
31
|
Scan the QR code printed in your terminal, or open the URL on any device.
|
|
32
32
|
|
|
33
|
-
###
|
|
33
|
+
### Secure by default
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
termbeam --generate-password
|
|
35
|
+
TermBeam starts with a tunnel and auto-generated password out of the box — just run `termbeam` and scan the QR code.
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
termbeam
|
|
37
|
+
```bash
|
|
38
|
+
termbeam # tunnel + auto-password (default)
|
|
39
|
+
termbeam --password mysecret # use a specific password
|
|
40
|
+
termbeam --no-tunnel # LAN-only (no tunnel)
|
|
41
|
+
termbeam --no-password # disable password protection
|
|
40
42
|
```
|
|
41
43
|
|
|
42
44
|
## Features
|
|
43
45
|
|
|
44
46
|
- **Mobile-first UI** with on-screen touch bar (arrow keys, Tab, Enter, Ctrl shortcuts, Esc) and touch-optimized controls
|
|
47
|
+
- **Copy/paste support** — Copy button opens text overlay for finger-selectable terminal content; Paste button with clipboard API + fallback modal
|
|
48
|
+
- **Image paste** — paste images from clipboard, uploaded to server
|
|
45
49
|
- **Tabbed multi-session terminal** — open, switch, and manage multiple sessions from a single tab bar with drag-to-reorder
|
|
46
50
|
- **Split view** — view two sessions side-by-side (horizontal on desktop, vertical on mobile)
|
|
47
51
|
- **Session colors** — assign a color to each session for quick identification
|
|
@@ -65,11 +69,14 @@ termbeam --password mysecret
|
|
|
65
69
|
## Remote Access
|
|
66
70
|
|
|
67
71
|
```bash
|
|
68
|
-
#
|
|
69
|
-
termbeam
|
|
72
|
+
# Tunnel is on by default
|
|
73
|
+
termbeam
|
|
70
74
|
|
|
71
75
|
# Persisted tunnel (stable URL you can bookmark, reused across restarts, 30-day expiry)
|
|
72
|
-
termbeam --persisted-tunnel
|
|
76
|
+
termbeam --persisted-tunnel
|
|
77
|
+
|
|
78
|
+
# LAN-only (no tunnel)
|
|
79
|
+
termbeam --no-tunnel
|
|
73
80
|
```
|
|
74
81
|
|
|
75
82
|
Requires the [Dev Tunnels CLI](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started):
|
|
@@ -88,20 +95,23 @@ termbeam --port 8080 # custom port (default: 3456)
|
|
|
88
95
|
termbeam --host 127.0.0.1 # restrict to localhost (default: 0.0.0.0)
|
|
89
96
|
```
|
|
90
97
|
|
|
91
|
-
| Flag | Description | Default
|
|
92
|
-
| --------------------- | ---------------------------------------- |
|
|
93
|
-
| `--password <pw>` | Set access password (also accepts `--password=<pw>`) |
|
|
94
|
-
| `--
|
|
95
|
-
| `--
|
|
96
|
-
| `--
|
|
97
|
-
| `--
|
|
98
|
-
| `--
|
|
98
|
+
| Flag | Description | Default |
|
|
99
|
+
| --------------------- | ---------------------------------------- | ---------------- |
|
|
100
|
+
| `--password <pw>` | Set access password (also accepts `--password=<pw>`) | Auto-generated |
|
|
101
|
+
| `--no-password` | Disable password | — |
|
|
102
|
+
| `--generate-password` | Auto-generate a secure password | On |
|
|
103
|
+
| `--tunnel` | Create an ephemeral devtunnel URL | On |
|
|
104
|
+
| `--no-tunnel` | Disable tunnel (LAN-only) | — |
|
|
105
|
+
| `--persisted-tunnel` | Create a reusable devtunnel URL | Off |
|
|
106
|
+
| `--port <port>` | Server port | `3456` |
|
|
107
|
+
| `--host <addr>` | Bind address | `0.0.0.0` |
|
|
108
|
+
| `--log-level <level>` | Log verbosity (error/warn/info/debug) | `info` |
|
|
99
109
|
|
|
100
|
-
Environment variables: `PORT`, `TERMBEAM_PASSWORD`, `TERMBEAM_CWD`, `SHELL` (Unix fallback), `COMSPEC` (Windows fallback). See [Configuration docs](https://dorlugasigal.github.io/TermBeam/configuration/).
|
|
110
|
+
Environment variables: `PORT`, `TERMBEAM_PASSWORD`, `TERMBEAM_CWD`, `TERMBEAM_LOG_LEVEL`, `SHELL` (Unix fallback), `COMSPEC` (Windows fallback). See [Configuration docs](https://dorlugasigal.github.io/TermBeam/configuration/).
|
|
101
111
|
|
|
102
112
|
## Security
|
|
103
113
|
|
|
104
|
-
TermBeam
|
|
114
|
+
TermBeam auto-generates a password and creates a tunnel by default, so your terminal is protected out of the box. Be aware that the tunnel exposes your terminal to the internet — use `--no-tunnel` for LAN-only access, or `--host 127.0.0.1` to restrict to your machine only.
|
|
105
115
|
|
|
106
116
|
Auth uses secure httpOnly cookies with 24-hour expiry, login is rate-limited to 5 attempts per minute, and security headers (X-Frame-Options, X-Content-Type-Options, etc.) are set on all responses. API clients that can't use cookies can authenticate with an `Authorization: Bearer <password>` header. See the [Security Guide](https://dorlugasigal.github.io/TermBeam/security/) for more.
|
|
107
117
|
|
|
@@ -115,4 +125,4 @@ Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
|
115
125
|
|
|
116
126
|
## Acknowledgments
|
|
117
127
|
|
|
118
|
-
Special thanks to [@tamirdresher](https://github.com/tamirdresher) for the [blog post](https://www.tamirdresher.com/blog/2026/02/26/squad-remote-control) that inspired the solution idea for this project, and for his [cli-tunnel](https://github.com/tamirdresher/cli-tunnel) implementation.
|
|
128
|
+
Special thanks to [@tamirdresher](https://github.com/tamirdresher) for the [blog post](https://www.tamirdresher.com/blog/2026/02/26/squad-remote-control) that inspired the solution idea for this project, and for his [cli-tunnel](https://github.com/tamirdresher/cli-tunnel) implementation.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "termbeam",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Beam your terminal to any device — mobile-optimized web terminal with multi-session support",
|
|
5
5
|
"main": "src/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/termbeam.js",
|
|
11
11
|
"dev": "node bin/termbeam.js --generate-password",
|
|
12
|
-
"test": "node --test
|
|
13
|
-
"test:coverage": "c8 --reporter=text --reporter=lcov --reporter=json-summary --reporter=json node --test
|
|
12
|
+
"test": "node -e \"require('child_process').execFileSync(process.execPath,['--test',...require('fs').readdirSync('test').filter(f=>f.endsWith('.test.js')).map(f=>'test/'+f)],{stdio:'inherit'})\"",
|
|
13
|
+
"test:coverage": "c8 --exclude=src/tunnel.js --reporter=text --reporter=lcov --reporter=json-summary --reporter=json node -e \"require('child_process').execFileSync(process.execPath,['--test','--test-reporter=spec','--test-reporter-destination=stdout',...require('fs').readdirSync('test').filter(f=>f.endsWith('.test.js')).map(f=>'test/'+f)],{stdio:'inherit'})\"",
|
|
14
14
|
"prepare": "husky",
|
|
15
15
|
"format": "prettier --write .",
|
|
16
16
|
"lint": "node --check src/*.js bin/*.js",
|
|
@@ -24,9 +24,18 @@
|
|
|
24
24
|
"remote-terminal",
|
|
25
25
|
"xterm",
|
|
26
26
|
"websocket",
|
|
27
|
-
"ssh-alternative"
|
|
27
|
+
"ssh-alternative",
|
|
28
|
+
"mobile-terminal",
|
|
29
|
+
"terminal-sharing",
|
|
30
|
+
"browser-terminal",
|
|
31
|
+
"remote-access",
|
|
32
|
+
"qr-code",
|
|
33
|
+
"touch-terminal",
|
|
34
|
+
"terminal-emulator",
|
|
35
|
+
"devtools",
|
|
36
|
+
"cli"
|
|
28
37
|
],
|
|
29
|
-
"author": "",
|
|
38
|
+
"author": "Dor Lugasi <dorlugasigal@gmail.com>",
|
|
30
39
|
"license": "MIT",
|
|
31
40
|
"homepage": "https://github.com/dorlugasigal/TermBeam",
|
|
32
41
|
"repository": {
|
package/public/index.html
CHANGED
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
10
10
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
11
11
|
<meta name="theme-color" content="#1e1e1e" />
|
|
12
|
+
<meta name="description" content="TermBeam — beam your terminal to any device. Mobile-optimized web terminal with multi-session support, touch controls, and QR code connection. No SSH needed." />
|
|
12
13
|
<link rel="manifest" href="/manifest.json" />
|
|
13
14
|
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
|
|
14
|
-
<title>TermBeam</title>
|
|
15
|
+
<title>TermBeam — Beam Your Terminal to Any Device</title>
|
|
15
16
|
<style>
|
|
16
17
|
:root {
|
|
17
18
|
--bg: #1e1e1e;
|
|
@@ -647,7 +648,7 @@
|
|
|
647
648
|
</div>
|
|
648
649
|
<label for="sess-cwd">Working Directory</label>
|
|
649
650
|
<div class="cwd-picker">
|
|
650
|
-
<input type="text" id="sess-cwd" placeholder="
|
|
651
|
+
<input type="text" id="sess-cwd" placeholder="Uses server default" />
|
|
651
652
|
<button type="button" class="cwd-browse-btn" id="browse-btn" title="Browse folders">
|
|
652
653
|
<svg
|
|
653
654
|
width="18"
|
|
@@ -838,6 +839,10 @@
|
|
|
838
839
|
try {
|
|
839
840
|
const res = await fetch('/api/shells');
|
|
840
841
|
const data = await res.json();
|
|
842
|
+
if (data.cwd) {
|
|
843
|
+
document.getElementById('sess-cwd').placeholder = data.cwd;
|
|
844
|
+
hubServerCwd = data.cwd;
|
|
845
|
+
}
|
|
841
846
|
shellSelect.innerHTML = '';
|
|
842
847
|
for (const s of data.shells) {
|
|
843
848
|
const opt = document.createElement('option');
|
|
@@ -952,9 +957,16 @@
|
|
|
952
957
|
const browserBreadcrumb = document.getElementById('browser-breadcrumb');
|
|
953
958
|
const browserPath = document.getElementById('browser-path');
|
|
954
959
|
let currentBrowsePath = '/';
|
|
960
|
+
let hubServerCwd = '/';
|
|
955
961
|
|
|
956
|
-
document.getElementById('browse-btn').addEventListener('click', () => {
|
|
957
|
-
|
|
962
|
+
document.getElementById('browse-btn').addEventListener('click', async () => {
|
|
963
|
+
if (hubServerCwd === '/') {
|
|
964
|
+
try {
|
|
965
|
+
const data = await fetch('/api/shells').then(r => r.json());
|
|
966
|
+
if (data.cwd) hubServerCwd = data.cwd;
|
|
967
|
+
} catch {}
|
|
968
|
+
}
|
|
969
|
+
const initial = cwdInput.value.trim() || hubServerCwd;
|
|
958
970
|
navigateTo(initial);
|
|
959
971
|
browserOverlay.classList.add('visible');
|
|
960
972
|
});
|
|
@@ -980,11 +992,17 @@
|
|
|
980
992
|
try {
|
|
981
993
|
const res = await fetch(`/api/dirs?q=${encodeURIComponent(dir + '/')}`);
|
|
982
994
|
const data = await res.json();
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
995
|
+
let items = '';
|
|
996
|
+
// Add parent (..) entry unless at root
|
|
997
|
+
const parent = dir.replace(/[/\\][^/\\]+$/, '') || (dir.includes('\\') ? dir.match(/^[A-Za-z]:\\/)?.[0] : '/');
|
|
998
|
+
if (parent && parent !== dir) {
|
|
999
|
+
items += `<div class="folder-item" data-path="${esc(parent)}">
|
|
1000
|
+
<span class="folder-icon">📁</span>
|
|
1001
|
+
<span class="folder-name">..</span>
|
|
1002
|
+
<span class="folder-arrow">›</span>
|
|
1003
|
+
</div>`;
|
|
986
1004
|
}
|
|
987
|
-
|
|
1005
|
+
items += data.dirs
|
|
988
1006
|
.map((d) => {
|
|
989
1007
|
const name = d.split(/[/\\]/).pop();
|
|
990
1008
|
return `<div class="folder-item" data-path="${esc(d)}">
|
|
@@ -995,6 +1013,7 @@
|
|
|
995
1013
|
})
|
|
996
1014
|
.join('');
|
|
997
1015
|
|
|
1016
|
+
browserList.innerHTML = items || '<div class="browser-empty">No subfolders</div>';
|
|
998
1017
|
browserList.querySelectorAll('.folder-item').forEach((el) => {
|
|
999
1018
|
el.addEventListener('click', () => navigateTo(el.dataset.path));
|
|
1000
1019
|
});
|
|
@@ -1005,13 +1024,15 @@
|
|
|
1005
1024
|
}
|
|
1006
1025
|
|
|
1007
1026
|
function renderBreadcrumb(dir) {
|
|
1008
|
-
const
|
|
1009
|
-
|
|
1010
|
-
|
|
1027
|
+
const sep = dir.includes('\\') ? '\\' : '/';
|
|
1028
|
+
const parts = dir.split(/[/\\]/).filter(Boolean);
|
|
1029
|
+
const isWindows = /^[A-Za-z]:/.test(dir);
|
|
1030
|
+
let html = isWindows ? '' : `<button class="crumb" data-path="/">/</button>`;
|
|
1031
|
+
let accumulated = isWindows ? '' : '';
|
|
1011
1032
|
parts.forEach((part, i) => {
|
|
1012
|
-
accumulated += '
|
|
1033
|
+
accumulated += (i === 0 && isWindows ? '' : sep) + part;
|
|
1013
1034
|
const isCurrent = i === parts.length - 1;
|
|
1014
|
-
html += `<span class="crumb-sep">›</span>`;
|
|
1035
|
+
if (i > 0 || isWindows) html += `<span class="crumb-sep">›</span>`;
|
|
1015
1036
|
html += `<button class="crumb${isCurrent ? ' current' : ''}" data-path="${esc(accumulated)}">${esc(part)}</button>`;
|
|
1016
1037
|
});
|
|
1017
1038
|
browserBreadcrumb.innerHTML = html;
|
|
@@ -1031,6 +1052,7 @@
|
|
|
1031
1052
|
.catch(() => {});
|
|
1032
1053
|
|
|
1033
1054
|
loadSessions();
|
|
1055
|
+
loadShells();
|
|
1034
1056
|
setInterval(loadSessions, 3000);
|
|
1035
1057
|
|
|
1036
1058
|
// Share button
|
package/public/sw.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const CACHE_NAME = 'termbeam-
|
|
1
|
+
const CACHE_NAME = 'termbeam-v5';
|
|
2
2
|
const SHELL_URLS = ['/', '/terminal'];
|
|
3
3
|
|
|
4
4
|
self.addEventListener('install', (event) => {
|
|
@@ -56,7 +56,21 @@ self.addEventListener('fetch', (event) => {
|
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
//
|
|
59
|
+
// Network-first for HTML pages (always get latest code)
|
|
60
|
+
if (event.request.mode === 'navigate' || event.request.headers.get('accept')?.includes('text/html')) {
|
|
61
|
+
event.respondWith(
|
|
62
|
+
fetch(event.request).then((response) => {
|
|
63
|
+
if (response.ok) {
|
|
64
|
+
const clone = response.clone();
|
|
65
|
+
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
|
|
66
|
+
}
|
|
67
|
+
return response;
|
|
68
|
+
}).catch(() => caches.match(event.request))
|
|
69
|
+
);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Cache-first for static assets (JS, CSS, images)
|
|
60
74
|
event.respondWith(
|
|
61
75
|
caches.match(event.request).then((cached) => {
|
|
62
76
|
if (cached) return cached;
|