mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
feat(cli): display custom profile alias names in profile list/show (#40371)
profile list and profile show assumed the wrapper script is always named after the profile (wrapper_dir / name). When a custom alias exists — e.g. `hermes profile alias steve --name qiaobusi` creates ~/.local/bin/qiaobusi pointing at `hermes -p steve` — the display silently showed the profile name (or nothing) instead of the alias the user actually typed. The custom-alias *creation* path (create_wrapper_script(name, target)) was added later; the *display* path was never updated to match. Add find_alias_for_profile() — a reverse lookup that scans the wrapper dir for our own wrappers (alias-named file containing 'hermes -p <profile>'), prefers a custom alias over the profile-named one, strips .bat on Windows, and sorts for deterministic output. Populate ProfileInfo.alias_name and wire it into the three display sites (profile describe, list, show). Credit: salvages the intent of #11506 by wss434631143, reimplemented on current main against the post-#11506 custom-alias (--name/target) mechanism. Tests: 6 new (profile-named, custom-name, none, unrelated-file rejection, windows .bat strip, list_profiles surfacing). All 123 in test_profiles pass. E2E verified against the real CLI for both custom and profile-named aliases.
This commit is contained in:
parent
c79b6f23e6
commit
5af899c7ca
3 changed files with 123 additions and 7 deletions
|
|
@ -11676,7 +11676,8 @@ def cmd_profile(args):
|
|||
)
|
||||
print(f"Skills: {p.skill_count} installed")
|
||||
if p.alias_path:
|
||||
print(f"Alias: {p.name} → hermes -p {p.name}")
|
||||
alias_display = p.alias_name or p.name
|
||||
print(f"Alias: {alias_display} → hermes -p {p.name}")
|
||||
break
|
||||
print()
|
||||
return
|
||||
|
|
@ -11708,7 +11709,7 @@ def cmd_profile(args):
|
|||
name = p.name
|
||||
model = (p.model or "—")[:26]
|
||||
gw = "running" if p.gateway_running else "stopped"
|
||||
alias = p.name if p.alias_path else "—"
|
||||
alias = (p.alias_name or p.name) if p.alias_path else "—"
|
||||
if p.is_default:
|
||||
alias = "—"
|
||||
if p.distribution_name:
|
||||
|
|
@ -11958,6 +11959,8 @@ def cmd_profile(args):
|
|||
_check_gateway_running,
|
||||
_count_skills,
|
||||
_read_distribution_meta,
|
||||
_get_wrapper_dir,
|
||||
find_alias_for_profile,
|
||||
)
|
||||
|
||||
if not profile_exists(name):
|
||||
|
|
@ -11968,7 +11971,7 @@ def cmd_profile(args):
|
|||
gw = _check_gateway_running(profile_dir)
|
||||
skills = _count_skills(profile_dir)
|
||||
dist_name, dist_version, dist_source = _read_distribution_meta(profile_dir)
|
||||
wrapper = _get_wrapper_dir() / name
|
||||
alias_name = find_alias_for_profile(name)
|
||||
|
||||
print(f"\nProfile: {name}")
|
||||
print(f"Path: {profile_dir}")
|
||||
|
|
@ -11987,8 +11990,10 @@ def cmd_profile(args):
|
|||
if dist_source:
|
||||
print(f"Installed from: {dist_source}")
|
||||
print(f" (run `hermes profile info {name}` for full manifest)")
|
||||
if wrapper.exists():
|
||||
print(f"Alias: {wrapper}")
|
||||
if alias_name:
|
||||
is_windows = sys.platform == "win32"
|
||||
wrapper = _get_wrapper_dir() / (f"{alias_name}.bat" if is_windows else alias_name)
|
||||
print(f"Alias: {alias_name} → hermes -p {name} ({wrapper})")
|
||||
print()
|
||||
|
||||
elif action == "alias":
|
||||
|
|
|
|||
|
|
@ -422,6 +422,50 @@ def remove_wrapper_script(name: str) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def find_alias_for_profile(profile_name: str) -> Optional[str]:
|
||||
"""Return the alias name of the wrapper that activates *profile_name*, or None.
|
||||
|
||||
A wrapper created by :func:`create_wrapper_script` is a file named after the
|
||||
alias whose body invokes ``hermes -p <profile>``. When the alias name equals
|
||||
the profile name this is trivial, but a custom alias (``hermes profile alias
|
||||
<profile> --name <custom>``) produces a differently-named file — so the
|
||||
display side cannot assume ``wrapper == profile`` and must reverse-look-up.
|
||||
|
||||
A custom alias (name != profile) is preferred over the profile-named wrapper
|
||||
so ``profile list``/``show`` surface the command the user actually typed.
|
||||
Results are sorted for deterministic output when several aliases match.
|
||||
"""
|
||||
wrapper_dir = _get_wrapper_dir()
|
||||
if not wrapper_dir.is_dir():
|
||||
return None
|
||||
canon = normalize_profile_name(profile_name)
|
||||
is_windows = sys.platform == "win32"
|
||||
needle = f"hermes -p {canon}"
|
||||
|
||||
custom: Optional[str] = None
|
||||
profile_named: Optional[str] = None
|
||||
for entry in sorted(wrapper_dir.iterdir()):
|
||||
if not entry.is_file():
|
||||
continue
|
||||
# Only our own wrappers are named with the alias and (on Windows) .bat.
|
||||
if is_windows and entry.suffix != ".bat":
|
||||
continue
|
||||
if not is_windows and entry.suffix:
|
||||
continue
|
||||
try:
|
||||
content = entry.read_text()
|
||||
except (OSError, UnicodeDecodeError):
|
||||
continue
|
||||
if needle not in content:
|
||||
continue
|
||||
alias = entry.stem if is_windows else entry.name
|
||||
if alias == canon:
|
||||
profile_named = alias
|
||||
elif custom is None:
|
||||
custom = alias
|
||||
return custom if custom is not None else profile_named
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ProfileInfo
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -438,6 +482,10 @@ class ProfileInfo:
|
|||
has_env: bool = False
|
||||
skill_count: int = 0
|
||||
alias_path: Optional[Path] = None
|
||||
# Custom alias name (the wrapper file name) when it differs from ``name``;
|
||||
# falls back to ``name`` when a profile-named wrapper exists. None if no
|
||||
# wrapper points at this profile. See ``find_alias_for_profile``.
|
||||
alias_name: Optional[str] = None
|
||||
# Distribution metadata (None if the profile wasn't installed from a distribution).
|
||||
distribution_name: Optional[str] = None
|
||||
distribution_version: Optional[str] = None
|
||||
|
|
@ -638,7 +686,12 @@ def list_profiles() -> List[ProfileInfo]:
|
|||
if not _PROFILE_ID_RE.match(name):
|
||||
continue
|
||||
model, provider = _read_config_model(entry)
|
||||
alias_path = wrapper_dir / name
|
||||
alias_name = find_alias_for_profile(name)
|
||||
if alias_name:
|
||||
is_windows = sys.platform == "win32"
|
||||
alias_path = wrapper_dir / (f"{alias_name}.bat" if is_windows else alias_name)
|
||||
else:
|
||||
alias_path = None
|
||||
dist_name, dist_version, dist_source = _read_distribution_meta(entry)
|
||||
meta = read_profile_meta(entry)
|
||||
profiles.append(ProfileInfo(
|
||||
|
|
@ -650,7 +703,8 @@ def list_profiles() -> List[ProfileInfo]:
|
|||
provider=provider,
|
||||
has_env=(entry / ".env").exists(),
|
||||
skill_count=_count_skills(entry),
|
||||
alias_path=alias_path if alias_path.exists() else None,
|
||||
alias_path=alias_path if (alias_path and alias_path.exists()) else None,
|
||||
alias_name=alias_name,
|
||||
distribution_name=dist_name,
|
||||
distribution_version=dist_version,
|
||||
distribution_source=dist_source,
|
||||
|
|
|
|||
|
|
@ -709,6 +709,63 @@ class TestWrapperScript:
|
|||
assert "#!/bin/sh" not in content
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# TestFindAliasForProfile — display-side reverse lookup
|
||||
# ===================================================================
|
||||
|
||||
class TestFindAliasForProfile:
|
||||
"""Tests for find_alias_for_profile() and alias display in list/show."""
|
||||
|
||||
def test_profile_named_alias(self, profile_env, monkeypatch):
|
||||
monkeypatch.setattr("sys.platform", "darwin")
|
||||
from hermes_cli.profiles import create_wrapper_script, find_alias_for_profile
|
||||
create_wrapper_script("steve")
|
||||
assert find_alias_for_profile("steve") == "steve"
|
||||
|
||||
def test_custom_alias_name_preferred(self, profile_env, monkeypatch):
|
||||
# qiaobusi -> steve-jobs: the custom alias name must surface, not the
|
||||
# profile name, because that's the command the user actually typed.
|
||||
monkeypatch.setattr("sys.platform", "darwin")
|
||||
from hermes_cli.profiles import create_wrapper_script, find_alias_for_profile
|
||||
create_wrapper_script("qiaobusi", target="steve")
|
||||
assert find_alias_for_profile("steve") == "qiaobusi"
|
||||
|
||||
def test_no_alias_returns_none(self, profile_env, monkeypatch):
|
||||
monkeypatch.setattr("sys.platform", "darwin")
|
||||
from hermes_cli.profiles import find_alias_for_profile
|
||||
assert find_alias_for_profile("steve") is None
|
||||
|
||||
def test_ignores_unrelated_files(self, profile_env, monkeypatch):
|
||||
# ~/.local/bin commonly holds unrelated binaries; they must not match.
|
||||
monkeypatch.setattr("sys.platform", "darwin")
|
||||
from hermes_cli.profiles import _get_wrapper_dir, find_alias_for_profile
|
||||
wrapper_dir = _get_wrapper_dir()
|
||||
wrapper_dir.mkdir(parents=True, exist_ok=True)
|
||||
(wrapper_dir / "pip").write_text("#!/bin/sh\nexec python -m pip \"$@\"\n")
|
||||
assert find_alias_for_profile("steve") is None
|
||||
|
||||
def test_custom_alias_on_windows(self, profile_env, monkeypatch):
|
||||
monkeypatch.setattr("sys.platform", "win32")
|
||||
from hermes_cli.profiles import create_wrapper_script, find_alias_for_profile
|
||||
create_wrapper_script("qiaobusi", target="steve")
|
||||
# The .bat extension must be stripped from the returned alias name.
|
||||
assert find_alias_for_profile("steve") == "qiaobusi"
|
||||
|
||||
def test_list_profiles_surfaces_custom_alias(self, profile_env, monkeypatch):
|
||||
monkeypatch.setattr("sys.platform", "darwin")
|
||||
from hermes_cli.profiles import (
|
||||
create_profile,
|
||||
create_wrapper_script,
|
||||
list_profiles,
|
||||
)
|
||||
create_profile("steve", no_alias=True)
|
||||
create_wrapper_script("qiaobusi", target="steve")
|
||||
info = next(p for p in list_profiles() if p.name == "steve")
|
||||
assert info.alias_name == "qiaobusi"
|
||||
assert info.alias_path is not None
|
||||
assert info.alias_path.name == "qiaobusi"
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# TestRenameProfile
|
||||
# ===================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue