Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1055,10 +1055,11 @@ def _ensure_hermes_home_managed(home: Path):
"updates": {
# Run a full ``hermes backup``-style zip of HERMES_HOME before every
# ``hermes update``. Backups land in ``<HERMES_HOME>/backups/`` and
# can be restored with ``hermes import <path>``. Set to false to
# skip the backup entirely; use the ``--no-backup`` flag on a single
# update invocation to override just that run.
"pre_update_backup": True,
# can be restored with ``hermes import <path>``. Off by default —
# on large HERMES_HOME directories the zip can add minutes to every
# update. Set to true to re-enable, or pass ``--backup`` to opt in
# for a single update run.
"pre_update_backup": False,
# How many pre-update backup zips to retain. Older ones are pruned
# automatically after each successful backup.
"backup_keep": 5,
Expand Down
32 changes: 22 additions & 10 deletions hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6145,16 +6145,21 @@ def _ensure_fhs_path_guard() -> None:
def _run_pre_update_backup(args) -> None:
"""Create a full zip backup of HERMES_HOME before running the update.

Gated on ``updates.pre_update_backup`` in config (default true). The
``--no-backup`` flag on ``hermes update`` overrides it for one run.
Never raises — a backup failure should not block the update itself.
Gated on ``updates.pre_update_backup`` in config (default false). Off
by default because the zip can add minutes to every update on large
HERMES_HOME directories. The ``--backup`` flag on ``hermes update``
opts in for a single run; ``--no-backup`` forces it off when config
has it enabled. Never raises — a backup failure should not block the
update itself.
"""
# CLI flag wins over config
# CLI flags win over config. --no-backup beats --backup if both are set.
if getattr(args, "no_backup", False):
print("◆ Pre-update backup: skipped (--no-backup)")
print()
return

force_backup = bool(getattr(args, "backup", False))

try:
from hermes_cli.config import load_config
cfg = load_config()
Expand All @@ -6163,12 +6168,13 @@ def _run_pre_update_backup(args) -> None:
cfg = {}

updates_cfg = cfg.get("updates", {}) if isinstance(cfg, dict) else {}
enabled = updates_cfg.get("pre_update_backup", True)
enabled = updates_cfg.get("pre_update_backup", False)
keep = updates_cfg.get("backup_keep", 5)

if not enabled:
print("◆ Pre-update backup: disabled (updates.pre_update_backup=false in config.yaml)")
print()
if not enabled and not force_backup:
# Silent by default — the backup is off, most users don't need to
# hear about it on every update. They can opt in via --backup
# or by flipping the config knob.
return

try:
Expand Down Expand Up @@ -6221,8 +6227,8 @@ def _run_pre_update_backup(args) -> None:

print(f" Saved: {display_path} ({size_str}, {elapsed:.1f}s)")
print(f" Restore: hermes import {out_path}")
print(f" Disable: set updates.pre_update_backup: false in config.yaml")
print(f" (or pass --no-backup on a single update)")
print(f" Disable: omit --backup (backups are off by default)")
print(f" set updates.pre_update_backup: false in config.yaml")
print()


Expand Down Expand Up @@ -9671,6 +9677,12 @@ def cmd_claw(args):
default=False,
help="Skip the pre-update backup for this run (overrides updates.pre_update_backup)",
)
update_parser.add_argument(
"--backup",
action="store_true",
default=False,
help="Force a pre-update backup for this run (off by default; overrides updates.pre_update_backup)",
)
update_parser.set_defaults(func=cmd_update)

# =========================================================================
Expand Down
50 changes: 42 additions & 8 deletions tests/hermes_cli/test_backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -1344,9 +1344,10 @@ def hermes_home(self, tmp_path, monkeypatch):
del __import__("sys").modules[mod]
return root

def test_default_enabled_creates_backup(self, hermes_home, capsys):
def test_backup_flag_creates_backup(self, hermes_home, capsys):
"""--backup forces the pre-update backup for one run even when config is off."""
from hermes_cli.main import _run_pre_update_backup
_run_pre_update_backup(Namespace(no_backup=False))
_run_pre_update_backup(Namespace(no_backup=False, backup=True))
out = capsys.readouterr().out
assert "Creating pre-update backup" in out
assert "Saved:" in out
Expand All @@ -1357,9 +1358,20 @@ def test_default_enabled_creates_backup(self, hermes_home, capsys):
backups = list((hermes_home / "backups").glob("pre-update-*.zip"))
assert len(backups) == 1

def test_default_disabled_is_silent(self, hermes_home, capsys):
"""With the default-off config and no --backup flag, the hook is silent
and creates no backup. This is the common case for every update."""
from hermes_cli.main import _run_pre_update_backup
_run_pre_update_backup(Namespace(no_backup=False, backup=False))
out = capsys.readouterr().out
assert out == ""
assert not (hermes_home / "backups").exists() or not list(
(hermes_home / "backups").glob("pre-update-*.zip")
)

def test_no_backup_flag_skips(self, hermes_home, capsys):
from hermes_cli.main import _run_pre_update_backup
_run_pre_update_backup(Namespace(no_backup=True))
_run_pre_update_backup(Namespace(no_backup=True, backup=False))
out = capsys.readouterr().out
assert "skipped (--no-backup)" in out
assert "Creating pre-update backup" not in out
Expand All @@ -1368,7 +1380,30 @@ def test_no_backup_flag_skips(self, hermes_home, capsys):
(hermes_home / "backups").glob("pre-update-*.zip")
)

def test_config_disabled_skips(self, hermes_home, capsys):
def test_config_enabled_creates_backup(self, hermes_home, capsys):
"""Users who explicitly set updates.pre_update_backup: true still get
a backup on every update — this is the opt-in legacy behavior."""
import yaml
(hermes_home / "config.yaml").write_text(yaml.safe_dump({
"_config_version": 22,
"updates": {"pre_update_backup": True},
}))
import sys as _sys
for mod in list(_sys.modules.keys()):
if mod.startswith("hermes_cli.config"):
del _sys.modules[mod]

from hermes_cli.main import _run_pre_update_backup
_run_pre_update_backup(Namespace(no_backup=False, backup=False))
out = capsys.readouterr().out
assert "Creating pre-update backup" in out
assert "Saved:" in out
backups = list((hermes_home / "backups").glob("pre-update-*.zip"))
assert len(backups) == 1

def test_config_disabled_is_silent(self, hermes_home, capsys):
"""Explicit pre_update_backup: false behaves the same as the default —
silent no-op, no message spam."""
import yaml
(hermes_home / "config.yaml").write_text(yaml.safe_dump({
"_config_version": 22,
Expand All @@ -1381,10 +1416,9 @@ def test_config_disabled_skips(self, hermes_home, capsys):
del _sys.modules[mod]

from hermes_cli.main import _run_pre_update_backup
_run_pre_update_backup(Namespace(no_backup=False))
_run_pre_update_backup(Namespace(no_backup=False, backup=False))
out = capsys.readouterr().out
assert "disabled" in out
assert "updates.pre_update_backup=false" in out
assert out == ""
assert not list((hermes_home / "backups").glob("pre-update-*.zip")) \
if (hermes_home / "backups").exists() else True

Expand All @@ -1401,6 +1435,6 @@ def test_cli_flag_overrides_enabled_config(self, hermes_home, capsys):
del _sys.modules[mod]

from hermes_cli.main import _run_pre_update_backup
_run_pre_update_backup(Namespace(no_backup=True))
_run_pre_update_backup(Namespace(no_backup=True, backup=False))
out = capsys.readouterr().out
assert "skipped (--no-backup)" in out
Loading