termbeam 1.2.4 → 1.2.6
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 +13 -2
- package/package.json +3 -3
- package/public/sw.js +35 -33
- package/src/cli.js +15 -2
- package/src/devtunnel-install.js +141 -0
- package/src/logger.js +3 -2
- package/src/server.js +55 -22
- package/src/sessions.js +8 -2
- package/src/tunnel.js +39 -17
package/README.md
CHANGED
|
@@ -15,6 +15,16 @@ I built this because I kept needing to run quick commands on my dev machine whil
|
|
|
15
15
|
|
|
16
16
|
https://github.com/user-attachments/assets/9dd4f3d7-f017-4314-9b3a-f6a5688e3671
|
|
17
17
|
|
|
18
|
+
### Mobile UI
|
|
19
|
+
|
|
20
|
+
<table align="center">
|
|
21
|
+
<tr>
|
|
22
|
+
<td align="center"><img src="docs/assets/screenshots/mobile-session-hub.jpeg" alt="Session hub on mobile" width="250" /></td>
|
|
23
|
+
<td align="center"><img src="docs/assets/screenshots/mobile-session-preview.jpeg" alt="Session preview on mobile" width="250" /></td>
|
|
24
|
+
<td align="center"><img src="docs/assets/screenshots/mobile-terminal.jpeg" alt="Terminal on mobile" width="250" /></td>
|
|
25
|
+
</tr>
|
|
26
|
+
</table>
|
|
27
|
+
|
|
18
28
|
## Quick Start
|
|
19
29
|
|
|
20
30
|
```bash
|
|
@@ -81,7 +91,7 @@ termbeam --persisted-tunnel
|
|
|
81
91
|
termbeam --no-tunnel
|
|
82
92
|
```
|
|
83
93
|
|
|
84
|
-
|
|
94
|
+
If the [Dev Tunnels CLI](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started) is not installed, TermBeam will offer to install it for you automatically. You can also install it manually:
|
|
85
95
|
|
|
86
96
|
- **Windows:** `winget install Microsoft.devtunnel`
|
|
87
97
|
- **macOS:** `brew install --cask devtunnel`
|
|
@@ -102,9 +112,10 @@ termbeam --host 127.0.0.1 # restrict to localhost (default: 0.0.0.0)
|
|
|
102
112
|
| `--password <pw>` | Set access password (also accepts `--password=<pw>`) | Auto-generated |
|
|
103
113
|
| `--no-password` | Disable password | — |
|
|
104
114
|
| `--generate-password` | Auto-generate a secure password | On |
|
|
105
|
-
| `--tunnel` | Create an ephemeral devtunnel URL
|
|
115
|
+
| `--tunnel` | Create an ephemeral devtunnel URL (private) | On |
|
|
106
116
|
| `--no-tunnel` | Disable tunnel (LAN-only) | — |
|
|
107
117
|
| `--persisted-tunnel` | Create a reusable devtunnel URL | Off |
|
|
118
|
+
| `--public` | Allow public tunnel access | Off |
|
|
108
119
|
| `--port <port>` | Server port | `3456` |
|
|
109
120
|
| `--host <addr>` | Bind address | `0.0.0.0` |
|
|
110
121
|
| `--log-level <level>` | Log verbosity (error/warn/info/debug) | `info` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "termbeam",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
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 -e \"require('child_process').execFileSync(process.execPath,['--test',...require('fs').readdirSync('test').filter(f=>f.endsWith('.test.js')&&!f.startsWith('e2e-')).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')&&!f.startsWith('e2e-')).map(f=>'test/'+f)],{stdio:'inherit'})\"",
|
|
12
|
+
"test": "node -e \"require('child_process').execFileSync(process.execPath,['--test',...require('fs').readdirSync('test').filter(f=>f.endsWith('.test.js')&&!f.startsWith('e2e-')&&f!=='devtunnel-install.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')&&!f.startsWith('e2e-')&&f!=='devtunnel-install.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",
|
package/public/sw.js
CHANGED
|
@@ -2,17 +2,17 @@ const CACHE_NAME = 'termbeam-v5';
|
|
|
2
2
|
const SHELL_URLS = ['/', '/terminal'];
|
|
3
3
|
|
|
4
4
|
self.addEventListener('install', (event) => {
|
|
5
|
-
event.waitUntil(
|
|
6
|
-
caches.open(CACHE_NAME).then((cache) => cache.addAll(SHELL_URLS))
|
|
7
|
-
);
|
|
5
|
+
event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(SHELL_URLS)));
|
|
8
6
|
self.skipWaiting();
|
|
9
7
|
});
|
|
10
8
|
|
|
11
9
|
self.addEventListener('activate', (event) => {
|
|
12
10
|
event.waitUntil(
|
|
13
|
-
caches
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
caches
|
|
12
|
+
.keys()
|
|
13
|
+
.then((keys) =>
|
|
14
|
+
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))),
|
|
15
|
+
),
|
|
16
16
|
);
|
|
17
17
|
self.clients.claim();
|
|
18
18
|
});
|
|
@@ -21,11 +21,7 @@ self.addEventListener('fetch', (event) => {
|
|
|
21
21
|
const url = new URL(event.request.url);
|
|
22
22
|
|
|
23
23
|
// Don't cache WebSocket upgrades
|
|
24
|
-
if (
|
|
25
|
-
event.request.mode === 'websocket' ||
|
|
26
|
-
url.protocol === 'ws:' ||
|
|
27
|
-
url.protocol === 'wss:'
|
|
28
|
-
) {
|
|
24
|
+
if (event.request.mode === 'websocket' || url.protocol === 'ws:' || url.protocol === 'wss:') {
|
|
29
25
|
return;
|
|
30
26
|
}
|
|
31
27
|
|
|
@@ -42,7 +38,7 @@ self.addEventListener('fetch', (event) => {
|
|
|
42
38
|
}
|
|
43
39
|
return response;
|
|
44
40
|
});
|
|
45
|
-
})
|
|
41
|
+
}),
|
|
46
42
|
);
|
|
47
43
|
}
|
|
48
44
|
return;
|
|
@@ -50,37 +46,43 @@ self.addEventListener('fetch', (event) => {
|
|
|
50
46
|
|
|
51
47
|
// Network-first for API calls
|
|
52
48
|
if (url.pathname.startsWith('/api/')) {
|
|
53
|
-
event.respondWith(
|
|
54
|
-
fetch(event.request).catch(() => caches.match(event.request))
|
|
55
|
-
);
|
|
49
|
+
event.respondWith(fetch(event.request).catch(() => caches.match(event.request)));
|
|
56
50
|
return;
|
|
57
51
|
}
|
|
58
52
|
|
|
59
53
|
// Network-first for HTML pages (always get latest code)
|
|
60
|
-
if (
|
|
54
|
+
if (
|
|
55
|
+
event.request.mode === 'navigate' ||
|
|
56
|
+
event.request.headers.get('accept')?.includes('text/html')
|
|
57
|
+
) {
|
|
61
58
|
event.respondWith(
|
|
62
|
-
fetch(event.request)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
fetch(event.request)
|
|
60
|
+
.then((response) => {
|
|
61
|
+
if (response.ok) {
|
|
62
|
+
const clone = response.clone();
|
|
63
|
+
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
|
|
64
|
+
}
|
|
65
|
+
return response;
|
|
66
|
+
})
|
|
67
|
+
.catch(() => caches.match(event.request)),
|
|
69
68
|
);
|
|
70
69
|
return;
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
// Cache-first for static assets (JS, CSS, images)
|
|
74
73
|
event.respondWith(
|
|
75
|
-
caches
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
caches
|
|
75
|
+
.match(event.request)
|
|
76
|
+
.then((cached) => {
|
|
77
|
+
if (cached) return cached;
|
|
78
|
+
return fetch(event.request).then((response) => {
|
|
79
|
+
if (response.ok) {
|
|
80
|
+
const clone = response.clone();
|
|
81
|
+
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
|
|
82
|
+
}
|
|
83
|
+
return response;
|
|
84
|
+
});
|
|
85
|
+
})
|
|
86
|
+
.catch(() => new Response('Offline', { status: 503, statusText: 'Service Unavailable' })),
|
|
85
87
|
);
|
|
86
88
|
});
|
package/src/cli.js
CHANGED
|
@@ -15,9 +15,10 @@ Options:
|
|
|
15
15
|
--password <pw> Set access password (or TERMBEAM_PASSWORD env var)
|
|
16
16
|
--generate-password Auto-generate a secure password (default: auto)
|
|
17
17
|
--no-password Disable password authentication
|
|
18
|
-
--tunnel Create a
|
|
18
|
+
--tunnel Create a devtunnel URL (default: on, private access)
|
|
19
19
|
--no-tunnel Disable tunnel (LAN-only mode)
|
|
20
20
|
--persisted-tunnel Create a reusable devtunnel URL (stable across restarts)
|
|
21
|
+
--public Allow public tunnel access (default: private, owner-only)
|
|
21
22
|
--port <port> Set port (default: 3456, or PORT env var)
|
|
22
23
|
--host <addr> Bind address (default: 0.0.0.0)
|
|
23
24
|
--log-level <level> Set log verbosity: error, warn, info, debug (default: info)
|
|
@@ -26,7 +27,9 @@ Options:
|
|
|
26
27
|
|
|
27
28
|
Defaults:
|
|
28
29
|
By default, TermBeam enables tunnel + auto-generated password for secure
|
|
29
|
-
mobile access (clipboard, HTTPS).
|
|
30
|
+
mobile access (clipboard, HTTPS). Tunnels are private (owner-only via
|
|
31
|
+
Microsoft login). Use --public for public access, or
|
|
32
|
+
--no-tunnel for LAN-only mode.
|
|
30
33
|
|
|
31
34
|
Examples:
|
|
32
35
|
termbeam Start with tunnel + auto password
|
|
@@ -227,6 +230,7 @@ function parseArgs() {
|
|
|
227
230
|
let useTunnel = true;
|
|
228
231
|
let noTunnel = false;
|
|
229
232
|
let persistedTunnel = false;
|
|
233
|
+
let anonymousTunnel = false;
|
|
230
234
|
let explicitPassword = !!password;
|
|
231
235
|
|
|
232
236
|
const args = process.argv.slice(2);
|
|
@@ -243,6 +247,8 @@ function parseArgs() {
|
|
|
243
247
|
} else if (args[i] === '--persisted-tunnel') {
|
|
244
248
|
useTunnel = true;
|
|
245
249
|
persistedTunnel = true;
|
|
250
|
+
} else if (args[i] === '--public') {
|
|
251
|
+
anonymousTunnel = true;
|
|
246
252
|
} else if (args[i].startsWith('--password=')) {
|
|
247
253
|
password = args[i].split('=')[1];
|
|
248
254
|
explicitPassword = true;
|
|
@@ -278,6 +284,12 @@ function parseArgs() {
|
|
|
278
284
|
// --no-tunnel disables the default tunnel
|
|
279
285
|
if (noTunnel) useTunnel = false;
|
|
280
286
|
|
|
287
|
+
// --public requires a tunnel
|
|
288
|
+
if (anonymousTunnel && !useTunnel) {
|
|
289
|
+
console.error('Error: --public requires a tunnel. Remove --no-tunnel or remove --public.');
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
|
|
281
293
|
const shell = filteredArgs[0] || defaultShell;
|
|
282
294
|
const shellArgs = filteredArgs.slice(1);
|
|
283
295
|
|
|
@@ -290,6 +302,7 @@ function parseArgs() {
|
|
|
290
302
|
password,
|
|
291
303
|
useTunnel,
|
|
292
304
|
persistedTunnel,
|
|
305
|
+
anonymousTunnel,
|
|
293
306
|
shell,
|
|
294
307
|
shellArgs,
|
|
295
308
|
cwd,
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
const { execSync, execFileSync } = require('child_process');
|
|
6
|
+
const log = require('./logger');
|
|
7
|
+
|
|
8
|
+
const INSTALL_DIR = path.join(os.homedir(), 'bin');
|
|
9
|
+
|
|
10
|
+
function getInstallDir() {
|
|
11
|
+
return INSTALL_DIR;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getBinaryName() {
|
|
15
|
+
return process.platform === 'win32' ? 'devtunnel.exe' : 'devtunnel';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function promptUser(question) {
|
|
19
|
+
if (!process.stdin.isTTY) {
|
|
20
|
+
return Promise.resolve('');
|
|
21
|
+
}
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
24
|
+
rl.question(question, (answer) => {
|
|
25
|
+
rl.close();
|
|
26
|
+
resolve(answer.trim().toLowerCase());
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
32
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
33
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
34
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
35
|
+
|
|
36
|
+
async function promptInstall() {
|
|
37
|
+
if (
|
|
38
|
+
process.platform !== 'darwin' &&
|
|
39
|
+
process.platform !== 'linux' &&
|
|
40
|
+
process.platform !== 'win32'
|
|
41
|
+
) {
|
|
42
|
+
log.error(`Unsupported platform: ${process.platform}/${process.arch}`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
process.stderr.write('\n');
|
|
47
|
+
process.stderr.write(` ${yellow('⚠')} ${bold('DevTunnel CLI is not installed.')}\n`);
|
|
48
|
+
process.stderr.write(` ${cyan('TermBeam uses tunnels by default for remote access.')}\n`);
|
|
49
|
+
process.stderr.write('\n');
|
|
50
|
+
const answer = await promptUser(` Would you like me to install it for you? ${bold('(y/n)')} `);
|
|
51
|
+
if (answer !== 'y') {
|
|
52
|
+
log.info('Skipping DevTunnel install.');
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return installDevtunnel();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function installDevtunnel() {
|
|
60
|
+
try {
|
|
61
|
+
const platform = process.platform;
|
|
62
|
+
|
|
63
|
+
if (platform === 'darwin') {
|
|
64
|
+
log.info('Installing devtunnel via brew...');
|
|
65
|
+
execSync('brew install --cask devtunnel', { stdio: 'inherit', timeout: 120000 });
|
|
66
|
+
} else if (platform === 'linux') {
|
|
67
|
+
log.info('Installing devtunnel via official install script...');
|
|
68
|
+
execSync('curl -sL https://aka.ms/DevTunnelCliInstall | bash', {
|
|
69
|
+
stdio: 'inherit',
|
|
70
|
+
timeout: 120000,
|
|
71
|
+
});
|
|
72
|
+
} else if (platform === 'win32') {
|
|
73
|
+
log.info('Installing devtunnel via winget...');
|
|
74
|
+
execSync(
|
|
75
|
+
'winget install Microsoft.devtunnel --accept-source-agreements --accept-package-agreements',
|
|
76
|
+
{
|
|
77
|
+
stdio: 'inherit',
|
|
78
|
+
timeout: 120000,
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Find the installed binary
|
|
84
|
+
const found = findInstalledBinary();
|
|
85
|
+
if (found) {
|
|
86
|
+
log.info(`${green('✔')} DevTunnel CLI installed and verified successfully.`);
|
|
87
|
+
return found;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
log.error('DevTunnel was installed but could not be found on PATH.');
|
|
91
|
+
return null;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
log.error(`DevTunnel install failed: ${err.message}`);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function findInstalledBinary() {
|
|
99
|
+
// Check PATH first
|
|
100
|
+
try {
|
|
101
|
+
execSync('devtunnel --version', { stdio: 'pipe', timeout: 10000 });
|
|
102
|
+
return 'devtunnel';
|
|
103
|
+
} catch {}
|
|
104
|
+
|
|
105
|
+
// On Windows, winget modifies PATH but the current process won't see it.
|
|
106
|
+
// Use 'where' to find it via the system PATH registry.
|
|
107
|
+
if (process.platform === 'win32') {
|
|
108
|
+
try {
|
|
109
|
+
const wherePath = execSync('where devtunnel.exe', {
|
|
110
|
+
encoding: 'utf-8',
|
|
111
|
+
stdio: 'pipe',
|
|
112
|
+
timeout: 10000,
|
|
113
|
+
})
|
|
114
|
+
.trim()
|
|
115
|
+
.split(/\r?\n/)[0];
|
|
116
|
+
if (wherePath && fs.existsSync(wherePath)) return wherePath;
|
|
117
|
+
} catch {}
|
|
118
|
+
|
|
119
|
+
const candidates = [
|
|
120
|
+
path.join(process.env.LOCALAPPDATA || '', 'Microsoft', 'WinGet', 'Links', 'devtunnel.exe'),
|
|
121
|
+
path.join(process.env.LOCALAPPDATA || '', 'Microsoft', 'WindowsApps', 'devtunnel.exe'),
|
|
122
|
+
path.join(process.env.PROGRAMFILES || '', 'Microsoft', 'devtunnel', 'devtunnel.exe'),
|
|
123
|
+
];
|
|
124
|
+
for (const p of candidates) {
|
|
125
|
+
if (fs.existsSync(p)) return p;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check ~/bin (where the Linux install script puts it)
|
|
130
|
+
const homeBin = path.join(os.homedir(), 'bin', getBinaryName());
|
|
131
|
+
if (fs.existsSync(homeBin)) {
|
|
132
|
+
try {
|
|
133
|
+
execFileSync(homeBin, ['--version'], { stdio: 'pipe', timeout: 10000 });
|
|
134
|
+
return homeBin;
|
|
135
|
+
} catch {}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = { installDevtunnel, promptInstall, getInstallDir };
|
package/src/logger.js
CHANGED
|
@@ -13,10 +13,11 @@ const log = {
|
|
|
13
13
|
if (l !== undefined) currentLevel = l;
|
|
14
14
|
},
|
|
15
15
|
getLevel() {
|
|
16
|
-
return Object.keys(LEVELS).find(k => LEVELS[k] === currentLevel);
|
|
16
|
+
return Object.keys(LEVELS).find((k) => LEVELS[k] === currentLevel);
|
|
17
17
|
},
|
|
18
18
|
error(...args) {
|
|
19
|
-
if (currentLevel >= LEVELS.error)
|
|
19
|
+
if (currentLevel >= LEVELS.error)
|
|
20
|
+
console.error(`[${timestamp()}]`, `[${LABELS.error}]`, ...args);
|
|
20
21
|
},
|
|
21
22
|
warn(...args) {
|
|
22
23
|
if (currentLevel >= LEVELS.warn) console.warn(`[${timestamp()}]`, `[${LABELS.warn}]`, ...args);
|
package/src/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const readline = require('readline');
|
|
4
5
|
const express = require('express');
|
|
5
6
|
const cookieParser = require('cookie-parser');
|
|
6
7
|
const http = require('http');
|
|
@@ -26,6 +27,16 @@ function getLocalIP() {
|
|
|
26
27
|
return '127.0.0.1';
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
function confirmAnonymousTunnel() {
|
|
31
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
rl.question(' Do you want to continue with anonymous access? (y/N): ', (answer) => {
|
|
34
|
+
rl.close();
|
|
35
|
+
resolve(answer.trim().toLowerCase() === 'y');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
/**
|
|
30
41
|
* Create a TermBeam server instance without starting it.
|
|
31
42
|
* @param {object} [overrides] - Optional overrides
|
|
@@ -78,28 +89,47 @@ function createTermBeamServer(overrides = {}) {
|
|
|
78
89
|
wss.close();
|
|
79
90
|
}
|
|
80
91
|
|
|
81
|
-
function start() {
|
|
82
|
-
//
|
|
92
|
+
async function start() {
|
|
93
|
+
// If tunnel mode is on but devtunnel is missing, offer to install it
|
|
83
94
|
if (config.useTunnel && !findDevtunnel()) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
'
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
95
|
+
const { promptInstall } = require('./devtunnel-install');
|
|
96
|
+
const installed = await promptInstall();
|
|
97
|
+
if (!installed) {
|
|
98
|
+
log.error('❌ DevTunnel CLI is not available.');
|
|
99
|
+
log.error('');
|
|
100
|
+
log.error(' Use --no-tunnel for LAN-only mode, or install manually:');
|
|
101
|
+
log.error(' Windows: winget install Microsoft.devtunnel');
|
|
102
|
+
log.error(' macOS: brew install --cask devtunnel');
|
|
103
|
+
log.error(' Linux: curl -sL https://aka.ms/DevTunnelCliInstall | bash');
|
|
104
|
+
log.error('');
|
|
105
|
+
log.error(
|
|
106
|
+
' Docs: https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started',
|
|
107
|
+
);
|
|
108
|
+
log.error('');
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Warn and require consent for anonymous tunnel access
|
|
114
|
+
if (config.useTunnel && config.anonymousTunnel) {
|
|
115
|
+
const rd = '\x1b[31m';
|
|
116
|
+
const yl = '\x1b[33m';
|
|
117
|
+
const rs = '\x1b[0m';
|
|
118
|
+
const bd = '\x1b[1m';
|
|
119
|
+
console.log('');
|
|
120
|
+
console.log(` ${rd}${bd}⚠️ DANGER: Public tunnel access requested${rs}`);
|
|
121
|
+
console.log('');
|
|
122
|
+
console.log(` ${yl}This will make your terminal accessible to ANYONE with the URL.${rs}`);
|
|
123
|
+
console.log(` ${yl}No Microsoft login will be required to reach the tunnel.${rs}`);
|
|
124
|
+
console.log(` ${yl}Only the TermBeam password will protect your terminal.${rs}`);
|
|
125
|
+
console.log('');
|
|
126
|
+
const confirmed = await confirmAnonymousTunnel();
|
|
127
|
+
if (!confirmed) {
|
|
128
|
+
console.log('');
|
|
129
|
+
console.log(' Aborted. Restart without --public for private access.');
|
|
130
|
+
console.log('');
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
103
133
|
}
|
|
104
134
|
|
|
105
135
|
return new Promise((resolve) => {
|
|
@@ -146,7 +176,10 @@ function createTermBeamServer(overrides = {}) {
|
|
|
146
176
|
|
|
147
177
|
let publicUrl = null;
|
|
148
178
|
if (config.useTunnel) {
|
|
149
|
-
const tunnel = await startTunnel(config.port, {
|
|
179
|
+
const tunnel = await startTunnel(config.port, {
|
|
180
|
+
persisted: config.persistedTunnel,
|
|
181
|
+
anonymous: config.anonymousTunnel,
|
|
182
|
+
});
|
|
150
183
|
if (tunnel) {
|
|
151
184
|
publicUrl = tunnel.url;
|
|
152
185
|
state.shareBaseUrl = publicUrl;
|
package/src/sessions.js
CHANGED
|
@@ -3,8 +3,14 @@ const pty = require('node-pty');
|
|
|
3
3
|
const log = require('./logger');
|
|
4
4
|
|
|
5
5
|
const SESSION_COLORS = [
|
|
6
|
-
'#4a9eff',
|
|
7
|
-
'#
|
|
6
|
+
'#4a9eff',
|
|
7
|
+
'#4ade80',
|
|
8
|
+
'#fbbf24',
|
|
9
|
+
'#c084fc',
|
|
10
|
+
'#f87171',
|
|
11
|
+
'#22d3ee',
|
|
12
|
+
'#fb923c',
|
|
13
|
+
'#f472b6',
|
|
8
14
|
];
|
|
9
15
|
|
|
10
16
|
class SessionManager {
|
package/src/tunnel.js
CHANGED
|
@@ -3,6 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const log = require('./logger');
|
|
6
|
+
const { promptInstall } = require('./devtunnel-install');
|
|
6
7
|
|
|
7
8
|
const TUNNEL_CONFIG_DIR = path.join(os.homedir(), '.termbeam');
|
|
8
9
|
const TUNNEL_CONFIG_PATH = path.join(TUNNEL_CONFIG_DIR, 'tunnel.json');
|
|
@@ -33,6 +34,19 @@ function findDevtunnel() {
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
// Check ~/bin (where the Linux install script places it)
|
|
38
|
+
const homeBin = path.join(
|
|
39
|
+
os.homedir(),
|
|
40
|
+
'bin',
|
|
41
|
+
process.platform === 'win32' ? 'devtunnel.exe' : 'devtunnel',
|
|
42
|
+
);
|
|
43
|
+
if (fs.existsSync(homeBin)) {
|
|
44
|
+
try {
|
|
45
|
+
execFileSync(homeBin, ['--version'], { stdio: 'pipe' });
|
|
46
|
+
return homeBin;
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
return null;
|
|
37
51
|
}
|
|
38
52
|
|
|
@@ -85,22 +99,18 @@ let isPersisted = false;
|
|
|
85
99
|
|
|
86
100
|
async function startTunnel(port, options = {}) {
|
|
87
101
|
// Check if devtunnel CLI is installed
|
|
88
|
-
|
|
102
|
+
let found = findDevtunnel();
|
|
89
103
|
if (!found) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
log.error('
|
|
104
|
+
found = await promptInstall();
|
|
105
|
+
}
|
|
106
|
+
if (!found) {
|
|
107
|
+
log.error('❌ DevTunnel CLI is not available.');
|
|
94
108
|
log.error('');
|
|
95
|
-
log.error('
|
|
109
|
+
log.error(' Use --no-tunnel for LAN-only mode, or install manually:');
|
|
96
110
|
log.error(' Windows: winget install Microsoft.devtunnel');
|
|
97
|
-
log.error(
|
|
98
|
-
' or: Invoke-WebRequest -Uri https://aka.ms/TunnelsCliDownload/win-x64 -OutFile devtunnel.exe',
|
|
99
|
-
);
|
|
100
111
|
log.error(' macOS: brew install --cask devtunnel');
|
|
101
112
|
log.error(' Linux: curl -sL https://aka.ms/DevTunnelCliInstall | bash');
|
|
102
113
|
log.error('');
|
|
103
|
-
log.error(' Then restart your terminal and try again.');
|
|
104
114
|
log.error(' Docs: https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started');
|
|
105
115
|
log.error('');
|
|
106
116
|
return null;
|
|
@@ -186,13 +196,25 @@ async function startTunnel(port, options = {}) {
|
|
|
186
196
|
{ stdio: 'pipe' },
|
|
187
197
|
);
|
|
188
198
|
} catch {}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
199
|
+
// Set tunnel access: public (anonymous) or private (owner-only via Microsoft login)
|
|
200
|
+
if (options.anonymous) {
|
|
201
|
+
try {
|
|
202
|
+
execFileSync(
|
|
203
|
+
devtunnelCmd,
|
|
204
|
+
['access', 'create', tunnelId, '-p', String(port), '--anonymous'],
|
|
205
|
+
{ stdio: 'pipe' },
|
|
206
|
+
);
|
|
207
|
+
} catch {}
|
|
208
|
+
log.info('Tunnel access: public (anonymous)');
|
|
209
|
+
} else {
|
|
210
|
+
// Remove any existing anonymous access to ensure the tunnel is private
|
|
211
|
+
try {
|
|
212
|
+
execFileSync(devtunnelCmd, ['access', 'reset', tunnelId], {
|
|
213
|
+
stdio: 'pipe',
|
|
214
|
+
});
|
|
215
|
+
} catch {}
|
|
216
|
+
log.info('Tunnel access: private (owner-only via Microsoft login)');
|
|
217
|
+
}
|
|
196
218
|
|
|
197
219
|
const hostProc = spawn(devtunnelCmd, ['host', tunnelId], {
|
|
198
220
|
stdio: ['pipe', 'pipe', 'pipe'],
|