feat: Windows native support via Git Bash
- Add scripts/install.cmd batch wrapper for CMD users (delegates to install.ps1) - Add _find_shell() in local.py: detects Git Bash on Windows via HERMES_GIT_BASH_PATH env var, shutil.which, or common install paths (same pattern as Claude Code's CLAUDE_CODE_GIT_BASH_PATH) - Use _find_shell() in process_registry.py for background processes - Fix hermes_cli/gateway.py: use wmic instead of ps aux on Windows, skip SIGKILL (doesn't exist on Windows), fix venv path (Scripts/python.exe vs bin/python) - Update README with three install commands (Linux/macOS, PowerShell, CMD) and Windows native documentation Requires Git for Windows, which bundles bash.exe. The terminal tool transparently uses Git Bash for shell commands regardless of whether the user launched hermes from PowerShell or CMD.
This commit is contained in:
parent
68cc81a74d
commit
de59d91add
5 changed files with 133 additions and 37 deletions
|
|
@ -12,6 +12,43 @@ _IS_WINDOWS = platform.system() == "Windows"
|
|||
|
||||
from tools.environments.base import BaseEnvironment
|
||||
|
||||
|
||||
def _find_shell() -> str:
|
||||
"""Find the best shell for command execution.
|
||||
|
||||
On Unix: uses $SHELL, falls back to bash.
|
||||
On Windows: uses Git Bash (bundled with Git for Windows).
|
||||
Raises RuntimeError if no suitable shell is found on Windows.
|
||||
"""
|
||||
if not _IS_WINDOWS:
|
||||
return os.environ.get("SHELL") or shutil.which("bash") or "/bin/bash"
|
||||
|
||||
# Windows: look for Git Bash (installed with Git for Windows).
|
||||
# Allow override via env var (same pattern as Claude Code).
|
||||
custom = os.environ.get("HERMES_GIT_BASH_PATH")
|
||||
if custom and os.path.isfile(custom):
|
||||
return custom
|
||||
|
||||
# shutil.which finds bash.exe if Git\bin is on PATH
|
||||
found = shutil.which("bash")
|
||||
if found:
|
||||
return found
|
||||
|
||||
# Check common Git for Windows install locations
|
||||
for candidate in (
|
||||
os.path.join(os.environ.get("ProgramFiles", r"C:\Program Files"), "Git", "bin", "bash.exe"),
|
||||
os.path.join(os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)"), "Git", "bin", "bash.exe"),
|
||||
os.path.join(os.environ.get("LOCALAPPDATA", ""), "Programs", "Git", "bin", "bash.exe"),
|
||||
):
|
||||
if candidate and os.path.isfile(candidate):
|
||||
return candidate
|
||||
|
||||
raise RuntimeError(
|
||||
"Git Bash not found. Hermes Agent requires Git for Windows on Windows.\n"
|
||||
"Install it from: https://git-scm.com/download/win\n"
|
||||
"Or set HERMES_GIT_BASH_PATH to your bash.exe location."
|
||||
)
|
||||
|
||||
# Noise lines emitted by interactive shells when stdin is not a terminal.
|
||||
# Filtered from output to keep tool results clean.
|
||||
_SHELL_NOISE_SUBSTRINGS = (
|
||||
|
|
@ -66,7 +103,7 @@ class LocalEnvironment(BaseEnvironment):
|
|||
# tools like nvm, pyenv, and cargo install their init scripts.
|
||||
# -l alone isn't enough: .profile sources .bashrc, but the guard
|
||||
# returns early because the shell isn't interactive.
|
||||
user_shell = os.environ.get("SHELL") or shutil.which("bash") or "/bin/bash"
|
||||
user_shell = _find_shell()
|
||||
proc = subprocess.Popen(
|
||||
[user_shell, "-lic", exec_command],
|
||||
text=True,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import time
|
|||
import uuid
|
||||
|
||||
_IS_WINDOWS = platform.system() == "Windows"
|
||||
from tools.environments.local import _find_shell
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
|
@ -148,7 +149,7 @@ class ProcessRegistry:
|
|||
# Try PTY mode for interactive CLI tools
|
||||
try:
|
||||
import ptyprocess
|
||||
user_shell = os.environ.get("SHELL") or shutil.which("bash") or "/bin/bash"
|
||||
user_shell = _find_shell()
|
||||
pty_env = os.environ | (env_vars or {})
|
||||
pty_env["PYTHONUNBUFFERED"] = "1"
|
||||
pty_proc = ptyprocess.PtyProcess.spawn(
|
||||
|
|
@ -186,7 +187,7 @@ class ProcessRegistry:
|
|||
# Standard Popen path (non-PTY or PTY fallback)
|
||||
# Use the user's login shell for consistency with LocalEnvironment --
|
||||
# ensures rc files are sourced and user tools are available.
|
||||
user_shell = os.environ.get("SHELL") or shutil.which("bash") or "/bin/bash"
|
||||
user_shell = _find_shell()
|
||||
# Force unbuffered output for Python scripts so progress is visible
|
||||
# during background execution (libraries like tqdm/datasets buffer when
|
||||
# stdout is a pipe, hiding output from process(action="poll")).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue