Skip to content

Commit c16d483

Browse files
committed
Add completion support for fish and PowerShell shells
1 parent a023990 commit c16d483

File tree

6 files changed

+168
-41
lines changed

6 files changed

+168
-41
lines changed

code_assistant_manager/cli/app.py

Lines changed: 130 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -62,21 +62,32 @@ def _get_logger():
6262

6363
# Lazy-loaded completion scripts to improve startup time
6464
_completion_scripts = {}
65+
SUPPORTED_COMPLETION_SHELLS = ("bash", "zsh", "fish", "powershell", "pwsh")
66+
67+
68+
def _normalize_completion_shell(shell: str) -> str:
69+
"""Normalize shell aliases for completion generation."""
70+
return "powershell" if shell == "pwsh" else shell
6571

6672

6773
def _generate_completion_script(shell: str) -> str:
6874
"""Generate a comprehensive completion script for the given shell."""
69-
if shell in _completion_scripts:
70-
return _completion_scripts[shell]
75+
normalized_shell = _normalize_completion_shell(shell)
76+
if normalized_shell in _completion_scripts:
77+
return _completion_scripts[normalized_shell]
7178

72-
if shell == "bash":
79+
if normalized_shell == "bash":
7380
script = _generate_bash_completion()
74-
elif shell == "zsh":
81+
elif normalized_shell == "zsh":
7582
script = _generate_zsh_completion()
83+
elif normalized_shell == "fish":
84+
script = _generate_fish_completion()
85+
elif normalized_shell == "powershell":
86+
script = _generate_powershell_completion()
7687
else:
7788
script = f"# Unsupported shell: {shell}"
7889

79-
_completion_scripts[shell] = script
90+
_completion_scripts[normalized_shell] = script
8091
return script
8192

8293

@@ -209,7 +220,7 @@ def _get_bash_completion_content() -> str:
209220
return 0
210221
;;
211222
completion|comp|c)
212-
COMPREPLY=( $(compgen -W "bash zsh" -- ${cur}) )
223+
COMPREPLY=( $(compgen -W "bash zsh fish powershell pwsh" -- ${cur}) )
213224
return 0
214225
;;
215226
--config)
@@ -468,7 +479,7 @@ def _get_bash_completion_content() -> str:
468479
;;
469480
completion|comp|c)
470481
case "${COMP_WORDS[2]}" in
471-
bash|zsh)
482+
bash|zsh|fish|powershell|pwsh)
472483
COMPREPLY=( $(compgen -W "--help" -- ${cur}) )
473484
return 0
474485
;;
@@ -901,7 +912,7 @@ def _get_zsh_completion_content() -> str:
901912
;;
902913
completion|comp|c)
903914
if (( CURRENT == 2 )); then
904-
_values 'shell' 'bash' 'zsh'
915+
_values 'shell' 'bash' 'zsh' 'fish' 'powershell' 'pwsh'
905916
else
906917
_values 'option' '--help[Show help]'
907918
fi
@@ -930,6 +941,65 @@ def _get_zsh_completion_content() -> str:
930941
compdef _code_assistant_manager cam"""
931942

932943

944+
def _generate_fish_completion() -> str:
945+
"""Generate fish completion script."""
946+
return """# code-assistant-manager fish completion
947+
948+
set -l __cam_commands launch l config cf mcp m prompt p skill s plugin pl agent ag extensions ext upgrade u install i uninstall un doctor d version v completion comp c
949+
set -l __cam_tools aichat claude codex copilot gemini droid qwen codebuddy iflow qodercli zed neovate crush cursor-agent ampcode
950+
set -l __cam_completion_shells bash zsh fish powershell pwsh
951+
952+
complete -c code-assistant-manager -f
953+
complete -c cam -f
954+
955+
complete -c code-assistant-manager -n "__fish_use_subcommand" -a "$__cam_commands"
956+
complete -c cam -n "__fish_use_subcommand" -a "$__cam_commands"
957+
958+
complete -c code-assistant-manager -n "__fish_seen_subcommand_from launch l upgrade u install i uninstall un" -a "$__cam_tools"
959+
complete -c cam -n "__fish_seen_subcommand_from launch l upgrade u install i uninstall un" -a "$__cam_tools"
960+
961+
complete -c code-assistant-manager -n "__fish_seen_subcommand_from completion comp c" -a "$__cam_completion_shells"
962+
complete -c cam -n "__fish_seen_subcommand_from completion comp c" -a "$__cam_completion_shells"
963+
"""
964+
965+
966+
def _generate_powershell_completion() -> str:
967+
"""Generate PowerShell completion script."""
968+
return """# code-assistant-manager powershell completion
969+
970+
$codeAssistantCommands = @('launch','l','config','cf','mcp','m','prompt','p','skill','s','plugin','pl','agent','ag','extensions','ext','upgrade','u','install','i','uninstall','un','doctor','d','version','v','completion','comp','c')
971+
$codeAssistantTools = @('aichat','claude','codex','copilot','gemini','droid','qwen','codebuddy','iflow','qodercli','zed','neovate','crush','cursor-agent','ampcode')
972+
$completionShells = @('bash','zsh','fish','powershell','pwsh')
973+
974+
Register-ArgumentCompleter -Native -CommandName code-assistant-manager, cam -ScriptBlock {
975+
param($wordToComplete, $commandAst, $cursorPosition)
976+
977+
$elements = $commandAst.CommandElements | ForEach-Object { $_.Value }
978+
979+
if ($elements.Count -le 1) {
980+
$codeAssistantCommands | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
981+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
982+
}
983+
return
984+
}
985+
986+
$subcommand = $elements[1]
987+
if ($subcommand -in @('launch','l','upgrade','u','install','i','uninstall','un')) {
988+
$codeAssistantTools | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
989+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
990+
}
991+
return
992+
}
993+
994+
if ($subcommand -in @('completion','comp','c')) {
995+
$completionShells | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
996+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
997+
}
998+
}
999+
}
1000+
"""
1001+
1002+
9331003
app = typer.Typer(
9341004
name="cam",
9351005
help="Code Assistant Manager - CLI utilities for working with AI coding assistants",
@@ -960,34 +1030,57 @@ def global_options(
9601030

9611031
# Completion commands - lightweight and always available
9621032
@app.command()
963-
def completion(shell: str = typer.Argument(..., help="Shell type (bash, zsh)")):
1033+
def completion(
1034+
shell: str = typer.Argument(
1035+
..., help="Shell type (bash, zsh, fish, powershell, pwsh)"
1036+
)
1037+
):
9641038
"""Generate shell completion scripts."""
965-
if shell not in ["bash", "zsh"]:
966-
typer.echo(f"Error: Unsupported shell {shell!r}. Supported shells: bash, zsh")
1039+
if shell not in SUPPORTED_COMPLETION_SHELLS:
1040+
supported_shells = ", ".join(SUPPORTED_COMPLETION_SHELLS)
1041+
typer.echo(
1042+
f"Error: Unsupported shell {shell!r}. Supported shells: {supported_shells}"
1043+
)
9671044
raise typer.Exit(1)
1045+
normalized_shell = _normalize_completion_shell(shell)
9681046

9691047
# Generate basic completion script with common commands
970-
completion_script = _generate_completion_script(shell)
1048+
completion_script = _generate_completion_script(normalized_shell)
9711049

972-
typer.echo(f"# Shell completion script for {shell}")
1050+
typer.echo(f"# Shell completion script for {normalized_shell}")
9731051
typer.echo("# To install, run one of the following:")
9741052
typer.echo("#")
975-
typer.echo("# Option 1: Add to ~/.bashrc or ~/.zshrc")
976-
typer.echo(
977-
f"# echo 'source <(code-assistant-manager completion {shell})' >> ~/.{shell}rc"
978-
)
979-
typer.echo("#")
980-
typer.echo("# Option 2: Save to file and source it")
981-
typer.echo(
982-
f"# code-assistant-manager completion {shell} > ~/.{shell}_completion_code_assistant_manager"
983-
)
984-
typer.echo(
985-
f"# echo 'source ~/.{shell}_completion_code_assistant_manager' >> ~/.{shell}rc"
986-
)
987-
typer.echo("#")
988-
typer.echo(
989-
"# Restart your shell or run 'source ~/.bashrc' (or ~/.zshrc) to apply changes"
990-
)
1053+
if normalized_shell in ["bash", "zsh"]:
1054+
typer.echo("# Option 1: Add to ~/.bashrc or ~/.zshrc")
1055+
typer.echo(
1056+
f"# echo 'source <(code-assistant-manager completion {normalized_shell})' >> ~/.{normalized_shell}rc"
1057+
)
1058+
typer.echo("#")
1059+
typer.echo("# Option 2: Save to file and source it")
1060+
typer.echo(
1061+
f"# code-assistant-manager completion {normalized_shell} > ~/.{normalized_shell}_completion_code_assistant_manager"
1062+
)
1063+
typer.echo(
1064+
f"# echo 'source ~/.{normalized_shell}_completion_code_assistant_manager' >> ~/.{normalized_shell}rc"
1065+
)
1066+
typer.echo("#")
1067+
typer.echo(
1068+
"# Restart your shell or run 'source ~/.bashrc' (or ~/.zshrc) to apply changes"
1069+
)
1070+
elif normalized_shell == "fish":
1071+
typer.echo("# Option 1: Install to fish completions directory")
1072+
typer.echo(
1073+
"# code-assistant-manager completion fish > ~/.config/fish/completions/code-assistant-manager.fish"
1074+
)
1075+
typer.echo("#")
1076+
typer.echo("# Option 2: Load for current session only")
1077+
typer.echo("# source (code-assistant-manager completion fish | psub)")
1078+
else:
1079+
typer.echo("# Save to your PowerShell profile and reload")
1080+
typer.echo(
1081+
"# code-assistant-manager completion powershell | Out-File -Encoding utf8 -Append $PROFILE"
1082+
)
1083+
typer.echo("# . $PROFILE")
9911084
typer.echo()
9921085
typer.echo("# Completion script:")
9931086
typer.echo("=" * 50)
@@ -996,14 +1089,20 @@ def completion(shell: str = typer.Argument(..., help="Shell type (bash, zsh)")):
9961089

9971090
@app.command("c", hidden=True)
9981091
def completion_alias_short(
999-
shell: str = typer.Argument(..., help="Shell type (bash, zsh)"),
1092+
shell: str = typer.Argument(
1093+
..., help="Shell type (bash, zsh, fish, powershell, pwsh)"
1094+
),
10001095
):
10011096
"""Alias for 'completion' command."""
10021097
return completion(shell)
10031098

10041099

10051100
@app.command("comp", hidden=True)
1006-
def completion_alias(shell: str = typer.Argument(..., help="Shell type (bash, zsh)")):
1101+
def completion_alias(
1102+
shell: str = typer.Argument(
1103+
..., help="Shell type (bash, zsh, fish, powershell, pwsh)"
1104+
)
1105+
):
10071106
"""Alias for 'completion' command."""
10081107
return completion(shell)
10091108

code_assistant_manager/cli/options.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
KEEP_CONFIG_OPTION = typer.Option(
1919
False, "--keep-config", "-k", help="Keep configuration files (don't backup)"
2020
)
21-
SHELL_OPTION = typer.Argument(..., help="Shell type (bash, zsh)")
21+
SHELL_OPTION = typer.Argument(
22+
..., help="Shell type (bash, zsh, fish, powershell, pwsh)"
23+
)
2224
SCOPE_OPTION = typer.Option(
2325
"user", "--scope", help="Configuration scope (user, project)",
2426
)

tests/test_cli_comprehensive.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,7 @@ def test_completion_alias_comp_bash(self):
724724

725725
def test_completion_invalid_shell(self):
726726
"""Test completion with invalid shell."""
727-
with patch("sys.argv", ["code-assistant-manager", "completion", "fish"]):
727+
with patch("sys.argv", ["code-assistant-manager", "completion", "nushell"]):
728728
with pytest.raises(SystemExit) as exc_info:
729729
main()
730730
assert exc_info.value.code == 1

tests/test_cli_integration_comprehensive.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -862,7 +862,7 @@ def test_completion_zsh(self, runner):
862862

863863
def test_completion_invalid_shell(self, runner):
864864
"""Test completion with invalid shell."""
865-
result = runner.invoke(app, ["completion", "fish"])
865+
result = runner.invoke(app, ["completion", "nushell"])
866866
assert result.exit_code == 1
867867
assert "Unsupported shell" in result.output
868868

@@ -992,4 +992,4 @@ def test_multiple_plugin_commands(self, MockPluginManager, runner):
992992

993993
# Verify PluginManager was used
994994
assert MockPluginManager.called
995-
mock_manager.get_all_repos.assert_called()
995+
mock_manager.get_all_repos.assert_called()

tests/test_cli_short_commands.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -583,9 +583,16 @@ def test_comp_zsh(self):
583583
main()
584584
assert exc_info.value.code == 0
585585

586-
def test_comp_invalid_shell(self):
587-
"""Test 'comp fish' with invalid shell."""
586+
def test_comp_fish(self):
587+
"""Test 'comp fish' generates fish completion."""
588588
with patch("sys.argv", ["code-assistant-manager", "comp", "fish"]):
589+
with pytest.raises(SystemExit) as exc_info:
590+
main()
591+
assert exc_info.value.code == 0
592+
593+
def test_comp_invalid_shell(self):
594+
"""Test 'comp nushell' with invalid shell."""
595+
with patch("sys.argv", ["code-assistant-manager", "comp", "nushell"]):
589596
with pytest.raises(SystemExit) as exc_info:
590597
main()
591598
assert exc_info.value.code == 1
@@ -615,9 +622,16 @@ def test_c_zsh(self):
615622
main()
616623
assert exc_info.value.code == 0
617624

618-
def test_c_invalid_shell(self):
619-
"""Test 'c fish' with invalid shell."""
625+
def test_c_fish(self):
626+
"""Test 'c fish' generates fish completion."""
620627
with patch("sys.argv", ["code-assistant-manager", "c", "fish"]):
628+
with pytest.raises(SystemExit) as exc_info:
629+
main()
630+
assert exc_info.value.code == 0
631+
632+
def test_c_invalid_shell(self):
633+
"""Test 'c nushell' with invalid shell."""
634+
with patch("sys.argv", ["code-assistant-manager", "c", "nushell"]):
621635
with pytest.raises(SystemExit) as exc_info:
622636
main()
623637
assert exc_info.value.code == 1

tests/test_comprehensive_cli_command_coverage.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,21 @@ def test_completion_zsh_command(self, runner):
3535
assert result.exit_code == 0
3636
assert "code-assistant-manager zsh completion" in result.output
3737

38+
def test_completion_fish_command(self, runner):
39+
"""Test fish completion command."""
40+
result = runner.invoke(app, ["completion", "fish"])
41+
assert result.exit_code == 0
42+
assert "code-assistant-manager fish completion" in result.output
43+
44+
def test_completion_powershell_command(self, runner):
45+
"""Test powershell completion command."""
46+
result = runner.invoke(app, ["completion", "powershell"])
47+
assert result.exit_code == 0
48+
assert "code-assistant-manager powershell completion" in result.output
49+
3850
def test_completion_invalid_shell(self, runner):
3951
"""Test completion with invalid shell."""
40-
result = runner.invoke(app, ["completion", "fish"])
52+
result = runner.invoke(app, ["completion", "nushell"])
4153
assert result.exit_code != 0
4254
assert "Unsupported shell" in result.output or "Error" in result.output
4355

@@ -460,4 +472,4 @@ def test_missing_required_argument(self, runner):
460472
# For commands that might have required args
461473
result = runner.invoke(app, ["uninstall"])
462474
# Should either show help or fail gracefully
463-
assert result.exit_code in [0, 1, 2]
475+
assert result.exit_code in [0, 1, 2]

0 commit comments

Comments
 (0)