fix(skills): name stale index entries instead of generic fetch failure

_resolve_source_meta_and_bundle already distinguishes index-hit-without-
files from unknown identifiers, but do_install printed the same generic
'Could not fetch' for both, sending users off to re-check spellings for
what is actually a stale skills.sh entry. Split the message, and add a
staleness caveat to do_search results from skills.sh.

Fixes #3259. Supersedes #3261 (stale since July — re-applied onto the
current _print_fetch_failure helper).
This commit is contained in:
nikkoxgonzales
2026-09-10 05:21:17 +08:00
committed by Teknium
parent 75ade17617
commit 257a704d18
2 changed files with 96 additions and 3 deletions

View File

@@ -341,7 +341,13 @@ def do_search(query: str, source: str = "all", limit: int = 10, console: Optiona
c.print(table)
c.print("[dim]Use: hermes skills inspect <identifier> to preview, "
"hermes skills install <identifier> to install "
"(--json for scripting)[/]\n")
"(--json for scripting)[/]")
if any(r.source in ("skills.sh", "skills-sh") for r in results):
c.print("[dim yellow]Note:[/] [dim]skills.sh results may include entries whose "
"upstream files were removed or renamed; a 'stale index entry' error "
"at install means the skill no longer exists there.[/]\n")
else:
c.print()
def _rank_and_page(all_results, page: int, page_size: int):
@@ -585,7 +591,16 @@ def _pinned_sources(c: Console, sources, source_id: Optional[str], identifier: s
return None
def _print_fetch_failure(c: Console, sources, identifier: str) -> None:
def _print_fetch_failure(c: Console, sources, identifier: str, meta=None, source=None) -> None:
# Index hit but files gone (GitHub 404): a stale index entry, not a user
# typo — name it so users stop re-trying spellings (#3259).
if meta is not None:
src_id = getattr(source, "source_id", lambda: "the registry")()
c.print(f"[bold red]Error:[/] '{identifier}' is listed in the {src_id} index, "
f"but its files no longer exist upstream.")
c.print("[dim]Stale index entry: the skill was likely renamed or removed by "
"its author. Try `hermes skills search` for an alternative.[/]\n")
return
rate_limited = any(getattr(src, "is_rate_limited", False)
or getattr(getattr(src, "github", None), "is_rate_limited", False)
for src in sources)
@@ -663,7 +678,7 @@ def do_install(identifier: str, category: str = "", force: bool = False,
c.print(f"\n[bold]Fetching:[/] {identifier}")
meta, bundle, _matched_source = _resolve_source_meta_and_bundle(identifier, sources)
if not bundle:
_print_fetch_failure(c, sources, identifier)
_print_fetch_failure(c, sources, identifier, meta=meta, source=_matched_source)
return
if not _resolve_url_bundle_name(c, bundle, meta, identifier, name_override, skip_confirm):
return

View File

@@ -496,3 +496,81 @@ def test_do_update_unmodified_skill_updates_normally(monkeypatch, tmp_path):
assert installs == ["someone/hub-skill"]
assert "Updated 1 skill(s)" in sink.getvalue()
# ---------------------------------------------------------------------------
# Stale index entry messages (#3259)
# ---------------------------------------------------------------------------
def _stale_env(monkeypatch):
"""do_install where the index has metadata but the files are gone (404)."""
import hermes_cli.skills_hub as cli_hub
import tools.skills_hub as hub
class StaleSource:
def source_id(self):
return "skills-sh"
meta = type("Meta", (), {"identifier": "skills-sh/org/gone-skill"})()
monkeypatch.setattr(hub, "ensure_hub_dirs", lambda: None)
monkeypatch.setattr(cli_hub, "_sources", lambda: [StaleSource()])
monkeypatch.setattr(
cli_hub, "_resolve_source_meta_and_bundle",
lambda identifier, sources: (meta, None, sources[0]))
sink = StringIO()
console = Console(file=sink, force_terminal=False, color_system=None)
return console, sink
def test_do_install_stale_index_names_the_problem(monkeypatch):
"""Index hit + missing files reads as a stale entry, not a typo (#3259)."""
from hermes_cli.skills_hub import do_install
console, sink = _stale_env(monkeypatch)
do_install("skills-sh/org/gone-skill", console=console, skip_confirm=True)
out = sink.getvalue()
assert "Stale index entry" in out
assert "skills-sh" in out
assert "Could not fetch" not in out
def test_do_install_unknown_identifier_stays_generic(monkeypatch):
"""No index hit at all keeps the original generic message."""
import hermes_cli.skills_hub as cli_hub
import tools.skills_hub as hub
from hermes_cli.skills_hub import do_install
monkeypatch.setattr(hub, "ensure_hub_dirs", lambda: None)
monkeypatch.setattr(cli_hub, "_sources", lambda: [object()])
monkeypatch.setattr(
cli_hub, "_resolve_source_meta_and_bundle",
lambda identifier, sources: (None, None, None))
sink = StringIO()
console = Console(file=sink, force_terminal=False, color_system=None)
do_install("nobody/nowhere/nothing", console=console, skip_confirm=True)
out = sink.getvalue()
assert "Could not fetch" in out
assert "Stale index entry" not in out
def test_do_search_warns_about_skills_sh_staleness(monkeypatch):
"""Search results from skills.sh carry the stale-index caveat."""
import tools.skills_hub_search as hub_search
from hermes_cli.skills_hub import do_search
import hermes_cli.skills_hub as cli_hub
row = type("Row", (), {
"name": "gone-skill", "description": "d", "source": "skills-sh",
"trust_level": "community",
"identifier": "skills-sh/org/gone-skill"})()
monkeypatch.setattr(cli_hub, "_sources", lambda: [])
monkeypatch.setattr(hub_search, "unified_search",
lambda query, sources, source_filter, limit: [row])
sink = StringIO()
console = Console(file=sink, force_terminal=False, color_system=None)
do_search("gone", console=console)
assert "stale index entry" in sink.getvalue()