| """Shell completion script generation for hermes CLI. |
| |
| Walks the live argparse parser tree to generate accurate, always-up-to-date |
| completion scripts — no hardcoded subcommand lists, no extra dependencies. |
| |
| Supports bash, zsh, and fish. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| from typing import Any |
|
|
|
|
| def _walk(parser: argparse.ArgumentParser) -> dict[str, Any]: |
| """Recursively extract subcommands and flags from a parser. |
| |
| Uses _SubParsersAction._choices_actions to get canonical names (no aliases) |
| along with their help text. |
| """ |
| flags: list[str] = [] |
| subcommands: dict[str, Any] = {} |
|
|
| for action in parser._actions: |
| if isinstance(action, argparse._SubParsersAction): |
| |
| |
| seen: set[str] = set() |
| for pseudo in action._choices_actions: |
| name = pseudo.dest |
| if name in seen: |
| continue |
| seen.add(name) |
| subparser = action.choices.get(name) |
| if subparser is None: |
| continue |
| info = _walk(subparser) |
| info["help"] = _clean(pseudo.help or "") |
| subcommands[name] = info |
| elif action.option_strings: |
| flags.extend(o for o in action.option_strings if o.startswith("-")) |
|
|
| return {"flags": flags, "subcommands": subcommands} |
|
|
|
|
| def _clean(text: str, maxlen: int = 60) -> str: |
| """Strip shell-unsafe characters and truncate.""" |
| return text.replace("'", "").replace('"', "").replace("\\", "")[:maxlen] |
|
|
|
|
| |
| |
| |
|
|
| def generate_bash(parser: argparse.ArgumentParser) -> str: |
| tree = _walk(parser) |
| top_cmds = " ".join(sorted(tree["subcommands"])) |
|
|
| cases: list[str] = [] |
| for cmd in sorted(tree["subcommands"]): |
| info = tree["subcommands"][cmd] |
| if cmd == "profile" and info["subcommands"]: |
| |
| |
| 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" |
| f" COMPREPLY=($(compgen -W \"{subcmds}\" -- \"$cur\"))\n" |
| f" return\n" |
| f" ;;" |
| ) |
| elif info["flags"]: |
| flags = " ".join(info["flags"]) |
| cases.append( |
| f" {cmd})\n" |
| f" COMPREPLY=($(compgen -W \"{flags}\" -- \"$cur\"))\n" |
| f" return\n" |
| f" ;;" |
| ) |
|
|
| cases_str = "\n".join(cases) |
|
|
| return f"""# Hermes Agent bash completion |
| # 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} |
| esac |
| fi |
| |
| if [[ $COMP_CWORD -eq 1 ]]; then |
| COMPREPLY=($(compgen -W "{top_cmds}" -- "$cur")) |
| fi |
| }} |
| |
| complete -F _hermes_completion hermes |
| """ |
|
|
|
|
| |
| |
| |
|
|
| def generate_zsh(parser: argparse.ArgumentParser) -> str: |
| tree = _walk(parser) |
|
|
| top_cmds_lines: list[str] = [] |
| for cmd in sorted(tree["subcommands"]): |
| help_text = _clean(tree["subcommands"][cmd].get("help", "")) |
| top_cmds_lines.append(f" '{cmd}:{help_text}'") |
| top_cmds_str = "\n".join(top_cmds_lines) |
|
|
| sub_cases: list[str] = [] |
| for cmd in sorted(tree["subcommands"]): |
| info = tree["subcommands"][cmd] |
| if not info["subcommands"]: |
| continue |
| if cmd == "profile": |
| |
| |
| 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 |
| # Hermes Agent zsh completion |
| # 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 |
| |
| _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' |
| |
| case $state in |
| commands) |
| local -a subcmds |
| subcmds=( |
| {top_cmds_str} |
| ) |
| _describe 'hermes command' subcmds |
| ;; |
| args) |
| case ${{line[1]}} in |
| {sub_cases_str} |
| esac |
| ;; |
| esac |
| }} |
| |
| _hermes "$@" |
| """ |
|
|
|
|
| |
| |
| |
|
|
| def generate_fish(parser: argparse.ArgumentParser) -> str: |
| tree = _walk(parser) |
| top_cmds = sorted(tree["subcommands"]) |
| top_cmds_str = " ".join(top_cmds) |
|
|
| lines: list[str] = [ |
| "# Hermes Agent fish completion", |
| "# 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", |
| ] |
|
|
| for cmd in top_cmds: |
| info = tree["subcommands"][cmd] |
| help_text = _clean(info.get("help", "")) |
| lines.append( |
| f"complete -c hermes -f " |
| f"-n 'not __fish_seen_subcommand_from {top_cmds_str}' " |
| f"-a {cmd} -d '{help_text}'" |
| ) |
|
|
| 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"]: |
| continue |
| lines.append(f"# {cmd}") |
| for sc in sorted(info["subcommands"]): |
| sinfo = info["subcommands"][sc] |
| sh = _clean(sinfo.get("help", "")) |
| lines.append( |
| f"complete -c hermes -f " |
| f"-n '__fish_seen_subcommand_from {cmd}' " |
| f"-a {sc} -d '{sh}'" |
| ) |
| |
| 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) |
|
|