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:
kshitij
2026-06-06 08:08:07 +00:00
committed by GitHub
parent c79b6f23e6
commit 5af899c7ca
3 changed files with 123 additions and 7 deletions
+56 -2
View File
@@ -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,