integrations: hermes windows install (#16487)

This commit is contained in:
Bruce MacDonald
2026-06-03 17:40:45 -07:00
committed by GitHub
parent d071237131
commit 1d955ed990
3 changed files with 96 additions and 29 deletions

View File

@@ -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/")
}

View File

@@ -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)
}
}
}