termbeam 1.2.5 → 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 CHANGED
@@ -91,7 +91,7 @@ termbeam --persisted-tunnel
91
91
  termbeam --no-tunnel
92
92
  ```
93
93
 
94
- Requires the [Dev Tunnels CLI](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started):
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`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termbeam",
3
- "version": "1.2.5",
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",
@@ -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
@@ -90,27 +90,24 @@ function createTermBeamServer(overrides = {}) {
90
90
  }
91
91
 
92
92
  async function start() {
93
- // Fail early if tunnel mode is on but devtunnel CLI is not installed
93
+ // If tunnel mode is on but devtunnel is missing, offer to install it
94
94
  if (config.useTunnel && !findDevtunnel()) {
95
- log.error('❌ devtunnel CLI is not installed.');
96
- log.error('');
97
- log.error(' TermBeam uses tunnels by default for remote access.');
98
- log.error(' Install the Azure Dev Tunnels CLI, or use --no-tunnel for LAN-only mode.');
99
- log.error('');
100
- log.error(' Install it:');
101
- log.error(' Windows: winget install Microsoft.devtunnel');
102
- log.error(
103
- ' or: Invoke-WebRequest -Uri https://aka.ms/TunnelsCliDownload/win-x64 -OutFile devtunnel.exe',
104
- );
105
- log.error(' macOS: brew install --cask devtunnel');
106
- log.error(' Linux: curl -sL https://aka.ms/DevTunnelCliInstall | bash');
107
- log.error('');
108
- log.error(' Then restart your terminal and try again.');
109
- log.error(
110
- ' Docs: https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started',
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
113
  // Warn and require consent for anonymous tunnel access
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
- const found = findDevtunnel();
102
+ let found = findDevtunnel();
89
103
  if (!found) {
90
- log.error('❌ devtunnel CLI is not installed.');
91
- log.error('');
92
- log.error(' TermBeam uses tunnels by default for remote access.');
93
- log.error(' Install the Azure Dev Tunnels CLI, or use --no-tunnel for LAN-only mode.');
104
+ found = await promptInstall();
105
+ }
106
+ if (!found) {
107
+ log.error(' DevTunnel CLI is not available.');
94
108
  log.error('');
95
- log.error(' Install it:');
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;