termbeam 1.2.5 → 1.2.7
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 +2 -2
- package/package.json +3 -3
- package/src/cli.js +19 -5
- package/src/devtunnel-install.js +141 -0
- package/src/server.js +23 -26
- package/src/tunnel.js +20 -10
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ termbeam --persisted-tunnel
|
|
|
91
91
|
termbeam --no-tunnel
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
|
|
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:
|
|
95
95
|
|
|
96
96
|
- **Windows:** `winget install Microsoft.devtunnel`
|
|
97
97
|
- **macOS:** `brew install --cask devtunnel`
|
|
@@ -110,7 +110,7 @@ termbeam --host 127.0.0.1 # restrict to localhost (default: 0.0.0.0)
|
|
|
110
110
|
| Flag | Description | Default |
|
|
111
111
|
| --------------------- | ---------------------------------------------------- | -------------- |
|
|
112
112
|
| `--password <pw>` | Set access password (also accepts `--password=<pw>`) | Auto-generated |
|
|
113
|
-
| `--no-password` | Disable password
|
|
113
|
+
| `--no-password` | Disable password (cannot combine with `--public`) | — |
|
|
114
114
|
| `--generate-password` | Auto-generate a secure password | On |
|
|
115
115
|
| `--tunnel` | Create an ephemeral devtunnel URL (private) | On |
|
|
116
116
|
| `--no-tunnel` | Disable tunnel (LAN-only) | — |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "termbeam",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
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/src/cli.js
CHANGED
|
@@ -230,7 +230,7 @@ function parseArgs() {
|
|
|
230
230
|
let useTunnel = true;
|
|
231
231
|
let noTunnel = false;
|
|
232
232
|
let persistedTunnel = false;
|
|
233
|
-
let
|
|
233
|
+
let publicTunnel = false;
|
|
234
234
|
let explicitPassword = !!password;
|
|
235
235
|
|
|
236
236
|
const args = process.argv.slice(2);
|
|
@@ -248,7 +248,7 @@ function parseArgs() {
|
|
|
248
248
|
useTunnel = true;
|
|
249
249
|
persistedTunnel = true;
|
|
250
250
|
} else if (args[i] === '--public') {
|
|
251
|
-
|
|
251
|
+
publicTunnel = true;
|
|
252
252
|
} else if (args[i].startsWith('--password=')) {
|
|
253
253
|
password = args[i].split('=')[1];
|
|
254
254
|
explicitPassword = true;
|
|
@@ -285,8 +285,22 @@ function parseArgs() {
|
|
|
285
285
|
if (noTunnel) useTunnel = false;
|
|
286
286
|
|
|
287
287
|
// --public requires a tunnel
|
|
288
|
-
if (
|
|
289
|
-
|
|
288
|
+
if (publicTunnel && !useTunnel) {
|
|
289
|
+
const rd = '\x1b[31m';
|
|
290
|
+
const rs = '\x1b[0m';
|
|
291
|
+
console.error(
|
|
292
|
+
`${rd}Error: --public requires a tunnel. Remove --no-tunnel or remove --public.${rs}`,
|
|
293
|
+
);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// --public requires password authentication
|
|
298
|
+
if (publicTunnel && !password) {
|
|
299
|
+
const rd = '\x1b[31m';
|
|
300
|
+
const rs = '\x1b[0m';
|
|
301
|
+
console.error(
|
|
302
|
+
`${rd}Error: Public tunnels require password authentication. Remove --no-password or remove --public.${rs}`,
|
|
303
|
+
);
|
|
290
304
|
process.exit(1);
|
|
291
305
|
}
|
|
292
306
|
|
|
@@ -302,7 +316,7 @@ function parseArgs() {
|
|
|
302
316
|
password,
|
|
303
317
|
useTunnel,
|
|
304
318
|
persistedTunnel,
|
|
305
|
-
|
|
319
|
+
publicTunnel,
|
|
306
320
|
shell,
|
|
307
321
|
shellArgs,
|
|
308
322
|
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/server.js
CHANGED
|
@@ -27,10 +27,10 @@ function getLocalIP() {
|
|
|
27
27
|
return '127.0.0.1';
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function
|
|
30
|
+
function confirmPublicTunnel() {
|
|
31
31
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
32
32
|
return new Promise((resolve) => {
|
|
33
|
-
rl.question(' Do you want to continue with
|
|
33
|
+
rl.question(' Do you want to continue with public access? (y/N): ', (answer) => {
|
|
34
34
|
rl.close();
|
|
35
35
|
resolve(answer.trim().toLowerCase() === 'y');
|
|
36
36
|
});
|
|
@@ -90,31 +90,28 @@ function createTermBeamServer(overrides = {}) {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
async function start() {
|
|
93
|
-
//
|
|
93
|
+
// If tunnel mode is on but devtunnel is missing, offer to install it
|
|
94
94
|
if (config.useTunnel && !findDevtunnel()) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
'
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
);
|
|
112
|
-
log.error('');
|
|
113
|
-
process.exit(1);
|
|
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
|
+
}
|
|
114
111
|
}
|
|
115
112
|
|
|
116
|
-
// Warn and require consent for
|
|
117
|
-
if (config.useTunnel && config.
|
|
113
|
+
// Warn and require consent for public tunnel access
|
|
114
|
+
if (config.useTunnel && config.publicTunnel) {
|
|
118
115
|
const rd = '\x1b[31m';
|
|
119
116
|
const yl = '\x1b[33m';
|
|
120
117
|
const rs = '\x1b[0m';
|
|
@@ -126,7 +123,7 @@ function createTermBeamServer(overrides = {}) {
|
|
|
126
123
|
console.log(` ${yl}No Microsoft login will be required to reach the tunnel.${rs}`);
|
|
127
124
|
console.log(` ${yl}Only the TermBeam password will protect your terminal.${rs}`);
|
|
128
125
|
console.log('');
|
|
129
|
-
const confirmed = await
|
|
126
|
+
const confirmed = await confirmPublicTunnel();
|
|
130
127
|
if (!confirmed) {
|
|
131
128
|
console.log('');
|
|
132
129
|
console.log(' Aborted. Restart without --public for private access.');
|
|
@@ -181,7 +178,7 @@ function createTermBeamServer(overrides = {}) {
|
|
|
181
178
|
if (config.useTunnel) {
|
|
182
179
|
const tunnel = await startTunnel(config.port, {
|
|
183
180
|
persisted: config.persistedTunnel,
|
|
184
|
-
anonymous: config.
|
|
181
|
+
anonymous: config.publicTunnel,
|
|
185
182
|
});
|
|
186
183
|
if (tunnel) {
|
|
187
184
|
publicUrl = tunnel.url;
|
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;
|