Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
25 changes: 25 additions & 0 deletions hermes_cli/skills_hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,31 @@ def do_uninstall(name: str, console: Optional[Console] = None,
success, msg = uninstall_skill(name)
if success:
c.print(f"[bold green]{msg}[/]\n")
# Prune any lingering disable entry so a future re-install of the same
# skill isn't silently disabled. The config.yaml disable state
# (skills.disabled / skills.platform_disabled) is separate from the hub
# lock file, which uninstall_skill already reconciles on its own.
try:
from hermes_cli.config import load_config, save_config
cfg = load_config()
skills_cfg = cfg.get("skills")
if isinstance(skills_cfg, dict):
changed = False
global_disabled = skills_cfg.get("disabled")
if isinstance(global_disabled, list) and name in global_disabled:
skills_cfg["disabled"] = [s for s in global_disabled if s != name]
changed = True
platform_disabled = skills_cfg.get("platform_disabled")
if isinstance(platform_disabled, dict):
for platform, names in list(platform_disabled.items()):
if isinstance(names, list) and name in names:
platform_disabled[platform] = [s for s in names if s != name]
changed = True
if changed:
save_config(cfg)
except Exception:
# Best-effort: a config prune must never abort a successful uninstall.
pass
if invalidate_cache:
try:
from agent.prompt_builder import clear_skills_system_prompt_cache
Expand Down
63 changes: 63 additions & 0 deletions tests/hermes_cli/test_skills_skip_confirm.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from unittest.mock import patch

import yaml


class TestHandleSkillsSlashInstallFlags:
Expand Down Expand Up @@ -171,3 +172,65 @@ def test_without_skip_confirm_cancel(self):
patch("builtins.input", return_value="n"):
do_uninstall("test-skill", skip_confirm=False)
mock_uninstall.assert_not_called()


class TestDoUninstallPrunesDisableConfig:
"""A successful uninstall must also clear the skill's lingering disable
state in config.yaml (skills.disabled / skills.platform_disabled), which is
stored separately from the hub lock file. Otherwise a later re-install of
the same name comes back silently disabled and the enabled/disabled count
is wrong.
"""

def test_uninstall_prunes_global_and_platform_disabled(self, tmp_path, monkeypatch):
from hermes_cli.skills_hub import do_uninstall

config_path = tmp_path / "config.yaml"
config_path.write_text(
"skills:\n"
" disabled:\n"
" - my-skill\n"
" - keep-skill\n"
" platform_disabled:\n"
" telegram:\n"
" - my-skill\n"
" - keep-skill\n"
" cli:\n"
" - keep-skill\n"
)
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
monkeypatch.delenv("HERMES_MANAGED", raising=False)

with patch("hermes_cli.skills_hub._console"), \
patch("tools.skills_hub.uninstall_skill", return_value=(True, "Removed")):
do_uninstall("my-skill", skip_confirm=True, invalidate_cache=False)

saved = yaml.safe_load(config_path.read_text())
skills_cfg = saved["skills"]
# The uninstalled skill is gone from both global and per-platform lists...
assert "my-skill" not in skills_cfg.get("disabled", [])
assert "my-skill" not in skills_cfg.get("platform_disabled", {}).get("telegram", [])
# ...while unrelated disable entries are left untouched.
assert "keep-skill" in skills_cfg.get("disabled", [])
assert "keep-skill" in skills_cfg.get("platform_disabled", {}).get("telegram", [])
assert "keep-skill" in skills_cfg.get("platform_disabled", {}).get("cli", [])

def test_uninstall_without_disable_entry_is_noop(self, tmp_path, monkeypatch):
"""A skill that was never disabled must not perturb config on uninstall."""
from hermes_cli.skills_hub import do_uninstall

config_path = tmp_path / "config.yaml"
config_path.write_text(
"skills:\n"
" disabled:\n"
" - other-skill\n"
)
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
monkeypatch.delenv("HERMES_MANAGED", raising=False)

with patch("hermes_cli.skills_hub._console"), \
patch("tools.skills_hub.uninstall_skill", return_value=(True, "Removed")):
do_uninstall("my-skill", skip_confirm=True, invalidate_cache=False)

saved = yaml.safe_load(config_path.read_text())
assert saved["skills"]["disabled"] == ["other-skill"]
Loading