mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix: preserve profile name completion in dynamic shell completion
The dynamic parser walker from the contributor's commit lost the profile name tab-completion that existed in the old static generators. This adds it back for all three shells: - Bash: _hermes_profiles() helper, -p/--profile completion, profile action→name completion (use/delete/show/alias/rename/export) - Zsh: _hermes_profiles() function, -p/--profile argument spec, profile action case with name completion - Fish: __hermes_profiles function, -s p -l profile flag, profile action completions Also removes the dead fallback path in cmd_completion() that imported the old static generators from profiles.py (parser is always available via the lambda wiring) and adds 11 regression-prevention tests for profile completion.
This commit is contained in:
parent
c95b1c5096
commit
b867171291
3 changed files with 196 additions and 30 deletions
|
|
@ -59,7 +59,26 @@ def generate_bash(parser: argparse.ArgumentParser) -> str:
|
|||
cases: list[str] = []
|
||||
for cmd in sorted(tree["subcommands"]):
|
||||
info = tree["subcommands"][cmd]
|
||||
if info["subcommands"]:
|
||||
if cmd == "profile" and info["subcommands"]:
|
||||
# Profile subcommand: complete actions, then profile names for
|
||||
# actions that accept a profile argument.
|
||||
subcmds = " ".join(sorted(info["subcommands"]))
|
||||
profile_actions = "use delete show alias rename export"
|
||||
cases.append(
|
||||
f" profile)\n"
|
||||
f" case \"$prev\" in\n"
|
||||
f" profile)\n"
|
||||
f" COMPREPLY=($(compgen -W \"{subcmds}\" -- \"$cur\"))\n"
|
||||
f" return\n"
|
||||
f" ;;\n"
|
||||
f" {profile_actions.replace(' ', '|')})\n"
|
||||
f" COMPREPLY=($(compgen -W \"$(_hermes_profiles)\" -- \"$cur\"))\n"
|
||||
f" return\n"
|
||||
f" ;;\n"
|
||||
f" esac\n"
|
||||
f" ;;"
|
||||
)
|
||||
elif info["subcommands"]:
|
||||
subcmds = " ".join(sorted(info["subcommands"]))
|
||||
cases.append(
|
||||
f" {cmd})\n"
|
||||
|
|
@ -82,12 +101,27 @@ def generate_bash(parser: argparse.ArgumentParser) -> str:
|
|||
# Add to ~/.bashrc:
|
||||
# eval "$(hermes completion bash)"
|
||||
|
||||
_hermes_profiles() {{
|
||||
local profiles_dir="$HOME/.hermes/profiles"
|
||||
local profiles="default"
|
||||
if [ -d "$profiles_dir" ]; then
|
||||
profiles="$profiles $(ls "$profiles_dir" 2>/dev/null)"
|
||||
fi
|
||||
echo "$profiles"
|
||||
}}
|
||||
|
||||
_hermes_completion() {{
|
||||
local cur prev
|
||||
COMPREPLY=()
|
||||
cur="${{COMP_WORDS[COMP_CWORD]}}"
|
||||
prev="${{COMP_WORDS[COMP_CWORD-1]}}"
|
||||
|
||||
# Complete profile names after -p / --profile
|
||||
if [[ "$prev" == "-p" || "$prev" == "--profile" ]]; then
|
||||
COMPREPLY=($(compgen -W "$(_hermes_profiles)" -- "$cur"))
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ $COMP_CWORD -ge 2 ]]; then
|
||||
case "${{COMP_WORDS[1]}}" in
|
||||
{cases_str}
|
||||
|
|
@ -121,21 +155,46 @@ def generate_zsh(parser: argparse.ArgumentParser) -> str:
|
|||
info = tree["subcommands"][cmd]
|
||||
if not info["subcommands"]:
|
||||
continue
|
||||
sub_lines: list[str] = []
|
||||
for sc in sorted(info["subcommands"]):
|
||||
sh = _clean(info["subcommands"][sc].get("help", ""))
|
||||
sub_lines.append(f" '{sc}:{sh}'")
|
||||
sub_str = "\n".join(sub_lines)
|
||||
safe = cmd.replace("-", "_")
|
||||
sub_cases.append(
|
||||
f" {cmd})\n"
|
||||
f" local -a {safe}_cmds\n"
|
||||
f" {safe}_cmds=(\n"
|
||||
f"{sub_str}\n"
|
||||
f" )\n"
|
||||
f" _describe '{cmd} command' {safe}_cmds\n"
|
||||
f" ;;"
|
||||
)
|
||||
if cmd == "profile":
|
||||
# Profile subcommand: complete actions, then profile names for
|
||||
# actions that accept a profile argument.
|
||||
sub_lines: list[str] = []
|
||||
for sc in sorted(info["subcommands"]):
|
||||
sh = _clean(info["subcommands"][sc].get("help", ""))
|
||||
sub_lines.append(f" '{sc}:{sh}'")
|
||||
sub_str = "\n".join(sub_lines)
|
||||
sub_cases.append(
|
||||
f" profile)\n"
|
||||
f" case ${{line[2]}} in\n"
|
||||
f" use|delete|show|alias|rename|export)\n"
|
||||
f" _hermes_profiles\n"
|
||||
f" ;;\n"
|
||||
f" *)\n"
|
||||
f" local -a profile_cmds\n"
|
||||
f" profile_cmds=(\n"
|
||||
f"{sub_str}\n"
|
||||
f" )\n"
|
||||
f" _describe 'profile command' profile_cmds\n"
|
||||
f" ;;\n"
|
||||
f" esac\n"
|
||||
f" ;;"
|
||||
)
|
||||
else:
|
||||
sub_lines = []
|
||||
for sc in sorted(info["subcommands"]):
|
||||
sh = _clean(info["subcommands"][sc].get("help", ""))
|
||||
sub_lines.append(f" '{sc}:{sh}'")
|
||||
sub_str = "\n".join(sub_lines)
|
||||
safe = cmd.replace("-", "_")
|
||||
sub_cases.append(
|
||||
f" {cmd})\n"
|
||||
f" local -a {safe}_cmds\n"
|
||||
f" {safe}_cmds=(\n"
|
||||
f"{sub_str}\n"
|
||||
f" )\n"
|
||||
f" _describe '{cmd} command' {safe}_cmds\n"
|
||||
f" ;;"
|
||||
)
|
||||
sub_cases_str = "\n".join(sub_cases)
|
||||
|
||||
return f"""#compdef hermes
|
||||
|
|
@ -143,6 +202,15 @@ def generate_zsh(parser: argparse.ArgumentParser) -> str:
|
|||
# Add to ~/.zshrc:
|
||||
# eval "$(hermes completion zsh)"
|
||||
|
||||
_hermes_profiles() {{
|
||||
local -a profiles
|
||||
profiles=(default)
|
||||
if [[ -d "$HOME/.hermes/profiles" ]]; then
|
||||
profiles+=("${{(@f)$(ls $HOME/.hermes/profiles 2>/dev/null)}}")
|
||||
fi
|
||||
_describe 'profile' profiles
|
||||
}}
|
||||
|
||||
_hermes() {{
|
||||
local context state line
|
||||
typeset -A opt_args
|
||||
|
|
@ -150,6 +218,7 @@ _hermes() {{
|
|||
_arguments -C \\
|
||||
'(-h --help){{-h,--help}}[Show help and exit]' \\
|
||||
'(-V --version){{-V,--version}}[Show version and exit]' \\
|
||||
'(-p --profile){{-p,--profile}}[Profile name]:profile:_hermes_profiles' \\
|
||||
'1:command:->commands' \\
|
||||
'*::arg:->args'
|
||||
|
||||
|
|
@ -187,9 +256,21 @@ def generate_fish(parser: argparse.ArgumentParser) -> str:
|
|||
"# Add to your config:",
|
||||
"# hermes completion fish | source",
|
||||
"",
|
||||
"# Helper: list available profiles",
|
||||
"function __hermes_profiles",
|
||||
" echo default",
|
||||
" if test -d $HOME/.hermes/profiles",
|
||||
" ls $HOME/.hermes/profiles 2>/dev/null",
|
||||
" end",
|
||||
"end",
|
||||
"",
|
||||
"# Disable file completion by default",
|
||||
"complete -c hermes -f",
|
||||
"",
|
||||
"# Complete profile names after -p / --profile",
|
||||
"complete -c hermes -f -s p -l profile"
|
||||
" -d 'Profile name' -xa '(__hermes_profiles)'",
|
||||
"",
|
||||
"# Top-level subcommands",
|
||||
]
|
||||
|
||||
|
|
@ -205,6 +286,8 @@ def generate_fish(parser: argparse.ArgumentParser) -> str:
|
|||
lines.append("")
|
||||
lines.append("# Subcommand completions")
|
||||
|
||||
profile_name_actions = {"use", "delete", "show", "alias", "rename", "export"}
|
||||
|
||||
for cmd in top_cmds:
|
||||
info = tree["subcommands"][cmd]
|
||||
if not info["subcommands"]:
|
||||
|
|
@ -218,6 +301,15 @@ def generate_fish(parser: argparse.ArgumentParser) -> str:
|
|||
f"-n '__fish_seen_subcommand_from {cmd}' "
|
||||
f"-a {sc} -d '{sh}'"
|
||||
)
|
||||
# For profile subcommand, complete profile names for relevant actions
|
||||
if cmd == "profile":
|
||||
for action in sorted(profile_name_actions):
|
||||
lines.append(
|
||||
f"complete -c hermes -f "
|
||||
f"-n '__fish_seen_subcommand_from {action}; "
|
||||
f"and __fish_seen_subcommand_from profile' "
|
||||
f"-a '(__hermes_profiles)' -d 'Profile name'"
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
|
|
|||
|
|
@ -4428,20 +4428,12 @@ def cmd_completion(args, parser=None):
|
|||
"""Print shell completion script."""
|
||||
from hermes_cli.completion import generate_bash, generate_zsh, generate_fish
|
||||
shell = getattr(args, "shell", "bash")
|
||||
if parser is not None:
|
||||
if shell == "zsh":
|
||||
print(generate_zsh(parser))
|
||||
elif shell == "fish":
|
||||
print(generate_fish(parser))
|
||||
else:
|
||||
print(generate_bash(parser))
|
||||
if shell == "zsh":
|
||||
print(generate_zsh(parser))
|
||||
elif shell == "fish":
|
||||
print(generate_fish(parser))
|
||||
else:
|
||||
# Fallback: parser not available (e.g. called outside main())
|
||||
from hermes_cli.profiles import generate_bash_completion, generate_zsh_completion
|
||||
if shell == "zsh":
|
||||
print(generate_zsh_completion())
|
||||
else:
|
||||
print(generate_bash_completion())
|
||||
print(generate_bash(parser))
|
||||
|
||||
|
||||
def cmd_logs(args):
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ def _make_parser() -> argparse.ArgumentParser:
|
|||
"""Build a minimal parser that mirrors the real hermes structure."""
|
||||
p = argparse.ArgumentParser(prog="hermes")
|
||||
p.add_argument("--version", "-V", action="store_true")
|
||||
p.add_argument("-p", "--profile", help="Profile name")
|
||||
sub = p.add_subparsers(dest="command")
|
||||
|
||||
chat = sub.add_parser("chat", help="Interactive chat with the agent")
|
||||
|
|
@ -39,6 +40,17 @@ def _make_parser() -> argparse.ArgumentParser:
|
|||
sess_sub.add_parser("list", help="List sessions")
|
||||
sess_sub.add_parser("delete", help="Delete a session")
|
||||
|
||||
prof = sub.add_parser("profile", help="Manage profiles")
|
||||
prof_sub = prof.add_subparsers(dest="profile_command")
|
||||
prof_sub.add_parser("list", help="List profiles")
|
||||
prof_sub.add_parser("use", help="Switch to a profile")
|
||||
prof_sub.add_parser("create", help="Create a new profile")
|
||||
prof_sub.add_parser("delete", help="Delete a profile")
|
||||
prof_sub.add_parser("show", help="Show profile details")
|
||||
prof_sub.add_parser("alias", help="Set profile alias")
|
||||
prof_sub.add_parser("rename", help="Rename a profile")
|
||||
prof_sub.add_parser("export", help="Export a profile")
|
||||
|
||||
sub.add_parser("version", help="Show version")
|
||||
|
||||
return p
|
||||
|
|
@ -51,7 +63,7 @@ def _make_parser() -> argparse.ArgumentParser:
|
|||
class TestWalk:
|
||||
def test_top_level_subcommands_extracted(self):
|
||||
tree = _walk(_make_parser())
|
||||
assert set(tree["subcommands"].keys()) == {"chat", "gateway", "sessions", "version"}
|
||||
assert set(tree["subcommands"].keys()) == {"chat", "gateway", "sessions", "profile", "version"}
|
||||
|
||||
def test_nested_subcommands_extracted(self):
|
||||
tree = _walk(_make_parser())
|
||||
|
|
@ -187,3 +199,73 @@ class TestSubcommandDrift:
|
|||
}
|
||||
missing = required - defined
|
||||
assert not missing, f"Missing from _SUBCOMMANDS: {missing}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 6. Profile completion (regression prevention)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestProfileCompletion:
|
||||
"""Ensure profile name completion is present in all shell outputs."""
|
||||
|
||||
def test_bash_has_profiles_helper(self):
|
||||
out = generate_bash(_make_parser())
|
||||
assert "_hermes_profiles()" in out
|
||||
assert 'profiles_dir="$HOME/.hermes/profiles"' in out
|
||||
|
||||
def test_bash_completes_profiles_after_p_flag(self):
|
||||
out = generate_bash(_make_parser())
|
||||
assert '"-p"' in out or "== \"-p\"" in out
|
||||
assert '"--profile"' in out or '== "--profile"' in out
|
||||
assert "_hermes_profiles" in out
|
||||
|
||||
def test_bash_profile_subcommand_has_action_completion(self):
|
||||
out = generate_bash(_make_parser())
|
||||
assert "use|delete|show|alias|rename|export)" in out
|
||||
|
||||
def test_bash_profile_actions_complete_profile_names(self):
|
||||
"""After 'hermes profile use', complete with profile names."""
|
||||
out = generate_bash(_make_parser())
|
||||
# The profile case should have _hermes_profiles for name-taking actions
|
||||
lines = out.split("\n")
|
||||
in_profile_case = False
|
||||
has_profiles_in_action = False
|
||||
for line in lines:
|
||||
if "profile)" in line:
|
||||
in_profile_case = True
|
||||
if in_profile_case and "_hermes_profiles" in line:
|
||||
has_profiles_in_action = True
|
||||
break
|
||||
assert has_profiles_in_action, "profile actions should complete with _hermes_profiles"
|
||||
|
||||
def test_zsh_has_profiles_helper(self):
|
||||
out = generate_zsh(_make_parser())
|
||||
assert "_hermes_profiles()" in out
|
||||
assert "$HOME/.hermes/profiles" in out
|
||||
|
||||
def test_zsh_has_profile_flag_completion(self):
|
||||
out = generate_zsh(_make_parser())
|
||||
assert "--profile" in out
|
||||
assert "_hermes_profiles" in out
|
||||
|
||||
def test_zsh_profile_actions_complete_names(self):
|
||||
out = generate_zsh(_make_parser())
|
||||
assert "use|delete|show|alias|rename|export)" in out
|
||||
|
||||
def test_fish_has_profiles_helper(self):
|
||||
out = generate_fish(_make_parser())
|
||||
assert "__hermes_profiles" in out
|
||||
assert "$HOME/.hermes/profiles" in out
|
||||
|
||||
def test_fish_has_profile_flag_completion(self):
|
||||
out = generate_fish(_make_parser())
|
||||
assert "-s p -l profile" in out
|
||||
assert "(__hermes_profiles)" in out
|
||||
|
||||
def test_fish_profile_actions_complete_names(self):
|
||||
out = generate_fish(_make_parser())
|
||||
# Should have profile name completion for actions like use, delete, etc.
|
||||
assert "__hermes_profiles" in out
|
||||
count = out.count("(__hermes_profiles)")
|
||||
# At least the -p flag + the profile action completions
|
||||
assert count >= 2, f"Expected >=2 profile completion entries, got {count}"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue