From 1d955ed990b3d98a893cb006184ababe62b9c71d Mon Sep 17 00:00:00 2001 From: Bruce MacDonald Date: Wed, 3 Jun 2026 17:40:45 -0700 Subject: [PATCH] integrations: hermes windows install (#16487) --- cmd/launch/hermes.go | 66 ++++++++++++++++++++++++++++-------- cmd/launch/hermes_test.go | 57 ++++++++++++++++++++++++------- docs/integrations/hermes.mdx | 2 -- 3 files changed, 96 insertions(+), 29 deletions(-) diff --git a/cmd/launch/hermes.go b/cmd/launch/hermes.go index 513662738..683b10b75 100644 --- a/cmd/launch/hermes.go +++ b/cmd/launch/hermes.go @@ -24,6 +24,8 @@ import ( const ( hermesInstallScript = "curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash -s -- --skip-setup" + hermesWindowsInstallURL = "https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1" + hermesWindowsInstallCmd = "& ([scriptblock]::Create((irm " + hermesWindowsInstallURL + "))) -SkipSetup" hermesProviderName = "Ollama" hermesProviderKey = "ollama-launch" hermesLegacyKey = "ollama" @@ -187,14 +189,12 @@ func (h *Hermes) ensureInstalled() error { return nil } - if hermesGOOS == "windows" { - return hermesWindowsHint() - } - var missing []string - for _, dep := range []string{"bash", "curl", "git"} { - if _, err := hermesLookPath(dep); err != nil { - missing = append(missing, dep) + if hermesGOOS != "windows" { + for _, dep := range []string{"bash", "curl", "git"} { + if _, err := hermesLookPath(dep); err != nil { + missing = append(missing, dep) + } } } if len(missing) > 0 { @@ -210,7 +210,7 @@ func (h *Hermes) ensureInstalled() error { } fmt.Fprintf(os.Stderr, "\nInstalling Hermes...\n") - if err := hermesAttachedCommand("bash", "-lc", hermesInstallScript).Run(); err != nil { + if err := h.runInstallScript(); err != nil { return fmt.Errorf("failed to install hermes: %w", err) } @@ -222,6 +222,13 @@ func (h *Hermes) ensureInstalled() error { return nil } +func (h *Hermes) runInstallScript() error { + if hermesGOOS == "windows" { + return hermesAttachedCommand("powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", hermesWindowsInstallCmd).Run() + } + return hermesAttachedCommand("bash", "-lc", hermesInstallScript).Run() +} + func (h *Hermes) listModels(defaultModel string) []string { client := hermesOllamaClient() resp, err := client.List(context.Background()) @@ -259,7 +266,12 @@ func (h *Hermes) binary() (string, error) { } if hermesGOOS == "windows" { - return "", hermesWindowsHint() + for _, fallback := range hermesWindowsBinaryFallbacks() { + if _, err := os.Stat(fallback); err == nil { + return fallback, nil + } + } + return "", fmt.Errorf("hermes is not installed") } home, err := hermesUserHome() @@ -274,6 +286,36 @@ func (h *Hermes) binary() (string, error) { return "", fmt.Errorf("hermes is not installed") } +func hermesWindowsBinaryFallbacks() []string { + var roots []string + add := func(root string) { + root = strings.TrimSpace(root) + if root != "" { + roots = append(roots, filepath.Clean(root)) + } + } + + add(os.Getenv("HERMES_HOME")) + add(os.Getenv("LOCALAPPDATA")) + if home, err := hermesUserHome(); err == nil { + add(filepath.Join(home, "AppData", "Local")) + } + + seen := make(map[string]bool, len(roots)) + var fallbacks []string + for _, root := range roots { + if seen[root] { + continue + } + seen[root] = true + fallbacks = append(fallbacks, filepath.Join(root, "hermes-agent", "venv", "Scripts", "hermes.exe")) + if filepath.Base(root) != "hermes" { + fallbacks = append(fallbacks, filepath.Join(root, "hermes", "hermes-agent", "venv", "Scripts", "hermes.exe")) + } + } + return fallbacks +} + func hermesConfigPath() (string, error) { home, err := hermesUserHome() if err != nil { @@ -671,9 +713,3 @@ func hermesAttachedCommand(name string, args ...string) *exec.Cmd { cmd.Stderr = os.Stderr return cmd } - -func hermesWindowsHint() error { - return fmt.Errorf("Hermes on Windows requires WSL2. Install WSL with: wsl --install\n" + - "Then run 'ollama launch hermes' from inside your WSL shell.\n" + - "Docs: https://hermes-agent.nousresearch.com/docs/getting-started/installation/") -} diff --git a/cmd/launch/hermes_test.go b/cmd/launch/hermes_test.go index fa2b17b4e..4c6451951 100644 --- a/cmd/launch/hermes_test.go +++ b/cmd/launch/hermes_test.go @@ -943,26 +943,59 @@ func TestHermesMessagingConfiguredRecognizesSupportedGatewayVars(t *testing.T) { } } -func TestHermesEnsureInstalledWindowsShowsWSLGuidance(t *testing.T) { +func TestHermesEnsureInstalledWindowsRunsPowerShellInstaller(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("uses a POSIX shell test binary") + } + tmpDir := t.TempDir() setTestHome(t, tmpDir) + withLauncherHooks(t) withHermesPlatform(t, "windows") t.Setenv("PATH", tmpDir) + t.Setenv("LOCALAPPDATA", filepath.Join(tmpDir, "AppData", "Local")) + + powershell := filepath.Join(tmpDir, "powershell.exe") + script := fmt.Sprintf(`#!/bin/sh +printf '%%s\n' "$*" >> %q +/bin/mkdir -p %q +/bin/cat > %q <<'EOS' +#!/bin/sh +exit 0 +EOS +/bin/chmod +x %q +exit 0 +`, + filepath.Join(tmpDir, "powershell.log"), + filepath.Dir(filepath.Join(tmpDir, "AppData", "Local", "hermes", "hermes-agent", "venv", "Scripts", "hermes.exe")), + filepath.Join(tmpDir, "AppData", "Local", "hermes", "hermes-agent", "venv", "Scripts", "hermes.exe"), + filepath.Join(tmpDir, "AppData", "Local", "hermes", "hermes-agent", "venv", "Scripts", "hermes.exe"), + ) + if err := os.WriteFile(powershell, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + + DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) { + if prompt != "Hermes is not installed. Install now?" { + t.Fatalf("unexpected install prompt %q", prompt) + } + return true, nil + } h := &Hermes{} - err := h.ensureInstalled() - if err == nil { - t.Fatal("expected WSL guidance error") + if err := h.ensureInstalled(); err != nil { + t.Fatalf("ensureInstalled returned error: %v", err) } - msg := err.Error() - if !strings.Contains(msg, "wsl --install") { - t.Fatalf("expected install command in guidance, got %v", err) + + data, err := os.ReadFile(filepath.Join(tmpDir, "powershell.log")) + if err != nil { + t.Fatal(err) } - if !strings.Contains(msg, "hermes-agent.nousresearch.com") { - t.Fatalf("expected docs link in guidance, got %v", err) - } - if strings.Contains(msg, "hermes is not installed") { - t.Fatalf("guidance should not lead with 'hermes is not installed', got %v", err) + logs := string(data) + for _, want := range []string{"-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", hermesWindowsInstallURL, "-SkipSetup"} { + if !strings.Contains(logs, want) { + t.Fatalf("expected PowerShell installer args to contain %q, got logs:\n%s", want, logs) + } } } diff --git a/docs/integrations/hermes.mdx b/docs/integrations/hermes.mdx index e42212f9c..cab17e54d 100644 --- a/docs/integrations/hermes.mdx +++ b/docs/integrations/hermes.mdx @@ -19,8 +19,6 @@ Ollama handles everything automatically: 3. **Onboarding** — Ollama configures the Ollama provider, points Hermes at `http://127.0.0.1:11434/v1`, and sets your model as the primary 4. **Gateway** — Optionally connects a messaging platform (Telegram, Discord, Slack, WhatsApp, Signal, Email) and launches the Hermes chat -Hermes on Windows requires WSL2. Install it with `wsl --install` and re-run from inside the WSL shell. - ## Recommended models **Cloud models**: