-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathstart.ps1
More file actions
210 lines (191 loc) · 9.03 KB
/
start.ps1
File metadata and controls
210 lines (191 loc) · 9.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
<#
.SYNOPSIS
Native Windows launcher for Hermes WebUI - PowerShell equivalent
of start.sh, bypassing bootstrap.py's platform refusal.
.DESCRIPTION
Mirrors start.sh's discovery: load optional .env, find Python,
locate the hermes-agent install, set sensible env defaults, then
invoke server.py directly. The bootstrap.py path is skipped
because it currently raises on platform.system() == 'Windows';
server.py itself runs cleanly on native Windows.
Assumes Python + hermes-agent + the WebUI Python deps are already
installed natively on Windows - same assumption start.sh makes
when invoked outside a fresh bootstrap. For first-time setup, the
native Windows path is to install Python 3.11+, then create a
Windows venv (`python -m venv venv`) and `pip install -r
requirements.txt` from the hermes-agent root in PowerShell - this
script then finds `venv\Scripts\python.exe` automatically. A venv
created inside WSL2 is a Linux virtual environment (`venv/bin/python`)
and cannot be used by native Windows Python, so the bootstrap.py-
inside-WSL2 path produces a venv `start.ps1` can't invoke.
.PARAMETER Port
TCP port the WebUI binds to. Overrides HERMES_WEBUI_PORT env.
Default: 8787.
.PARAMETER BindHost
Bind address. Overrides HERMES_WEBUI_HOST env.
Default: 127.0.0.1.
.EXAMPLE
.\start.ps1
# Bind to 127.0.0.1:8787, foreground.
.EXAMPLE
.\start.ps1 -Port 9000
# Bind to 127.0.0.1:9000.
.EXAMPLE
$env:HERMES_WEBUI_HOST = '0.0.0.0'
.\start.ps1
# Bind to all interfaces (set a password first via env or Settings).
.LINK
https://github.com/nesquena/hermes-webui/issues/1952
#>
[CmdletBinding()]
param(
[int]$Port = 0,
[string]$BindHost = ''
)
$ErrorActionPreference = 'Stop'
$RepoRoot = Split-Path -Parent $PSCommandPath
# === Load .env (mirroring start.sh's filtering) ========================
$envFile = Join-Path $RepoRoot '.env'
if (Test-Path $envFile) {
foreach ($line in Get-Content $envFile -Encoding UTF8) {
$trimmed = $line.Trim()
if (-not $trimmed -or $trimmed.StartsWith('#') -or -not $trimmed.Contains('=')) { continue }
$kv = $trimmed -split '=', 2
$key = ($kv[0].Trim() -replace '^export\s+', '')
# Filter out shell-readonly vars (UID, GID, EUID, EGID, PPID) per start.sh
if ($key -in @('UID', 'GID', 'EUID', 'EGID', 'PPID')) { continue }
if ($key -notmatch '^[A-Za-z_][A-Za-z0-9_]*$') { continue }
# Explicit $null check — an env var explicitly set to '' should still
# be considered "set" and NOT overridden by .env (empty string is
# falsey in PowerShell, so a plain truthy check would mis-skip).
if ($null -ne [Environment]::GetEnvironmentVariable($key)) { continue }
$val = $kv[1]
if ($val -match '^"(.*)"$') { $val = $Matches[1] }
elseif ($val -match "^'(.*)'$") { $val = $Matches[1] }
[Environment]::SetEnvironmentVariable($key, $val)
}
}
# === Find Python (matches start.sh order) ==============================
$Python = $env:HERMES_WEBUI_PYTHON
if (-not $Python) {
foreach ($candidate in @('python3', 'python', 'py')) {
$cmd = Get-Command $candidate -ErrorAction SilentlyContinue
if ($cmd) { $Python = $cmd.Source; break }
}
}
if (-not $Python) {
Write-Error 'Python 3 is required to run server.py (set HERMES_WEBUI_PYTHON or add python to PATH).'
exit 1
}
# === Find Hermes Agent dir (server.py imports from it) =================
# When HERMES_WEBUI_AGENT_DIR is set we still validate it on disk —
# an explicit override pointing at a missing dir should fail FAST
# with a clear message, not silently progress into a python3 launch
# that's about to crash on missing imports. Smoke-test feedback on
# PR #2783: nesquena/hermes-webui requested this guard.
$AgentDir = $env:HERMES_WEBUI_AGENT_DIR
if ($AgentDir -and -not (Test-Path (Join-Path $AgentDir 'hermes_cli') -PathType Container)) {
Write-Error "HERMES_WEBUI_AGENT_DIR is set to '$AgentDir' but no hermes_cli/ folder exists there. Unset the variable to fall back to auto-discovery, or fix the path."
exit 1
}
if (-not $AgentDir) {
# Build candidate list incrementally — ${env:ProgramFiles(x86)} is null on
# 32-bit Windows and in some constrained environments, and Join-Path throws
# on a null Path. Skip any system-wide root that isn't set so the launcher
# stays robust across Windows variants. USERPROFILE is always set so it
# stays unguarded; the dev-checkout sibling is path-derived, not env-based.
$candidates = @()
$candidates += (Join-Path $env:USERPROFILE '.hermes\hermes-agent')
foreach ($root in @($env:LOCALAPPDATA, ${env:ProgramW6432}, ${env:ProgramFiles}, ${env:ProgramFiles(x86)})) {
if ($root) { $candidates += (Join-Path $root 'hermes\hermes-agent') }
}
$candidates += (Join-Path (Split-Path -Parent $RepoRoot) 'hermes-agent')
# De-dup: when running in a WOW64 (32-bit-on-64-bit) PowerShell process,
# $env:ProgramFiles is redirected to C:\Program Files (x86), so without
# $env:ProgramW6432 (the canonical 64-bit override) we'd miss the real
# C:\Program Files\hermes\hermes-agent AND duplicate the x86 entry.
# Select-Object -Unique collapses any collisions regardless of cause.
$candidates = $candidates | Select-Object -Unique
foreach ($c in $candidates) {
if (Test-Path (Join-Path $c 'hermes_cli') -PathType Container) { $AgentDir = $c; break }
}
}
if (-not $AgentDir) {
$searched = $candidates -join ', '
Write-Error "hermes-agent not found. Searched: $searched. Set HERMES_WEBUI_AGENT_DIR explicitly to override."
exit 1
}
# === Prefer the agent's venv Python if available =======================
$agentVenvPython = Join-Path $AgentDir 'venv\Scripts\python.exe'
if (Test-Path $agentVenvPython) {
$Python = $agentVenvPython
}
# === Resolve bind + state defaults =====================================
$BindHostFinal = if ($BindHost) { $BindHost } elseif ($env:HERMES_WEBUI_HOST) { $env:HERMES_WEBUI_HOST } else { '127.0.0.1' }
$PortFinal = if ($Port) {
$Port
} elseif ($env:HERMES_WEBUI_PORT) {
# TryParse + range guard on the env var. A plain [int] cast on the
# env var throws InvalidCastException with no actionable context when
# the env var is set to a non-integer (typo, accidental shell
# expansion, etc.) — surface a targeted error message instead.
$parsedPort = 0
if (-not [int]::TryParse($env:HERMES_WEBUI_PORT, [ref]$parsedPort)) {
Write-Error "HERMES_WEBUI_PORT='$($env:HERMES_WEBUI_PORT)' is not a valid integer port. Unset the variable to use the default (8787), or set it to a number 1-65535."
exit 1
}
if ($parsedPort -lt 1 -or $parsedPort -gt 65535) {
Write-Error "HERMES_WEBUI_PORT=$parsedPort is out of TCP-port range. Must be 1-65535."
exit 1
}
$parsedPort
} else {
8787
}
$env:HERMES_WEBUI_HOST = $BindHostFinal
$env:HERMES_WEBUI_PORT = "$PortFinal"
if (-not $env:HERMES_HOME) {
if ($env:LOCALAPPDATA) {
$env:HERMES_HOME = Join-Path $env:LOCALAPPDATA 'hermes'
} else {
$env:HERMES_HOME = Join-Path $env:USERPROFILE '.hermes'
}
}
if (-not $env:HERMES_WEBUI_STATE_DIR) {
$env:HERMES_WEBUI_STATE_DIR = Join-Path $env:HERMES_HOME 'webui'
}
# === Ensure dirs exist =================================================
New-Item -ItemType Directory -Force -Path $env:HERMES_HOME | Out-Null
New-Item -ItemType Directory -Force -Path $env:HERMES_WEBUI_STATE_DIR | Out-Null
# === Launch (foreground, matches start.sh) =============================
Write-Host "[start.ps1] Hermes WebUI native Windows launcher" -ForegroundColor Cyan
Write-Host "[start.ps1] Python: $Python"
Write-Host "[start.ps1] Agent dir: $AgentDir"
Write-Host "[start.ps1] State dir: $env:HERMES_WEBUI_STATE_DIR"
Write-Host "[start.ps1] Binding: ${BindHostFinal}:${PortFinal}"
Write-Host ""
$serverPath = Join-Path $RepoRoot 'server.py'
if (-not (Test-Path $serverPath)) {
Write-Error "server.py not found at $serverPath - is this the hermes-webui repo root?"
exit 1
}
# Capture exit code, let finally{} run Pop-Location, exit AFTER the try.
# Plain `exit $LASTEXITCODE` inside the try block can prevent the finally
# from running in some termination paths (especially when dot-sourced or
# in interactive sessions), leaving the caller's working directory stuck
# at $RepoRoot.
$script:serverExitCode = 0
Push-Location $RepoRoot
try {
# @args was non-functional here — PowerShell does NOT populate $args when the
# script declares [CmdletBinding()] with an explicit param() block (Copilot's
# finding on PR #2807). Dropped rather than added a ValueFromRemainingArguments
# parameter, because the existing tracked use case is the launcher running
# server.py with the env-var-driven config — no pass-through args are needed.
# If pass-through becomes a requirement later, add a [Parameter(ValueFromRemainingArguments=$true)] [string[]]$ServerArgs and splat that.
& $Python $serverPath
$script:serverExitCode = $LASTEXITCODE
} finally {
Pop-Location
}
exit $script:serverExitCode