diff --git a/README.md b/README.md index f0074e2b..d82ecf3c 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,13 @@ The bootstrap will: > Native Windows is not supported for this bootstrap yet. Use Linux, macOS, or WSL2. > For Windows / WSL auto-start at login, see [`docs/wsl-autostart.md`](docs/wsl-autostart.md). -> A community-maintained native Windows guide is tracked in [#1952](https://github.com/nesquena/hermes-webui/issues/1952). + +A community-maintained native Windows setup is documented at [@markwang2658/hermes-windows-native-guide](https://github.com/markwang2658/hermes-windows-native-guide) (companion setup repo: [@markwang2658/hermes-windows-native](https://github.com/markwang2658/hermes-windows-native)). Notes from the community report in [#1952](https://github.com/nesquena/hermes-webui/issues/1952): + +- **Memory:** community-measured ~330 MB native vs ~1080 MB with WSL2+Docker (varies by configuration). +- **What works:** chat, workspace browser, session management, all themes. +- **Known limitations:** some POSIX-style file paths surface in the workspace browser; bash-assuming agent tools may not work natively. +- **WSL2 relationship:** WSL2 is recommended *once* for first-time venv creation (since `bootstrap.py` currently refuses on native Windows). After the venv exists, `start.ps1` at the repo root runs the WebUI natively by invoking `server.py` directly — no WSL2 needed for day-to-day use. If provider setup is still incomplete after install, the onboarding wizard will point you to finish it with `hermes model` instead of trying to replicate the full CLI setup in-browser. For a step-by-step walkthrough of the wizard, provider choices, local model server Base URLs, and safe re-runs, see [`docs/onboarding.md`](docs/onboarding.md). diff --git a/start.ps1 b/start.ps1 new file mode 100644 index 00000000..191db6c1 --- /dev/null +++ b/start.ps1 @@ -0,0 +1,156 @@ +<# +.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 (Split-Path -Parent $RepoRoot) 'hermes-agent') + ) + foreach ($c in $candidates) { + if (Test-Path (Join-Path $c 'hermes_cli')) { $AgentDir = $c; break } + } +} +if (-not $AgentDir) { + $expectedPrimary = Join-Path $env:USERPROFILE '.hermes\hermes-agent' + $expectedSibling = Join-Path (Split-Path -Parent $RepoRoot) 'hermes-agent' + Write-Error "hermes-agent not found at $expectedPrimary or $expectedSibling. Set HERMES_WEBUI_AGENT_DIR explicitly." + 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 +}