termbeam 0.0.4 → 0.0.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/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/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 */