diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 480aba7bf..a414f01c8 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -2278,7 +2278,7 @@ def cmd_pairing(args): skills_inspect.add_argument("identifier", help="Skill identifier") skills_list = skills_subparsers.add_parser("list", help="List installed skills") - skills_list.add_argument("--source", default="all", choices=["all", "hub", "builtin"]) + skills_list.add_argument("--source", default="all", choices=["all", "hub", "builtin", "local"]) skills_audit = skills_subparsers.add_parser("audit", help="Re-scan installed hub skills") skills_audit.add_argument("name", nargs="?", help="Specific skill to audit (default: all)") diff --git a/hermes_cli/skills_hub.py b/hermes_cli/skills_hub.py index 8b72fe4f4..0fa3755cf 100644 --- a/hermes_cli/skills_hub.py +++ b/hermes_cli/skills_hub.py @@ -407,14 +407,16 @@ def do_inspect(identifier: str, console: Optional[Console] = None) -> None: def do_list(source_filter: str = "all", console: Optional[Console] = None) -> None: - """List installed skills, distinguishing builtins from hub-installed.""" + """List installed skills, distinguishing hub, builtin, and local skills.""" from tools.skills_hub import HubLockFile, ensure_hub_dirs + from tools.skills_sync import _read_manifest from tools.skills_tool import _find_all_skills c = console or _console ensure_hub_dirs() lock = HubLockFile() hub_installed = {e["name"]: e for e in lock.list_installed()} + bundled_manifest = set(_read_manifest().keys()) all_skills = _find_all_skills() @@ -424,21 +426,32 @@ def do_list(source_filter: str = "all", console: Optional[Console] = None) -> No table.add_column("Source", style="dim") table.add_column("Trust", style="dim") + hub_count = 0 + builtin_count = 0 + local_count = 0 + for skill in sorted(all_skills, key=lambda s: (s.get("category") or "", s["name"])): name = skill["name"] category = skill.get("category", "") hub_entry = hub_installed.get(name) if hub_entry: + source_kind = "hub" source_display = hub_entry.get("source", "hub") trust = hub_entry.get("trust_level", "community") - else: + hub_count += 1 + elif name in bundled_manifest: + source_kind = "builtin" source_display = "builtin" trust = "builtin" + builtin_count += 1 + else: + source_kind = "local" + source_display = "local" + trust = "community" + local_count += 1 - if source_filter == "hub" and not hub_entry: - continue - if source_filter == "builtin" and hub_entry: + if source_filter != "all" and source_filter != source_kind: continue trust_style = {"builtin": "bright_cyan", "trusted": "green", "community": "yellow"}.get(trust, "dim") @@ -446,8 +459,7 @@ def do_list(source_filter: str = "all", console: Optional[Console] = None) -> No table.add_row(name, category, source_display, f"[{trust_style}]{trust_label}[/]") c.print(table) - c.print(f"[dim]{len(hub_installed)} hub-installed, " - f"{len(all_skills) - len(hub_installed)} builtin[/]\n") + c.print(f"[dim]{hub_count} hub-installed, {builtin_count} builtin, {local_count} local[/]\n") def do_audit(name: Optional[str] = None, console: Optional[Console] = None) -> None: @@ -1014,7 +1026,7 @@ def _print_skills_help(console: Console) -> None: " [cyan]search[/] Search registries for skills\n" " [cyan]install[/] Install a skill (with security scan)\n" " [cyan]inspect[/] Preview a skill without installing\n" - " [cyan]list[/] [--source hub|builtin] List installed skills\n" + " [cyan]list[/] [--source hub|builtin|local] List installed skills\n" " [cyan]audit[/] [name] Re-scan hub skills for security\n" " [cyan]uninstall[/] Remove a hub-installed skill\n" " [cyan]publish[/] --repo Publish a skill to GitHub via PR\n" diff --git a/tests/hermes_cli/test_skills_hub.py b/tests/hermes_cli/test_skills_hub.py index 7b1165bec..bb773a43d 100644 --- a/tests/hermes_cli/test_skills_hub.py +++ b/tests/hermes_cli/test_skills_hub.py @@ -29,3 +29,62 @@ def test_do_list_initializes_hub_dir(monkeypatch, tmp_path): assert (hub_dir / "lock.json").exists() assert (hub_dir / "quarantine").is_dir() assert (hub_dir / "index-cache").is_dir() + + +def test_do_list_distinguishes_builtin_hub_and_local(monkeypatch): + import tools.skills_hub as hub + import tools.skills_sync as skills_sync + import tools.skills_tool as skills_tool + + class _FakeLock: + def list_installed(self): + return [{"name": "hub-skill", "source": "github", "trust_level": "community"}] + + monkeypatch.setattr(hub, "ensure_hub_dirs", lambda: None) + monkeypatch.setattr(hub, "HubLockFile", _FakeLock) + monkeypatch.setattr(skills_tool, "_find_all_skills", lambda: [ + {"name": "builtin-skill", "category": "content"}, + {"name": "hub-skill", "category": "content"}, + {"name": "local-skill", "category": "content"}, + ]) + monkeypatch.setattr(skills_sync, "_read_manifest", lambda: {"builtin-skill": "abc123"}) + + out = StringIO() + console = Console(file=out, force_terminal=False, color_system=None) + + do_list(console=console) + + rendered = out.getvalue() + assert "builtin-skill" in rendered and "builtin" in rendered + assert "hub-skill" in rendered and "github" in rendered + assert "local-skill" in rendered and "local" in rendered + assert "1 hub-installed, 1 builtin, 1 local" in rendered + + +def test_do_list_source_filter_local(monkeypatch): + import tools.skills_hub as hub + import tools.skills_sync as skills_sync + import tools.skills_tool as skills_tool + + class _FakeLock: + def list_installed(self): + return [{"name": "hub-skill", "source": "github", "trust_level": "community"}] + + monkeypatch.setattr(hub, "ensure_hub_dirs", lambda: None) + monkeypatch.setattr(hub, "HubLockFile", _FakeLock) + monkeypatch.setattr(skills_tool, "_find_all_skills", lambda: [ + {"name": "builtin-skill", "category": "content"}, + {"name": "hub-skill", "category": "content"}, + {"name": "local-skill", "category": "content"}, + ]) + monkeypatch.setattr(skills_sync, "_read_manifest", lambda: {"builtin-skill": "abc123"}) + + out = StringIO() + console = Console(file=out, force_terminal=False, color_system=None) + + do_list(source_filter="local", console=console) + + rendered = out.getvalue() + assert "local-skill" in rendered + assert "builtin-skill" not in rendered + assert "hub-skill" not in rendered