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/README.md +10 -0
- package/package.json +2 -2
- package/public/index.html +237 -70
- package/public/terminal.html +286 -100
- package/src/cli.js +52 -1
- package/src/routes.js +9 -1
- package/src/sessions.js +6 -1
- package/src/shells.js +77 -0
- package/src/tunnel.js +61 -8
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(
|
|
11
|
-
|
|
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
|
-
|
|
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(
|
|
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(`
|
|
21
|
-
execSync(`
|
|
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(
|
|
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(`
|
|
112
|
+
execSync(`"${devtunnelCmd}" delete ${tunnelId} -f`, { stdio: 'pipe' });
|
|
60
113
|
console.log('[termbeam] Tunnel cleaned up');
|
|
61
114
|
} catch {
|
|
62
115
|
/* best effort */
|