refactor(update): lift ZIP download/swap and deps reinstall out of _update_via_zip (327 -> 137 LOC)

This commit is contained in:
Teknium
2026-09-02 16:25:31 -07:00
parent e118abc61a
commit dc4de83c62
3 changed files with 86 additions and 60 deletions
+78 -58
View File
@@ -237,63 +237,18 @@ def _abort_zip_update_if_dirty_tree() -> None:
_m().sys.exit(1)
def _update_via_zip(args, *, had_desktop_app_before_update: bool = False) -> bool:
"""Update Hermes Agent by downloading a ZIP archive.
def _download_and_swap_zip(branch: str, zip_url: str) -> None:
"""Download the source ZIP for *branch* and two-phase swap it into the checkout.
Used on Windows when git file I/O is broken (antivirus, NTFS filter
drivers causing 'Invalid argument' errors on file creation).
Returns ``False`` when a Desktop rebuild ran and failed; ``True`` otherwise.
Exits the process (``sys.exit(1)``) on any failure; the two-phase replace
guarantees the install is either fully updated or fully rolled back.
"""
from hermes_cli.update_cmd import (
_ensure_uv_for_termux,
_ensure_venv_pip,
_finish_dashboard_update_cleanup,
_m,
_print_bundled_skills_sync_report,
_print_curator_first_run_notice,
_print_curator_recent_run_notice,
_print_update_summary,
_read_project_version,
_rebuild_desktop_after_update,
_refuse_update_for_contended_shims,
_shim_quarantine_error_type,
_sweep_bytecode_after_update,
_update_node_dependencies,
_validate_critical_modules_import,
_verify_and_restore_state_dbs_post_update,
)
active_tool_dependencies = _m()._capture_active_tool_dependencies()
from hermes_cli.update_cmd import _m
import tempfile
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 static GitHub archive is fine for "main" but would silently ignore
# --branch — the exact silent-divergence bug --branch was added to
# prevent. Refuse rather than lie.
branch = _m()._resolve_update_branch(args)
if branch != "main":
print(
f"✗ --branch={branch} is not supported on the Windows ZIP-fallback "
"update path."
)
print(
" This path runs when git file I/O is broken on the system. "
"Either resolve the git-side breakage (typically an antivirus "
"or NTFS filter holding files open) and rerun `hermes update "
f"--branch {branch}`, or update against main with `hermes update`."
)
_m().sys.exit(1)
_abort_zip_update_if_dirty_tree()
zip_url = (
f"https://github.com/NousResearch/hermes-agent/archive/refs/heads/{branch}.zip"
)
print("→ Downloading latest version...")
tmp_dir = tempfile.mkdtemp(prefix="hermes-update-")
try:
@@ -434,15 +389,16 @@ def _update_via_zip(args, *, had_desktop_app_before_update: bool = False) -> boo
finally:
shutil.rmtree(tmp_dir, ignore_errors=True)
_sweep_bytecode_after_update(branch)
# Reinstall Python deps: prefer .[all]; if one extra breaks, keep base
# deps and retry the remaining extras individually so working
# capabilities aren't silently stripped. Self-lock deferral (#86735): the
# code swap is committed; defer only the dependency sync when this
# process holds a native extension the sync must rewrite.
_m()._abort_dependency_sync_if_self_locked()
print("→ Updating Python dependencies...")
def _reinstall_python_deps_after_zip(active_tool_dependencies) -> None:
"""Reinstall Python deps (uv preferred, pip fallback) and re-arm active tool deps."""
from hermes_cli.update_cmd import (
_ensure_uv_for_termux,
_ensure_venv_pip,
_m,
_refuse_update_for_contended_shims,
_shim_quarantine_error_type,
)
from hermes_cli.managed_uv import ensure_uv, update_managed_uv
@@ -490,6 +446,70 @@ def _update_via_zip(args, *, had_desktop_app_before_update: bool = False) -> boo
# #70636).
_m()._refresh_active_memory_provider_dependencies()
def _update_via_zip(args, *, had_desktop_app_before_update: bool = False) -> bool:
"""Update Hermes Agent by downloading a ZIP archive.
Used on Windows when git file I/O is broken (antivirus, NTFS filter
drivers causing 'Invalid argument' errors on file creation).
Returns ``False`` when a Desktop rebuild ran and failed; ``True`` otherwise.
"""
from hermes_cli.update_cmd import (
_finish_dashboard_update_cleanup,
_m,
_print_bundled_skills_sync_report,
_print_curator_first_run_notice,
_print_curator_recent_run_notice,
_print_update_summary,
_read_project_version,
_rebuild_desktop_after_update,
_sweep_bytecode_after_update,
_update_node_dependencies,
_validate_critical_modules_import,
_verify_and_restore_state_dbs_post_update,
)
active_tool_dependencies = _m()._capture_active_tool_dependencies()
# 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 static GitHub archive is fine for "main" but would silently ignore
# --branch — the exact silent-divergence bug --branch was added to
# prevent. Refuse rather than lie.
branch = _m()._resolve_update_branch(args)
if branch != "main":
print(
f"✗ --branch={branch} is not supported on the Windows ZIP-fallback "
"update path."
)
print(
" This path runs when git file I/O is broken on the system. "
"Either resolve the git-side breakage (typically an antivirus "
"or NTFS filter holding files open) and rerun `hermes update "
f"--branch {branch}`, or update against main with `hermes update`."
)
_m().sys.exit(1)
_abort_zip_update_if_dirty_tree()
zip_url = (
f"https://github.com/NousResearch/hermes-agent/archive/refs/heads/{branch}.zip"
)
_download_and_swap_zip(branch, zip_url)
_sweep_bytecode_after_update(branch)
# Reinstall Python deps: prefer .[all]; if one extra breaks, keep base
# deps and retry the remaining extras individually so working
# capabilities aren't silently stripped. Self-lock deferral (#86735): the
# code swap is committed; defer only the dependency sync when this
# process holds a native extension the sync must rewrite.
_m()._abort_dependency_sync_if_self_locked()
print("→ Updating Python dependencies...")
_reinstall_python_deps_after_zip(active_tool_dependencies)
# Verify the tree actually imports (catches the parse-OK-but-skewed tree
# an interrupted copy leaves). Placed *after* the dependency reinstall so
# a genuinely-new third-party requirement isn't misreported as a partial
@@ -339,7 +339,10 @@ def test_swap_preserve_set_is_the_module_constant():
truth for the preserved entries (no comment-synced duplicate)."""
import inspect
src = inspect.getsource(update_cmd._update_via_zip)
from hermes_cli import update_cmd_zip
# The swap loop lives in the download/swap collaborator the ZIP path calls.
src = inspect.getsource(update_cmd_zip._download_and_swap_zip)
assert "preserve = _ZIP_PRESERVED_TOP_LEVEL" in src
@@ -439,7 +439,10 @@ def test_update_via_zip_wires_discard_into_the_commit_failure_path():
import inspect
import textwrap
src = textwrap.dedent(inspect.getsource(update_cmd._update_via_zip))
# The swap lives in the download/swap collaborator the ZIP path calls.
from hermes_cli import update_cmd_zip
src = textwrap.dedent(inspect.getsource(update_cmd_zip._download_and_swap_zip))
tree = ast.parse(src)
def _calls(node, name):