mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-09 03:11:58 +00:00
User hit a real failure case: their system Git was in a half-installed state
(can neither uninstall nor reinstall) and winget refused to work around it.
We were one step away from shipping an installer that would have left users
with exactly the problem he already had.
What other agents do (reality check):
- Claude Code: requires pre-installed Git; breaks if user doesn't have it.
- OpenCode, Codex: don't need bash at all — PowerShell-first design.
- Cline: uses whatever shell VSCode is configured with; installs nothing.
None of them solve the "broken system Git" problem. We need to own our Git.
Changes:
- scripts/install.ps1::Install-Git: dropped winget path entirely. Now:
(1) use existing git if present; (2) download portable MinGit from the
official git-for-windows GitHub release to %LOCALAPPDATA%\hermes\git.
No winget, no admin, no Windows installer registry, no system impact.
- Added %LOCALAPPDATA%\hermes\git\{cmd,usr\bin} to User PATH so git + bash
+ POSIX coreutils (which, env, grep, …) resolve in fresh shells.
- tools/environments/local.py::_find_bash: reorder so Hermes' portable
MinGit install is checked BEFORE falling through to shutil.which("bash")
or system install locations. This way a broken system Git can't
hijack the bash lookup.
- README + installation docs reworded to reflect the new story: "portable
Git Bash, isolated from any system install, recoverable via rm -rf if it
ever breaks."
Recoverability: if Hermes' Git install ever breaks, ``Remove-Item %LOCALAPPDATA%\hermes\git``
and re-run the installer — no system impact, no uninstall drama, no winget
to fight with.
1069 lines
42 KiB
PowerShell
1069 lines
42 KiB
PowerShell
# ============================================================================
|
|
# Hermes Agent Installer for Windows
|
|
# ============================================================================
|
|
# Installation script for Windows (PowerShell).
|
|
# Uses uv for fast Python provisioning and package management.
|
|
#
|
|
# Usage:
|
|
# irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1 | iex
|
|
#
|
|
# Or download and run with options:
|
|
# .\install.ps1 -NoVenv -SkipSetup
|
|
#
|
|
# ============================================================================
|
|
|
|
param(
|
|
[switch]$NoVenv,
|
|
[switch]$SkipSetup,
|
|
[string]$Branch = "main",
|
|
[string]$HermesHome = "$env:LOCALAPPDATA\hermes",
|
|
[string]$InstallDir = "$env:LOCALAPPDATA\hermes\hermes-agent"
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
# ============================================================================
|
|
# Configuration
|
|
# ============================================================================
|
|
|
|
$RepoUrlSsh = "git@github.com:NousResearch/hermes-agent.git"
|
|
$RepoUrlHttps = "https://github.com/NousResearch/hermes-agent.git"
|
|
$PythonVersion = "3.11"
|
|
$NodeVersion = "22"
|
|
|
|
# ============================================================================
|
|
# Helper functions
|
|
# ============================================================================
|
|
|
|
function Write-Banner {
|
|
Write-Host ""
|
|
Write-Host "┌─────────────────────────────────────────────────────────┐" -ForegroundColor Magenta
|
|
Write-Host "│ ⚕ Hermes Agent Installer │" -ForegroundColor Magenta
|
|
Write-Host "├─────────────────────────────────────────────────────────┤" -ForegroundColor Magenta
|
|
Write-Host "│ An open source AI agent by Nous Research. │" -ForegroundColor Magenta
|
|
Write-Host "└─────────────────────────────────────────────────────────┘" -ForegroundColor Magenta
|
|
Write-Host ""
|
|
}
|
|
|
|
function Write-Info {
|
|
param([string]$Message)
|
|
Write-Host "→ $Message" -ForegroundColor Cyan
|
|
}
|
|
|
|
function Write-Success {
|
|
param([string]$Message)
|
|
Write-Host "✓ $Message" -ForegroundColor Green
|
|
}
|
|
|
|
function Write-Warn {
|
|
param([string]$Message)
|
|
Write-Host "⚠ $Message" -ForegroundColor Yellow
|
|
}
|
|
|
|
function Write-Err {
|
|
param([string]$Message)
|
|
Write-Host "✗ $Message" -ForegroundColor Red
|
|
}
|
|
|
|
# ============================================================================
|
|
# Dependency checks
|
|
# ============================================================================
|
|
|
|
function Install-Uv {
|
|
Write-Info "Checking for uv package manager..."
|
|
|
|
# Check if uv is already available
|
|
if (Get-Command uv -ErrorAction SilentlyContinue) {
|
|
$version = uv --version
|
|
$script:UvCmd = "uv"
|
|
Write-Success "uv found ($version)"
|
|
return $true
|
|
}
|
|
|
|
# Check common install locations
|
|
$uvPaths = @(
|
|
"$env:USERPROFILE\.local\bin\uv.exe",
|
|
"$env:USERPROFILE\.cargo\bin\uv.exe"
|
|
)
|
|
foreach ($uvPath in $uvPaths) {
|
|
if (Test-Path $uvPath) {
|
|
$script:UvCmd = $uvPath
|
|
$version = & $uvPath --version
|
|
Write-Success "uv found at $uvPath ($version)"
|
|
return $true
|
|
}
|
|
}
|
|
|
|
# Install uv
|
|
Write-Info "Installing uv (fast Python package manager)..."
|
|
try {
|
|
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" 2>&1 | Out-Null
|
|
|
|
# Find the installed binary
|
|
$uvExe = "$env:USERPROFILE\.local\bin\uv.exe"
|
|
if (-not (Test-Path $uvExe)) {
|
|
$uvExe = "$env:USERPROFILE\.cargo\bin\uv.exe"
|
|
}
|
|
if (-not (Test-Path $uvExe)) {
|
|
# Refresh PATH and try again
|
|
$env:Path = [Environment]::GetEnvironmentVariable("Path", "User") + ";" + [Environment]::GetEnvironmentVariable("Path", "Machine")
|
|
if (Get-Command uv -ErrorAction SilentlyContinue) {
|
|
$uvExe = (Get-Command uv).Source
|
|
}
|
|
}
|
|
|
|
if (Test-Path $uvExe) {
|
|
$script:UvCmd = $uvExe
|
|
$version = & $uvExe --version
|
|
Write-Success "uv installed ($version)"
|
|
return $true
|
|
}
|
|
|
|
Write-Err "uv installed but not found on PATH"
|
|
Write-Info "Try restarting your terminal and re-running"
|
|
return $false
|
|
} catch {
|
|
Write-Err "Failed to install uv"
|
|
Write-Info "Install manually: https://docs.astral.sh/uv/getting-started/installation/"
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Test-Python {
|
|
Write-Info "Checking Python $PythonVersion..."
|
|
|
|
# Let uv find or install Python
|
|
try {
|
|
$pythonPath = & $UvCmd python find $PythonVersion 2>$null
|
|
if ($pythonPath) {
|
|
$ver = & $pythonPath --version 2>$null
|
|
Write-Success "Python found: $ver"
|
|
return $true
|
|
}
|
|
} catch { }
|
|
|
|
# Python not found — use uv to install it (no admin needed!)
|
|
Write-Info "Python $PythonVersion not found, installing via uv..."
|
|
try {
|
|
$uvOutput = & $UvCmd python install $PythonVersion 2>&1
|
|
if ($LASTEXITCODE -eq 0) {
|
|
$pythonPath = & $UvCmd python find $PythonVersion 2>$null
|
|
if ($pythonPath) {
|
|
$ver = & $pythonPath --version 2>$null
|
|
Write-Success "Python installed: $ver"
|
|
return $true
|
|
}
|
|
} else {
|
|
Write-Warn "uv python install output:"
|
|
Write-Host $uvOutput -ForegroundColor DarkGray
|
|
}
|
|
} catch {
|
|
Write-Warn "uv python install error: $_"
|
|
}
|
|
|
|
# Fallback: check if ANY Python 3.10+ is already available on the system
|
|
Write-Info "Trying to find any existing Python 3.10+..."
|
|
foreach ($fallbackVer in @("3.12", "3.13", "3.10")) {
|
|
try {
|
|
$pythonPath = & $UvCmd python find $fallbackVer 2>$null
|
|
if ($pythonPath) {
|
|
$ver = & $pythonPath --version 2>$null
|
|
Write-Success "Found fallback: $ver"
|
|
$script:PythonVersion = $fallbackVer
|
|
return $true
|
|
}
|
|
} catch { }
|
|
}
|
|
|
|
# Fallback: try system python
|
|
if (Get-Command python -ErrorAction SilentlyContinue) {
|
|
$sysVer = python --version 2>$null
|
|
if ($sysVer -match "3\.(1[0-9]|[1-9][0-9])") {
|
|
Write-Success "Using system Python: $sysVer"
|
|
return $true
|
|
}
|
|
}
|
|
|
|
Write-Err "Failed to install Python $PythonVersion"
|
|
Write-Info "Install Python 3.11 manually, then re-run this script:"
|
|
Write-Info " https://www.python.org/downloads/"
|
|
Write-Info " Or: winget install Python.Python.3.11"
|
|
return $false
|
|
}
|
|
|
|
function Install-Git {
|
|
<#
|
|
.SYNOPSIS
|
|
Ensure Git (and Git Bash) are installed. Git for Windows bundles bash.exe
|
|
which Hermes uses to run shell commands.
|
|
|
|
Priority order (deliberately simple — no winget, no registry, no system
|
|
package manager):
|
|
1. Existing ``git`` on PATH — use it as-is (the common fast path).
|
|
2. Download portable **MinGit** from the official git-for-windows GitHub
|
|
release and unpack it to ``%LOCALAPPDATA%\hermes\git`` — never touches
|
|
system Git, never requires admin, works even on locked-down machines
|
|
and machines with a broken system Git install.
|
|
|
|
We deliberately skip winget because it fails badly when the system Git
|
|
install is in a half-installed state (partially registered, or uninstall-
|
|
blocked). Owning the Hermes copy of Git ourselves is predictable and
|
|
recoverable: if it ever breaks, ``Remove-Item %LOCALAPPDATA%\hermes\git``
|
|
and re-running this installer fully recovers.
|
|
|
|
After install we locate ``bash.exe`` and persist the path in
|
|
``HERMES_GIT_BASH_PATH`` (User scope) so Hermes can find it in a fresh
|
|
shell without a second PATH refresh.
|
|
#>
|
|
Write-Info "Checking Git..."
|
|
|
|
if (Get-Command git -ErrorAction SilentlyContinue) {
|
|
$version = git --version
|
|
Write-Success "Git found ($version)"
|
|
Set-GitBashEnvVar
|
|
return $true
|
|
}
|
|
|
|
# Download portable MinGit into $HermesHome\git. This always works as
|
|
# long as we can reach github.com — no admin, no winget, no reliance on
|
|
# the user's possibly-broken system Git install.
|
|
Write-Info "Git not found — downloading portable MinGit to $HermesHome\git\ ..."
|
|
Write-Info "(no admin rights required; isolated from any system Git install)"
|
|
|
|
try {
|
|
$arch = if ([Environment]::Is64BitOperatingSystem) { "64-bit" } else { "32-bit" }
|
|
# Query the GitHub API for the latest git-for-windows release and pick
|
|
# the MinGit zip for our architecture.
|
|
$releaseApi = "https://api.github.com/repos/git-for-windows/git/releases/latest"
|
|
$release = Invoke-RestMethod -Uri $releaseApi -UseBasicParsing -Headers @{ "User-Agent" = "hermes-installer" }
|
|
$assetPattern = if ($arch -eq "64-bit") { "MinGit-*-64-bit.zip" } else { "MinGit-*-32-bit.zip" }
|
|
# Prefer the non-busybox MinGit — it ships the real bash.exe which is
|
|
# what Hermes' _find_bash() looks for. Busybox MinGit replaces bash
|
|
# with busybox.exe (ash), which Hermes is NOT tested against.
|
|
$asset = $release.assets | Where-Object { $_.name -like $assetPattern -and $_.name -notlike "*busybox*" } | Select-Object -First 1
|
|
|
|
if (-not $asset) {
|
|
throw "Could not find MinGit zip in latest git-for-windows release"
|
|
}
|
|
|
|
$downloadUrl = $asset.browser_download_url
|
|
$tmpZip = "$env:TEMP\$($asset.name)"
|
|
$gitDir = "$HermesHome\git"
|
|
|
|
Write-Info "Downloading $($asset.name) ($([math]::Round($asset.size / 1MB, 1)) MB)..."
|
|
Invoke-WebRequest -Uri $downloadUrl -OutFile $tmpZip -UseBasicParsing
|
|
|
|
if (Test-Path $gitDir) {
|
|
Write-Info "Removing previous MinGit install at $gitDir ..."
|
|
Remove-Item -Recurse -Force $gitDir
|
|
}
|
|
New-Item -ItemType Directory -Path $gitDir -Force | Out-Null
|
|
Expand-Archive -Path $tmpZip -DestinationPath $gitDir -Force
|
|
Remove-Item -Force $tmpZip -ErrorAction SilentlyContinue
|
|
|
|
# MinGit layout: cmd\git.exe + mingw64\bin\git.exe + usr\bin\bash.exe.
|
|
# (Note: MinGit puts bash under usr\bin, NOT bin like the full
|
|
# Git for Windows installer does. Our _find_bash() in
|
|
# tools/environments/local.py checks both locations.)
|
|
$gitExe = "$gitDir\cmd\git.exe"
|
|
if (-not (Test-Path $gitExe)) {
|
|
throw "MinGit extraction did not produce git.exe at $gitExe"
|
|
}
|
|
|
|
# Add cmd\ to session PATH so the rest of this install run (which
|
|
# needs git clone) can see the new git.exe.
|
|
$env:Path = "$gitDir\cmd;$env:Path"
|
|
|
|
# Also add to persisted User PATH so fresh shells see it. We add
|
|
# cmd\ (for git) and usr\bin\ (for bash + coreutils) — without usr\bin,
|
|
# bash-launched commands like `which`, `env`, `grep` etc. that Hermes
|
|
# tools call would be missing.
|
|
$newPathEntries = @("$gitDir\cmd", "$gitDir\usr\bin")
|
|
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
|
$userPathItems = if ($userPath) { $userPath -split ";" } else { @() }
|
|
$changed = $false
|
|
foreach ($entry in $newPathEntries) {
|
|
if ($userPathItems -notcontains $entry) {
|
|
$userPathItems += $entry
|
|
$changed = $true
|
|
}
|
|
}
|
|
if ($changed) {
|
|
[Environment]::SetEnvironmentVariable("Path", ($userPathItems -join ";"), "User")
|
|
}
|
|
|
|
$version = & $gitExe --version
|
|
Write-Success "Git $version installed to $gitDir (portable, user-scoped)"
|
|
Set-GitBashEnvVar
|
|
return $true
|
|
} catch {
|
|
Write-Err "Could not install MinGit portable: $_"
|
|
Write-Info ""
|
|
Write-Info "Fallback: install Git manually from https://git-scm.com/download/win"
|
|
Write-Info "then re-run this installer. Hermes needs Git Bash on Windows to run"
|
|
Write-Info "shell commands (same as Claude Code and other coding agents)."
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Set-GitBashEnvVar {
|
|
<#
|
|
.SYNOPSIS
|
|
Locate ``bash.exe`` from an already-installed Git and persist the path in
|
|
``HERMES_GIT_BASH_PATH`` (User env scope) so Hermes can find it even before
|
|
PATH propagation completes in a newly-spawned shell.
|
|
#>
|
|
$candidates = @()
|
|
|
|
# Our own portable MinGit install is ALWAYS checked first, so a broken
|
|
# system Git doesn't hijack us. If the user had a working system Git
|
|
# we'd have returned early from Install-Git's fast path and never called
|
|
# this with a system-Git-only installation anyway.
|
|
$candidates += "$HermesHome\git\usr\bin\bash.exe" # MinGit layout
|
|
$candidates += "$HermesHome\git\bin\bash.exe" # safety — non-MinGit portable layouts
|
|
|
|
# git.exe on PATH can tell us where the install root is
|
|
$gitCmd = Get-Command git -ErrorAction SilentlyContinue
|
|
if ($gitCmd) {
|
|
$gitExe = $gitCmd.Source
|
|
# Git for Windows (full installer): <root>\cmd\git.exe + <root>\bin\bash.exe
|
|
# MinGit: <root>\cmd\git.exe + <root>\usr\bin\bash.exe
|
|
$gitRoot = Split-Path (Split-Path $gitExe -Parent) -Parent
|
|
$candidates += "$gitRoot\bin\bash.exe"
|
|
$candidates += "$gitRoot\usr\bin\bash.exe"
|
|
}
|
|
|
|
# Standard system install locations as a final fallback. Note:
|
|
# ProgramFiles(x86) can't be referenced via ${env:...} string interpolation
|
|
# because of the parens — use [Environment]::GetEnvironmentVariable().
|
|
$candidates += "${env:ProgramFiles}\Git\bin\bash.exe"
|
|
$pf86 = [Environment]::GetEnvironmentVariable("ProgramFiles(x86)")
|
|
if ($pf86) { $candidates += "$pf86\Git\bin\bash.exe" }
|
|
$candidates += "${env:LocalAppData}\Programs\Git\bin\bash.exe"
|
|
|
|
foreach ($candidate in $candidates) {
|
|
if ($candidate -and (Test-Path $candidate)) {
|
|
[Environment]::SetEnvironmentVariable("HERMES_GIT_BASH_PATH", $candidate, "User")
|
|
$env:HERMES_GIT_BASH_PATH = $candidate
|
|
Write-Info "Set HERMES_GIT_BASH_PATH=$candidate"
|
|
return
|
|
}
|
|
}
|
|
|
|
Write-Warn "Could not locate bash.exe — Hermes may not find Git Bash."
|
|
Write-Info "If needed, set HERMES_GIT_BASH_PATH manually to your bash.exe path."
|
|
}
|
|
|
|
function Test-Node {
|
|
Write-Info "Checking Node.js (for browser tools)..."
|
|
|
|
if (Get-Command node -ErrorAction SilentlyContinue) {
|
|
$version = node --version
|
|
Write-Success "Node.js $version found"
|
|
$script:HasNode = $true
|
|
return $true
|
|
}
|
|
|
|
# Check our own managed install from a previous run
|
|
$managedNode = "$HermesHome\node\node.exe"
|
|
if (Test-Path $managedNode) {
|
|
$version = & $managedNode --version
|
|
$env:Path = "$HermesHome\node;$env:Path"
|
|
Write-Success "Node.js $version found (Hermes-managed)"
|
|
$script:HasNode = $true
|
|
return $true
|
|
}
|
|
|
|
Write-Info "Node.js not found — installing Node.js $NodeVersion LTS..."
|
|
|
|
# Try winget first (cleanest on modern Windows)
|
|
if (Get-Command winget -ErrorAction SilentlyContinue) {
|
|
Write-Info "Installing via winget..."
|
|
try {
|
|
winget install OpenJS.NodeJS.LTS --silent --accept-package-agreements --accept-source-agreements 2>&1 | Out-Null
|
|
# Refresh PATH
|
|
$env:Path = [Environment]::GetEnvironmentVariable("Path", "User") + ";" + [Environment]::GetEnvironmentVariable("Path", "Machine")
|
|
if (Get-Command node -ErrorAction SilentlyContinue) {
|
|
$version = node --version
|
|
Write-Success "Node.js $version installed via winget"
|
|
$script:HasNode = $true
|
|
return $true
|
|
}
|
|
} catch { }
|
|
}
|
|
|
|
# Fallback: download binary zip to ~/.hermes/node/
|
|
Write-Info "Downloading Node.js $NodeVersion binary..."
|
|
try {
|
|
$arch = if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" }
|
|
$indexUrl = "https://nodejs.org/dist/latest-v${NodeVersion}.x/"
|
|
$indexPage = Invoke-WebRequest -Uri $indexUrl -UseBasicParsing
|
|
$zipName = ($indexPage.Content | Select-String -Pattern "node-v${NodeVersion}\.\d+\.\d+-win-${arch}\.zip" -AllMatches).Matches[0].Value
|
|
|
|
if ($zipName) {
|
|
$downloadUrl = "${indexUrl}${zipName}"
|
|
$tmpZip = "$env:TEMP\$zipName"
|
|
$tmpDir = "$env:TEMP\hermes-node-extract"
|
|
|
|
Invoke-WebRequest -Uri $downloadUrl -OutFile $tmpZip -UseBasicParsing
|
|
if (Test-Path $tmpDir) { Remove-Item -Recurse -Force $tmpDir }
|
|
Expand-Archive -Path $tmpZip -DestinationPath $tmpDir -Force
|
|
|
|
$extractedDir = Get-ChildItem $tmpDir -Directory | Select-Object -First 1
|
|
if ($extractedDir) {
|
|
if (Test-Path "$HermesHome\node") { Remove-Item -Recurse -Force "$HermesHome\node" }
|
|
Move-Item $extractedDir.FullName "$HermesHome\node"
|
|
$env:Path = "$HermesHome\node;$env:Path"
|
|
|
|
$version = & "$HermesHome\node\node.exe" --version
|
|
Write-Success "Node.js $version installed to ~/.hermes/node/"
|
|
$script:HasNode = $true
|
|
|
|
Remove-Item -Force $tmpZip -ErrorAction SilentlyContinue
|
|
Remove-Item -Recurse -Force $tmpDir -ErrorAction SilentlyContinue
|
|
return $true
|
|
}
|
|
}
|
|
} catch {
|
|
Write-Warn "Download failed: $_"
|
|
}
|
|
|
|
Write-Warn "Could not auto-install Node.js"
|
|
Write-Info "Install manually: https://nodejs.org/en/download/"
|
|
$script:HasNode = $false
|
|
return $true
|
|
}
|
|
|
|
function Install-SystemPackages {
|
|
$script:HasRipgrep = $false
|
|
$script:HasFfmpeg = $false
|
|
$needRipgrep = $false
|
|
$needFfmpeg = $false
|
|
|
|
Write-Info "Checking ripgrep (fast file search)..."
|
|
if (Get-Command rg -ErrorAction SilentlyContinue) {
|
|
$version = rg --version | Select-Object -First 1
|
|
Write-Success "$version found"
|
|
$script:HasRipgrep = $true
|
|
} else {
|
|
$needRipgrep = $true
|
|
}
|
|
|
|
Write-Info "Checking ffmpeg (TTS voice messages)..."
|
|
if (Get-Command ffmpeg -ErrorAction SilentlyContinue) {
|
|
Write-Success "ffmpeg found"
|
|
$script:HasFfmpeg = $true
|
|
} else {
|
|
$needFfmpeg = $true
|
|
}
|
|
|
|
if (-not $needRipgrep -and -not $needFfmpeg) { return }
|
|
|
|
# Build description and package lists for each package manager
|
|
$descParts = @()
|
|
$wingetPkgs = @()
|
|
$chocoPkgs = @()
|
|
$scoopPkgs = @()
|
|
|
|
if ($needRipgrep) {
|
|
$descParts += "ripgrep for faster file search"
|
|
$wingetPkgs += "BurntSushi.ripgrep.MSVC"
|
|
$chocoPkgs += "ripgrep"
|
|
$scoopPkgs += "ripgrep"
|
|
}
|
|
if ($needFfmpeg) {
|
|
$descParts += "ffmpeg for TTS voice messages"
|
|
$wingetPkgs += "Gyan.FFmpeg"
|
|
$chocoPkgs += "ffmpeg"
|
|
$scoopPkgs += "ffmpeg"
|
|
}
|
|
|
|
$description = $descParts -join " and "
|
|
$hasWinget = Get-Command winget -ErrorAction SilentlyContinue
|
|
$hasChoco = Get-Command choco -ErrorAction SilentlyContinue
|
|
$hasScoop = Get-Command scoop -ErrorAction SilentlyContinue
|
|
|
|
# Try winget first (most common on modern Windows)
|
|
if ($hasWinget) {
|
|
Write-Info "Installing $description via winget..."
|
|
foreach ($pkg in $wingetPkgs) {
|
|
try {
|
|
winget install $pkg --silent --accept-package-agreements --accept-source-agreements 2>&1 | Out-Null
|
|
} catch { }
|
|
}
|
|
# Refresh PATH and recheck
|
|
$env:Path = [Environment]::GetEnvironmentVariable("Path", "User") + ";" + [Environment]::GetEnvironmentVariable("Path", "Machine")
|
|
if ($needRipgrep -and (Get-Command rg -ErrorAction SilentlyContinue)) {
|
|
Write-Success "ripgrep installed"
|
|
$script:HasRipgrep = $true
|
|
$needRipgrep = $false
|
|
}
|
|
if ($needFfmpeg -and (Get-Command ffmpeg -ErrorAction SilentlyContinue)) {
|
|
Write-Success "ffmpeg installed"
|
|
$script:HasFfmpeg = $true
|
|
$needFfmpeg = $false
|
|
}
|
|
if (-not $needRipgrep -and -not $needFfmpeg) { return }
|
|
}
|
|
|
|
# Fallback: choco
|
|
if ($hasChoco -and ($needRipgrep -or $needFfmpeg)) {
|
|
Write-Info "Trying Chocolatey..."
|
|
foreach ($pkg in $chocoPkgs) {
|
|
try { choco install $pkg -y 2>&1 | Out-Null } catch { }
|
|
}
|
|
if ($needRipgrep -and (Get-Command rg -ErrorAction SilentlyContinue)) {
|
|
Write-Success "ripgrep installed via chocolatey"
|
|
$script:HasRipgrep = $true
|
|
$needRipgrep = $false
|
|
}
|
|
if ($needFfmpeg -and (Get-Command ffmpeg -ErrorAction SilentlyContinue)) {
|
|
Write-Success "ffmpeg installed via chocolatey"
|
|
$script:HasFfmpeg = $true
|
|
$needFfmpeg = $false
|
|
}
|
|
}
|
|
|
|
# Fallback: scoop
|
|
if ($hasScoop -and ($needRipgrep -or $needFfmpeg)) {
|
|
Write-Info "Trying Scoop..."
|
|
foreach ($pkg in $scoopPkgs) {
|
|
try { scoop install $pkg 2>&1 | Out-Null } catch { }
|
|
}
|
|
if ($needRipgrep -and (Get-Command rg -ErrorAction SilentlyContinue)) {
|
|
Write-Success "ripgrep installed via scoop"
|
|
$script:HasRipgrep = $true
|
|
$needRipgrep = $false
|
|
}
|
|
if ($needFfmpeg -and (Get-Command ffmpeg -ErrorAction SilentlyContinue)) {
|
|
Write-Success "ffmpeg installed via scoop"
|
|
$script:HasFfmpeg = $true
|
|
$needFfmpeg = $false
|
|
}
|
|
}
|
|
|
|
# Show manual instructions for anything still missing
|
|
if ($needRipgrep) {
|
|
Write-Warn "ripgrep not installed (file search will use findstr fallback)"
|
|
Write-Info " winget install BurntSushi.ripgrep.MSVC"
|
|
}
|
|
if ($needFfmpeg) {
|
|
Write-Warn "ffmpeg not installed (TTS voice messages will be limited)"
|
|
Write-Info " winget install Gyan.FFmpeg"
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# Installation
|
|
# ============================================================================
|
|
|
|
function Install-Repository {
|
|
Write-Info "Installing to $InstallDir..."
|
|
|
|
if (Test-Path $InstallDir) {
|
|
if (Test-Path "$InstallDir\.git") {
|
|
Write-Info "Existing installation found, updating..."
|
|
Push-Location $InstallDir
|
|
git -c windows.appendAtomically=false fetch origin
|
|
git -c windows.appendAtomically=false checkout $Branch
|
|
git -c windows.appendAtomically=false pull origin $Branch
|
|
Pop-Location
|
|
} else {
|
|
Write-Err "Directory exists but is not a git repository: $InstallDir"
|
|
Write-Info "Remove it or choose a different directory with -InstallDir"
|
|
throw "Directory exists but is not a git repository: $InstallDir"
|
|
}
|
|
} else {
|
|
$cloneSuccess = $false
|
|
|
|
# Fix Windows git "copy-fd: write returned: Invalid argument" error.
|
|
# Git for Windows can fail on atomic file operations (hook templates,
|
|
# config lock files) due to antivirus, OneDrive, or NTFS filter drivers.
|
|
# The -c flag injects config before any file I/O occurs.
|
|
Write-Info "Configuring git for Windows compatibility..."
|
|
$env:GIT_CONFIG_COUNT = "1"
|
|
$env:GIT_CONFIG_KEY_0 = "windows.appendAtomically"
|
|
$env:GIT_CONFIG_VALUE_0 = "false"
|
|
git config --global windows.appendAtomically false 2>$null
|
|
|
|
# Try SSH first, then HTTPS, with -c flag for atomic write fix
|
|
Write-Info "Trying SSH clone..."
|
|
$env:GIT_SSH_COMMAND = "ssh -o BatchMode=yes -o ConnectTimeout=5"
|
|
try {
|
|
git -c windows.appendAtomically=false clone --branch $Branch --recurse-submodules $RepoUrlSsh $InstallDir
|
|
if ($LASTEXITCODE -eq 0) { $cloneSuccess = $true }
|
|
} catch { }
|
|
$env:GIT_SSH_COMMAND = $null
|
|
|
|
if (-not $cloneSuccess) {
|
|
if (Test-Path $InstallDir) { Remove-Item -Recurse -Force $InstallDir -ErrorAction SilentlyContinue }
|
|
Write-Info "SSH failed, trying HTTPS..."
|
|
try {
|
|
git -c windows.appendAtomically=false clone --branch $Branch --recurse-submodules $RepoUrlHttps $InstallDir
|
|
if ($LASTEXITCODE -eq 0) { $cloneSuccess = $true }
|
|
} catch { }
|
|
}
|
|
|
|
# Fallback: download ZIP archive (bypasses git file I/O issues entirely)
|
|
if (-not $cloneSuccess) {
|
|
if (Test-Path $InstallDir) { Remove-Item -Recurse -Force $InstallDir -ErrorAction SilentlyContinue }
|
|
Write-Warn "Git clone failed — downloading ZIP archive instead..."
|
|
try {
|
|
$zipUrl = "https://github.com/NousResearch/hermes-agent/archive/refs/heads/$Branch.zip"
|
|
$zipPath = "$env:TEMP\hermes-agent-$Branch.zip"
|
|
$extractPath = "$env:TEMP\hermes-agent-extract"
|
|
|
|
Invoke-WebRequest -Uri $zipUrl -OutFile $zipPath -UseBasicParsing
|
|
if (Test-Path $extractPath) { Remove-Item -Recurse -Force $extractPath }
|
|
Expand-Archive -Path $zipPath -DestinationPath $extractPath -Force
|
|
|
|
# GitHub ZIPs extract to repo-branch/ subdirectory
|
|
$extractedDir = Get-ChildItem $extractPath -Directory | Select-Object -First 1
|
|
if ($extractedDir) {
|
|
New-Item -ItemType Directory -Force -Path (Split-Path $InstallDir) -ErrorAction SilentlyContinue | Out-Null
|
|
Move-Item $extractedDir.FullName $InstallDir -Force
|
|
Write-Success "Downloaded and extracted"
|
|
|
|
# Initialize git repo so updates work later
|
|
Push-Location $InstallDir
|
|
git -c windows.appendAtomically=false init 2>$null
|
|
git -c windows.appendAtomically=false config windows.appendAtomically false 2>$null
|
|
git remote add origin $RepoUrlHttps 2>$null
|
|
Pop-Location
|
|
Write-Success "Git repo initialized for future updates"
|
|
|
|
$cloneSuccess = $true
|
|
}
|
|
|
|
# Cleanup temp files
|
|
Remove-Item -Force $zipPath -ErrorAction SilentlyContinue
|
|
Remove-Item -Recurse -Force $extractPath -ErrorAction SilentlyContinue
|
|
} catch {
|
|
Write-Err "ZIP download also failed: $_"
|
|
}
|
|
}
|
|
|
|
if (-not $cloneSuccess) {
|
|
throw "Failed to download repository (tried git clone SSH, HTTPS, and ZIP)"
|
|
}
|
|
}
|
|
|
|
# Set per-repo config (harmless if it fails)
|
|
Push-Location $InstallDir
|
|
git -c windows.appendAtomically=false config windows.appendAtomically false 2>$null
|
|
|
|
# Ensure submodules are initialized and updated
|
|
Write-Info "Initializing submodules..."
|
|
git -c windows.appendAtomically=false submodule update --init --recursive 2>$null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Warn "Submodule init failed (terminal/RL tools may need manual setup)"
|
|
} else {
|
|
Write-Success "Submodules ready"
|
|
}
|
|
Pop-Location
|
|
|
|
Write-Success "Repository ready"
|
|
}
|
|
|
|
function Install-Venv {
|
|
if ($NoVenv) {
|
|
Write-Info "Skipping virtual environment (-NoVenv)"
|
|
return
|
|
}
|
|
|
|
Write-Info "Creating virtual environment with Python $PythonVersion..."
|
|
|
|
Push-Location $InstallDir
|
|
|
|
if (Test-Path "venv") {
|
|
Write-Info "Virtual environment already exists, recreating..."
|
|
Remove-Item -Recurse -Force "venv"
|
|
}
|
|
|
|
# uv creates the venv and pins the Python version in one step
|
|
& $UvCmd venv venv --python $PythonVersion
|
|
|
|
Pop-Location
|
|
|
|
Write-Success "Virtual environment ready (Python $PythonVersion)"
|
|
}
|
|
|
|
function Install-Dependencies {
|
|
Write-Info "Installing dependencies..."
|
|
|
|
Push-Location $InstallDir
|
|
|
|
if (-not $NoVenv) {
|
|
# Tell uv to install into our venv (no activation needed)
|
|
$env:VIRTUAL_ENV = "$InstallDir\venv"
|
|
}
|
|
|
|
# Install main package with all extras
|
|
try {
|
|
& $UvCmd pip install -e ".[all]" 2>&1 | Out-Null
|
|
} catch {
|
|
& $UvCmd pip install -e "." | Out-Null
|
|
}
|
|
|
|
Write-Success "Main package installed"
|
|
|
|
# Install optional submodules
|
|
Write-Info "Installing tinker-atropos (RL training backend)..."
|
|
if (Test-Path "tinker-atropos\pyproject.toml") {
|
|
try {
|
|
& $UvCmd pip install -e ".\tinker-atropos" 2>&1 | Out-Null
|
|
Write-Success "tinker-atropos installed"
|
|
} catch {
|
|
Write-Warn "tinker-atropos install failed (RL tools may not work)"
|
|
}
|
|
} else {
|
|
Write-Warn "tinker-atropos not found (run: git submodule update --init)"
|
|
}
|
|
|
|
Pop-Location
|
|
|
|
Write-Success "All dependencies installed"
|
|
}
|
|
|
|
function Set-PathVariable {
|
|
Write-Info "Setting up hermes command..."
|
|
|
|
if ($NoVenv) {
|
|
$hermesBin = "$InstallDir"
|
|
} else {
|
|
$hermesBin = "$InstallDir\venv\Scripts"
|
|
}
|
|
|
|
# Add the venv Scripts dir to user PATH so hermes is globally available
|
|
# On Windows, the hermes.exe in venv\Scripts\ has the venv Python baked in
|
|
$currentPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
|
|
|
if ($currentPath -notlike "*$hermesBin*") {
|
|
[Environment]::SetEnvironmentVariable(
|
|
"Path",
|
|
"$hermesBin;$currentPath",
|
|
"User"
|
|
)
|
|
Write-Success "Added to user PATH: $hermesBin"
|
|
} else {
|
|
Write-Info "PATH already configured"
|
|
}
|
|
|
|
# Set HERMES_HOME so the Python code finds config/data in the right place.
|
|
# Only needed on Windows where we install to %LOCALAPPDATA%\hermes instead
|
|
# of the Unix default ~/.hermes
|
|
$currentHermesHome = [Environment]::GetEnvironmentVariable("HERMES_HOME", "User")
|
|
if (-not $currentHermesHome -or $currentHermesHome -ne $HermesHome) {
|
|
[Environment]::SetEnvironmentVariable("HERMES_HOME", $HermesHome, "User")
|
|
Write-Success "Set HERMES_HOME=$HermesHome"
|
|
}
|
|
$env:HERMES_HOME = $HermesHome
|
|
|
|
# Update current session
|
|
$env:Path = "$hermesBin;$env:Path"
|
|
|
|
Write-Success "hermes command ready"
|
|
}
|
|
|
|
function Copy-ConfigTemplates {
|
|
Write-Info "Setting up configuration files..."
|
|
|
|
# Create ~/.hermes directory structure
|
|
New-Item -ItemType Directory -Force -Path "$HermesHome\cron" | Out-Null
|
|
New-Item -ItemType Directory -Force -Path "$HermesHome\sessions" | Out-Null
|
|
New-Item -ItemType Directory -Force -Path "$HermesHome\logs" | Out-Null
|
|
New-Item -ItemType Directory -Force -Path "$HermesHome\pairing" | Out-Null
|
|
New-Item -ItemType Directory -Force -Path "$HermesHome\hooks" | Out-Null
|
|
New-Item -ItemType Directory -Force -Path "$HermesHome\image_cache" | Out-Null
|
|
New-Item -ItemType Directory -Force -Path "$HermesHome\audio_cache" | Out-Null
|
|
New-Item -ItemType Directory -Force -Path "$HermesHome\memories" | Out-Null
|
|
New-Item -ItemType Directory -Force -Path "$HermesHome\skills" | Out-Null
|
|
|
|
|
|
# Create .env
|
|
$envPath = "$HermesHome\.env"
|
|
if (-not (Test-Path $envPath)) {
|
|
$examplePath = "$InstallDir\.env.example"
|
|
if (Test-Path $examplePath) {
|
|
Copy-Item $examplePath $envPath
|
|
Write-Success "Created ~/.hermes/.env from template"
|
|
} else {
|
|
New-Item -ItemType File -Force -Path $envPath | Out-Null
|
|
Write-Success "Created ~/.hermes/.env"
|
|
}
|
|
} else {
|
|
Write-Info "~/.hermes/.env already exists, keeping it"
|
|
}
|
|
|
|
# Create config.yaml
|
|
$configPath = "$HermesHome\config.yaml"
|
|
if (-not (Test-Path $configPath)) {
|
|
$examplePath = "$InstallDir\cli-config.yaml.example"
|
|
if (Test-Path $examplePath) {
|
|
Copy-Item $examplePath $configPath
|
|
Write-Success "Created ~/.hermes/config.yaml from template"
|
|
}
|
|
} else {
|
|
Write-Info "~/.hermes/config.yaml already exists, keeping it"
|
|
}
|
|
|
|
# Create SOUL.md if it doesn't exist (global persona file)
|
|
$soulPath = "$HermesHome\SOUL.md"
|
|
if (-not (Test-Path $soulPath)) {
|
|
@"
|
|
# Hermes Agent Persona
|
|
|
|
<!--
|
|
This file defines the agent's personality and tone.
|
|
The agent will embody whatever you write here.
|
|
Edit this to customize how Hermes communicates with you.
|
|
|
|
Examples:
|
|
- "You are a warm, playful assistant who uses kaomoji occasionally."
|
|
- "You are a concise technical expert. No fluff, just facts."
|
|
- "You speak like a friendly coworker who happens to know everything."
|
|
|
|
This file is loaded fresh each message -- no restart needed.
|
|
Delete the contents (or this file) to use the default personality.
|
|
-->
|
|
"@ | Set-Content -Path $soulPath -Encoding UTF8
|
|
Write-Success "Created ~/.hermes/SOUL.md (edit to customize personality)"
|
|
}
|
|
|
|
Write-Success "Configuration directory ready: ~/.hermes/"
|
|
|
|
# Seed bundled skills into ~/.hermes/skills/ (manifest-based, one-time per skill)
|
|
Write-Info "Syncing bundled skills to ~/.hermes/skills/ ..."
|
|
$pythonExe = "$InstallDir\venv\Scripts\python.exe"
|
|
if (Test-Path $pythonExe) {
|
|
try {
|
|
& $pythonExe "$InstallDir\tools\skills_sync.py" 2>$null
|
|
Write-Success "Skills synced to ~/.hermes/skills/"
|
|
} catch {
|
|
# Fallback: simple directory copy
|
|
$bundledSkills = "$InstallDir\skills"
|
|
$userSkills = "$HermesHome\skills"
|
|
if ((Test-Path $bundledSkills) -and -not (Get-ChildItem $userSkills -Exclude '.bundled_manifest' -ErrorAction SilentlyContinue)) {
|
|
Copy-Item -Path "$bundledSkills\*" -Destination $userSkills -Recurse -Force -ErrorAction SilentlyContinue
|
|
Write-Success "Skills copied to ~/.hermes/skills/"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Install-NodeDeps {
|
|
if (-not $HasNode) {
|
|
Write-Info "Skipping Node.js dependencies (Node not installed)"
|
|
return
|
|
}
|
|
|
|
Push-Location $InstallDir
|
|
|
|
if (Test-Path "package.json") {
|
|
Write-Info "Installing Node.js dependencies (browser tools)..."
|
|
try {
|
|
npm install --silent 2>&1 | Out-Null
|
|
Write-Success "Node.js dependencies installed"
|
|
} catch {
|
|
Write-Warn "npm install failed (browser tools may not work)"
|
|
}
|
|
}
|
|
|
|
# Install TUI dependencies
|
|
$tuiDir = "$InstallDir\ui-tui"
|
|
if (Test-Path "$tuiDir\package.json") {
|
|
Write-Info "Installing TUI dependencies..."
|
|
Push-Location $tuiDir
|
|
try {
|
|
npm install --silent 2>&1 | Out-Null
|
|
Write-Success "TUI dependencies installed"
|
|
} catch {
|
|
Write-Warn "TUI npm install failed (hermes --tui may not work)"
|
|
}
|
|
Pop-Location
|
|
}
|
|
|
|
|
|
|
|
Pop-Location
|
|
}
|
|
|
|
function Invoke-SetupWizard {
|
|
if ($SkipSetup) {
|
|
Write-Info "Skipping setup wizard (-SkipSetup)"
|
|
return
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Info "Starting setup wizard..."
|
|
Write-Host ""
|
|
|
|
Push-Location $InstallDir
|
|
|
|
# Run hermes setup using the venv Python directly (no activation needed)
|
|
if (-not $NoVenv) {
|
|
& ".\venv\Scripts\python.exe" -m hermes_cli.main setup
|
|
} else {
|
|
python -m hermes_cli.main setup
|
|
}
|
|
|
|
Pop-Location
|
|
}
|
|
|
|
function Start-GatewayIfConfigured {
|
|
$envPath = "$HermesHome\.env"
|
|
if (-not (Test-Path $envPath)) { return }
|
|
|
|
$hasMessaging = $false
|
|
$content = Get-Content $envPath -ErrorAction SilentlyContinue
|
|
foreach ($var in @("TELEGRAM_BOT_TOKEN", "DISCORD_BOT_TOKEN", "SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "WHATSAPP_ENABLED")) {
|
|
$match = $content | Where-Object { $_ -match "^${var}=.+" -and $_ -notmatch "your-token-here" }
|
|
if ($match) { $hasMessaging = $true; break }
|
|
}
|
|
|
|
if (-not $hasMessaging) { return }
|
|
|
|
$hermesCmd = "$InstallDir\venv\Scripts\hermes.exe"
|
|
if (-not (Test-Path $hermesCmd)) {
|
|
$hermesCmd = "hermes"
|
|
}
|
|
|
|
# If WhatsApp is enabled but not yet paired, run foreground for QR scan
|
|
$whatsappEnabled = $content | Where-Object { $_ -match "^WHATSAPP_ENABLED=true" }
|
|
$whatsappSession = "$HermesHome\whatsapp\session\creds.json"
|
|
if ($whatsappEnabled -and -not (Test-Path $whatsappSession)) {
|
|
Write-Host ""
|
|
Write-Info "WhatsApp is enabled but not yet paired."
|
|
Write-Info "Running 'hermes whatsapp' to pair via QR code..."
|
|
Write-Host ""
|
|
$response = Read-Host "Pair WhatsApp now? [Y/n]"
|
|
if ($response -eq "" -or $response -match "^[Yy]") {
|
|
try {
|
|
& $hermesCmd whatsapp
|
|
} catch {
|
|
# Expected after pairing completes
|
|
}
|
|
}
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Info "Messaging platform token detected!"
|
|
Write-Info "The gateway handles messaging platforms and cron job execution."
|
|
Write-Host ""
|
|
$response = Read-Host "Would you like to start the gateway now? [Y/n]"
|
|
|
|
if ($response -eq "" -or $response -match "^[Yy]") {
|
|
Write-Info "Starting gateway in background..."
|
|
try {
|
|
$logFile = "$HermesHome\logs\gateway.log"
|
|
Start-Process -FilePath $hermesCmd -ArgumentList "gateway" `
|
|
-RedirectStandardOutput $logFile `
|
|
-RedirectStandardError "$HermesHome\logs\gateway-error.log" `
|
|
-WindowStyle Hidden
|
|
Write-Success "Gateway started! Your bot is now online."
|
|
Write-Info "Logs: $logFile"
|
|
Write-Info "To stop: close the gateway process from Task Manager"
|
|
} catch {
|
|
Write-Warn "Failed to start gateway. Run manually: hermes gateway"
|
|
}
|
|
} else {
|
|
Write-Info "Skipped. Start the gateway later with: hermes gateway"
|
|
}
|
|
}
|
|
|
|
function Write-Completion {
|
|
Write-Host ""
|
|
Write-Host "┌─────────────────────────────────────────────────────────┐" -ForegroundColor Green
|
|
Write-Host "│ ✓ Installation Complete! │" -ForegroundColor Green
|
|
Write-Host "└─────────────────────────────────────────────────────────┘" -ForegroundColor Green
|
|
Write-Host ""
|
|
|
|
# Show file locations
|
|
Write-Host "📁 Your files:" -ForegroundColor Cyan
|
|
Write-Host ""
|
|
Write-Host " Config: " -NoNewline -ForegroundColor Yellow
|
|
Write-Host "$HermesHome\config.yaml"
|
|
Write-Host " API Keys: " -NoNewline -ForegroundColor Yellow
|
|
Write-Host "$HermesHome\.env"
|
|
Write-Host " Data: " -NoNewline -ForegroundColor Yellow
|
|
Write-Host "$HermesHome\cron\, sessions\, logs\"
|
|
Write-Host " Code: " -NoNewline -ForegroundColor Yellow
|
|
Write-Host "$HermesHome\hermes-agent\"
|
|
Write-Host ""
|
|
|
|
Write-Host "─────────────────────────────────────────────────────────" -ForegroundColor Cyan
|
|
Write-Host ""
|
|
Write-Host "🚀 Commands:" -ForegroundColor Cyan
|
|
Write-Host ""
|
|
Write-Host " hermes " -NoNewline -ForegroundColor Green
|
|
Write-Host "Start chatting"
|
|
Write-Host " hermes setup " -NoNewline -ForegroundColor Green
|
|
Write-Host "Configure API keys & settings"
|
|
Write-Host " hermes config " -NoNewline -ForegroundColor Green
|
|
Write-Host "View/edit configuration"
|
|
Write-Host " hermes config edit " -NoNewline -ForegroundColor Green
|
|
Write-Host "Open config in editor"
|
|
Write-Host " hermes gateway " -NoNewline -ForegroundColor Green
|
|
Write-Host "Start messaging gateway (Telegram, Discord, etc.)"
|
|
Write-Host " hermes update " -NoNewline -ForegroundColor Green
|
|
Write-Host "Update to latest version"
|
|
Write-Host ""
|
|
|
|
Write-Host "─────────────────────────────────────────────────────────" -ForegroundColor Cyan
|
|
Write-Host ""
|
|
Write-Host "⚡ Restart your terminal for PATH changes to take effect" -ForegroundColor Yellow
|
|
Write-Host ""
|
|
|
|
if (-not $HasNode) {
|
|
Write-Host "Note: Node.js could not be installed automatically." -ForegroundColor Yellow
|
|
Write-Host "Browser tools need Node.js. Install manually:" -ForegroundColor Yellow
|
|
Write-Host " https://nodejs.org/en/download/" -ForegroundColor Yellow
|
|
Write-Host ""
|
|
}
|
|
|
|
if (-not $HasRipgrep) {
|
|
Write-Host "Note: ripgrep (rg) was not installed. For faster file search:" -ForegroundColor Yellow
|
|
Write-Host " winget install BurntSushi.ripgrep.MSVC" -ForegroundColor Yellow
|
|
Write-Host ""
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# Main
|
|
# ============================================================================
|
|
|
|
function Main {
|
|
Write-Banner
|
|
|
|
if (-not (Install-Uv)) { throw "uv installation failed — cannot continue" }
|
|
if (-not (Test-Python)) { throw "Python $PythonVersion not available — cannot continue" }
|
|
if (-not (Install-Git)) { throw "Git not available and auto-install failed — install from https://git-scm.com/download/win then re-run" }
|
|
Test-Node # Auto-installs if missing
|
|
Install-SystemPackages # ripgrep + ffmpeg in one step
|
|
|
|
Install-Repository
|
|
Install-Venv
|
|
Install-Dependencies
|
|
Install-NodeDeps
|
|
Set-PathVariable
|
|
Copy-ConfigTemplates
|
|
Invoke-SetupWizard
|
|
Start-GatewayIfConfigured
|
|
|
|
Write-Completion
|
|
}
|
|
|
|
# Wrap in try/catch so errors don't kill the terminal when run via:
|
|
# irm https://...install.ps1 | iex
|
|
# (exit/throw inside iex kills the entire PowerShell session)
|
|
try {
|
|
Main
|
|
} catch {
|
|
Write-Host ""
|
|
Write-Err "Installation failed: $_"
|
|
Write-Host ""
|
|
Write-Info "If the error is unclear, try downloading and running the script directly:"
|
|
Write-Host " Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1' -OutFile install.ps1" -ForegroundColor Yellow
|
|
Write-Host " .\install.ps1" -ForegroundColor Yellow
|
|
Write-Host ""
|
|
}
|