fix(cli): use uv tool upgrade when Hermes is a uv tool install (#29700)

Hermes installed via `uv tool install hermes-agent` lives outside any
venv. `_cmd_update_pip` previously ran `uv pip install --upgrade`, which
errors with `No virtual environment found; run uv venv ...`. The user
hits this on the very first `hermes update` after a standard
non-`--system` install with `uv` on PATH.

Add `is_uv_tool_install()` in `hermes_cli/config.py`: fast path inspects
`sys.prefix` for the standard `uv/tools/hermes-agent/` layout, falls
back to `uv tool list` for non-standard prefixes. Both the
user-facing `recommended_update_command_for_method("pip")` string and
the actual subprocess invocation in `_cmd_update_pip` now switch to
`uv tool upgrade hermes-agent` when detected. Non-tool installs and the
no-`uv` fallback keep their existing commands unchanged.
This commit is contained in:
briandevans
2026-05-30 02:08:11 -07:00
committed by Teknium
parent 39f6b6e9d2
commit 1bdb29d938
3 changed files with 223 additions and 1 deletions
+38
View File
@@ -329,6 +329,42 @@ def stamp_install_method(method: str) -> None:
pass
def is_uv_tool_install(uv_path: Optional[str] = None) -> bool:
"""Return True when Hermes is installed via ``uv tool install hermes-agent``.
``uv tool`` installs live outside any virtualenv, so ``uv pip install``
(the previous update path) fails with ``No virtual environment found``.
The fast path inspects ``sys.prefix`` for the standard uv tool layout
(``.../uv/tools/hermes-agent/...``); the authoritative fallback shells
out to ``uv tool list``. Returns False on any error so callers fall
back to the legacy pip path.
"""
prefix = os.path.normpath(sys.prefix).replace(os.sep, "/").lower()
if "/uv/tools/hermes-agent/" in prefix + "/":
return True
if uv_path is None:
import shutil
uv_path = shutil.which("uv")
if not uv_path:
return False
try:
result = subprocess.run(
[uv_path, "tool", "list"],
capture_output=True,
text=True,
timeout=15,
)
except (OSError, subprocess.SubprocessError):
return False
if result.returncode != 0:
return False
for line in result.stdout.splitlines():
tokens = line.strip().split()
if tokens and tokens[0] == "hermes-agent":
return True
return False
def recommended_update_command_for_method(method: str) -> str:
"""Return the update command or guidance for a given install method."""
if method == "nixos":
@@ -341,6 +377,8 @@ def recommended_update_command_for_method(method: str) -> str:
import shutil
uv = shutil.which("uv")
if uv:
if is_uv_tool_install(uv):
return "uv tool upgrade hermes-agent"
return "uv pip install --upgrade hermes-agent"
return "pip install --upgrade hermes-agent"
return "hermes update"
+5 -1
View File
@@ -8971,13 +8971,17 @@ def cmd_update(args):
def _cmd_update_pip(args):
"""Update Hermes via pip (for PyPI installs)."""
from hermes_cli import __version__
from hermes_cli.config import is_uv_tool_install
print(f"→ Current version: {__version__}")
print("→ Checking PyPI for updates...")
uv = shutil.which("uv")
if uv:
cmd = [uv, "pip", "install", "--upgrade", "hermes-agent"]
if is_uv_tool_install(uv):
cmd = [uv, "tool", "upgrade", "hermes-agent"]
else:
cmd = [uv, "pip", "install", "--upgrade", "hermes-agent"]
else:
cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "hermes-agent"]