fix: Windows/WSL compatibility for shell tools and CLI helpers

- exec_tool: add Windows shell detection (pwsh/cmd), use taskkill for process kill
- shell_detect: skip Unix paths on Windows, add pwsh/cmd fallback chain
- dev.py: replace all `which` subprocess calls with shutil.which()
- test_hanzo_dev: use shutil.which() instead of subprocess which
This commit is contained in:
2026-03-09 21:10:37 -07:00
parent 8131396ac9
commit 65ba7952cd
7 changed files with 52 additions and 23 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ wheels = [
[[package]]
name = "hanzo-async"
version = "0.1.0"
version = "0.1.2"
source = { editable = "." }
dependencies = [
{ name = "aiofiles" },
+3 -3
View File
@@ -1290,7 +1290,7 @@ wheels = [
[[package]]
name = "hanzo-mcp"
version = "0.12.6"
version = "0.12.7"
source = { editable = "." }
dependencies = [
{ name = "fastmcp" },
@@ -1323,7 +1323,6 @@ dependencies = [
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "typing-extensions" },
{ name = "uvloop", marker = "sys_platform != 'win32'" },
]
[package.optional-dependencies]
@@ -1363,6 +1362,7 @@ memory = [
performance = [
{ name = "orjson" },
{ name = "ujson" },
{ name = "uvloop", marker = "sys_platform != 'win32'" },
]
publish = [
{ name = "build" },
@@ -1441,7 +1441,7 @@ requires-dist = [
{ name = "types-setuptools", marker = "extra == 'dev'", specifier = ">=69.5.0" },
{ name = "typing-extensions", specifier = ">=4.13.0" },
{ name = "ujson", marker = "extra == 'performance'", specifier = ">=5.7.0" },
{ name = "uvloop", marker = "sys_platform != 'win32'", specifier = ">=0.22.1" },
{ name = "uvloop", marker = "sys_platform != 'win32' and extra == 'performance'", specifier = ">=0.22.1" },
{ name = "websockets", marker = "extra == 'ide'", specifier = ">=12.0" },
]
provides-extras = ["memory", "repl", "ide", "interactive", "dev", "docs", "test", "performance", "publish"]
@@ -58,13 +58,22 @@ class ExecTool(BaseTool):
self._register_proc_actions()
def _resolve_shell(self) -> str:
"""Resolve shell - prefer zsh, fallback to bash/sh."""
"""Resolve shell - prefer zsh, fallback to bash/sh. On Windows: pwsh/cmd."""
import sys
import shutil
force_shell = os.environ.get("HANZO_MCP_FORCE_SHELL")
if force_shell:
return force_shell
# On Windows, prefer PowerShell/cmd
if sys.platform == "win32":
for shell in ("pwsh", "powershell", "cmd"):
found = shutil.which(shell)
if found:
return found
return "cmd.exe"
for shell in ["zsh", "bash", "fish", "dash", "sh"]:
for prefix in ["/opt/homebrew/bin", "/usr/local/bin", "/bin", "/usr/bin"]:
full_path = f"{prefix}/{shell}"
@@ -372,6 +381,8 @@ Auto-backgrounds commands after {AUTO_BACKGROUND_TIMEOUT}s.
)
# Resolve signal
import sys as _sys
if isinstance(signal, str):
signal_map = {
"TERM": 15,
@@ -385,7 +396,17 @@ Auto-backgrounds commands after {AUTO_BACKGROUND_TIMEOUT}s.
sig_num = signal
try:
os.kill(pid, sig_num)
if _sys.platform == "win32":
# On Windows, os.kill() only supports SIGTERM (terminate)
# and signal 0 (existence check). Use terminate semantics.
import subprocess
subprocess.call(
["taskkill", "/F", "/PID", str(pid)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
else:
os.kill(pid, sig_num)
return {
"proc_id": proc_id,
"pid": pid,
@@ -280,6 +280,10 @@ def resolve_shell_path(shell_name: str) -> Optional[str]:
if os.path.isfile(force_shell) and os.access(force_shell, os.X_OK):
return force_shell
# On Windows, skip Unix-specific paths and use shutil.which directly
if os.name == "nt":
return shutil.which(shell_name)
# Priority paths - Homebrew first on macOS
search_paths = [
f"/opt/homebrew/bin/{shell_name}", # Apple Silicon Homebrew
@@ -337,7 +341,14 @@ def get_active_shell() -> Tuple[str, str]:
best_path = resolve_shell_path(chosen_name)
return chosen_name, best_path or chosen_path
# Fallback: if detected shell isn't supported, default to zsh
# Fallback: if detected shell isn't supported, default to zsh (or pwsh on Windows)
if os.name == "nt":
for fallback in ["pwsh", "powershell", "cmd"]:
path = shutil.which(fallback)
if path:
return fallback, path
return "cmd", "cmd.exe"
for fallback in ["zsh", "bash", "fish", "dash"]:
path = resolve_shell_path(fallback)
if path:
+1 -1
View File
@@ -439,7 +439,7 @@ wheels = [
[[package]]
name = "hanzo-tools-shell"
version = "0.6.1"
version = "0.6.2"
source = { editable = "." }
dependencies = [
{ name = "hanzo-async" },
+10 -10
View File
@@ -12,6 +12,7 @@ import os
import sys
import json
import time
import shutil
import signal
import asyncio
import logging
@@ -244,6 +245,8 @@ class HanzoDevOrchestrator:
def _find_claude_code(self) -> str:
"""Find Claude Code executable."""
import shutil
# Check common locations
possible_paths = [
"/usr/local/bin/claude",
@@ -255,7 +258,7 @@ class HanzoDevOrchestrator:
for path in possible_paths:
expanded = Path(path).expanduser()
if expanded.exists() or (
path == "claude" and os.system(f"which {path} >/dev/null 2>&1") == 0
path == "claude" and shutil.which(path) is not None
):
return str(expanded) if expanded.exists() else path
@@ -1472,13 +1475,13 @@ Examples:
"""Use OpenAI CLI (Codex) - the official OpenAI CLI tool."""
try:
import json
import shutil
import subprocess
console.print("[dim]Using OpenAI CLI (Codex)...[/dim]")
# Check if openai CLI is installed
result = subprocess.run(["which", "openai"], capture_output=True, text=True)
if result.returncode != 0:
if not shutil.which("openai"):
console.print("[red]OpenAI CLI not installed![/red]")
console.print("[yellow]To install:[/yellow]")
console.print(" • pip install openai-cli")
@@ -1517,6 +1520,7 @@ Examples:
"""Use Claude Desktop/Code CLI."""
try:
import os
import shutil
import subprocess
console.print("[dim]Using Claude Desktop...[/dim]")
@@ -1531,11 +1535,7 @@ Examples:
claude_path = None
for path in claude_paths:
if (
os.path.exists(path)
or subprocess.run(["which", path], capture_output=True).returncode
== 0
):
if os.path.exists(path) or shutil.which(path) is not None:
claude_path = path
break
@@ -1587,13 +1587,13 @@ Examples:
async def _use_gemini_cli(self, message: str):
"""Use Gemini CLI."""
try:
import shutil
import subprocess
console.print("[dim]Using Gemini CLI...[/dim]")
# Check if gemini CLI is installed
result = subprocess.run(["which", "gemini"], capture_output=True, text=True)
if result.returncode != 0:
if not shutil.which("gemini"):
console.print("[red]Gemini CLI not installed![/red]")
console.print("[yellow]To install:[/yellow]")
console.print(" • pip install google-generativeai-cli")
+2 -5
View File
@@ -7,6 +7,7 @@ Tests all orchestrator modes and ensures everything works.
import os
import sys
import time
import shutil
import asyncio
import subprocess
from pathlib import Path
@@ -24,11 +25,7 @@ def print_test(name):
def check_command_exists(cmd):
"""Check if a command exists."""
try:
result = subprocess.run(["which", cmd], capture_output=True, text=True)
return result.returncode == 0
except Exception:
return False
return shutil.which(cmd) is not None
async def test_imports():