termbeam 0.0.3 → 0.0.5

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
@@ -12,63 +12,13 @@
12
12
  Access your terminal from your phone, tablet, or any browser.
13
13
  Multi-session, mobile-optimized, with touch controls.
14
14
 
15
- [Getting Started](#-quick-start) · [Screenshots](#-screenshots) · [Documentation](https://dorlugasigal.github.io/TermBeam/) · [Contributing](CONTRIBUTING.md)
15
+ [Getting Started](#-quick-start) · [Demo](#-demo) · [Documentation](https://dorlugasigal.github.io/TermBeam/) · [Contributing](CONTRIBUTING.md)
16
16
 
17
17
  </div>
18
18
 
19
19
  ---
20
20
 
21
- ## 📸 Screenshots
22
-
23
- <!--
24
- To add screenshots:
25
- 1. Run TermBeam: npx termbeam --generate-password
26
- 2. Take screenshots on your phone/browser
27
- 3. Save images to docs/assets/screenshots/
28
- 4. Uncomment the img tags below and update filenames
29
-
30
- Recommended screenshots:
31
- - session-manager.png (main screen with session list)
32
- - terminal.png (active terminal session)
33
- - folder-browser.png (folder picker sheet)
34
- - login.png (password login screen)
35
- - qr-code.png (server startup with QR code in terminal)
36
- -->
37
-
38
- <div align="center">
39
- <table>
40
- <tr>
41
- <td align="center"><strong>Session Manager</strong></td>
42
- <td align="center"><strong>Terminal</strong></td>
43
- <td align="center"><strong>Folder Browser</strong></td>
44
- </tr>
45
- <tr>
46
- <td>
47
-
48
- <!-- ![Session Manager](docs/assets/screenshots/session-manager.png) -->
49
-
50
- _Manage multiple terminal sessions with swipe-to-delete_
51
-
52
- </td>
53
- <td>
54
-
55
- <!-- ![Terminal](docs/assets/screenshots/terminal.png) -->
56
-
57
- _Full terminal with touch controls and Nerd Font rendering_
58
-
59
- </td>
60
- <td>
61
-
62
- <!-- ![Folder Browser](docs/assets/screenshots/folder-browser.png) -->
63
-
64
- _Visual directory picker for working directory_
65
-
66
- </td>
67
- </tr>
68
- </table>
69
- </div>
70
-
71
- > **💡 Screenshots coming soon!** Run `npx termbeam` and see for yourself.
21
+ https://github.com/user-attachments/assets/c91ca15d-0c84-400f-bbfa-3d58d1be07ee
72
22
 
73
23
  ## ✨ Features
74
24
 
@@ -115,6 +65,12 @@ termbeam --password mysecret
115
65
  termbeam --tunnel --generate-password
116
66
  ```
117
67
 
68
+ > Requires the [Azure Dev Tunnels CLI](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started):
69
+ >
70
+ > - **Windows:** `winget install Microsoft.devtunnel`
71
+ > - **macOS:** `brew install --cask devtunnel`
72
+ > - **Linux:** `curl -sL https://aka.ms/DevTunnelCliInstall | bash`
73
+
118
74
  ## 📖 Usage
119
75
 
120
76
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termbeam",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
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": {
@@ -57,7 +57,7 @@
57
57
  "dependencies": {
58
58
  "cookie-parser": "^1.4.7",
59
59
  "express": "^5.2.1",
60
- "node-pty": "1.0.0",
60
+ "node-pty": "^1.1.0",
61
61
  "qrcode": "^1.5.4",
62
62
  "ws": "^8.19.0"
63
63
  },
package/public/index.html CHANGED
@@ -231,8 +231,18 @@
231
231
  color: #e0e0e0;
232
232
  font-size: 15px;
233
233
  outline: none;
234
+ -webkit-appearance: none;
235
+ appearance: none;
234
236
  }
235
- .modal input:focus {
237
+ .modal select {
238
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
239
+ background-repeat: no-repeat;
240
+ background-position: right 12px center;
241
+ padding-right: 32px;
242
+ cursor: pointer;
243
+ }
244
+ .modal input:focus,
245
+ .modal select:focus {
236
246
  border-color: #533483;
237
247
  }
238
248
  .modal-actions {
@@ -459,8 +469,12 @@
459
469
  <h2>New Session</h2>
460
470
  <label for="sess-name">Name</label>
461
471
  <input type="text" id="sess-name" placeholder="My Session" />
462
- <label for="sess-shell">Shell / Command</label>
463
- <input type="text" id="sess-shell" placeholder="/bin/zsh" />
472
+ <label for="sess-shell">Shell</label>
473
+ <select id="sess-shell">
474
+ <option value="">Loading shells…</option>
475
+ </select>
476
+ <label for="sess-cmd">Initial Command <span style="color:#666;font-weight:normal">(optional)</span></label>
477
+ <input type="text" id="sess-cmd" placeholder="e.g. copilot, htop, vim" />
464
478
  <label for="sess-cwd">Working Directory</label>
465
479
  <div class="cwd-picker">
466
480
  <input type="text" id="sess-cwd" placeholder="/Users/dorlugasigal" />
@@ -568,6 +582,7 @@
568
582
  }
569
583
 
570
584
  document.getElementById('new-session-btn').addEventListener('click', () => {
585
+ loadShells();
571
586
  modal.classList.add('visible');
572
587
  });
573
588
  document.getElementById('modal-cancel').addEventListener('click', () => {
@@ -581,11 +596,13 @@
581
596
  const name = document.getElementById('sess-name').value.trim();
582
597
  const shell = document.getElementById('sess-shell').value.trim();
583
598
  const cwd = document.getElementById('sess-cwd').value.trim();
599
+ const initialCommand = document.getElementById('sess-cmd').value.trim();
584
600
 
585
601
  const body = {};
586
602
  if (name) body.name = name;
587
603
  if (shell) body.shell = shell;
588
604
  if (cwd) body.cwd = cwd;
605
+ if (initialCommand) body.initialCommand = initialCommand;
589
606
 
590
607
  const res = await fetch('/api/sessions', {
591
608
  method: 'POST',
@@ -596,6 +613,30 @@
596
613
  location.href = data.url;
597
614
  });
598
615
 
616
+ // --- Shell detection ---
617
+ let shellsLoaded = false;
618
+ async function loadShells() {
619
+ if (shellsLoaded) return;
620
+ const shellSelect = document.getElementById('sess-shell');
621
+ try {
622
+ const res = await fetch('/api/shells');
623
+ const data = await res.json();
624
+ shellSelect.innerHTML = '';
625
+ for (const s of data.shells) {
626
+ const opt = document.createElement('option');
627
+ opt.value = s.cmd;
628
+ opt.textContent = `${s.name} (${s.cmd})`;
629
+ if (s.cmd === data.default || s.path === data.default) {
630
+ opt.selected = true;
631
+ }
632
+ shellSelect.appendChild(opt);
633
+ }
634
+ shellsLoaded = true;
635
+ } catch {
636
+ shellSelect.innerHTML = '<option value="">Could not detect shells</option>';
637
+ }
638
+ }
639
+
599
640
  // --- Swipe to delete ---
600
641
  async function deleteSession(id, e) {
601
642
  e.stopPropagation();
@@ -65,6 +65,11 @@
65
65
  align-items: center;
66
66
  gap: 8px;
67
67
  }
68
+ #status-bar .right {
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 8px;
72
+ }
68
73
  #back-btn {
69
74
  background: none;
70
75
  border: none;
@@ -76,6 +81,22 @@
76
81
  #back-btn:active {
77
82
  color: #e0e0e0;
78
83
  }
84
+ #stop-btn {
85
+ background: #e74c3c;
86
+ border: none;
87
+ color: white;
88
+ font-size: 11px;
89
+ font-weight: 600;
90
+ cursor: pointer;
91
+ padding: 4px 10px;
92
+ border-radius: 6px;
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 4px;
96
+ }
97
+ #stop-btn:active {
98
+ background: #c0392b;
99
+ }
79
100
  #status-dot {
80
101
  width: 8px;
81
102
  height: 8px;
@@ -207,8 +228,11 @@
207
228
  <span id="status-dot"></span>
208
229
  <span id="session-name">…</span>
209
230
  </div>
210
- <span id="status-text">Connecting…</span>
211
- <span id="version-text" style="font-size: 11px; color: #555; margin-left: 8px"></span>
231
+ <div class="right">
232
+ <span id="status-text">Connecting…</span>
233
+ <span id="version-text" style="font-size: 11px; color: #555"></span>
234
+ <button id="stop-btn" title="Stop session">■ Stop</button>
235
+ </div>
212
236
  </div>
213
237
 
214
238
  <div id="terminal-container"></div>
@@ -437,6 +461,15 @@
437
461
  connect();
438
462
  });
439
463
 
464
+ // Stop session
465
+ document.getElementById('stop-btn').addEventListener('click', async () => {
466
+ if (!confirm('Stop this session? The process will be killed.')) return;
467
+ try {
468
+ await fetch(`/api/sessions/${sessionId}`, { method: 'DELETE' });
469
+ } catch {}
470
+ location.href = '/';
471
+ });
472
+
440
473
  // Tap terminal area to toggle keyboard (intentional user action)
441
474
  container.addEventListener('click', () => term.focus());
442
475
 
package/src/cli.js CHANGED
@@ -32,10 +32,61 @@ Environment:
32
32
  `);
33
33
  }
34
34
 
35
+ function getDefaultShell() {
36
+ const { execFileSync } = require('child_process');
37
+ const ppid = process.ppid;
38
+ console.log(`[termbeam] Detecting shell (parent PID: ${ppid}, platform: ${os.platform()})`);
39
+
40
+ if (os.platform() === 'win32') {
41
+ // Detect parent process on Windows via WMIC
42
+ try {
43
+ const result = execFileSync(
44
+ 'wmic',
45
+ ['process', 'where', `ProcessId=${ppid}`, 'get', 'Name', '/value'],
46
+ { stdio: ['pipe', 'pipe', 'ignore'], encoding: 'utf8', timeout: 3000 },
47
+ );
48
+ const match = result.match(/Name=(.+)/);
49
+ if (match) {
50
+ const name = match[1].trim().toLowerCase();
51
+ console.log(`[termbeam] Detected parent process: ${name}`);
52
+ if (name === 'pwsh.exe') return 'pwsh.exe';
53
+ if (name === 'powershell.exe') return 'powershell.exe';
54
+ }
55
+ } catch (err) {
56
+ console.log(`[termbeam] Could not detect parent process: ${err.message}`);
57
+ }
58
+ const fallback = process.env.COMSPEC || 'cmd.exe';
59
+ console.log(`[termbeam] Falling back to: ${fallback}`);
60
+ return fallback;
61
+ }
62
+
63
+ // Unix: detect parent shell via ps
64
+ try {
65
+ const result = execFileSync('ps', ['-o', 'comm=', '-p', String(ppid)], {
66
+ stdio: ['pipe', 'pipe', 'ignore'],
67
+ encoding: 'utf8',
68
+ timeout: 3000,
69
+ });
70
+ const comm = result.trim();
71
+ if (comm) {
72
+ const shell = comm.startsWith('-') ? comm.slice(1) : comm;
73
+ console.log(`[termbeam] Detected parent shell: ${shell}`);
74
+ return shell;
75
+ }
76
+ } catch (err) {
77
+ console.log(`[termbeam] Could not detect parent shell: ${err.message}`);
78
+ }
79
+
80
+ // Fallback to SHELL env or /bin/sh
81
+ const fallback = process.env.SHELL || '/bin/sh';
82
+ console.log(`[termbeam] Falling back to: ${fallback}`);
83
+ return fallback;
84
+ }
85
+
35
86
  function parseArgs() {
36
87
  let port = parseInt(process.env.PORT || '3456', 10);
37
88
  let host = '0.0.0.0';
38
- const defaultShell = process.env.SHELL || '/bin/zsh';
89
+ const defaultShell = getDefaultShell();
39
90
  const cwd = process.env.TERMBEAM_CWD || process.env.PTY_CWD || process.cwd();
40
91
  let password = process.env.TERMBEAM_PASSWORD || process.env.PTY_PASSWORD || null;
41
92
  let useTunnel = false;
package/src/routes.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const path = require('path');
2
2
  const os = require('os');
3
3
  const fs = require('fs');
4
+ const { detectShells } = require('./shells');
4
5
 
5
6
  const PUBLIC_DIR = path.join(__dirname, '..', 'public');
6
7
 
@@ -46,16 +47,23 @@ function setupRoutes(app, { auth, sessions, config }) {
46
47
  });
47
48
 
48
49
  app.post('/api/sessions', auth.middleware, (req, res) => {
49
- const { name, shell, args: shellArgs, cwd } = req.body || {};
50
+ const { name, shell, args: shellArgs, cwd, initialCommand } = req.body || {};
50
51
  const id = sessions.create({
51
52
  name: name || `Session ${sessions.sessions.size + 1}`,
52
53
  shell: shell || config.defaultShell,
53
54
  args: shellArgs || [],
54
55
  cwd: cwd || config.cwd,
56
+ initialCommand: initialCommand || null,
55
57
  });
56
58
  res.json({ id, url: `/terminal?id=${id}` });
57
59
  });
58
60
 
61
+ // Available shells
62
+ app.get('/api/shells', auth.middleware, (_req, res) => {
63
+ const shells = detectShells();
64
+ res.json({ shells, default: config.defaultShell });
65
+ });
66
+
59
67
  app.delete('/api/sessions/:id', auth.middleware, (req, res) => {
60
68
  if (sessions.delete(req.params.id)) {
61
69
  res.json({ ok: true });
package/src/server.js CHANGED
@@ -76,13 +76,27 @@ server.listen(config.port, config.host, async () => {
76
76
  cwd: config.cwd,
77
77
  });
78
78
 
79
+ const lp = '\x1b[38;5;141m'; // light purple
80
+ const rs = '\x1b[0m'; // reset
79
81
  console.log('');
80
- console.log(' ████████╗███████╗██████╗ ███╗ ███╗██████╗ ███████╗ █████╗ ███╗ ███╗');
81
- console.log(' ╚══██╔══╝██╔════╝██╔══██╗████╗ ████║██╔══██╗██╔════╝██╔══██╗████╗ ████║');
82
- console.log(' ██║ █████╗ ██████╔╝██╔████╔██║██████╔╝█████╗ ███████║██╔████╔██║');
83
- console.log(' ██║ ██╔══╝ ██╔══██╗██║╚██╔╝██║██╔══██╗██╔══╝ ██╔══██║██║╚██╔╝██║');
84
- console.log(' ██║ ███████╗██║ ██║██║ ╚═╝ ██║██████╔╝███████╗██║ ██║██║ ╚═╝ ██║');
85
- console.log(' ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝');
82
+ console.log(
83
+ `${lp} ████████╗███████╗██████╗ ███╗ ███╗██████╗ ███████╗ █████╗ ███╗ ███╗${rs}`,
84
+ );
85
+ console.log(
86
+ `${lp} ╚══██╔══╝██╔════╝██╔══██╗████╗ ████║██╔══██╗██╔════╝██╔══██╗████╗ ████║${rs}`,
87
+ );
88
+ console.log(
89
+ `${lp} ██║ █████╗ ██████╔╝██╔████╔██║██████╔╝█████╗ ███████║██╔████╔██║${rs}`,
90
+ );
91
+ console.log(
92
+ `${lp} ██║ ██╔══╝ ██╔══██╗██║╚██╔╝██║██╔══██╗██╔══╝ ██╔══██║██║╚██╔╝██║${rs}`,
93
+ );
94
+ console.log(
95
+ `${lp} ██║ ███████╗██║ ██║██║ ╚═╝ ██║██████╔╝███████╗██║ ██║██║ ╚═╝ ██║${rs}`,
96
+ );
97
+ console.log(
98
+ `${lp} ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝${rs}`,
99
+ );
86
100
  console.log('');
87
101
  console.log(` Beam your terminal to any device 📡 v${config.version}`);
88
102
  console.log('');
@@ -93,7 +107,8 @@ server.listen(config.port, config.host, async () => {
93
107
  }
94
108
  console.log(` Shell: ${config.shell}`);
95
109
  console.log(` Session: ${defaultId}`);
96
- console.log(` Auth: ${config.password ? '🔒 password' : '🔓 none'}`);
110
+ const gn = '\x1b[38;5;114m'; // green
111
+ console.log(` Auth: ${config.password ? `${gn}🔒 password${rs}` : '🔓 none'}`);
97
112
 
98
113
  let publicUrl = null;
99
114
  if (config.useTunnel) {
@@ -117,6 +132,6 @@ server.listen(config.port, config.host, async () => {
117
132
  }
118
133
 
119
134
  console.log(` Scan the QR code or open: ${qrUrl}`);
120
- if (config.password) console.log(` Password: ${config.password}`);
135
+ if (config.password) console.log(` Password: ${gn}${config.password}${rs}`);
121
136
  console.log('');
122
137
  });
package/src/sessions.js CHANGED
@@ -6,7 +6,7 @@ class SessionManager {
6
6
  this.sessions = new Map();
7
7
  }
8
8
 
9
- create({ name, shell, args = [], cwd }) {
9
+ create({ name, shell, args = [], cwd, initialCommand = null }) {
10
10
  const id = crypto.randomBytes(4).toString('hex');
11
11
  const ptyProcess = pty.spawn(shell, args, {
12
12
  name: 'xterm-256color',
@@ -16,6 +16,11 @@ class SessionManager {
16
16
  env: { ...process.env, TERM: 'xterm-256color' },
17
17
  });
18
18
 
19
+ // Send initial command once the shell is ready
20
+ if (initialCommand) {
21
+ setTimeout(() => ptyProcess.write(initialCommand + '\r'), 300);
22
+ }
23
+
19
24
  const session = {
20
25
  pty: ptyProcess,
21
26
  name,
package/src/shells.js ADDED
@@ -0,0 +1,77 @@
1
+ const os = require('os');
2
+ const fs = require('fs');
3
+ const { execFileSync } = require('child_process');
4
+
5
+ const KNOWN_WINDOWS_SHELLS = [
6
+ { name: 'PowerShell (Core)', cmd: 'pwsh.exe' },
7
+ { name: 'Windows PowerShell', cmd: 'powershell.exe' },
8
+ { name: 'Command Prompt', cmd: 'cmd.exe' },
9
+ { name: 'Git Bash', cmd: 'bash.exe' },
10
+ { name: 'WSL', cmd: 'wsl.exe' },
11
+ ];
12
+
13
+ function detectShells() {
14
+ if (os.platform() === 'win32') {
15
+ return detectWindowsShells();
16
+ }
17
+ return detectUnixShells();
18
+ }
19
+
20
+ function detectWindowsShells() {
21
+ const shells = [];
22
+ for (const { name, cmd } of KNOWN_WINDOWS_SHELLS) {
23
+ try {
24
+ const result = execFileSync('where', [cmd], {
25
+ stdio: ['pipe', 'pipe', 'ignore'],
26
+ encoding: 'utf8',
27
+ timeout: 3000,
28
+ });
29
+ const fullPath = result.trim().split('\n')[0].trim();
30
+ if (fullPath) {
31
+ shells.push({ name, path: fullPath, cmd });
32
+ }
33
+ } catch {
34
+ // not installed
35
+ }
36
+ }
37
+ return shells;
38
+ }
39
+
40
+ function detectUnixShells() {
41
+ const shells = [];
42
+ const seen = new Set();
43
+
44
+ // Read /etc/shells
45
+ try {
46
+ const content = fs.readFileSync('/etc/shells', 'utf8');
47
+ for (const line of content.split('\n')) {
48
+ const trimmed = line.trim();
49
+ if (trimmed && !trimmed.startsWith('#')) {
50
+ const name = trimmed.split('/').pop();
51
+ if (!seen.has(name)) {
52
+ seen.add(name);
53
+ shells.push({ name, path: trimmed, cmd: trimmed });
54
+ }
55
+ }
56
+ }
57
+ } catch {
58
+ // /etc/shells not available, try common paths
59
+ const common = ['/bin/bash', '/bin/zsh', '/bin/sh', '/usr/bin/fish', '/bin/fish'];
60
+ for (const p of common) {
61
+ try {
62
+ fs.accessSync(p, fs.constants.X_OK);
63
+ const name = p.split('/').pop();
64
+ if (!seen.has(name)) {
65
+ seen.add(name);
66
+ shells.push({ name, path: p, cmd: p });
67
+ }
68
+ } catch {
69
+ // not installed
70
+ }
71
+ }
72
+ }
73
+
74
+ return shells;
75
+ }
76
+
77
+ module.exports = { detectShells };
package/src/tunnel.js CHANGED
@@ -1,26 +1,79 @@
1
1
  const { execSync, spawn } = require('child_process');
2
+ const path = require('path');
3
+ const fs = require('fs');
2
4
 
3
5
  let tunnelId = null;
4
6
  let tunnelProc = null;
7
+ let devtunnelCmd = 'devtunnel';
8
+
9
+ function findDevtunnel() {
10
+ // Try devtunnel directly
11
+ try {
12
+ execSync('devtunnel --version', { stdio: 'pipe' });
13
+ return 'devtunnel';
14
+ } catch {}
15
+
16
+ // On Windows, check common install locations
17
+ if (process.platform === 'win32') {
18
+ const candidates = [
19
+ path.join(process.env.LOCALAPPDATA || '', 'Microsoft', 'WindowsApps', 'devtunnel.exe'),
20
+ path.join(process.env.PROGRAMFILES || '', 'Microsoft', 'devtunnel', 'devtunnel.exe'),
21
+ ];
22
+ for (const p of candidates) {
23
+ if (fs.existsSync(p)) {
24
+ return p;
25
+ }
26
+ }
27
+ }
28
+
29
+ return null;
30
+ }
5
31
 
6
32
  async function startTunnel(port) {
33
+ // Check if devtunnel CLI is installed
34
+ const found = findDevtunnel();
35
+ if (!found) {
36
+ console.error('[termbeam] ❌ devtunnel CLI is not installed.');
37
+ console.error('');
38
+ console.error(' The --tunnel flag requires the Azure Dev Tunnels CLI.');
39
+ console.error('');
40
+ console.error(' Install it:');
41
+ console.error(' Windows: winget install Microsoft.devtunnel');
42
+ console.error(' or: Invoke-WebRequest -Uri https://aka.ms/TunnelsCliDownload/win-x64 -OutFile devtunnel.exe');
43
+ console.error(' macOS: brew install --cask devtunnel');
44
+ console.error(' Linux: curl -sL https://aka.ms/DevTunnelCliInstall | bash');
45
+ console.error('');
46
+ console.error(' Then restart your terminal and try again.');
47
+ console.error(' Docs: https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started');
48
+ console.error('');
49
+ return null;
50
+ }
51
+ devtunnelCmd = found;
52
+
7
53
  console.log('[termbeam] Starting devtunnel...');
8
54
  try {
55
+ // Ensure user is logged in
56
+ let loggedIn = false;
9
57
  try {
10
- execSync('devtunnel user show', { stdio: 'pipe' });
11
- } catch {
58
+ const userOut = execSync(`"${devtunnelCmd}" user show`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
59
+ // user show can succeed but show "not logged in" status
60
+ loggedIn = userOut && !userOut.toLowerCase().includes('not logged in');
61
+ } catch {}
62
+
63
+ if (!loggedIn) {
12
64
  console.log('[termbeam] devtunnel not logged in, launching login...');
13
- execSync('devtunnel user login -g', { stdio: 'inherit' });
65
+ console.log('[termbeam] A browser window will open for authentication.');
66
+ execSync(`"${devtunnelCmd}" user login`, { stdio: 'inherit' });
14
67
  }
15
68
 
16
- const createOut = execSync('devtunnel create --expiration 1d --json', { encoding: 'utf-8' });
69
+ const createOut = execSync(`"${devtunnelCmd}" create --expiration 1d --json`, { encoding: 'utf-8' });
17
70
  const tunnelData = JSON.parse(createOut);
18
71
  tunnelId = tunnelData.tunnel.tunnelId;
19
72
 
20
- execSync(`devtunnel port create ${tunnelId} -p ${port} --protocol http`, { stdio: 'pipe' });
21
- execSync(`devtunnel access create ${tunnelId} -p ${port} --anonymous`, { stdio: 'pipe' });
73
+ execSync(`"${devtunnelCmd}" port create ${tunnelId} -p ${port} --protocol http`, { stdio: 'pipe' });
74
+ execSync(`"${devtunnelCmd}" access create ${tunnelId} -p ${port} --anonymous`, { stdio: 'pipe' });
22
75
 
23
- const hostProc = spawn('devtunnel', ['host', tunnelId], {
76
+ const hostProc = spawn(devtunnelCmd, ['host', tunnelId], {
24
77
  stdio: ['pipe', 'pipe', 'pipe'],
25
78
  detached: true,
26
79
  });
@@ -56,7 +109,7 @@ function cleanupTunnel() {
56
109
  if (tunnelId) {
57
110
  try {
58
111
  if (tunnelProc) tunnelProc.kill();
59
- execSync(`devtunnel delete ${tunnelId} -f`, { stdio: 'pipe' });
112
+ execSync(`"${devtunnelCmd}" delete ${tunnelId} -f`, { stdio: 'pipe' });
60
113
  console.log('[termbeam] Tunnel cleaned up');
61
114
  } catch {
62
115
  /* best effort */