Files
hermes-webui/start.ps1
T
Dustin 6822cbbbb5 feat(start.ps1): expand hermes-agent candidate paths for Windows installers
Maintainer follow-up on PR #2783: the auto-discovery list only checked
`%USERPROFILE%\.hermes\hermes-agent` and `../hermes-agent`, but the
official Windows installer puts hermes-agent under
`%LOCALAPPDATA%\hermes\hermes-agent`, and MSI installs commonly land in
`Program Files`. Users hitting `start.ps1` from a fresh clone of an
installer-deployed agent currently have to set `HERMES_WEBUI_AGENT_DIR`
manually — which is the exact friction the PR was trying to remove.

Adds three new candidate paths to the discovery list:

- `%LOCALAPPDATA%\hermes\hermes-agent`   (official Windows installer)
- `%PROGRAMFILES%\hermes\hermes-agent`   (MSI to Program Files)
- `%PROGRAMFILES(X86)%\hermes\hermes-agent`  (MSI to x86 path)

Order matters: existing `%USERPROFILE%\.hermes\hermes-agent` stays
first (user-local installs win), then the three new system-wide paths,
then the sibling-of-RepoRoot `../hermes-agent` developer-checkout case
stays last. Same `Test-Path (Join-Path \$c 'hermes_cli')` validation
applies to every candidate.

Also restructures the not-found error message to enumerate every
searched path via `\$candidates -join ', '` — when discovery fails the
user now sees the exact list checked, so it's obvious where to drop
the agent or which `HERMES_WEBUI_AGENT_DIR` value to set.

Stacked on top of #2783. Once #2783 lands, this rebases cleanly onto
master.
2026-05-23 15:36:21 -05:00

159 lines
6.0 KiB
PowerShell

<#
.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 - same assumption start.sh makes when invoked outside
a fresh bootstrap. For first-time setup, run bootstrap.py inside
WSL2 once to create the venv, then this script can use that venv.
.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'))) {
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) {
$candidates = @(
(Join-Path $env:USERPROFILE '.hermes\hermes-agent'),
(Join-Path $env:LOCALAPPDATA 'hermes\hermes-agent'),
(Join-Path ${env:ProgramFiles} 'hermes\hermes-agent'),
(Join-Path ${env:ProgramFiles(x86)} 'hermes\hermes-agent'),
(Join-Path (Split-Path -Parent $RepoRoot) 'hermes-agent')
)
foreach ($c in $candidates) {
if (Test-Path (Join-Path $c 'hermes_cli')) { $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) { [int]$env:HERMES_WEBUI_PORT } else { 8787 }
$env:HERMES_WEBUI_HOST = $BindHostFinal
$env:HERMES_WEBUI_PORT = "$PortFinal"
if (-not $env:HERMES_WEBUI_STATE_DIR) {
$env:HERMES_WEBUI_STATE_DIR = Join-Path $env:USERPROFILE '.hermes\webui'
}
if (-not $env:HERMES_HOME) {
$env:HERMES_HOME = Join-Path $env:USERPROFILE '.hermes'
}
# === 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
}
Push-Location $RepoRoot
try {
& $Python $serverPath @args
exit $LASTEXITCODE
} finally {
Pop-Location
}