Skip to content

Commit 4f4f55e

Browse files
authored
feat: add shell completion support (#3)
## Summary - Add shell completion support for Bash, Zsh, Fish, and PowerShell - Implement dynamic completions for branch names and worktree names - Add `agentree completion` command to generate shell-specific scripts ## Changes - Added `completion.go` with shell script generation command - Added `completion_helpers.go` with dynamic completion functions - Enhanced `git.Repository` with `ListWorktrees()` method - Updated create and remove commands with completion functions - Added shell completion setup documentation to README ## Test Plan - [x] Built and tested completion generation for all shells - [x] Verified dynamic completions work for branch names - [x] Verified dynamic completions work for worktree names - [x] All tests passing (`make test`) - [x] Linting passes (`make lint`) ## Documentation Updated README with installation instructions for each shell type. 🤖 Generated with [Claude Code](https://claude.ai/code)
1 parent 6ed46c7 commit 4f4f55e

File tree

7 files changed

+232
-2
lines changed

7 files changed

+232
-2
lines changed

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,41 @@ agentree -b experiment-ml -r
182182
agentree rm agent/auth-system -R
183183
```
184184

185+
## Shell Completion
186+
187+
agentree supports tab completion for commands, flags, branch names, and worktree names.
188+
189+
### Bash
190+
191+
```bash
192+
# Add to ~/.bashrc or ~/.bash_profile:
193+
source <(agentree completion bash)
194+
```
195+
196+
### Zsh
197+
198+
```bash
199+
# Add to ~/.zshrc:
200+
source <(agentree completion zsh)
201+
202+
# Or for oh-my-zsh users, add to completions directory:
203+
agentree completion zsh > ~/.oh-my-zsh/completions/_agentree
204+
```
205+
206+
### Fish
207+
208+
```bash
209+
# Add to ~/.config/fish/completions/:
210+
agentree completion fish > ~/.config/fish/completions/agentree.fish
211+
```
212+
213+
### PowerShell
214+
215+
```powershell
216+
# Add to your PowerShell profile:
217+
agentree completion powershell | Out-String | Invoke-Expression
218+
```
219+
185220
## Development
186221

187222
```bash

cmd/completion.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/spf13/cobra"
8+
)
9+
10+
// completionCmd represents the completion command
11+
var completionCmd = &cobra.Command{
12+
Use: "completion [bash|zsh|fish|powershell]",
13+
Short: "Generate shell completion script",
14+
Long: `Generate a shell completion script for agentree.
15+
16+
This command generates shell completion scripts that enable tab completion
17+
for agentree commands, flags, branch names, worktree names, and more.
18+
19+
Examples:
20+
# Bash (add to ~/.bashrc or ~/.bash_profile):
21+
source <(agentree completion bash)
22+
23+
# Zsh (add to ~/.zshrc):
24+
source <(agentree completion zsh)
25+
# Or for oh-my-zsh, add to ~/.oh-my-zsh/completions/:
26+
agentree completion zsh > ~/.oh-my-zsh/completions/_agentree
27+
28+
# Fish (add to ~/.config/fish/completions/):
29+
agentree completion fish > ~/.config/fish/completions/agentree.fish
30+
31+
# PowerShell (add to your PowerShell profile):
32+
agentree completion powershell | Out-String | Invoke-Expression`,
33+
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
34+
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
35+
DisableFlagsInUseLine: true,
36+
RunE: func(cmd *cobra.Command, args []string) error {
37+
switch args[0] {
38+
case "bash":
39+
return rootCmd.GenBashCompletionV2(os.Stdout, true)
40+
case "zsh":
41+
return rootCmd.GenZshCompletion(os.Stdout)
42+
case "fish":
43+
return rootCmd.GenFishCompletion(os.Stdout, true)
44+
case "powershell":
45+
return rootCmd.GenPowerShellCompletionWithDesc(os.Stdout)
46+
default:
47+
return fmt.Errorf("unsupported shell: %s", args[0])
48+
}
49+
},
50+
}
51+
52+
func init() {
53+
rootCmd.AddCommand(completionCmd)
54+
}

cmd/completion_helpers.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package cmd
2+
3+
import (
4+
"path/filepath"
5+
"strings"
6+
7+
"github.com/AryaLabsHQ/agentree/internal/git"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
// getBranchCompletions returns all available git branches for completion
12+
func getBranchCompletions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
13+
repo, err := git.NewRepository()
14+
if err != nil {
15+
return nil, cobra.ShellCompDirectiveError
16+
}
17+
18+
branches, err := repo.ListBranches()
19+
if err != nil {
20+
return nil, cobra.ShellCompDirectiveError
21+
}
22+
23+
// Filter branches that start with the toComplete string
24+
var completions []string
25+
for _, branch := range branches {
26+
if strings.HasPrefix(branch, toComplete) {
27+
completions = append(completions, branch)
28+
}
29+
}
30+
31+
return completions, cobra.ShellCompDirectiveNoFileComp
32+
}
33+
34+
// getWorktreeCompletions returns all existing worktrees for completion
35+
func getWorktreeCompletions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
36+
repo, err := git.NewRepository()
37+
if err != nil {
38+
return nil, cobra.ShellCompDirectiveError
39+
}
40+
41+
worktrees, err := repo.ListWorktrees()
42+
if err != nil {
43+
return nil, cobra.ShellCompDirectiveError
44+
}
45+
46+
// Extract just the directory names for easier completion
47+
var completions []string
48+
for _, worktree := range worktrees {
49+
// Use the base name of the path for completion
50+
name := filepath.Base(worktree)
51+
if strings.HasPrefix(name, toComplete) {
52+
completions = append(completions, name)
53+
}
54+
}
55+
56+
return completions, cobra.ShellCompDirectiveNoFileComp
57+
}
58+
59+
// Commented out for future use when agent types and config commands are implemented
60+
61+
// // getAgentTypeCompletions returns available agent types for completion
62+
// func getAgentTypeCompletions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
63+
// // These are placeholder agent types - will be expanded in the future
64+
// agentTypes := []string{
65+
// "claude",
66+
// "cursor",
67+
// "copilot",
68+
// "generic",
69+
// }
70+
71+
// var completions []string
72+
// for _, agent := range agentTypes {
73+
// if strings.HasPrefix(agent, toComplete) {
74+
// completions = append(completions, agent)
75+
// }
76+
// }
77+
78+
// return completions, cobra.ShellCompDirectiveNoFileComp
79+
// }
80+
81+
// // getConfigKeyCompletions returns available configuration keys for completion
82+
// func getConfigKeyCompletions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
83+
// // Configuration keys that can be set
84+
// configKeys := []string{
85+
// "defaultSetup",
86+
// "npmSetup",
87+
// "pnpmSetup",
88+
// "yarnSetup",
89+
// "postCreateScripts",
90+
// "env.enabled",
91+
// "env.includePatterns",
92+
// "env.excludePatterns",
93+
// }
94+
95+
// var completions []string
96+
// for _, key := range configKeys {
97+
// if strings.HasPrefix(key, toComplete) {
98+
// completions = append(completions, key)
99+
// }
100+
// }
101+
102+
// return completions, cobra.ShellCompDirectiveNoFileComp
103+
// }

cmd/create.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ func init() {
5858

5959
// Make branch required unless in interactive mode
6060
_ = createCmd.MarkFlagRequired("branch")
61+
62+
// Register custom completion functions
63+
_ = createCmd.RegisterFlagCompletionFunc("from", getBranchCompletions)
64+
_ = createCmd.RegisterFlagCompletionFunc("base", getBranchCompletions)
6165
}
6266

6367
// For backward compatibility, also make flags available at root level

cmd/remove.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ Use -y to skip confirmation and force removal of dirty worktrees.
2222
Use -R to also delete the local branch after removing the worktree.`,
2323
Args: cobra.ExactArgs(1),
2424
RunE: runRemove,
25+
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
26+
if len(args) != 0 {
27+
return nil, cobra.ShellCompDirectiveNoFileComp
28+
}
29+
// Provide both branch and worktree completions
30+
branches, _ := getBranchCompletions(cmd, args, toComplete)
31+
worktrees, _ := getWorktreeCompletions(cmd, args, toComplete)
32+
return append(branches, worktrees...), cobra.ShellCompDirectiveNoFileComp
33+
},
2534
}
2635

2736
var (
@@ -40,6 +49,10 @@ func init() {
4049
// Define flags
4150
removeCmd.Flags().BoolVarP(&force, "yes", "y", false, "Force removal without confirmation")
4251
removeCmd.Flags().BoolVarP(&deleteBranch, "delete-branch", "R", false, "Also delete the local branch")
52+
53+
// Copy flags to alias
54+
removeAlias.Flags().BoolVarP(&force, "yes", "y", false, "Force removal without confirmation")
55+
removeAlias.Flags().BoolVarP(&deleteBranch, "delete-branch", "R", false, "Also delete the local branch")
4356
}
4457

4558
func runRemove(cmd *cobra.Command, args []string) error {

cmd/root.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,5 @@ func Execute() error {
4444
}
4545

4646
func init() {
47-
// Disable Cobra's default completion command
48-
rootCmd.CompletionOptions.DisableDefaultCmd = true
47+
// Custom completion functions will be registered in individual command files
4948
}

internal/git/git.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,4 +223,26 @@ func (r *Repository) DeleteBranch(branch string) error {
223223
}
224224

225225
return nil
226+
}
227+
228+
// ListWorktrees returns a list of all worktree paths
229+
func (r *Repository) ListWorktrees() ([]string, error) {
230+
cmd := exec.Command("git", "worktree", "list", "--porcelain")
231+
cmd.Dir = r.Root
232+
output, err := cmd.Output()
233+
if err != nil {
234+
return nil, fmt.Errorf("failed to list worktrees: %w", err)
235+
}
236+
237+
lines := strings.Split(string(output), "\n")
238+
var worktrees []string
239+
240+
for _, line := range lines {
241+
parts := strings.Fields(line)
242+
if len(parts) >= 2 && parts[0] == "worktree" {
243+
worktrees = append(worktrees, parts[1])
244+
}
245+
}
246+
247+
return worktrees, nil
226248
}

0 commit comments

Comments
 (0)