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 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/c91ca15d-0c84-400f-bbfa-3d58d1be07ee
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
- ### Password protection (recommended)
33
+ ### Secure by default
34
34
 
35
- ```bash
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
- # or set your own
39
- termbeam --password mysecret
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
- # One-off tunnel (deleted on shutdown)
69
- termbeam --tunnel --generate-password
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 --generate-password
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>`) | None |
94
- | `--generate-password` | Auto-generate a secure password | — |
95
- | `--tunnel` | Create an ephemeral devtunnel URL | Off |
96
- | `--persisted-tunnel` | Create a reusable devtunnel URL | Off |
97
- | `--port <port>` | Server port | `3456` |
98
- | `--host <addr>` | Bind address | `0.0.0.0` |
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 binds to all interfaces (`0.0.0.0`) by default, so it's accessible on your local network out of the box. **Always set a password** when running on a shared network, or pass `--host 127.0.0.1` to restrict access to your machine only.
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": "0.1.0",
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 test/*.test.js",
13
- "test:coverage": "c8 --reporter=text --reporter=lcov --reporter=json-summary --reporter=json node --test --test-reporter=spec --test-reporter-destination=stdout test/*.test.js",
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="/Users/dorlugasigal" />
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
- const initial = cwdInput.value.trim() || '/';
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
- if (!data.dirs.length) {
984
- browserList.innerHTML = '<div class="browser-empty">No subfolders</div>';
985
- return;
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
- browserList.innerHTML = data.dirs
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 parts = dir.split('/').filter(Boolean);
1009
- let html = `<button class="crumb" data-path="/">/</button>`;
1010
- let accumulated = '';
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 += '/' + part;
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-v2';
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
- // Cache-first for static assets
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;