@geekbeer/minion 1.3.3 → 1.4.1
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/minion-cli.sh +26 -9
- package/package.json +2 -1
- package/routes/terminal.js +84 -88
- package/server.js +11 -1
- package/terminal-proxy.js +158 -0
package/minion-cli.sh
CHANGED
|
@@ -24,6 +24,22 @@ set -euo pipefail
|
|
|
24
24
|
# Ensure HOME is set (cloud-init may not set it)
|
|
25
25
|
export HOME="${HOME:-$(getent passwd "$(whoami)" | cut -d: -f6)}"
|
|
26
26
|
|
|
27
|
+
# Detect target user: when running as root (e.g., cloud-init), install user-level
|
|
28
|
+
# tools for the 'ubuntu' user instead of root
|
|
29
|
+
if [ "$(id -u)" -eq 0 ] && getent passwd ubuntu &>/dev/null; then
|
|
30
|
+
TARGET_USER="ubuntu"
|
|
31
|
+
TARGET_HOME=$(getent passwd "$TARGET_USER" | cut -d: -f6)
|
|
32
|
+
RUN_AS="sudo -u $TARGET_USER"
|
|
33
|
+
elif [ "$(id -u)" -eq 0 ]; then
|
|
34
|
+
TARGET_USER="root"
|
|
35
|
+
TARGET_HOME="$HOME"
|
|
36
|
+
RUN_AS=""
|
|
37
|
+
else
|
|
38
|
+
TARGET_USER="$(whoami)"
|
|
39
|
+
TARGET_HOME="$HOME"
|
|
40
|
+
RUN_AS=""
|
|
41
|
+
fi
|
|
42
|
+
|
|
27
43
|
# Resolve version from package.json (installed location)
|
|
28
44
|
CLI_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
|
|
29
45
|
CLI_VERSION="$(node -p "require('${CLI_DIR}/package.json').version")"
|
|
@@ -144,13 +160,14 @@ do_setup() {
|
|
|
144
160
|
|
|
145
161
|
# Step 1: Install Claude Code
|
|
146
162
|
echo "[1/${TOTAL_STEPS}] Installing Claude Code..."
|
|
147
|
-
|
|
148
|
-
|
|
163
|
+
# Install as target user (claude native installer writes to $HOME/.local/bin)
|
|
164
|
+
# Note: sudo -u resets PATH, so we must set it explicitly in subshells
|
|
165
|
+
export PATH="${TARGET_HOME}/.local/bin:$HOME/.local/bin:$PATH"
|
|
166
|
+
if $RUN_AS bash -c 'export PATH="$HOME/.local/bin:$PATH"; command -v claude' &>/dev/null; then
|
|
167
|
+
echo " -> Claude Code already installed ($($RUN_AS bash -c 'export PATH="$HOME/.local/bin:$PATH"; claude --version' 2>/dev/null || echo 'unknown version'))"
|
|
149
168
|
else
|
|
150
|
-
curl -fsSL https://claude.ai/install.sh | bash
|
|
151
|
-
|
|
152
|
-
export PATH="$HOME/.local/bin:$PATH"
|
|
153
|
-
if command -v claude &>/dev/null; then
|
|
169
|
+
$RUN_AS bash -c 'curl -fsSL https://claude.ai/install.sh | bash'
|
|
170
|
+
if $RUN_AS bash -c 'export PATH="$HOME/.local/bin:$PATH"; command -v claude' &>/dev/null; then
|
|
154
171
|
echo " -> Claude Code installed successfully"
|
|
155
172
|
else
|
|
156
173
|
echo " WARNING: Claude Code installation may have failed"
|
|
@@ -321,11 +338,11 @@ SUPEOF
|
|
|
321
338
|
# Step 8: Deploy bundled skills
|
|
322
339
|
echo "[8/${TOTAL_STEPS}] Deploying bundled skills..."
|
|
323
340
|
local BUNDLED_SKILLS_DIR="${NPM_ROOT}/@geekbeer/minion/skills"
|
|
324
|
-
local CLAUDE_SKILLS_DIR="$
|
|
341
|
+
local CLAUDE_SKILLS_DIR="${TARGET_HOME}/.claude/skills"
|
|
325
342
|
|
|
326
343
|
if [ -d "$BUNDLED_SKILLS_DIR" ]; then
|
|
327
344
|
# Create Claude skills directory if it doesn't exist
|
|
328
|
-
mkdir -p "$CLAUDE_SKILLS_DIR"
|
|
345
|
+
$RUN_AS mkdir -p "$CLAUDE_SKILLS_DIR"
|
|
329
346
|
|
|
330
347
|
# Copy each bundled skill
|
|
331
348
|
for skill_dir in "$BUNDLED_SKILLS_DIR"/*/; do
|
|
@@ -334,7 +351,7 @@ SUPEOF
|
|
|
334
351
|
local target_dir="${CLAUDE_SKILLS_DIR}/${skill_name}"
|
|
335
352
|
|
|
336
353
|
# Copy skill files
|
|
337
|
-
cp -r "$skill_dir" "$target_dir"
|
|
354
|
+
$RUN_AS cp -r "$skill_dir" "$target_dir"
|
|
338
355
|
echo " -> Deployed skill: $skill_name"
|
|
339
356
|
fi
|
|
340
357
|
done
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekbeer/minion",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "AI Agent runtime for Minion - manages status and skill deployment on VPS",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"server.js",
|
|
11
11
|
"config.js",
|
|
12
12
|
"api.js",
|
|
13
|
+
"terminal-proxy.js",
|
|
13
14
|
"workflow-runner.js",
|
|
14
15
|
"workflow-store.js",
|
|
15
16
|
"execution-store.js",
|
package/routes/terminal.js
CHANGED
|
@@ -34,8 +34,8 @@ const homeDir = os.homedir()
|
|
|
34
34
|
*/
|
|
35
35
|
const ttydProcesses = new Map()
|
|
36
36
|
|
|
37
|
-
// Base port for ttyd instances (
|
|
38
|
-
const TTYD_BASE_PORT =
|
|
37
|
+
// Base port for ttyd instances (7681 is reserved for the WebSocket proxy)
|
|
38
|
+
const TTYD_BASE_PORT = 7682
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Find an available port for ttyd
|
|
@@ -63,6 +63,75 @@ async function isTtydInstalled() {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Start ttyd for a tmux session (shared logic used by routes and proxy).
|
|
68
|
+
* @param {string} session - tmux session name
|
|
69
|
+
* @returns {Promise<{port: number, alreadyRunning: boolean}>}
|
|
70
|
+
*/
|
|
71
|
+
async function startTtydForSession(session) {
|
|
72
|
+
// Check if already running
|
|
73
|
+
const existing = ttydProcesses.get(session)
|
|
74
|
+
if (existing && !existing.proc.killed) {
|
|
75
|
+
return { port: existing.port, alreadyRunning: true }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check if ttyd is installed
|
|
79
|
+
const installed = await isTtydInstalled()
|
|
80
|
+
if (!installed) {
|
|
81
|
+
throw new Error('ttyd is not installed')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Verify tmux session exists
|
|
85
|
+
try {
|
|
86
|
+
await execAsync(`tmux has-session -t "${session}"`, {
|
|
87
|
+
timeout: 5000,
|
|
88
|
+
env: { ...process.env, HOME: homeDir },
|
|
89
|
+
})
|
|
90
|
+
} catch {
|
|
91
|
+
throw new Error(`tmux session '${session}' not found`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const port = findAvailablePort()
|
|
95
|
+
|
|
96
|
+
const proc = spawn('ttyd', [
|
|
97
|
+
'-p', port.toString(),
|
|
98
|
+
'-t', 'fontSize=14',
|
|
99
|
+
'-t', 'theme={"background": "#1a1a2e", "foreground": "#eee"}',
|
|
100
|
+
'tmux', 'attach', '-t', session,
|
|
101
|
+
], {
|
|
102
|
+
env: { ...process.env, HOME: homeDir },
|
|
103
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
104
|
+
detached: false,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
proc.on('error', (err) => {
|
|
108
|
+
console.error(`[Terminal] ttyd error for session '${session}': ${err.message}`)
|
|
109
|
+
ttydProcesses.delete(session)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
proc.on('exit', (code) => {
|
|
113
|
+
console.log(`[Terminal] ttyd exited for session '${session}' with code ${code}`)
|
|
114
|
+
ttydProcesses.delete(session)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
proc.stderr.on('data', (data) => {
|
|
118
|
+
console.log(`[Terminal] ttyd[${session}]: ${data.toString().trim()}`)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
ttydProcesses.set(session, {
|
|
122
|
+
proc,
|
|
123
|
+
port,
|
|
124
|
+
startedAt: new Date().toISOString(),
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
console.log(`[Terminal] Started ttyd for session '${session}' on port ${port}`)
|
|
128
|
+
|
|
129
|
+
// Wait for ttyd to start listening
|
|
130
|
+
await new Promise(resolve => setTimeout(resolve, 500))
|
|
131
|
+
|
|
132
|
+
return { port, alreadyRunning: false }
|
|
133
|
+
}
|
|
134
|
+
|
|
66
135
|
/**
|
|
67
136
|
* Register terminal routes as Fastify plugin
|
|
68
137
|
* @param {import('fastify').FastifyInstance} fastify
|
|
@@ -446,103 +515,28 @@ async function terminalRoutes(fastify) {
|
|
|
446
515
|
return { success: false, error: 'session is required' }
|
|
447
516
|
}
|
|
448
517
|
|
|
449
|
-
// Sanitize session name
|
|
450
518
|
if (!/^[\w-]+$/.test(session)) {
|
|
451
519
|
reply.code(400)
|
|
452
520
|
return { success: false, error: 'Invalid session name' }
|
|
453
521
|
}
|
|
454
522
|
|
|
455
|
-
// Check if ttyd is installed
|
|
456
|
-
const installed = await isTtydInstalled()
|
|
457
|
-
if (!installed) {
|
|
458
|
-
reply.code(503)
|
|
459
|
-
return {
|
|
460
|
-
success: false,
|
|
461
|
-
error: 'ttyd is not installed. Install with: apt-get install ttyd',
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Check if already running for this session
|
|
466
|
-
const existing = ttydProcesses.get(session)
|
|
467
|
-
if (existing && !existing.proc.killed) {
|
|
468
|
-
return {
|
|
469
|
-
success: true,
|
|
470
|
-
session,
|
|
471
|
-
port: existing.port,
|
|
472
|
-
message: 'ttyd already running for this session',
|
|
473
|
-
already_running: true,
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Verify tmux session exists
|
|
478
523
|
try {
|
|
479
|
-
|
|
480
|
-
timeout: 5000,
|
|
481
|
-
env: { ...process.env, HOME: homeDir },
|
|
482
|
-
})
|
|
483
|
-
} catch {
|
|
484
|
-
reply.code(404)
|
|
485
|
-
return {
|
|
486
|
-
success: false,
|
|
487
|
-
error: `tmux session '${session}' not found`,
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Find available port and start ttyd
|
|
492
|
-
const port = findAvailablePort()
|
|
493
|
-
|
|
494
|
-
try {
|
|
495
|
-
const proc = spawn('ttyd', [
|
|
496
|
-
'-p', port.toString(),
|
|
497
|
-
'-W', // Writable (allow input)
|
|
498
|
-
'-t', 'fontSize=14',
|
|
499
|
-
'-t', 'theme={"background": "#1a1a2e", "foreground": "#eee"}',
|
|
500
|
-
'tmux', 'attach', '-t', session,
|
|
501
|
-
], {
|
|
502
|
-
env: { ...process.env, HOME: homeDir },
|
|
503
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
504
|
-
detached: false,
|
|
505
|
-
})
|
|
506
|
-
|
|
507
|
-
proc.on('error', (err) => {
|
|
508
|
-
console.error(`[Terminal] ttyd error for session '${session}': ${err.message}`)
|
|
509
|
-
ttydProcesses.delete(session)
|
|
510
|
-
})
|
|
511
|
-
|
|
512
|
-
proc.on('exit', (code) => {
|
|
513
|
-
console.log(`[Terminal] ttyd exited for session '${session}' with code ${code}`)
|
|
514
|
-
ttydProcesses.delete(session)
|
|
515
|
-
})
|
|
516
|
-
|
|
517
|
-
// Log stderr for debugging
|
|
518
|
-
proc.stderr.on('data', (data) => {
|
|
519
|
-
console.log(`[Terminal] ttyd[${session}]: ${data.toString().trim()}`)
|
|
520
|
-
})
|
|
521
|
-
|
|
522
|
-
ttydProcesses.set(session, {
|
|
523
|
-
proc,
|
|
524
|
-
port,
|
|
525
|
-
startedAt: new Date().toISOString(),
|
|
526
|
-
})
|
|
527
|
-
|
|
528
|
-
console.log(`[Terminal] Started ttyd for session '${session}' on port ${port}`)
|
|
529
|
-
|
|
530
|
-
// Wait a moment for ttyd to start
|
|
531
|
-
await new Promise(resolve => setTimeout(resolve, 500))
|
|
532
|
-
|
|
524
|
+
const result = await startTtydForSession(session)
|
|
533
525
|
return {
|
|
534
526
|
success: true,
|
|
535
527
|
session,
|
|
536
|
-
port,
|
|
537
|
-
message:
|
|
528
|
+
port: result.port,
|
|
529
|
+
message: result.alreadyRunning
|
|
530
|
+
? 'ttyd already running for this session'
|
|
531
|
+
: `ttyd started on port ${result.port}`,
|
|
532
|
+
already_running: result.alreadyRunning,
|
|
538
533
|
}
|
|
539
534
|
} catch (error) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
535
|
+
const status = error.message.includes('not found') ? 404
|
|
536
|
+
: error.message.includes('not installed') ? 503
|
|
537
|
+
: 500
|
|
538
|
+
reply.code(status)
|
|
539
|
+
return { success: false, error: error.message }
|
|
546
540
|
}
|
|
547
541
|
})
|
|
548
542
|
|
|
@@ -643,4 +637,6 @@ function cleanupTtyd() {
|
|
|
643
637
|
module.exports = {
|
|
644
638
|
terminalRoutes,
|
|
645
639
|
cleanupTtyd,
|
|
640
|
+
ttydProcesses,
|
|
641
|
+
startTtydForSession,
|
|
646
642
|
}
|
package/server.js
CHANGED
|
@@ -19,6 +19,7 @@ const workflowStore = require('./workflow-store')
|
|
|
19
19
|
const { registerRoutes, setOffline, getProcessManager, getAllowedCommands } = require('./routes')
|
|
20
20
|
const { SUDO } = require('./lib/process-manager')
|
|
21
21
|
const { cleanupTtyd } = require('./routes/terminal')
|
|
22
|
+
const { startTerminalProxy, stopTerminalProxy } = require('./terminal-proxy')
|
|
22
23
|
|
|
23
24
|
// Validate configuration before starting
|
|
24
25
|
validate()
|
|
@@ -36,7 +37,8 @@ async function shutdown(signal) {
|
|
|
36
37
|
// Stop workflow runner
|
|
37
38
|
workflowRunner.stopAll()
|
|
38
39
|
|
|
39
|
-
// Stop all ttyd processes
|
|
40
|
+
// Stop terminal proxy and all ttyd processes
|
|
41
|
+
stopTerminalProxy()
|
|
40
42
|
cleanupTtyd()
|
|
41
43
|
|
|
42
44
|
// Close server
|
|
@@ -116,6 +118,14 @@ async function start() {
|
|
|
116
118
|
|
|
117
119
|
console.log(`[Server] Minion agent listening on port ${config.AGENT_PORT}`)
|
|
118
120
|
|
|
121
|
+
// Start terminal WebSocket proxy on port 7681
|
|
122
|
+
try {
|
|
123
|
+
await startTerminalProxy()
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.error(`[Server] Terminal proxy failed to start: ${err.message}`)
|
|
126
|
+
console.error('[Server] Terminal WebSocket connections will not work')
|
|
127
|
+
}
|
|
128
|
+
|
|
119
129
|
// Load cached workflows and start workflow runner
|
|
120
130
|
try {
|
|
121
131
|
const cachedWorkflows = await workflowStore.load()
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal WebSocket Proxy
|
|
3
|
+
*
|
|
4
|
+
* Listens on port 7681 and routes WebSocket connections to the correct
|
|
5
|
+
* ttyd instance based on the session name in the URL path.
|
|
6
|
+
*
|
|
7
|
+
* URL format: /terminal/ws/{sessionName} (via tunnel) or /ws/{sessionName} (dev)
|
|
8
|
+
*
|
|
9
|
+
* Each tmux session gets its own ttyd on a unique port (7682+).
|
|
10
|
+
* If ttyd is not running for a session, it is auto-started on demand.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const http = require('http')
|
|
14
|
+
const net = require('net')
|
|
15
|
+
const { ttydProcesses, startTtydForSession } = require('./routes/terminal')
|
|
16
|
+
|
|
17
|
+
const PROXY_PORT = 7681
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse session name from URL path.
|
|
21
|
+
* Handles both /terminal/ws/{session} and /ws/{session}.
|
|
22
|
+
* Falls back to 'main' if no session specified.
|
|
23
|
+
*/
|
|
24
|
+
function parseSessionName(url) {
|
|
25
|
+
const match = url?.match(/\/ws\/([^/?]+)/)
|
|
26
|
+
return match ? match[1] : 'main'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Try to connect to a local port with retries.
|
|
31
|
+
* ttyd may take a moment to start listening after spawn.
|
|
32
|
+
*/
|
|
33
|
+
function connectWithRetry(port, retries, delay) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
function attempt(remaining) {
|
|
36
|
+
const conn = net.connect(port, '127.0.0.1')
|
|
37
|
+
|
|
38
|
+
conn.once('connect', () => resolve(conn))
|
|
39
|
+
|
|
40
|
+
conn.once('error', (err) => {
|
|
41
|
+
conn.destroy()
|
|
42
|
+
if (remaining > 0) {
|
|
43
|
+
setTimeout(() => attempt(remaining - 1), delay)
|
|
44
|
+
} else {
|
|
45
|
+
reject(err)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
attempt(retries)
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build the HTTP upgrade request to forward to ttyd.
|
|
56
|
+
* Rewrites the path to /ws (what ttyd expects) and forwards all headers.
|
|
57
|
+
*/
|
|
58
|
+
function buildUpgradeRequest(req) {
|
|
59
|
+
let lines = [`GET /ws HTTP/${req.httpVersion}`]
|
|
60
|
+
|
|
61
|
+
// Forward all headers (ttyd needs Upgrade, Connection, Sec-WebSocket-* headers)
|
|
62
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
63
|
+
lines.push(`${key}: ${value}`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return lines.join('\r\n') + '\r\n\r\n'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const server = http.createServer((req, res) => {
|
|
70
|
+
// Only WebSocket upgrade requests are handled
|
|
71
|
+
res.writeHead(426, { 'Content-Type': 'text/plain' })
|
|
72
|
+
res.end('WebSocket connections only')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
server.on('upgrade', async (req, socket, head) => {
|
|
76
|
+
const sessionName = parseSessionName(req.url)
|
|
77
|
+
console.log(`[Terminal Proxy] WebSocket upgrade for session '${sessionName}' (url: ${req.url})`)
|
|
78
|
+
|
|
79
|
+
// Validate session name
|
|
80
|
+
if (!/^[\w-]+$/.test(sessionName)) {
|
|
81
|
+
console.log(`[Terminal Proxy] Invalid session name: '${sessionName}'`)
|
|
82
|
+
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n')
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Ensure ttyd is running for this session
|
|
87
|
+
let entry = ttydProcesses.get(sessionName)
|
|
88
|
+
if (!entry || entry.proc.killed) {
|
|
89
|
+
try {
|
|
90
|
+
console.log(`[Terminal Proxy] Auto-starting ttyd for session '${sessionName}'`)
|
|
91
|
+
await startTtydForSession(sessionName)
|
|
92
|
+
entry = ttydProcesses.get(sessionName)
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error(`[Terminal Proxy] Failed to start ttyd for '${sessionName}': ${err.message}`)
|
|
95
|
+
socket.end('HTTP/1.1 502 Bad Gateway\r\n\r\n')
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!entry) {
|
|
101
|
+
console.error(`[Terminal Proxy] No ttyd entry after start for '${sessionName}'`)
|
|
102
|
+
socket.end('HTTP/1.1 502 Bad Gateway\r\n\r\n')
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const targetPort = entry.port
|
|
107
|
+
console.log(`[Terminal Proxy] Routing '${sessionName}' to ttyd on port ${targetPort}`)
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// Connect to the local ttyd instance (retry up to 3 times with 300ms delay)
|
|
111
|
+
const target = await connectWithRetry(targetPort, 3, 300)
|
|
112
|
+
|
|
113
|
+
// Forward the upgrade request with path rewritten to /ws
|
|
114
|
+
const upgradeReq = buildUpgradeRequest(req)
|
|
115
|
+
target.write(upgradeReq)
|
|
116
|
+
if (head.length > 0) {
|
|
117
|
+
target.write(head)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Pipe data bidirectionally (raw TCP after upgrade)
|
|
121
|
+
socket.pipe(target)
|
|
122
|
+
target.pipe(socket)
|
|
123
|
+
|
|
124
|
+
// Clean up on close/error
|
|
125
|
+
socket.on('error', () => target.destroy())
|
|
126
|
+
socket.on('close', () => target.destroy())
|
|
127
|
+
target.on('error', () => socket.destroy())
|
|
128
|
+
target.on('close', () => socket.destroy())
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error(`[Terminal Proxy] Failed to connect to ttyd on port ${targetPort}: ${err.message}`)
|
|
131
|
+
socket.end('HTTP/1.1 502 Bad Gateway\r\n\r\n')
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Start the terminal WebSocket proxy server.
|
|
137
|
+
*/
|
|
138
|
+
function startTerminalProxy() {
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
server.listen(PROXY_PORT, '0.0.0.0', () => {
|
|
141
|
+
console.log(`[Terminal Proxy] WebSocket proxy listening on port ${PROXY_PORT}`)
|
|
142
|
+
resolve()
|
|
143
|
+
})
|
|
144
|
+
server.on('error', (err) => {
|
|
145
|
+
console.error(`[Terminal Proxy] Failed to start: ${err.message}`)
|
|
146
|
+
reject(err)
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Stop the terminal proxy server.
|
|
153
|
+
*/
|
|
154
|
+
function stopTerminalProxy() {
|
|
155
|
+
server.close()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = { startTerminalProxy, stopTerminalProxy }
|