From 384ec9684e86081c4add84d671d2bbf7c8ee69d4 Mon Sep 17 00:00:00 2001 From: alt-glitch Date: Fri, 15 May 2026 12:00:07 +0000 Subject: [PATCH] feat(banner): check PyPI for updates when not a git install For pip-installed hermes-agent (no .git directory), fall back to querying PyPI's JSON API to compare __version__ against the latest published release, using stdlib only (urllib + json, no packaging dep). --- hermes_cli/banner.py | 48 +++++++++++++++++++++- tests/hermes_cli/test_banner_pip_update.py | 35 ++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 tests/hermes_cli/test_banner_pip_update.py diff --git a/hermes_cli/banner.py b/hermes_cli/banner.py index 036412ac072..061992b4746 100644 --- a/hermes_cli/banner.py +++ b/hermes_cli/banner.py @@ -175,6 +175,49 @@ def _check_via_local_git(repo_dir: Path) -> Optional[int]: return None +def _version_tuple(v: str) -> tuple[int, ...]: + """Parse '0.13.0' into (0, 13, 0) for comparison. Non-numeric segments become 0.""" + parts = [] + for segment in v.split("."): + try: + parts.append(int(segment)) + except ValueError: + parts.append(0) + return tuple(parts) + + +def _fetch_pypi_latest(package: str = "hermes-agent") -> Optional[str]: + """Fetch the latest version of a package from PyPI. Returns None on failure.""" + try: + import urllib.request + import json as _json + url = f"https://pypi.org/pypi/{package}/json" + req = urllib.request.Request(url, headers={"Accept": "application/json"}) + with urllib.request.urlopen(req, timeout=5) as resp: + data = _json.loads(resp.read()) + return data.get("info", {}).get("version") + except Exception: + return None + + +def _check_via_pypi() -> Optional[int]: + """Compare installed version against PyPI latest. + + Returns 0 if up-to-date, 1 if behind, None on failure. + """ + latest = _fetch_pypi_latest() + if latest is None: + return None + if latest == VERSION: + return 0 + try: + if _version_tuple(latest) > _version_tuple(VERSION): + return 1 + return 0 + except Exception: + return 1 if latest != VERSION else 0 + + def check_for_updates() -> Optional[int]: """Check whether a Hermes update is available. @@ -213,8 +256,9 @@ def check_for_updates() -> Optional[int]: if not (repo_dir / ".git").exists(): repo_dir = hermes_home / "hermes-agent" if not (repo_dir / ".git").exists(): - return None - behind = _check_via_local_git(repo_dir) + behind = _check_via_pypi() + else: + behind = _check_via_local_git(repo_dir) try: cache_file.write_text(json.dumps({"ts": now, "behind": behind, "rev": embedded_rev})) diff --git a/tests/hermes_cli/test_banner_pip_update.py b/tests/hermes_cli/test_banner_pip_update.py new file mode 100644 index 00000000000..a0e9266f698 --- /dev/null +++ b/tests/hermes_cli/test_banner_pip_update.py @@ -0,0 +1,35 @@ +from unittest.mock import patch + + +def test_check_via_pypi_detects_update(): + """_check_via_pypi returns 1 when PyPI has newer version.""" + from hermes_cli.banner import _check_via_pypi + with patch("hermes_cli.banner.VERSION", "0.12.0"): + with patch("hermes_cli.banner._fetch_pypi_latest", return_value="0.13.0"): + result = _check_via_pypi() + assert result == 1 + + +def test_check_via_pypi_up_to_date(): + """_check_via_pypi returns 0 when versions match.""" + from hermes_cli.banner import _check_via_pypi + with patch("hermes_cli.banner.VERSION", "0.13.0"): + with patch("hermes_cli.banner._fetch_pypi_latest", return_value="0.13.0"): + result = _check_via_pypi() + assert result == 0 + + +def test_check_via_pypi_network_failure(): + """_check_via_pypi returns None on network error.""" + from hermes_cli.banner import _check_via_pypi + with patch("hermes_cli.banner._fetch_pypi_latest", return_value=None): + result = _check_via_pypi() + assert result is None + + +def test_version_tuple_comparison(): + """Version comparison works with multi-segment versions.""" + from hermes_cli.banner import _version_tuple + assert _version_tuple("0.13.0") > _version_tuple("0.12.0") + assert _version_tuple("0.13.0") == _version_tuple("0.13.0") + assert _version_tuple("1.0.0") > _version_tuple("0.99.99")