@geekbeer/minion 2.23.0 → 2.32.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/core/lib/platform.js +117 -0
- package/{routes → core/routes}/health.js +1 -1
- package/{routes → core/routes}/routines.js +44 -4
- package/{routes → core/routes}/skills.js +3 -3
- package/{routes → core/routes}/workflows.js +4 -4
- package/{chat-store.js → core/stores/chat-store.js} +1 -1
- package/{execution-store.js → core/stores/execution-store.js} +1 -1
- package/{routine-store.js → core/stores/routine-store.js} +1 -1
- package/{workflow-store.js → core/stores/workflow-store.js} +1 -1
- package/{minion-cli.sh → linux/minion-cli.sh} +245 -4
- package/{routes → linux/routes}/chat.js +3 -3
- package/{routes → linux/routes}/commands.js +1 -1
- package/{routes → linux/routes}/config.js +3 -3
- package/{routes → linux/routes}/directives.js +5 -5
- package/{routes → linux/routes}/files.js +2 -2
- package/{routes → linux/routes}/terminal.js +2 -2
- package/{routine-runner.js → linux/routine-runner.js} +4 -4
- package/{server.js → linux/server.js} +71 -36
- package/{workflow-runner.js → linux/workflow-runner.js} +4 -4
- package/package.json +16 -20
- package/win/bin/hq-win.js +18 -0
- package/win/bin/hq.ps1 +108 -0
- package/win/bin/minion-cli-win.js +20 -0
- package/win/lib/llm-checker.js +115 -0
- package/win/lib/log-manager.js +119 -0
- package/win/lib/process-manager.js +112 -0
- package/win/minion-cli.ps1 +869 -0
- package/win/routes/chat.js +280 -0
- package/win/routes/commands.js +101 -0
- package/win/routes/config.js +227 -0
- package/win/routes/directives.js +136 -0
- package/win/routes/files.js +283 -0
- package/win/routes/terminal.js +316 -0
- package/win/routine-runner.js +324 -0
- package/win/server.js +230 -0
- package/win/terminal-server.js +234 -0
- package/win/workflow-runner.js +380 -0
- package/routes/index.js +0 -106
- /package/{api.js → core/api.js} +0 -0
- /package/{config.js → core/config.js} +0 -0
- /package/{lib → core/lib}/auth.js +0 -0
- /package/{lib → core/lib}/llm-checker.js +0 -0
- /package/{lib → core/lib}/log-manager.js +0 -0
- /package/{routes → core/routes}/auth.js +0 -0
- /package/{bin → linux/bin}/hq +0 -0
- /package/{lib → linux/lib}/process-manager.js +0 -0
- /package/{terminal-proxy.js → linux/terminal-proxy.js} +0 -0
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
#Requires -Version 5.1
|
|
2
|
+
# Minion Agent CLI for Windows (@geekbeer/minion)
|
|
3
|
+
# Usage:
|
|
4
|
+
# minion-cli-win setup --hq-url https://... --minion-id <UUID> --api-token <TOKEN>
|
|
5
|
+
# minion-cli-win setup --setup-tunnel
|
|
6
|
+
# minion-cli-win start | stop | restart | status | health | version | help
|
|
7
|
+
|
|
8
|
+
# Parse arguments manually to avoid issues with npm wrapper passing $args as array
|
|
9
|
+
$Command = 'help'
|
|
10
|
+
$HqUrl = ''
|
|
11
|
+
$MinionId = ''
|
|
12
|
+
$ApiToken = ''
|
|
13
|
+
$SetupTunnel = $false
|
|
14
|
+
|
|
15
|
+
$i = 0
|
|
16
|
+
while ($i -lt $args.Count) {
|
|
17
|
+
$arg = [string]$args[$i]
|
|
18
|
+
switch -Regex ($arg) {
|
|
19
|
+
'^(setup|reconfigure|start|stop|restart|status|health|version|help)$' { $Command = $arg }
|
|
20
|
+
'^(-v|--version)$' { $Command = 'version' }
|
|
21
|
+
'^--hq-url$' { $i++; if ($i -lt $args.Count) { $HqUrl = [string]$args[$i] } }
|
|
22
|
+
'^--minion-id$' { $i++; if ($i -lt $args.Count) { $MinionId = [string]$args[$i] } }
|
|
23
|
+
'^--api-token$' { $i++; if ($i -lt $args.Count) { $ApiToken = [string]$args[$i] } }
|
|
24
|
+
'^--setup-tunnel$' { $SetupTunnel = $true }
|
|
25
|
+
'^(-h|--help)$' { $Command = 'help' }
|
|
26
|
+
}
|
|
27
|
+
$i++
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
$ErrorActionPreference = 'Stop'
|
|
31
|
+
|
|
32
|
+
# Constants
|
|
33
|
+
$DataDir = Join-Path $env:USERPROFILE '.minion'
|
|
34
|
+
$EnvFile = Join-Path $DataDir '.env'
|
|
35
|
+
$PidFile = Join-Path $DataDir 'minion-agent.pid'
|
|
36
|
+
$LogDir = Join-Path $DataDir 'logs'
|
|
37
|
+
$StartAgentScript = Join-Path $DataDir 'start-agent.ps1'
|
|
38
|
+
$AgentUrl = if ($env:MINION_AGENT_URL) { $env:MINION_AGENT_URL } else { 'http://localhost:8080' }
|
|
39
|
+
|
|
40
|
+
# Resolve CLI version from package.json
|
|
41
|
+
$CliDir = Split-Path -Parent $PSScriptRoot
|
|
42
|
+
$CliVersion = try {
|
|
43
|
+
(Get-Content (Join-Path $CliDir 'package.json') -Raw | ConvertFrom-Json).version
|
|
44
|
+
}
|
|
45
|
+
catch { 'unknown' }
|
|
46
|
+
|
|
47
|
+
# ============================================================
|
|
48
|
+
# Utility functions
|
|
49
|
+
# ============================================================
|
|
50
|
+
|
|
51
|
+
function Write-Step {
|
|
52
|
+
param([int]$Current, [int]$Total, [string]$Message)
|
|
53
|
+
Write-Host "[$Current/$Total] $Message" -ForegroundColor Cyan
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function Write-Detail {
|
|
57
|
+
param([string]$Message)
|
|
58
|
+
Write-Host " -> $Message" -ForegroundColor Green
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function Write-Warn {
|
|
62
|
+
param([string]$Message)
|
|
63
|
+
Write-Host " WARNING: $Message" -ForegroundColor Yellow
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function Test-CommandExists {
|
|
67
|
+
param([string]$Name)
|
|
68
|
+
$null -ne (Get-Command $Name -ErrorAction SilentlyContinue)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function Get-LanIPAddress {
|
|
72
|
+
try {
|
|
73
|
+
$ip = (Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
|
|
74
|
+
Where-Object {
|
|
75
|
+
($_.PrefixOrigin -eq 'Dhcp' -or $_.PrefixOrigin -eq 'Manual') -and
|
|
76
|
+
$_.IPAddress -notlike '169.254.*' -and
|
|
77
|
+
$_.IPAddress -ne '127.0.0.1' -and
|
|
78
|
+
$_.InterfaceAlias -notlike 'vEthernet*'
|
|
79
|
+
} |
|
|
80
|
+
Select-Object -First 1).IPAddress
|
|
81
|
+
return $ip
|
|
82
|
+
}
|
|
83
|
+
catch { return $null }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function Get-MinionServerJs {
|
|
87
|
+
$npmRoot = & npm root -g 2>$null
|
|
88
|
+
return Join-Path $npmRoot '@geekbeer\minion\win\server.js'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function Read-EnvFile {
|
|
92
|
+
param([string]$Path)
|
|
93
|
+
$result = @{}
|
|
94
|
+
if (-not (Test-Path $Path)) { return $result }
|
|
95
|
+
Get-Content $Path | ForEach-Object {
|
|
96
|
+
$line = $_.Trim()
|
|
97
|
+
if ($line -and -not $line.StartsWith('#') -and $line.Contains('=')) {
|
|
98
|
+
$idx = $line.IndexOf('=')
|
|
99
|
+
$key = $line.Substring(0, $idx).Trim()
|
|
100
|
+
$value = $line.Substring($idx + 1).Trim()
|
|
101
|
+
$result[$key] = $value
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return $result
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function Write-EnvFile {
|
|
108
|
+
param([string]$Path, [hashtable]$Values)
|
|
109
|
+
$lines = @(
|
|
110
|
+
'# Minion Agent Configuration',
|
|
111
|
+
'# Generated by minion-cli.ps1 setup',
|
|
112
|
+
''
|
|
113
|
+
)
|
|
114
|
+
foreach ($key in ($Values.Keys | Sort-Object)) {
|
|
115
|
+
$lines += "$key=$($Values[$key])"
|
|
116
|
+
}
|
|
117
|
+
$lines += ''
|
|
118
|
+
New-Item -Path (Split-Path $Path) -ItemType Directory -Force | Out-Null
|
|
119
|
+
Set-Content -Path $Path -Value ($lines -join "`n") -Encoding UTF8 -NoNewline
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function Invoke-HealthCheck {
|
|
123
|
+
param([int]$Retries = 5, [int]$DelaySeconds = 2)
|
|
124
|
+
for ($i = 1; $i -le $Retries; $i++) {
|
|
125
|
+
try {
|
|
126
|
+
$response = Invoke-RestMethod -Uri "$AgentUrl/api/health" -TimeoutSec 5 -ErrorAction Stop
|
|
127
|
+
if ($response) { return $true }
|
|
128
|
+
}
|
|
129
|
+
catch { }
|
|
130
|
+
Write-Host " Waiting for agent to start... (attempt $i/$Retries)"
|
|
131
|
+
Start-Sleep -Seconds $DelaySeconds
|
|
132
|
+
}
|
|
133
|
+
return $false
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function Start-MinionProcess {
|
|
137
|
+
# Check if already running via PID file
|
|
138
|
+
if (Test-Path $PidFile) {
|
|
139
|
+
$existingPid = (Get-Content $PidFile -Raw).Trim()
|
|
140
|
+
$proc = Get-Process -Id $existingPid -ErrorAction SilentlyContinue
|
|
141
|
+
if ($proc) {
|
|
142
|
+
Write-Host "minion-agent is already running (PID: $existingPid)"
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
Remove-Item $PidFile -Force
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (-not (Test-Path $StartAgentScript)) {
|
|
149
|
+
Write-Error "start-agent.ps1 not found. Run 'minion-cli-win setup' first."
|
|
150
|
+
exit 1
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# Launch start-agent.ps1 as a hidden background process
|
|
154
|
+
Start-Process powershell -ArgumentList "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"$StartAgentScript`"" `
|
|
155
|
+
-WindowStyle Hidden
|
|
156
|
+
Start-Sleep -Seconds 2
|
|
157
|
+
|
|
158
|
+
if (Test-Path $PidFile) {
|
|
159
|
+
$newPid = (Get-Content $PidFile -Raw).Trim()
|
|
160
|
+
Write-Host "minion-agent started (PID: $newPid)"
|
|
161
|
+
} else {
|
|
162
|
+
Write-Host "minion-agent starting... (check logs at $LogDir)"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function Stop-MinionProcess {
|
|
167
|
+
if (-not (Test-Path $PidFile)) {
|
|
168
|
+
Write-Host "minion-agent is not running (no PID file)"
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
$agentPid = (Get-Content $PidFile -Raw).Trim()
|
|
172
|
+
$proc = Get-Process -Id $agentPid -ErrorAction SilentlyContinue
|
|
173
|
+
if ($proc) {
|
|
174
|
+
# Also stop child processes (node, websockify, cloudflared)
|
|
175
|
+
Get-CimInstance Win32_Process -Filter "ParentProcessId = $agentPid" -ErrorAction SilentlyContinue |
|
|
176
|
+
ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
|
|
177
|
+
Stop-Process -Id $agentPid -Force
|
|
178
|
+
Write-Host "minion-agent stopped (PID: $agentPid)"
|
|
179
|
+
} else {
|
|
180
|
+
Write-Host "minion-agent was not running (stale PID file)"
|
|
181
|
+
}
|
|
182
|
+
Remove-Item $PidFile -Force -ErrorAction SilentlyContinue
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function Restart-MinionProcess {
|
|
186
|
+
Stop-MinionProcess
|
|
187
|
+
Start-Sleep -Seconds 2
|
|
188
|
+
Start-MinionProcess
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# ============================================================
|
|
192
|
+
# Setup
|
|
193
|
+
# ============================================================
|
|
194
|
+
|
|
195
|
+
function Invoke-Setup {
|
|
196
|
+
$totalSteps = 8
|
|
197
|
+
if ($SetupTunnel) { $totalSteps = 9 }
|
|
198
|
+
|
|
199
|
+
# Minionization warning
|
|
200
|
+
Write-Host ""
|
|
201
|
+
Write-Host "=========================================" -ForegroundColor Red
|
|
202
|
+
Write-Host " WARNING: This machine will be minionized" -ForegroundColor Red
|
|
203
|
+
Write-Host "=========================================" -ForegroundColor Red
|
|
204
|
+
Write-Host ""
|
|
205
|
+
|
|
206
|
+
# Check if current user is in Administrators group
|
|
207
|
+
$adminGroupMembers = net localgroup Administrators 2>$null
|
|
208
|
+
if ($adminGroupMembers -match [regex]::Escape($env:USERNAME)) {
|
|
209
|
+
Write-Host " SECURITY WARNING: User '$env:USERNAME' has Administrator privileges." -ForegroundColor Yellow
|
|
210
|
+
Write-Host " We recommend setting up the minion under a Standard user account." -ForegroundColor Yellow
|
|
211
|
+
Write-Host " If this machine is compromised, an Administrator account gives" -ForegroundColor Yellow
|
|
212
|
+
Write-Host " the attacker full control over the system." -ForegroundColor Yellow
|
|
213
|
+
Write-Host ""
|
|
214
|
+
Write-Host " To create a Standard user:" -ForegroundColor Gray
|
|
215
|
+
Write-Host " net user minion <password> /add" -ForegroundColor Gray
|
|
216
|
+
Write-Host " Then log in as that user and run setup again." -ForegroundColor Gray
|
|
217
|
+
Write-Host ""
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
Write-Host " This setup will:" -ForegroundColor Yellow
|
|
221
|
+
Write-Host " - Install and configure software (Node.js, Claude Code)"
|
|
222
|
+
Write-Host " - Register auto-start via Startup folder"
|
|
223
|
+
Write-Host " - Overwrite Claude Code settings (permissions, rules, skills)"
|
|
224
|
+
Write-Host ""
|
|
225
|
+
Write-Host " This machine should be DEDICATED to the minion agent." -ForegroundColor Yellow
|
|
226
|
+
Write-Host " Do not use it as your daily workstation after setup." -ForegroundColor Yellow
|
|
227
|
+
Write-Host ""
|
|
228
|
+
$confirm = Read-Host " Type 'yes' to continue"
|
|
229
|
+
if ($confirm -ne 'yes') {
|
|
230
|
+
Write-Host "Setup cancelled."
|
|
231
|
+
exit 0
|
|
232
|
+
}
|
|
233
|
+
Write-Host ""
|
|
234
|
+
|
|
235
|
+
# Load existing .env for redeploy scenario
|
|
236
|
+
$envValues = @{}
|
|
237
|
+
if (-not $HqUrl -and -not $MinionId -and -not $ApiToken -and (Test-Path $EnvFile)) {
|
|
238
|
+
Write-Host "Reading existing .env values (redeploy mode)..."
|
|
239
|
+
$envValues = Read-EnvFile $EnvFile
|
|
240
|
+
if (-not $HqUrl -and $envValues['HQ_URL']) { $HqUrl = $envValues['HQ_URL'] }
|
|
241
|
+
if (-not $MinionId -and $envValues['MINION_ID']) { $MinionId = $envValues['MINION_ID'] }
|
|
242
|
+
if (-not $ApiToken -and $envValues['API_TOKEN']) { $ApiToken = $envValues['API_TOKEN'] }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
Write-Host "=========================================" -ForegroundColor Cyan
|
|
246
|
+
Write-Host " @geekbeer/minion Windows Setup" -ForegroundColor Cyan
|
|
247
|
+
Write-Host "=========================================" -ForegroundColor Cyan
|
|
248
|
+
Write-Host "Platform: Windows (no admin required)"
|
|
249
|
+
Write-Host "User: $env:USERNAME ($env:USERPROFILE)"
|
|
250
|
+
Write-Host "Data: $DataDir"
|
|
251
|
+
if ($HqUrl) {
|
|
252
|
+
Write-Host "Mode: Connected to HQ ($HqUrl)"
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
Write-Host "Mode: Standalone (no HQ connection)"
|
|
256
|
+
}
|
|
257
|
+
if ($SetupTunnel) { Write-Host "Tunnel: Enabled" }
|
|
258
|
+
Write-Host ""
|
|
259
|
+
|
|
260
|
+
# Step 1: Check/Install Node.js
|
|
261
|
+
Write-Step 1 $totalSteps "Checking Node.js..."
|
|
262
|
+
if (Test-CommandExists 'node') {
|
|
263
|
+
$nodeVersion = & node --version 2>$null
|
|
264
|
+
Write-Detail "Node.js $nodeVersion already installed"
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
Write-Host " Installing Node.js via winget..."
|
|
268
|
+
try {
|
|
269
|
+
& winget install --id OpenJS.NodeJS.LTS --accept-package-agreements --accept-source-agreements
|
|
270
|
+
# Refresh PATH
|
|
271
|
+
$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('PATH', 'User')
|
|
272
|
+
if (Test-CommandExists 'node') {
|
|
273
|
+
Write-Detail "Node.js installed successfully"
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
Write-Warn "Node.js installed but not in PATH. Please restart this terminal."
|
|
277
|
+
exit 1
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
Write-Error "Failed to install Node.js. Please install manually from https://nodejs.org/"
|
|
282
|
+
exit 1
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# Step 2: Install Claude Code CLI
|
|
287
|
+
Write-Step 2 $totalSteps "Installing Claude Code..."
|
|
288
|
+
if (Test-CommandExists 'claude') {
|
|
289
|
+
$claudeVersion = & claude --version 2>$null
|
|
290
|
+
Write-Detail "Claude Code already installed ($claudeVersion)"
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
Write-Host " Installing Claude Code via npm..."
|
|
294
|
+
& npm install -g @anthropic-ai/claude-code
|
|
295
|
+
if (Test-CommandExists 'claude') {
|
|
296
|
+
Write-Detail "Claude Code installed successfully"
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
Write-Warn "Claude Code installation may have failed"
|
|
300
|
+
Write-Host " You can install manually: npm install -g @anthropic-ai/claude-code"
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
Write-Host ""
|
|
304
|
+
Write-Host " IMPORTANT: Claude Code requires authentication." -ForegroundColor Yellow
|
|
305
|
+
Write-Host " Please run 'claude' in a terminal to complete the authentication process." -ForegroundColor Yellow
|
|
306
|
+
Write-Host ""
|
|
307
|
+
|
|
308
|
+
# Step 3: Create config directory and .env
|
|
309
|
+
Write-Step 3 $totalSteps "Creating config directory and .env..."
|
|
310
|
+
New-Item -Path $DataDir -ItemType Directory -Force | Out-Null
|
|
311
|
+
New-Item -Path $LogDir -ItemType Directory -Force | Out-Null
|
|
312
|
+
$envValues = @{
|
|
313
|
+
'AGENT_PORT' = '8080'
|
|
314
|
+
'HEARTBEAT_INTERVAL' = '30'
|
|
315
|
+
'MINION_USER' = $env:USERNAME
|
|
316
|
+
}
|
|
317
|
+
if ($HqUrl) { $envValues['HQ_URL'] = $HqUrl }
|
|
318
|
+
if ($ApiToken) { $envValues['API_TOKEN'] = $ApiToken }
|
|
319
|
+
if ($MinionId) { $envValues['MINION_ID'] = $MinionId }
|
|
320
|
+
Write-EnvFile $EnvFile $envValues
|
|
321
|
+
Write-Detail "$EnvFile generated"
|
|
322
|
+
|
|
323
|
+
# Step 4: Install node-pty (required for Windows terminal management)
|
|
324
|
+
Write-Step 4 $totalSteps "Installing terminal support (node-pty)..."
|
|
325
|
+
$npmRoot = & npm root -g 2>$null
|
|
326
|
+
$minionPkgDir = Join-Path $npmRoot '@geekbeer\minion'
|
|
327
|
+
if (Test-Path $minionPkgDir) {
|
|
328
|
+
Push-Location $minionPkgDir
|
|
329
|
+
$ptyInstalled = $false
|
|
330
|
+
|
|
331
|
+
# Try prebuilt version first (no Build Tools required)
|
|
332
|
+
try {
|
|
333
|
+
& npm install node-pty-prebuilt-multiarch 2>$null
|
|
334
|
+
Write-Detail "node-pty-prebuilt-multiarch installed (no Build Tools needed)"
|
|
335
|
+
$ptyInstalled = $true
|
|
336
|
+
} catch {}
|
|
337
|
+
|
|
338
|
+
# Fallback: source-compiled version (requires Visual Studio Build Tools)
|
|
339
|
+
if (-not $ptyInstalled) {
|
|
340
|
+
try {
|
|
341
|
+
& npm install node-pty 2>$null
|
|
342
|
+
Write-Detail "node-pty installed (compiled from source)"
|
|
343
|
+
$ptyInstalled = $true
|
|
344
|
+
} catch {}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (-not $ptyInstalled) {
|
|
348
|
+
Write-Warn "node-pty installation failed. Terminal/workflow features may not work."
|
|
349
|
+
Write-Host " Try manually: cd $minionPkgDir && npm install node-pty-prebuilt-multiarch"
|
|
350
|
+
}
|
|
351
|
+
Pop-Location
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
Write-Warn "Minion package not found at $minionPkgDir"
|
|
355
|
+
Write-Host " Please run: npm install -g @geekbeer/minion"
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
# Step 5: Generate start-agent.ps1 and register auto-start
|
|
359
|
+
Write-Step 5 $totalSteps "Registering auto-start..."
|
|
360
|
+
$serverJs = Join-Path $minionPkgDir 'win\server.js'
|
|
361
|
+
if (-not (Test-Path $serverJs)) {
|
|
362
|
+
$serverJs = Join-Path $minionPkgDir 'win' 'server.js'
|
|
363
|
+
}
|
|
364
|
+
if (-not (Test-Path $serverJs)) {
|
|
365
|
+
Write-Error "server.js not found at $serverJs"
|
|
366
|
+
Write-Host " Please run: npm install -g @geekbeer/minion"
|
|
367
|
+
exit 1
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
# Generate start-agent.ps1 (watchdog script)
|
|
371
|
+
$startAgentContent = @"
|
|
372
|
+
# Auto-generated by minion-cli-win setup
|
|
373
|
+
# Starts minion agent with watchdog (auto-restart on crash)
|
|
374
|
+
|
|
375
|
+
`$DataDir = '$DataDir'
|
|
376
|
+
`$EnvFile = Join-Path `$DataDir '.env'
|
|
377
|
+
`$PidFile = Join-Path `$DataDir 'minion-agent.pid'
|
|
378
|
+
`$LogDir = Join-Path `$DataDir 'logs'
|
|
379
|
+
`$ServerJs = '$serverJs'
|
|
380
|
+
|
|
381
|
+
# Load .env
|
|
382
|
+
if (Test-Path `$EnvFile) {
|
|
383
|
+
Get-Content `$EnvFile | ForEach-Object {
|
|
384
|
+
`$line = `$_.Trim()
|
|
385
|
+
if (`$line -and -not `$line.StartsWith('#') -and `$line.Contains('=')) {
|
|
386
|
+
`$idx = `$line.IndexOf('=')
|
|
387
|
+
`$key = `$line.Substring(0, `$idx).Trim()
|
|
388
|
+
`$value = `$line.Substring(`$idx + 1).Trim()
|
|
389
|
+
[Environment]::SetEnvironmentVariable(`$key, `$value, 'Process')
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
New-Item -Path `$LogDir -ItemType Directory -Force | Out-Null
|
|
395
|
+
|
|
396
|
+
# Write PID of this watchdog process
|
|
397
|
+
Set-Content -Path `$PidFile -Value `$PID
|
|
398
|
+
|
|
399
|
+
# Start TightVNC + websockify if available
|
|
400
|
+
`$vncExe = `$null
|
|
401
|
+
if (Test-Path 'C:\Program Files\TightVNC\tvnserver.exe') {
|
|
402
|
+
`$vncExe = 'C:\Program Files\TightVNC\tvnserver.exe'
|
|
403
|
+
} elseif (Test-Path (Join-Path `$DataDir 'tightvnc\PFiles\TightVNC\tvnserver.exe')) {
|
|
404
|
+
`$vncExe = Join-Path `$DataDir 'tightvnc\PFiles\TightVNC\tvnserver.exe'
|
|
405
|
+
}
|
|
406
|
+
if (`$vncExe) {
|
|
407
|
+
# Start VNC server in application mode (no admin, no service registration)
|
|
408
|
+
`$vncProc = Get-Process -Name tvnserver -ErrorAction SilentlyContinue
|
|
409
|
+
if (-not `$vncProc) {
|
|
410
|
+
Start-Process -FilePath `$vncExe -ArgumentList '-run' -WindowStyle Hidden
|
|
411
|
+
# Wait for VNC server to bind port 5900 before starting websockify
|
|
412
|
+
Start-Sleep -Seconds 3
|
|
413
|
+
}
|
|
414
|
+
# Reload registry config (ensures no-auth settings are applied)
|
|
415
|
+
& `$vncExe -controlapp -reload 2>`$null
|
|
416
|
+
if (Get-Command websockify -ErrorAction SilentlyContinue) {
|
|
417
|
+
`$wsProc = Get-Process -Name websockify -ErrorAction SilentlyContinue
|
|
418
|
+
if (-not `$wsProc) {
|
|
419
|
+
Start-Process -FilePath (Get-Command websockify).Source -ArgumentList '6080', 'localhost:5900' -WindowStyle Hidden
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
# Start cloudflared tunnel if configured
|
|
425
|
+
`$cfConfig = Join-Path `$env:USERPROFILE '.cloudflared\config.yml'
|
|
426
|
+
if ((Test-Path `$cfConfig) -and (Get-Command cloudflared -ErrorAction SilentlyContinue)) {
|
|
427
|
+
Start-Process -FilePath (Get-Command cloudflared).Source -ArgumentList 'tunnel', 'run' -WindowStyle Hidden
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
# Watchdog loop: restart node if it crashes
|
|
431
|
+
`$nodePath = (Get-Command node).Source
|
|
432
|
+
while (`$true) {
|
|
433
|
+
`$stdoutLog = Join-Path `$LogDir 'service-stdout.log'
|
|
434
|
+
`$stderrLog = Join-Path `$LogDir 'service-stderr.log'
|
|
435
|
+
`$proc = Start-Process -FilePath `$nodePath -ArgumentList `$ServerJs ``
|
|
436
|
+
-WorkingDirectory `$DataDir ``
|
|
437
|
+
-RedirectStandardOutput `$stdoutLog ``
|
|
438
|
+
-RedirectStandardError `$stderrLog ``
|
|
439
|
+
-WindowStyle Hidden -PassThru
|
|
440
|
+
`$proc.WaitForExit()
|
|
441
|
+
if (`$proc.ExitCode -eq 0) { break }
|
|
442
|
+
Start-Sleep -Seconds 5
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
# Cleanup PID file on normal exit
|
|
446
|
+
Remove-Item `$PidFile -Force -ErrorAction SilentlyContinue
|
|
447
|
+
"@
|
|
448
|
+
Set-Content -Path $StartAgentScript -Value $startAgentContent -Encoding UTF8
|
|
449
|
+
Write-Detail "Generated $StartAgentScript"
|
|
450
|
+
|
|
451
|
+
# Create shortcut in Startup folder
|
|
452
|
+
$startupDir = [Environment]::GetFolderPath('Startup')
|
|
453
|
+
$shortcutPath = Join-Path $startupDir 'MinionAgent.lnk'
|
|
454
|
+
$wsShell = New-Object -ComObject WScript.Shell
|
|
455
|
+
$shortcut = $wsShell.CreateShortcut($shortcutPath)
|
|
456
|
+
$shortcut.TargetPath = 'powershell.exe'
|
|
457
|
+
$shortcut.Arguments = "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"$StartAgentScript`""
|
|
458
|
+
$shortcut.WorkingDirectory = $DataDir
|
|
459
|
+
$shortcut.WindowStyle = 7 # Minimized
|
|
460
|
+
$shortcut.Description = 'Minion Agent auto-start'
|
|
461
|
+
$shortcut.Save()
|
|
462
|
+
Write-Detail "Startup shortcut created: $shortcutPath"
|
|
463
|
+
|
|
464
|
+
# Step 6: Install TightVNC Server
|
|
465
|
+
Write-Step 6 $totalSteps "Setting up TightVNC Server..."
|
|
466
|
+
$vncSystemPath = 'C:\Program Files\TightVNC\tvnserver.exe'
|
|
467
|
+
$vncPortableDir = Join-Path $DataDir 'tightvnc'
|
|
468
|
+
$vncPortablePath = Join-Path $vncPortableDir 'PFiles\TightVNC\tvnserver.exe'
|
|
469
|
+
|
|
470
|
+
if (Test-Path $vncSystemPath) {
|
|
471
|
+
Write-Detail "TightVNC Server already installed (system)"
|
|
472
|
+
}
|
|
473
|
+
elseif (Test-Path $vncPortablePath) {
|
|
474
|
+
Write-Detail "TightVNC Server already installed (portable)"
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
Write-Host " Downloading TightVNC portable..."
|
|
478
|
+
try {
|
|
479
|
+
$msiUrl = 'https://www.tightvnc.com/download/2.8.85/tightvnc-2.8.85-gpl-setup-64bit.msi'
|
|
480
|
+
$msiPath = Join-Path $env:TEMP 'tightvnc-setup.msi'
|
|
481
|
+
Invoke-WebRequest -Uri $msiUrl -OutFile $msiPath -UseBasicParsing
|
|
482
|
+
New-Item -Path $vncPortableDir -ItemType Directory -Force | Out-Null
|
|
483
|
+
# Extract MSI without admin (administrative install = extract only)
|
|
484
|
+
# Creates PFiles\TightVNC\ subdirectory structure
|
|
485
|
+
Start-Process msiexec -ArgumentList "/a `"$msiPath`" /qn TARGETDIR=`"$vncPortableDir`"" -Wait -NoNewWindow
|
|
486
|
+
Remove-Item $msiPath -Force -ErrorAction SilentlyContinue
|
|
487
|
+
if (Test-Path $vncPortablePath) {
|
|
488
|
+
Write-Detail "TightVNC portable extracted to $vncPortableDir"
|
|
489
|
+
} else {
|
|
490
|
+
Write-Warn "TightVNC extraction failed. Check $vncPortableDir"
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
Write-Warn "Failed to download TightVNC: $_"
|
|
495
|
+
Write-Host " Manual download: https://www.tightvnc.com/download.php" -ForegroundColor Gray
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
# Configure TightVNC for no-auth localhost-only mode via registry
|
|
500
|
+
# - LoopbackOnly=1: only accept connections from localhost (security)
|
|
501
|
+
# - AllowLoopback=1: enable loopback connections
|
|
502
|
+
# - UseVncAuthentication=0: no password prompt (websockify connects via localhost,
|
|
503
|
+
# HQ WebSocket proxy provides its own Supabase session authentication)
|
|
504
|
+
$vncRegPath = 'HKCU:\Software\TightVNC\Server'
|
|
505
|
+
if ((Test-Path $vncSystemPath) -or (Test-Path $vncPortablePath)) {
|
|
506
|
+
if (-not (Test-Path $vncRegPath)) {
|
|
507
|
+
New-Item -Path $vncRegPath -Force | Out-Null
|
|
508
|
+
}
|
|
509
|
+
Set-ItemProperty -Path $vncRegPath -Name 'LoopbackOnly' -Value 1 -Type DWord
|
|
510
|
+
Set-ItemProperty -Path $vncRegPath -Name 'AllowLoopback' -Value 1 -Type DWord
|
|
511
|
+
Set-ItemProperty -Path $vncRegPath -Name 'UseVncAuthentication' -Value 0 -Type DWord
|
|
512
|
+
Set-ItemProperty -Path $vncRegPath -Name 'UseControlAuthentication' -Value 0 -Type DWord
|
|
513
|
+
Set-ItemProperty -Path $vncRegPath -Name 'RfbPort' -Value 5900 -Type DWord
|
|
514
|
+
# Reload config if tvnserver is already running (re-setup scenario)
|
|
515
|
+
$vncProc = Get-Process -Name tvnserver -ErrorAction SilentlyContinue
|
|
516
|
+
if ($vncProc) {
|
|
517
|
+
$vncExePath = if (Test-Path $vncSystemPath) { $vncSystemPath } else { $vncPortablePath }
|
|
518
|
+
& $vncExePath -controlapp -reload 2>$null
|
|
519
|
+
Write-Detail "TightVNC config reloaded"
|
|
520
|
+
}
|
|
521
|
+
Write-Detail "TightVNC configured: localhost-only, no VNC password (auth via HQ proxy)"
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
# Step 7: Setup websockify (WebSocket proxy for VNC)
|
|
525
|
+
Write-Step 7 $totalSteps "Setting up websockify..."
|
|
526
|
+
if (Test-CommandExists 'websockify') {
|
|
527
|
+
Write-Detail "websockify already installed"
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
# Ensure Python is installed (Microsoft Store stub doesn't include pip)
|
|
531
|
+
$pythonUsable = $false
|
|
532
|
+
if (Test-CommandExists 'python') {
|
|
533
|
+
$pyVer = & python --version 2>&1
|
|
534
|
+
# Microsoft Store stub returns just "Python" with no version number
|
|
535
|
+
if ($pyVer -match '\d+\.\d+') { $pythonUsable = $true }
|
|
536
|
+
}
|
|
537
|
+
if (-not $pythonUsable) {
|
|
538
|
+
Write-Host " Python not found. Installing via winget..."
|
|
539
|
+
try {
|
|
540
|
+
& winget install --id Python.Python.3.12 --accept-package-agreements --accept-source-agreements
|
|
541
|
+
$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('PATH', 'User')
|
|
542
|
+
Write-Detail "Python installed"
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
Write-Warn "Failed to install Python: $_"
|
|
546
|
+
Write-Host " Install manually: winget install Python.Python.3.12"
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
Write-Host " Installing websockify via pip..."
|
|
551
|
+
try {
|
|
552
|
+
if (Test-CommandExists 'pip') {
|
|
553
|
+
& pip install websockify 2>$null
|
|
554
|
+
Write-Detail "websockify installed"
|
|
555
|
+
}
|
|
556
|
+
elseif (Test-CommandExists 'pip3') {
|
|
557
|
+
& pip3 install websockify 2>$null
|
|
558
|
+
Write-Detail "websockify installed"
|
|
559
|
+
}
|
|
560
|
+
elseif (Test-CommandExists 'python') {
|
|
561
|
+
& python -m pip install websockify 2>$null
|
|
562
|
+
Write-Detail "websockify installed (via python -m pip)"
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
Write-Warn "pip not found. Install Python first, then: pip install websockify"
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
Write-Warn "Failed to install websockify: $_"
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
# Step 8 (optional): Cloudflare Tunnel
|
|
574
|
+
if ($SetupTunnel) {
|
|
575
|
+
$currentStep = $totalSteps - 1
|
|
576
|
+
Write-Step $currentStep $totalSteps "Setting up Cloudflare Tunnel..."
|
|
577
|
+
|
|
578
|
+
if (-not $HqUrl -or -not $ApiToken) {
|
|
579
|
+
Write-Error "--setup-tunnel requires --hq-url and --api-token"
|
|
580
|
+
exit 1
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
# Install cloudflared
|
|
584
|
+
if (-not (Test-CommandExists 'cloudflared')) {
|
|
585
|
+
Write-Host " Installing cloudflared..."
|
|
586
|
+
if (Test-CommandExists 'winget') {
|
|
587
|
+
& winget install --id Cloudflare.cloudflared --accept-package-agreements --accept-source-agreements
|
|
588
|
+
$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('PATH', 'User')
|
|
589
|
+
}
|
|
590
|
+
elseif (Test-CommandExists 'choco') {
|
|
591
|
+
& choco install cloudflared -y
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
Write-Host " Downloading cloudflared..."
|
|
595
|
+
$cfUrl = 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe'
|
|
596
|
+
$cfPath = Join-Path $DataDir 'cloudflared.exe'
|
|
597
|
+
Invoke-WebRequest -Uri $cfUrl -OutFile $cfPath
|
|
598
|
+
Write-Detail "cloudflared downloaded to $cfPath"
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
Write-Detail "cloudflared already installed"
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
# Fetch tunnel config from HQ
|
|
606
|
+
Write-Host " Fetching tunnel configuration from HQ..."
|
|
607
|
+
try {
|
|
608
|
+
$headers = @{ 'Authorization' = "Bearer $ApiToken" }
|
|
609
|
+
$tunnelData = Invoke-RestMethod -Uri "$HqUrl/api/minion/tunnel-credentials" -Headers $headers
|
|
610
|
+
|
|
611
|
+
if ($tunnelData.tunnel_id) {
|
|
612
|
+
$cfConfigDir = Join-Path $env:USERPROFILE '.cloudflared'
|
|
613
|
+
New-Item -Path $cfConfigDir -ItemType Directory -Force | Out-Null
|
|
614
|
+
|
|
615
|
+
# Save credentials
|
|
616
|
+
$tunnelData.credentials_json | Set-Content (Join-Path $cfConfigDir "$($tunnelData.tunnel_id).json") -Encoding UTF8
|
|
617
|
+
Write-Detail "Tunnel credentials saved"
|
|
618
|
+
|
|
619
|
+
# Save config
|
|
620
|
+
$tunnelData.config_yml | Set-Content (Join-Path $cfConfigDir 'config.yml') -Encoding UTF8
|
|
621
|
+
Write-Detail "Tunnel config saved (will run as user process on start)"
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
Write-Warn "Tunnel not configured for this minion"
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
Write-Warn "Failed to fetch tunnel credentials: $_"
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
# Deploy bundled skills
|
|
633
|
+
Write-Host ""
|
|
634
|
+
Write-Host "Deploying bundled assets..."
|
|
635
|
+
$claudeSkillsDir = Join-Path $env:USERPROFILE '.claude\skills'
|
|
636
|
+
$bundledSkillsDir = Join-Path $minionPkgDir 'skills'
|
|
637
|
+
if (Test-Path $bundledSkillsDir) {
|
|
638
|
+
New-Item -Path $claudeSkillsDir -ItemType Directory -Force | Out-Null
|
|
639
|
+
Get-ChildItem -Path $bundledSkillsDir -Directory | ForEach-Object {
|
|
640
|
+
Copy-Item $_.FullName -Destination (Join-Path $claudeSkillsDir $_.Name) -Recurse -Force
|
|
641
|
+
Write-Detail "Deployed skill: $($_.Name)"
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
# Deploy bundled rules
|
|
646
|
+
$claudeRulesDir = Join-Path $env:USERPROFILE '.claude\rules'
|
|
647
|
+
$bundledRulesDir = Join-Path $minionPkgDir 'rules'
|
|
648
|
+
if (Test-Path $bundledRulesDir) {
|
|
649
|
+
New-Item -Path $claudeRulesDir -ItemType Directory -Force | Out-Null
|
|
650
|
+
$coreRule = Join-Path $bundledRulesDir 'core.md'
|
|
651
|
+
if (Test-Path $coreRule) {
|
|
652
|
+
Copy-Item $coreRule -Destination (Join-Path $claudeRulesDir 'core.md') -Force
|
|
653
|
+
Write-Detail "Deployed rules: core.md"
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
# Start (or restart) agent so updated .env takes effect
|
|
658
|
+
Write-Step $totalSteps $totalSteps "Starting agent..."
|
|
659
|
+
Restart-MinionProcess
|
|
660
|
+
|
|
661
|
+
# Health check
|
|
662
|
+
if (Invoke-HealthCheck) {
|
|
663
|
+
Write-Detail "Health check passed"
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
Write-Warn "Health check failed after 5 attempts"
|
|
667
|
+
Write-Host " Check logs at: $LogDir"
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
# Firewall notice
|
|
671
|
+
Write-Host ""
|
|
672
|
+
Write-Host "NOTE: Firewall rules are NOT configured automatically (requires admin)." -ForegroundColor Yellow
|
|
673
|
+
Write-Host " If you need LAN access without Cloudflare Tunnel, ask an administrator to run:" -ForegroundColor Yellow
|
|
674
|
+
Write-Host " New-NetFirewallRule -DisplayName 'Minion Agent' -Direction Inbound -Protocol TCP -LocalPort 8080 -Action Allow" -ForegroundColor Gray
|
|
675
|
+
Write-Host " New-NetFirewallRule -DisplayName 'Minion Terminal' -Direction Inbound -Protocol TCP -LocalPort 7681 -Action Allow" -ForegroundColor Gray
|
|
676
|
+
Write-Host " New-NetFirewallRule -DisplayName 'Minion VNC' -Direction Inbound -Protocol TCP -LocalPort 6080 -Action Allow" -ForegroundColor Gray
|
|
677
|
+
Write-Host " Or use Cloudflare Tunnel (--setup-tunnel) to avoid firewall configuration." -ForegroundColor Yellow
|
|
678
|
+
|
|
679
|
+
# Notify HQ
|
|
680
|
+
if ($HqUrl -and $ApiToken) {
|
|
681
|
+
Write-Host ""
|
|
682
|
+
Write-Host "Notifying HQ of setup completion..."
|
|
683
|
+
try {
|
|
684
|
+
$lanIp = Get-LanIPAddress
|
|
685
|
+
$headers = @{
|
|
686
|
+
'Authorization' = "Bearer $ApiToken"
|
|
687
|
+
'Content-Type' = 'application/json'
|
|
688
|
+
}
|
|
689
|
+
$bodyHash = @{}
|
|
690
|
+
if ($lanIp) {
|
|
691
|
+
$bodyHash['ip_address'] = $lanIp
|
|
692
|
+
$bodyHash['internal_ip_address'] = $lanIp
|
|
693
|
+
} else {
|
|
694
|
+
$bodyHash['internal_ip_address'] = [System.Net.Dns]::GetHostName()
|
|
695
|
+
}
|
|
696
|
+
$body = $bodyHash | ConvertTo-Json
|
|
697
|
+
Invoke-RestMethod -Uri "$HqUrl/api/minion/setup-complete" -Method Post -Headers $headers -Body $body -ErrorAction Stop | Out-Null
|
|
698
|
+
if ($lanIp) {
|
|
699
|
+
Write-Detail "HQ notified successfully (LAN IP: $lanIp)"
|
|
700
|
+
} else {
|
|
701
|
+
Write-Detail "HQ notified successfully (LAN IP detection failed, hostname sent)"
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
Write-Detail "HQ notification skipped (HQ may not be reachable)"
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
Write-Host ""
|
|
710
|
+
Write-Host "=========================================" -ForegroundColor Green
|
|
711
|
+
Write-Host " Setup Complete!" -ForegroundColor Green
|
|
712
|
+
Write-Host "=========================================" -ForegroundColor Green
|
|
713
|
+
Write-Host ""
|
|
714
|
+
Write-Host "Useful commands:"
|
|
715
|
+
Write-Host " minion-cli-win status # Agent status"
|
|
716
|
+
Write-Host " minion-cli-win health # Health check"
|
|
717
|
+
Write-Host " minion-cli-win restart # Restart agent"
|
|
718
|
+
Write-Host " minion-cli-win stop # Stop agent"
|
|
719
|
+
Write-Host " Get-Content $(Join-Path $LogDir 'service-stdout.log') -Tail 50 # View logs"
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
# ============================================================
|
|
723
|
+
# Reconfigure
|
|
724
|
+
# ============================================================
|
|
725
|
+
|
|
726
|
+
function Invoke-Reconfigure {
|
|
727
|
+
if (-not $HqUrl -or -not $MinionId -or -not $ApiToken) {
|
|
728
|
+
Write-Error "All three parameters are required: --hq-url, --minion-id, --api-token"
|
|
729
|
+
exit 1
|
|
730
|
+
}
|
|
731
|
+
if (-not (Test-Path $EnvFile)) {
|
|
732
|
+
Write-Error "$EnvFile not found. Run 'setup' first."
|
|
733
|
+
exit 1
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
Write-Host "=========================================" -ForegroundColor Cyan
|
|
737
|
+
Write-Host " @geekbeer/minion Reconfigure" -ForegroundColor Cyan
|
|
738
|
+
Write-Host "=========================================" -ForegroundColor Cyan
|
|
739
|
+
Write-Host "HQ: $HqUrl"
|
|
740
|
+
Write-Host "Minion ID: $MinionId"
|
|
741
|
+
Write-Host ""
|
|
742
|
+
|
|
743
|
+
# Step 1: Update .env
|
|
744
|
+
Write-Step 1 4 "Updating .env credentials..."
|
|
745
|
+
$existing = Read-EnvFile $EnvFile
|
|
746
|
+
$existing['HQ_URL'] = $HqUrl
|
|
747
|
+
$existing['API_TOKEN'] = $ApiToken
|
|
748
|
+
$existing['MINION_ID'] = $MinionId
|
|
749
|
+
Write-EnvFile $EnvFile $existing
|
|
750
|
+
Write-Detail "$EnvFile updated"
|
|
751
|
+
|
|
752
|
+
# Step 2: Restart agent process
|
|
753
|
+
Write-Step 2 4 "Restarting minion-agent..."
|
|
754
|
+
Restart-MinionProcess
|
|
755
|
+
|
|
756
|
+
# Step 3: Health check
|
|
757
|
+
Write-Step 3 4 "Verifying agent health..."
|
|
758
|
+
if (Invoke-HealthCheck) {
|
|
759
|
+
Write-Detail "Agent is healthy"
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
Write-Warn "Agent health check failed after 5 attempts"
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
# Step 4: Notify HQ
|
|
766
|
+
Write-Step 4 4 "Notifying HQ..."
|
|
767
|
+
try {
|
|
768
|
+
$lanIp = Get-LanIPAddress
|
|
769
|
+
$headers = @{
|
|
770
|
+
'Authorization' = "Bearer $ApiToken"
|
|
771
|
+
'Content-Type' = 'application/json'
|
|
772
|
+
}
|
|
773
|
+
$bodyHash = @{}
|
|
774
|
+
if ($lanIp) {
|
|
775
|
+
$bodyHash['ip_address'] = $lanIp
|
|
776
|
+
$bodyHash['internal_ip_address'] = $lanIp
|
|
777
|
+
} else {
|
|
778
|
+
$bodyHash['internal_ip_address'] = [System.Net.Dns]::GetHostName()
|
|
779
|
+
}
|
|
780
|
+
$body = $bodyHash | ConvertTo-Json
|
|
781
|
+
Invoke-RestMethod -Uri "$HqUrl/api/minion/setup-complete" -Method Post -Headers $headers -Body $body -ErrorAction Stop | Out-Null
|
|
782
|
+
if ($lanIp) {
|
|
783
|
+
Write-Detail "HQ notified successfully (LAN IP: $lanIp)"
|
|
784
|
+
} else {
|
|
785
|
+
Write-Detail "HQ notified successfully (LAN IP detection failed, hostname sent)"
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
Write-Detail "Skipped (heartbeat will notify HQ within 30s)"
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
Write-Host ""
|
|
793
|
+
Write-Host "=========================================" -ForegroundColor Green
|
|
794
|
+
Write-Host " Reconfigure Complete!" -ForegroundColor Green
|
|
795
|
+
Write-Host "=========================================" -ForegroundColor Green
|
|
796
|
+
Write-Host "The minion should appear online in HQ shortly."
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
# ============================================================
|
|
800
|
+
# Main command dispatch
|
|
801
|
+
# ============================================================
|
|
802
|
+
|
|
803
|
+
switch ($Command) {
|
|
804
|
+
'setup' {
|
|
805
|
+
Invoke-Setup
|
|
806
|
+
}
|
|
807
|
+
'reconfigure' {
|
|
808
|
+
Invoke-Reconfigure
|
|
809
|
+
}
|
|
810
|
+
'start' {
|
|
811
|
+
Start-MinionProcess
|
|
812
|
+
}
|
|
813
|
+
'stop' {
|
|
814
|
+
Stop-MinionProcess
|
|
815
|
+
}
|
|
816
|
+
'restart' {
|
|
817
|
+
Restart-MinionProcess
|
|
818
|
+
}
|
|
819
|
+
'status' {
|
|
820
|
+
try {
|
|
821
|
+
$response = Invoke-RestMethod -Uri "$AgentUrl/api/status" -TimeoutSec 5
|
|
822
|
+
$response | ConvertTo-Json -Depth 5
|
|
823
|
+
}
|
|
824
|
+
catch {
|
|
825
|
+
Write-Error "Failed to get status. Is the agent running?"
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
'health' {
|
|
829
|
+
try {
|
|
830
|
+
$response = Invoke-RestMethod -Uri "$AgentUrl/api/health" -TimeoutSec 5
|
|
831
|
+
$response | ConvertTo-Json -Depth 5
|
|
832
|
+
}
|
|
833
|
+
catch {
|
|
834
|
+
Write-Error "Health check failed. Is the agent running?"
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
'version' {
|
|
838
|
+
Write-Host "@geekbeer/minion v$CliVersion (Windows)"
|
|
839
|
+
}
|
|
840
|
+
'help' {
|
|
841
|
+
Write-Host "Minion Agent CLI (@geekbeer/minion) v$CliVersion (Windows)" -ForegroundColor Cyan
|
|
842
|
+
Write-Host ""
|
|
843
|
+
Write-Host "Usage (no admin required):"
|
|
844
|
+
Write-Host " minion-cli-win setup [options] # Set up agent (auto-start on login)"
|
|
845
|
+
Write-Host " minion-cli-win reconfigure [options] # Re-register with new HQ credentials"
|
|
846
|
+
Write-Host " minion-cli-win start # Start agent process"
|
|
847
|
+
Write-Host " minion-cli-win stop # Stop agent process"
|
|
848
|
+
Write-Host " minion-cli-win restart # Restart agent process"
|
|
849
|
+
Write-Host " minion-cli-win status # Get current status"
|
|
850
|
+
Write-Host " minion-cli-win health # Health check"
|
|
851
|
+
Write-Host " minion-cli-win version # Show version"
|
|
852
|
+
Write-Host ""
|
|
853
|
+
Write-Host "Setup options:"
|
|
854
|
+
Write-Host " --hq-url <URL> HQ server URL (optional, omit for standalone mode)"
|
|
855
|
+
Write-Host " --minion-id <UUID> Minion ID (optional)"
|
|
856
|
+
Write-Host " --api-token <TOKEN> API token (optional)"
|
|
857
|
+
Write-Host " --setup-tunnel Set up cloudflared tunnel (requires --hq-url and --api-token)"
|
|
858
|
+
Write-Host ""
|
|
859
|
+
Write-Host "Reconfigure options:"
|
|
860
|
+
Write-Host " --hq-url <URL> HQ server URL (required)"
|
|
861
|
+
Write-Host " --minion-id <UUID> Minion ID (required)"
|
|
862
|
+
Write-Host " --api-token <TOKEN> API token (required)"
|
|
863
|
+
Write-Host ""
|
|
864
|
+
Write-Host "Data directory: $DataDir"
|
|
865
|
+
Write-Host ""
|
|
866
|
+
Write-Host "Environment:"
|
|
867
|
+
Write-Host " MINION_AGENT_URL Agent URL (default: http://localhost:8080)"
|
|
868
|
+
}
|
|
869
|
+
}
|