From 64cfa38fa6e94ec603436b0994e09e719fb4ce6c Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 6 Aug 2026 17:21:40 -0700 Subject: [PATCH] Port from PrimeIntellect-ai/prime-agent#630: report version transition after hermes update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit '✓ Update complete!' now shows what the update actually delivered: '✓ Update complete! (v0.19.4 → v0.20.0)' when the pyproject version changed, '(v0.20.0)' when commits landed within one release, and the plain message when the version cannot be read. Reads the on-disk pyproject.toml (not importlib.metadata, which still describes the old install after a pull). Applied to both the git and Windows-ZIP paths. --- hermes_cli/update_cmd.py | 47 ++++++++++++- .../hermes_cli/test_update_version_report.py | 67 +++++++++++++++++++ 2 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 tests/hermes_cli/test_update_version_report.py diff --git a/hermes_cli/update_cmd.py b/hermes_cli/update_cmd.py index 7813722860..76d69af6c4 100644 --- a/hermes_cli/update_cmd.py +++ b/hermes_cli/update_cmd.py @@ -838,6 +838,42 @@ def _print_update_completion(message: str) -> None: print(f"=== hermes-update completed {action_id} ===") +def _read_project_version() -> str | None: + """Read the ``version`` field from the checkout's pyproject.toml. + + Reads the on-disk file (not importlib.metadata) because after a git + pull the installed distribution metadata still describes the OLD + version; the file is the only source that reflects what was just + pulled. Returns None on any failure — version reporting is cosmetic + and must never break an update. + """ + try: + import tomllib + + with open(_m().PROJECT_ROOT / "pyproject.toml", "rb") as fh: + version = tomllib.load(fh).get("project", {}).get("version") + return str(version) if version else None + except Exception: + return None + + +def _update_complete_message(pre_version: str | None) -> str: + """Completion line with the version transition when it is known. + + Ported from PrimeIntellect-ai/prime-agent#630: after a successful + self-update, show both versions (``v0.19.4 → v0.20.0``) so the user + can see what they actually got. Falls back to the plain message when + either side is unknown or the version did not change (e.g. several + commits landed within one release). + """ + post_version = _read_project_version() + if pre_version and post_version and pre_version != post_version: + return f"✓ Update complete! (v{pre_version} → v{post_version})" + if post_version: + return f"✓ Update complete! (v{post_version})" + return "✓ Update complete!" + + def _update_via_zip(args, *, had_desktop_app_before_update: bool = False): """Update Hermes Agent by downloading a ZIP archive. @@ -850,6 +886,10 @@ def _update_via_zip(args, *, had_desktop_app_before_update: bool = False): import zipfile from urllib.request import urlretrieve + # Snapshot the pre-update version before files are replaced so the + # completion line can report the transition (prime-agent#630 port). + pre_update_version = _read_project_version() + # The ZIP fallback exists for Windows git-file-I/O breakage. It pulls a # static archive from GitHub, which is fine for the default "main" # channel but would silently ignore --branch and update from main even @@ -1205,7 +1245,7 @@ def _update_via_zip(args, *, had_desktop_app_before_update: bool = False): print(" Code and Python deps are updated, but the dashboard/TUI may") print(" be in a mixed state until the Node deps are rebuilt.") else: - _print_update_completion("✓ Update complete!") + _print_update_completion(_update_complete_message(pre_update_version)) try: _print_curator_first_run_notice() except Exception as e: @@ -4406,6 +4446,9 @@ def _cmd_update_impl(args, gateway_mode: bool): active_lazy_features = _m()._capture_active_lazy_features() active_tool_dependencies = _m()._capture_active_tool_dependencies() + # Snapshot the pre-update version before any code is pulled so the + # completion line can report the transition (prime-agent#630 port). + pre_update_version = _read_project_version() # In gateway mode, use file-based IPC for prompts instead of stdin gw_input_fn = ( (lambda prompt, default="": _gateway_prompt(prompt, default)) @@ -5582,7 +5625,7 @@ def _cmd_update_impl(args, gateway_mode: bool): print(" Code and Python deps are updated, but the dashboard/TUI may") print(" be in a mixed state until the Node deps are rebuilt.") else: - _print_update_completion("✓ Update complete!") + _print_update_completion(_update_complete_message(pre_update_version)) # Search-index optimization notice (v23). Existing installs keep their # working search index untouched on update; the compact v23 layout — diff --git a/tests/hermes_cli/test_update_version_report.py b/tests/hermes_cli/test_update_version_report.py new file mode 100644 index 0000000000..00e3e30981 --- /dev/null +++ b/tests/hermes_cli/test_update_version_report.py @@ -0,0 +1,67 @@ +"""Version transition reporting after ``hermes update``. + +Ported from PrimeIntellect-ai/prime-agent#630: a successful self-update +reports both versions (``v0.19.4 → v0.20.0``) when the pyproject version +changed, and degrades gracefully when either side is unknown. +""" + +from pathlib import Path + +import pytest + +from hermes_cli import update_cmd + + +def _write_pyproject(root: Path, version: str) -> None: + (root / "pyproject.toml").write_text( + f'[project]\nname = "hermes-agent"\nversion = "{version}"\n', + encoding="utf-8", + ) + + +@pytest.fixture() +def fake_root(tmp_path, monkeypatch): + class _FakeMain: + PROJECT_ROOT = tmp_path + + monkeypatch.setattr(update_cmd, "_m", lambda: _FakeMain) + return tmp_path + + +class TestReadProjectVersion: + def test_reads_version(self, fake_root): + _write_pyproject(fake_root, "0.20.0") + assert update_cmd._read_project_version() == "0.20.0" + + def test_missing_file_returns_none(self, fake_root): + assert update_cmd._read_project_version() is None + + def test_malformed_toml_returns_none(self, fake_root): + (fake_root / "pyproject.toml").write_text("not [ toml", encoding="utf-8") + assert update_cmd._read_project_version() is None + + +class TestUpdateCompleteMessage: + def test_reports_transition_when_version_changed(self, fake_root): + _write_pyproject(fake_root, "0.20.0") + assert ( + update_cmd._update_complete_message("0.19.4") + == "✓ Update complete! (v0.19.4 → v0.20.0)" + ) + + def test_same_version_reports_single_version(self, fake_root): + _write_pyproject(fake_root, "0.20.0") + assert ( + update_cmd._update_complete_message("0.20.0") + == "✓ Update complete! (v0.20.0)" + ) + + def test_unknown_pre_version_still_shows_current(self, fake_root): + _write_pyproject(fake_root, "0.20.0") + assert ( + update_cmd._update_complete_message(None) + == "✓ Update complete! (v0.20.0)" + ) + + def test_unknown_post_version_falls_back_to_plain(self, fake_root): + assert update_cmd._update_complete_message("0.19.4") == "✓ Update complete!"