@geekbeer/minion 1.5.0 → 1.6.0

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 CHANGED
@@ -4,16 +4,17 @@
4
4
  # CLI tool for interacting with and setting up the minion agent
5
5
  #
6
6
  # Usage:
7
- # minion-cli setup [options] # Set up minion agent service
8
- # minion-cli start # Start agent service
9
- # minion-cli stop # Stop agent service
10
- # minion-cli restart # Restart agent service
7
+ # sudo minion-cli setup [options] # Set up minion agent service (root)
8
+ # sudo minion-cli start # Start agent service (root)
9
+ # sudo minion-cli stop # Stop agent service (root)
10
+ # sudo minion-cli restart # Restart agent service (root)
11
11
  # minion-cli status # Get current status
12
12
  # minion-cli health # Health check
13
13
  # minion-cli set-status busy "Running X" # Set status and task
14
14
  # minion-cli set-status online # Set status only
15
15
  #
16
16
  # Setup options:
17
+ # --user <USERNAME> Target user for the agent (required when running as root)
17
18
  # --hq-url <URL> HQ server URL (optional, omit for standalone mode)
18
19
  # --minion-id <UUID> Minion ID (optional)
19
20
  # --api-token <TOKEN> API token (optional)
@@ -24,13 +25,10 @@ set -euo pipefail
24
25
  # Ensure HOME is set (cloud-init may not set it)
25
26
  export HOME="${HOME:-$(getent passwd "$(whoami)" | cut -d: -f6)}"
26
27
 
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
28
+ # Detect target user
29
+ # Resolved after argument parsing in do_setup (--user flag may override)
30
+ # For non-setup commands, use current user
31
+ if [ "$(id -u)" -eq 0 ]; then
34
32
  TARGET_USER="root"
35
33
  TARGET_HOME="$HOME"
36
34
  RUN_AS=""
@@ -50,8 +48,16 @@ if [ "$(id -u)" -ne 0 ] && command -v sudo &>/dev/null; then
50
48
  SUDO="sudo"
51
49
  fi
52
50
 
51
+ # Require root for service management commands
52
+ require_root() {
53
+ if [ "$(id -u)" -ne 0 ]; then
54
+ echo "Error: 'minion-cli $1' requires root privileges."
55
+ echo "Usage: sudo minion-cli $1"
56
+ exit 1
57
+ fi
58
+ }
59
+
53
60
  # Detect process manager: systemd or supervisord
54
- # Detect process manager
55
61
  # Check if systemd is actually running (not just installed)
56
62
  # /run/systemd/system exists only when systemd is the init system
57
63
  SERVICE_NAME="minion-agent"
@@ -68,10 +74,10 @@ svc_control() {
68
74
  local action="$1"
69
75
  case "$PROC_MGR" in
70
76
  systemd)
71
- $SUDO systemctl "$action" "$SERVICE_NAME"
77
+ systemctl "$action" "$SERVICE_NAME"
72
78
  ;;
73
79
  supervisord)
74
- $SUDO supervisorctl "$action" "$SERVICE_NAME"
80
+ supervisorctl "$action" "$SERVICE_NAME"
75
81
  ;;
76
82
  *)
77
83
  echo "Error: No supported process manager found (systemd or supervisord)"
@@ -89,12 +95,17 @@ do_setup() {
89
95
  local HQ_URL=""
90
96
  local MINION_ID=""
91
97
  local API_TOKEN=""
98
+ local CLI_USER=""
92
99
  local SETUP_TUNNEL=false
93
100
  local ARGS_PROVIDED=false
94
101
 
95
102
  # Parse arguments
96
103
  while [[ $# -gt 0 ]]; do
97
104
  case "$1" in
105
+ --user)
106
+ CLI_USER="$2"
107
+ shift 2
108
+ ;;
98
109
  --hq-url)
99
110
  HQ_URL="$2"
100
111
  ARGS_PROVIDED=true
@@ -120,12 +131,47 @@ do_setup() {
120
131
  ;;
121
132
  *)
122
133
  echo "Unknown option: $1"
123
- echo "Usage: minion-cli setup [--hq-url <URL>] [--minion-id <UUID>] [--api-token <TOKEN>] [--setup-tunnel]"
134
+ echo "Usage: minion-cli setup --user <USERNAME> [--hq-url <URL>] [--minion-id <UUID>] [--api-token <TOKEN>] [--setup-tunnel]"
124
135
  exit 1
125
136
  ;;
126
137
  esac
127
138
  done
128
139
 
140
+ # Resolve target user for setup
141
+ if [ -n "$CLI_USER" ]; then
142
+ # Explicit --user flag provided
143
+ TARGET_USER="$CLI_USER"
144
+ TARGET_HOME=$(getent passwd "$TARGET_USER" | cut -d: -f6)
145
+ if [ -z "$TARGET_HOME" ]; then
146
+ echo "ERROR: User '$TARGET_USER' does not exist on this system."
147
+ echo "Create the user first: sudo useradd -m -s /bin/bash $TARGET_USER"
148
+ exit 1
149
+ fi
150
+ if [ "$(id -u)" -eq 0 ] && [ "$TARGET_USER" != "root" ]; then
151
+ RUN_AS="sudo -u $TARGET_USER"
152
+ fi
153
+ elif [ "$(id -u)" -eq 0 ]; then
154
+ # Running as root without --user
155
+ if [ "$PROC_MGR" = "supervisord" ]; then
156
+ # Container environment (supervisord) - allow root for backward compatibility
157
+ echo " NOTE: Running as root in container environment (supervisord detected)"
158
+ TARGET_USER="root"
159
+ TARGET_HOME="$HOME"
160
+ RUN_AS=""
161
+ else
162
+ echo "ERROR: Running as root without --user flag is not supported."
163
+ echo ""
164
+ echo "Usage: sudo minion-cli setup --user <USERNAME> [--hq-url <URL>] ..."
165
+ echo ""
166
+ echo "The --user flag specifies which system user the minion agent will run as."
167
+ echo "This user must already exist. Example:"
168
+ echo " sudo useradd -m -s /bin/bash minion"
169
+ echo " sudo minion-cli setup --user minion --hq-url https://..."
170
+ exit 1
171
+ fi
172
+ fi
173
+ # Non-root case: TARGET_USER is already set to $(whoami)
174
+
129
175
  # Preserve existing .env values if no arguments were provided (redeploy scenario)
130
176
  if [ "$ARGS_PROVIDED" = false ] && [ -f /opt/minion-agent/.env ]; then
131
177
  echo "Reading existing .env values (redeploy mode)..."
@@ -142,6 +188,7 @@ do_setup() {
142
188
  echo "========================================="
143
189
  echo " @geekbeer/minion Setup"
144
190
  echo "========================================="
191
+ echo "User: $TARGET_USER ($TARGET_HOME)"
145
192
 
146
193
  if [ -n "$HQ_URL" ]; then
147
194
  echo "Mode: Connected to HQ ($HQ_URL)"
@@ -202,6 +249,7 @@ do_setup() {
202
249
  # Step 3: Create config directory
203
250
  echo "[3/${TOTAL_STEPS}] Creating config directory..."
204
251
  $SUDO mkdir -p /opt/minion-agent
252
+ $SUDO chown "${TARGET_USER}:${TARGET_USER}" /opt/minion-agent 2>/dev/null || true
205
253
  echo " -> /opt/minion-agent/ created"
206
254
 
207
255
  # Step 4: Generate .env file
@@ -222,6 +270,7 @@ do_setup() {
222
270
 
223
271
  ENV_CONTENT+="AGENT_PORT=3001\n"
224
272
  ENV_CONTENT+="HEARTBEAT_INTERVAL=30\n"
273
+ ENV_CONTENT+="MINION_USER=${TARGET_USER}\n"
225
274
 
226
275
  echo -e "$ENV_CONTENT" | $SUDO tee /opt/minion-agent/.env > /dev/null
227
276
  echo " -> /opt/minion-agent/.env generated"
@@ -240,19 +289,39 @@ do_setup() {
240
289
 
241
290
  case "$PROC_MGR" in
242
291
  systemd)
292
+ # Create tmux-init service (ensures 'main' session exists)
293
+ $SUDO tee /etc/systemd/system/tmux-init.service > /dev/null <<TMUXEOF
294
+ [Unit]
295
+ Description=tmux session initialization (main)
296
+ After=network.target
297
+
298
+ [Service]
299
+ Type=oneshot
300
+ User=${TARGET_USER}
301
+ RemainAfterExit=yes
302
+ ExecStart=/bin/bash -c 'tmux has-session -t main 2>/dev/null || tmux new-session -d -s main; tmux set-option -g mouse on'
303
+
304
+ [Install]
305
+ WantedBy=multi-user.target
306
+ TMUXEOF
307
+ echo " -> /etc/systemd/system/tmux-init.service created"
308
+
309
+ # Create minion-agent service
243
310
  $SUDO tee /etc/systemd/system/minion-agent.service > /dev/null <<SVCEOF
244
311
  [Unit]
245
312
  Description=Minion Agent API (@geekbeer/minion)
246
- After=network.target
313
+ After=network.target tmux-init.service
314
+ Wants=tmux-init.service
247
315
 
248
316
  [Service]
249
317
  Type=simple
250
- User=root
318
+ User=${TARGET_USER}
251
319
  WorkingDirectory=/opt/minion-agent
252
320
  ExecStart=/usr/bin/node ${SERVER_PATH}
253
321
  Restart=always
254
322
  RestartSec=10
255
323
  EnvironmentFile=/opt/minion-agent/.env
324
+ Environment=MINION_USER=${TARGET_USER}
256
325
 
257
326
  [Install]
258
327
  WantedBy=multi-user.target
@@ -268,12 +337,20 @@ SVCEOF
268
337
  [[ -z "$key" || "$key" == \#* ]] && continue
269
338
  ENV_PAIRS+=("${key}=\"${value}\"")
270
339
  done < /opt/minion-agent/.env
340
+
271
341
  ENV_LINE+="$(IFS=,; echo "${ENV_PAIRS[*]}")"
272
342
 
343
+ # Build user line (omit for root)
344
+ local USER_LINE=""
345
+ if [ "$TARGET_USER" != "root" ]; then
346
+ USER_LINE="user=${TARGET_USER}"
347
+ fi
348
+
273
349
  $SUDO tee /etc/supervisor/conf.d/minion-agent.conf > /dev/null <<SUPEOF
274
350
  [program:minion-agent]
275
351
  command=/usr/bin/node ${SERVER_PATH}
276
352
  directory=/opt/minion-agent
353
+ ${USER_LINE}
277
354
  ${ENV_LINE}
278
355
  autorestart=true
279
356
  priority=500
@@ -295,6 +372,8 @@ SUPEOF
295
372
  case "$PROC_MGR" in
296
373
  systemd)
297
374
  $SUDO systemctl daemon-reload
375
+ $SUDO systemctl enable tmux-init
376
+ $SUDO systemctl start tmux-init
298
377
  $SUDO systemctl enable minion-agent
299
378
  $SUDO systemctl start minion-agent
300
379
  ;;
@@ -438,11 +517,13 @@ SUPEOF
438
517
  echo " Setup Complete!"
439
518
  echo "========================================="
440
519
  echo ""
520
+ echo "Agent user: $TARGET_USER"
521
+ echo ""
441
522
  echo "Useful commands:"
442
523
  echo " minion-cli status # Agent status"
443
524
  echo " minion-cli health # Health check"
444
- echo " minion-cli restart # Restart agent"
445
- echo " minion-cli stop # Stop agent"
525
+ echo " sudo minion-cli restart # Restart agent"
526
+ echo " sudo minion-cli stop # Stop agent"
446
527
  if [ "$PROC_MGR" = "systemd" ]; then
447
528
  echo " journalctl -u minion-agent -f # View logs"
448
529
  else
@@ -487,16 +568,19 @@ case "${1:-}" in
487
568
  ;;
488
569
 
489
570
  start)
571
+ require_root start
490
572
  svc_control start
491
573
  echo "minion-agent started ($PROC_MGR)"
492
574
  ;;
493
575
 
494
576
  stop)
577
+ require_root stop
495
578
  svc_control stop
496
579
  echo "minion-agent stopped ($PROC_MGR)"
497
580
  ;;
498
581
 
499
582
  restart)
583
+ require_root restart
500
584
  svc_control restart
501
585
  echo "minion-agent restarted ($PROC_MGR)"
502
586
  ;;
@@ -505,16 +589,17 @@ case "${1:-}" in
505
589
  echo "Minion Agent CLI (@geekbeer/minion) v${CLI_VERSION}"
506
590
  echo ""
507
591
  echo "Usage:"
508
- echo " minion-cli setup [options] # Set up agent service"
509
- echo " minion-cli start # Start agent service"
510
- echo " minion-cli stop # Stop agent service"
511
- echo " minion-cli restart # Restart agent service"
592
+ echo " sudo minion-cli setup [options] # Set up agent service (root)"
593
+ echo " sudo minion-cli start # Start agent service (root)"
594
+ echo " sudo minion-cli stop # Stop agent service (root)"
595
+ echo " sudo minion-cli restart # Restart agent service (root)"
512
596
  echo " minion-cli status # Get current status"
513
597
  echo " minion-cli health # Health check"
514
598
  echo " minion-cli set-status <status> [task] # Set status and optional task"
515
599
  echo " minion-cli --version # Show version"
516
600
  echo ""
517
601
  echo "Setup options:"
602
+ echo " --user <USERNAME> Target user for the agent (required when running as root)"
518
603
  echo " --hq-url <URL> HQ server URL (optional)"
519
604
  echo " --minion-id <UUID> Minion ID (optional)"
520
605
  echo " --api-token <TOKEN> API token (optional)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekbeer/minion",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "AI Agent runtime for Minion - manages status and skill deployment on VPS",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -4,7 +4,6 @@
4
4
  * Endpoints:
5
5
  * - GET /api/commands - List available commands
6
6
  * - POST /api/command - Execute a whitelisted command
7
- * - POST /api/vnc-password - Change VNC password
8
7
  */
9
8
 
10
9
  const { exec } = require('child_process')
@@ -12,7 +11,7 @@ const { promisify } = require('util')
12
11
  const execAsync = promisify(exec)
13
12
 
14
13
  const { verifyToken } = require('../lib/auth')
15
- const { SUDO, detectProcessManager, buildAllowedCommands } = require('../lib/process-manager')
14
+ const { detectProcessManager, buildAllowedCommands } = require('../lib/process-manager')
16
15
 
17
16
  const PROC_MGR = detectProcessManager()
18
17
  const ALLOWED_COMMANDS = buildAllowedCommands(PROC_MGR)
@@ -109,54 +108,6 @@ async function commandRoutes(fastify) {
109
108
  }
110
109
  })
111
110
 
112
- // Change VNC password
113
- fastify.post('/api/vnc-password', async (request, reply) => {
114
- if (!verifyToken(request)) {
115
- reply.code(401)
116
- return { success: false, error: 'Unauthorized' }
117
- }
118
-
119
- const { password } = request.body || {}
120
-
121
- if (!password || password.length < 1) {
122
- reply.code(400)
123
- return { success: false, error: 'password is required' }
124
- }
125
-
126
- console.log('[VNC] Changing VNC password')
127
-
128
- try {
129
- // Escape single quotes in password for shell safety
130
- const escapedPassword = password.replace(/'/g, "'\\''")
131
-
132
- // Use x11vnc to store the new password
133
- await execAsync(
134
- `x11vnc -storepasswd '${escapedPassword}' ~/.vnc/passwd`,
135
- { timeout: 10000 }
136
- )
137
-
138
- // Restart x11vnc to apply the new password
139
- if (PROC_MGR === 'systemd') {
140
- await execAsync(`${SUDO}systemctl restart x11vnc`, { timeout: 10000 })
141
- } else if (PROC_MGR === 'supervisord') {
142
- await execAsync(`${SUDO}supervisorctl restart x11vnc`, { timeout: 10000 })
143
- }
144
-
145
- console.log('[VNC] Password changed successfully')
146
-
147
- return {
148
- success: true,
149
- message: 'VNC password changed successfully',
150
- }
151
- } catch (error) {
152
- console.error(`[VNC] Failed to change password: ${error.message}`)
153
- reply.code(500)
154
- return {
155
- success: false,
156
- error: error.message,
157
- }
158
- }
159
- })
160
111
  }
161
112
 
162
113
  /**
package/routes/index.js CHANGED
@@ -13,7 +13,6 @@
13
13
  * Commands (routes/commands.js)
14
14
  * GET /api/commands - List available commands (auth required)
15
15
  * POST /api/command - Execute a whitelisted command (auth required)
16
- * POST /api/vnc-password - Change VNC password (auth required)
17
16
  *
18
17
  * Skills (routes/skills.js)
19
18
  * GET /api/list-skills - List deployed skills (auth required)
@@ -16,6 +16,7 @@
16
16
 
17
17
  const { exec, spawn } = require('child_process')
18
18
  const { promisify } = require('util')
19
+ const net = require('net')
19
20
  const os = require('os')
20
21
  const execAsync = promisify(exec)
21
22
 
@@ -38,16 +39,35 @@ const ttydProcesses = new Map()
38
39
  const TTYD_BASE_PORT = 7682
39
40
 
40
41
  /**
41
- * Find an available port for ttyd
42
- * @returns {number} Available port
42
+ * Check if a port is available by attempting to bind to it
43
+ * @param {number} port
44
+ * @returns {Promise<boolean>}
45
+ */
46
+ function isPortAvailable(port) {
47
+ return new Promise((resolve) => {
48
+ const server = net.createServer()
49
+ server.once('error', () => resolve(false))
50
+ server.once('listening', () => {
51
+ server.close(() => resolve(true))
52
+ })
53
+ server.listen(port, '0.0.0.0')
54
+ })
55
+ }
56
+
57
+ /**
58
+ * Find an available port for ttyd (checks OS-level availability)
59
+ * @returns {Promise<number>} Available port
43
60
  */
44
- function findAvailablePort() {
61
+ async function findAvailablePort() {
45
62
  const usedPorts = new Set([...ttydProcesses.values()].map(p => p.port))
46
63
  let port = TTYD_BASE_PORT
47
- while (usedPorts.has(port)) {
64
+ while (port < TTYD_BASE_PORT + 100) {
65
+ if (!usedPorts.has(port) && await isPortAvailable(port)) {
66
+ return port
67
+ }
48
68
  port++
49
69
  }
50
- return port
70
+ throw new Error('No available port found for ttyd')
51
71
  }
52
72
 
53
73
  /**
@@ -81,22 +101,34 @@ async function startTtydForSession(session) {
81
101
  throw new Error('ttyd is not installed')
82
102
  }
83
103
 
84
- // Verify tmux session exists
104
+ // Verify tmux session exists; auto-create if missing
85
105
  try {
86
106
  await execAsync(`tmux has-session -t "${session}"`, {
87
107
  timeout: 5000,
88
108
  env: { ...process.env, HOME: homeDir },
89
109
  })
90
110
  } catch {
91
- throw new Error(`tmux session '${session}' not found`)
111
+ console.log(`[Terminal] Session '${session}' not found, auto-creating...`)
112
+ try {
113
+ await execAsync(`tmux new-session -d -s "${session}" -c "${homeDir}"`, {
114
+ timeout: 5000,
115
+ env: { ...process.env, HOME: homeDir },
116
+ })
117
+ console.log(`[Terminal] Auto-created session '${session}'`)
118
+ } catch (createErr) {
119
+ throw new Error(
120
+ `tmux session '${session}' not found and auto-creation failed: ${createErr.message}`
121
+ )
122
+ }
92
123
  }
93
124
 
94
- const port = findAvailablePort()
125
+ const port = await findAvailablePort()
95
126
 
96
127
  const proc = spawn('ttyd', [
97
128
  '-p', port.toString(),
98
129
  '-t', 'fontSize=14',
99
130
  '-t', 'theme={"background": "#1a1a2e", "foreground": "#eee"}',
131
+ '--',
100
132
  'tmux', 'attach', '-t', session,
101
133
  ], {
102
134
  env: { ...process.env, HOME: homeDir },
@@ -104,13 +136,18 @@ async function startTtydForSession(session) {
104
136
  detached: false,
105
137
  })
106
138
 
139
+ // Track early exit to detect startup failures
140
+ let earlyExit = null
141
+
107
142
  proc.on('error', (err) => {
108
143
  console.error(`[Terminal] ttyd error for session '${session}': ${err.message}`)
144
+ earlyExit = { code: null, error: err.message }
109
145
  ttydProcesses.delete(session)
110
146
  })
111
147
 
112
- proc.on('exit', (code) => {
113
- console.log(`[Terminal] ttyd exited for session '${session}' with code ${code}`)
148
+ proc.on('exit', (code, signal) => {
149
+ console.log(`[Terminal] ttyd exited for session '${session}' with code ${code}, signal ${signal}`)
150
+ earlyExit = { code, signal }
114
151
  ttydProcesses.delete(session)
115
152
  })
116
153
 
@@ -126,8 +163,17 @@ async function startTtydForSession(session) {
126
163
 
127
164
  console.log(`[Terminal] Started ttyd for session '${session}' on port ${port}`)
128
165
 
129
- // Wait for ttyd to start listening
130
- await new Promise(resolve => setTimeout(resolve, 500))
166
+ // Wait for ttyd to start listening, checking for early exit
167
+ const maxWait = 500
168
+ const checkInterval = 50
169
+ for (let waited = 0; waited < maxWait; waited += checkInterval) {
170
+ await new Promise(resolve => setTimeout(resolve, checkInterval))
171
+ if (earlyExit) {
172
+ throw new Error(
173
+ `ttyd exited immediately for session '${session}' (code=${earlyExit.code}, signal=${earlyExit.signal || 'none'})`
174
+ )
175
+ }
176
+ }
131
177
 
132
178
  return { port, alreadyRunning: false }
133
179
  }
@@ -388,7 +434,7 @@ async function terminalRoutes(fastify) {
388
434
  // Create new detached session
389
435
  // -d: detached, -s: session name
390
436
  // Optionally run a command in the new session
391
- let createCommand = `tmux new-session -d -s "${sessionName}"`
437
+ let createCommand = `tmux new-session -d -s "${sessionName}" -c "${homeDir}"`
392
438
  if (command) {
393
439
  // Escape command for shell safety
394
440
  const escapedCommand = command.replace(/'/g, "'\\''")
@@ -634,9 +680,29 @@ function cleanupTtyd() {
634
680
  ttydProcesses.clear()
635
681
  }
636
682
 
683
+ /**
684
+ * Kill any stale ttyd processes left over from a previous agent run.
685
+ * Call this on server startup before accepting connections.
686
+ */
687
+ async function killStaleTtydProcesses() {
688
+ try {
689
+ const { stdout } = await execAsync("pgrep -f '^ttyd ' || true", { timeout: 5000 })
690
+ const pids = stdout.trim().split('\n').filter(Boolean)
691
+ if (pids.length > 0) {
692
+ console.log(`[Terminal] Found ${pids.length} stale ttyd process(es), killing: ${pids.join(', ')}`)
693
+ await execAsync(`kill ${pids.join(' ')}`, { timeout: 5000 }).catch(() => {})
694
+ // Wait briefly for processes to exit and release ports
695
+ await new Promise(resolve => setTimeout(resolve, 300))
696
+ }
697
+ } catch {
698
+ // Ignore errors (pgrep returns non-zero when no matches)
699
+ }
700
+ }
701
+
637
702
  module.exports = {
638
703
  terminalRoutes,
639
704
  cleanupTtyd,
705
+ killStaleTtydProcesses,
640
706
  ttydProcesses,
641
707
  startTtydForSession,
642
708
  }
package/server.js CHANGED
@@ -5,20 +5,13 @@
5
5
  * See routes/index.js for API documentation.
6
6
  */
7
7
 
8
- const { promisify } = require('util')
9
- const { exec } = require('child_process')
10
- const path = require('path')
11
- const os = require('os')
12
- const execAsync = promisify(exec)
13
-
14
8
  const fastify = require('fastify')({ logger: true })
15
9
  const { config, validate, isHqConfigured } = require('./config')
16
10
  const workflowRunner = require('./workflow-runner')
17
11
  const workflowStore = require('./workflow-store')
18
12
 
19
13
  const { registerRoutes, setOffline, getProcessManager, getAllowedCommands } = require('./routes')
20
- const { SUDO } = require('./lib/process-manager')
21
- const { cleanupTtyd } = require('./routes/terminal')
14
+ const { cleanupTtyd, killStaleTtydProcesses } = require('./routes/terminal')
22
15
  const { startTerminalProxy, stopTerminalProxy } = require('./terminal-proxy')
23
16
 
24
17
  // Validate configuration before starting
@@ -49,64 +42,6 @@ async function shutdown(signal) {
49
42
  process.on('SIGTERM', () => shutdown('SIGTERM'))
50
43
  process.on('SIGINT', () => shutdown('SIGINT'))
51
44
 
52
- // Sync VNC password from HQ
53
- async function syncVncPassword() {
54
- console.log('[VNC] Syncing VNC password from HQ...')
55
-
56
- try {
57
- const url = `${config.HQ_URL}/api/minion/vnc-password`
58
- const response = await fetch(url, {
59
- method: 'GET',
60
- headers: {
61
- 'Authorization': `Bearer ${config.API_TOKEN}`,
62
- },
63
- })
64
-
65
- if (!response.ok) {
66
- const errorText = await response.text()
67
- console.error(`[VNC] Failed to get VNC password from HQ: ${response.status} - ${errorText}`)
68
- return false
69
- }
70
-
71
- const data = await response.json()
72
- const vncPassword = data.vnc_password
73
-
74
- if (!vncPassword) {
75
- console.log('[VNC] No VNC password set in HQ, using default')
76
- return true
77
- }
78
-
79
- console.log('[VNC] Got VNC password from HQ, applying...')
80
-
81
- // Escape single quotes for shell safety
82
- const escapedPassword = vncPassword.replace(/'/g, "'\\''")
83
-
84
- // Ensure .vnc directory exists and set password
85
- const homeDir = os.homedir()
86
- const vncDir = path.join(homeDir, '.vnc')
87
- const vncPasswdPath = path.join(vncDir, 'passwd')
88
- await execAsync(`mkdir -p ${vncDir}`, { timeout: 5000 })
89
- await execAsync(
90
- `x11vnc -storepasswd '${escapedPassword}' ${vncPasswdPath}`,
91
- { timeout: 10000 }
92
- )
93
- await execAsync(`chmod 600 ${vncPasswdPath}`, { timeout: 5000 })
94
-
95
- // Restart x11vnc to apply the new password
96
- if (PROC_MGR === 'systemd') {
97
- await execAsync(`${SUDO}systemctl restart x11vnc`, { timeout: 10000 })
98
- } else if (PROC_MGR === 'supervisord') {
99
- await execAsync(`${SUDO}supervisorctl restart x11vnc`, { timeout: 10000 })
100
- }
101
-
102
- console.log('[VNC] VNC password synced successfully')
103
- return true
104
- } catch (error) {
105
- console.error(`[VNC] Failed to sync VNC password: ${error.message}`)
106
- return false
107
- }
108
- }
109
-
110
45
  // Start server
111
46
  async function start() {
112
47
  try {
@@ -118,7 +53,8 @@ async function start() {
118
53
 
119
54
  console.log(`[Server] Minion agent listening on port ${config.AGENT_PORT}`)
120
55
 
121
- // Start terminal WebSocket proxy on port 7681
56
+ // Kill any stale ttyd processes from a previous run, then start terminal proxy
57
+ await killStaleTtydProcesses()
122
58
  try {
123
59
  await startTerminalProxy()
124
60
  } catch (err) {
@@ -139,11 +75,6 @@ async function start() {
139
75
 
140
76
  if (isHqConfigured()) {
141
77
  console.log(`[Server] HQ URL: ${config.HQ_URL}`)
142
-
143
- // Sync VNC password from HQ (non-blocking, run in background)
144
- syncVncPassword().catch(err => {
145
- console.error('[VNC] Background sync failed:', err.message)
146
- })
147
78
  } else {
148
79
  console.log('[Server] Running in standalone mode (no HQ connection)')
149
80
  }