Skip to content

Commit 30155ac

Browse files
GreiteGauthier Painteaux
andauthored
feat(cli): add shell completion command with dynamic completions (#175)
* feat(cli): add shell completion command with dynamic completions Add `grepai completion [zsh|bash|fish|powershell]` command that generates shell completion scripts with intelligent completions for flag values and positional arguments. Static completions: - init --provider/--backend - workspace create --provider/--backend - trace callers/callees/graph --mode Dynamic completions: - --workspace flag on search, watch, mcp-serve, trace commands - --project flag on search (depends on --workspace value) - workspace show/delete/status args (workspace names) - workspace add args (workspace name + directory) - workspace remove args (workspace name + project names) * fix(cli): use cmd.OutOrStdout() to avoid data race in completion tests Redirect completion output through Cobra's OutOrStdout() instead of os.Stdout directly. Tests now use rootCmd.SetOut() to capture output without mutating the global os.Stdout, fixing the data race detected on Windows CI with -race -coverprofile. * docs: add shell completion to CHANGELOG and README --------- Co-authored-by: Gauthier Painteaux <gauthier.painteaux@koul.io>
1 parent 4ed9a15 commit 30155ac

File tree

4 files changed

+395
-0
lines changed

4 files changed

+395
-0
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- **Shell Completion**: New `grepai completion [zsh|bash|fish|powershell]` command for shell autocompletion (#175)
13+
- Static completions with descriptions for `--provider`, `--backend`, `--mode` flags
14+
- Dynamic completions for `--workspace` and `--project` flags (loaded from config)
15+
- Positional argument completions for workspace subcommands (names, project names, directories)
16+
- Installation instructions for Zsh (eval, Oh-My-Zsh plugin, manual fpath), Bash, Fish, PowerShell
1217
- **`.grepaiignore` Support**: New `.grepaiignore` file allows overriding `.gitignore` rules for grepai indexing. Supports negation patterns (`!`) to re-include files excluded by `.gitignore`, with directory-level precedence for nested files (#107)
1318

1419
## [0.34.0] - 2026-02-24

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,41 @@ grepai search "error handling" # Search semantically
6666
grepai trace callers "Login" # Find who calls a function
6767
```
6868

69+
## Shell Completion
70+
71+
grepai supports autocompletion for commands, flags, and dynamic values (workspace names, project names, providers, backends).
72+
73+
**Zsh (add to `~/.zshrc`):**
74+
```bash
75+
eval "$(grepai completion zsh)"
76+
```
77+
78+
**Oh-My-Zsh plugin:**
79+
```bash
80+
mkdir -p ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/grepai
81+
grepai completion zsh > ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/grepai/_grepai
82+
# Then add "grepai" to plugins=(...) in ~/.zshrc
83+
```
84+
85+
**Bash:**
86+
```bash
87+
# Linux
88+
grepai completion bash > /etc/bash_completion.d/grepai
89+
90+
# macOS (requires bash-completion@2)
91+
grepai completion bash > $(brew --prefix)/etc/bash_completion.d/grepai
92+
```
93+
94+
**Fish:**
95+
```bash
96+
grepai completion fish > ~/.config/fish/completions/grepai.fish
97+
```
98+
99+
**PowerShell:**
100+
```powershell
101+
grepai completion powershell | Out-String | Invoke-Expression
102+
```
103+
69104
## What developers say
70105

71106
> *"I just hit my limit and it took 13% of my max5 plan just to read my codebase. I am very, very excited about your new tool."*

cli/completion.go

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
package cli
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
"github.com/yoanbernabeu/grepai/config"
6+
)
7+
8+
var completionCmd = &cobra.Command{
9+
Use: "completion [shell]",
10+
Short: "Generate shell completion scripts",
11+
Long: `Generate shell completion scripts for grepai.
12+
13+
Zsh:
14+
15+
# Method 1: eval (add to ~/.zshrc)
16+
eval "$(grepai completion zsh)"
17+
18+
# Method 2: Oh-My-Zsh
19+
mkdir -p ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/grepai
20+
grepai completion zsh > ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/grepai/_grepai
21+
# Then add "grepai" to plugins=(...) in ~/.zshrc
22+
23+
# Method 3: Manual fpath
24+
grepai completion zsh > "${fpath[1]}/_grepai"
25+
# Then restart your shell
26+
27+
Bash:
28+
29+
# Linux
30+
grepai completion bash > /etc/bash_completion.d/grepai
31+
32+
# macOS (requires bash-completion@2)
33+
grepai completion bash > $(brew --prefix)/etc/bash_completion.d/grepai
34+
35+
Fish:
36+
37+
grepai completion fish > ~/.config/fish/completions/grepai.fish
38+
39+
PowerShell:
40+
41+
grepai completion powershell | Out-String | Invoke-Expression
42+
`,
43+
}
44+
45+
var completionZshCmd = &cobra.Command{
46+
Use: "zsh",
47+
Short: "Generate zsh completion script",
48+
Args: cobra.NoArgs,
49+
RunE: func(cmd *cobra.Command, args []string) error {
50+
return rootCmd.GenZshCompletion(cmd.OutOrStdout())
51+
},
52+
}
53+
54+
var completionBashCmd = &cobra.Command{
55+
Use: "bash",
56+
Short: "Generate bash completion script",
57+
Args: cobra.NoArgs,
58+
RunE: func(cmd *cobra.Command, args []string) error {
59+
return rootCmd.GenBashCompletionV2(cmd.OutOrStdout(), true)
60+
},
61+
}
62+
63+
var completionFishCmd = &cobra.Command{
64+
Use: "fish",
65+
Short: "Generate fish completion script",
66+
Args: cobra.NoArgs,
67+
RunE: func(cmd *cobra.Command, args []string) error {
68+
return rootCmd.GenFishCompletion(cmd.OutOrStdout(), true)
69+
},
70+
}
71+
72+
var completionPowershellCmd = &cobra.Command{
73+
Use: "powershell",
74+
Short: "Generate powershell completion script",
75+
Args: cobra.NoArgs,
76+
RunE: func(cmd *cobra.Command, args []string) error {
77+
return rootCmd.GenPowerShellCompletionWithDesc(cmd.OutOrStdout())
78+
},
79+
}
80+
81+
func init() {
82+
completionCmd.AddCommand(completionZshCmd)
83+
completionCmd.AddCommand(completionBashCmd)
84+
completionCmd.AddCommand(completionFishCmd)
85+
completionCmd.AddCommand(completionPowershellCmd)
86+
87+
rootCmd.AddCommand(completionCmd)
88+
89+
cobra.OnInitialize(registerCompletions)
90+
}
91+
92+
func registerCompletions() {
93+
// Static flag completions for initCmd
94+
_ = initCmd.RegisterFlagCompletionFunc("provider", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
95+
return []string{
96+
"ollama\tLocal embedding with Ollama",
97+
"lmstudio\tLocal embedding with LM Studio",
98+
"openai\tCloud embedding with OpenAI",
99+
"synthetic\tCloud embedding with Synthetic (free)",
100+
"openrouter\tCloud multi-provider gateway",
101+
}, cobra.ShellCompDirectiveNoFileComp
102+
})
103+
_ = initCmd.RegisterFlagCompletionFunc("backend", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
104+
return []string{
105+
"gob\tLocal file-based storage",
106+
"postgres\tPostgreSQL with pgvector",
107+
"qdrant\tQdrant vector database",
108+
}, cobra.ShellCompDirectiveNoFileComp
109+
})
110+
111+
// Static flag completions for workspaceCreateCmd
112+
_ = workspaceCreateCmd.RegisterFlagCompletionFunc("backend", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
113+
return []string{
114+
"postgres\tPostgreSQL with pgvector",
115+
"qdrant\tQdrant vector database",
116+
}, cobra.ShellCompDirectiveNoFileComp
117+
})
118+
_ = workspaceCreateCmd.RegisterFlagCompletionFunc("provider", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
119+
return []string{
120+
"ollama\tLocal embedding with Ollama",
121+
"lmstudio\tLocal embedding with LM Studio",
122+
"openai\tCloud embedding with OpenAI",
123+
"synthetic\tCloud embedding with Synthetic (free)",
124+
"openrouter\tCloud multi-provider gateway",
125+
}, cobra.ShellCompDirectiveNoFileComp
126+
})
127+
128+
// Static flag completions for trace commands (mode)
129+
for _, cmd := range []*cobra.Command{traceCallersCmd, traceCalleesCmd, traceGraphCmd} {
130+
cmd := cmd
131+
_ = cmd.RegisterFlagCompletionFunc("mode", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
132+
return []string{
133+
"fast\tRegex-based extraction (faster)",
134+
"precise\tTree-sitter extraction (more accurate)",
135+
}, cobra.ShellCompDirectiveNoFileComp
136+
})
137+
}
138+
139+
// Dynamic workspace name completions for --workspace flags
140+
workspaceCompleter := func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
141+
return completeWorkspaceNames(), cobra.ShellCompDirectiveNoFileComp
142+
}
143+
_ = searchCmd.RegisterFlagCompletionFunc("workspace", workspaceCompleter)
144+
_ = watchCmd.RegisterFlagCompletionFunc("workspace", workspaceCompleter)
145+
_ = mcpServeCmd.RegisterFlagCompletionFunc("workspace", workspaceCompleter)
146+
for _, cmd := range []*cobra.Command{traceCallersCmd, traceCalleesCmd, traceGraphCmd} {
147+
_ = cmd.RegisterFlagCompletionFunc("workspace", workspaceCompleter)
148+
}
149+
150+
// Dynamic project completion for searchCmd --project (depends on --workspace value)
151+
_ = searchCmd.RegisterFlagCompletionFunc("project", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
152+
wsName, _ := cmd.Flags().GetString("workspace")
153+
if wsName == "" {
154+
return nil, cobra.ShellCompDirectiveNoFileComp
155+
}
156+
return completeProjectNames(wsName), cobra.ShellCompDirectiveNoFileComp
157+
})
158+
159+
// Dynamic ValidArgsFunction for workspace subcommands
160+
workspaceShowCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
161+
if len(args) == 0 {
162+
return completeWorkspaceNames(), cobra.ShellCompDirectiveNoFileComp
163+
}
164+
return nil, cobra.ShellCompDirectiveNoFileComp
165+
}
166+
workspaceDeleteCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
167+
if len(args) == 0 {
168+
return completeWorkspaceNames(), cobra.ShellCompDirectiveNoFileComp
169+
}
170+
return nil, cobra.ShellCompDirectiveNoFileComp
171+
}
172+
workspaceStatusCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
173+
if len(args) == 0 {
174+
return completeWorkspaceNames(), cobra.ShellCompDirectiveNoFileComp
175+
}
176+
return nil, cobra.ShellCompDirectiveNoFileComp
177+
}
178+
workspaceAddCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
179+
if len(args) == 0 {
180+
return completeWorkspaceNames(), cobra.ShellCompDirectiveNoFileComp
181+
}
182+
if len(args) == 1 {
183+
return nil, cobra.ShellCompDirectiveFilterDirs
184+
}
185+
return nil, cobra.ShellCompDirectiveNoFileComp
186+
}
187+
workspaceRemoveCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
188+
if len(args) == 0 {
189+
return completeWorkspaceNames(), cobra.ShellCompDirectiveNoFileComp
190+
}
191+
if len(args) == 1 {
192+
return completeProjectNames(args[0]), cobra.ShellCompDirectiveNoFileComp
193+
}
194+
return nil, cobra.ShellCompDirectiveNoFileComp
195+
}
196+
}
197+
198+
func completeWorkspaceNames() []string {
199+
cfg, err := config.LoadWorkspaceConfig()
200+
if err != nil || cfg == nil {
201+
return nil
202+
}
203+
return cfg.ListWorkspaces()
204+
}
205+
206+
func completeProjectNames(workspaceName string) []string {
207+
cfg, err := config.LoadWorkspaceConfig()
208+
if err != nil || cfg == nil {
209+
return nil
210+
}
211+
ws, err := cfg.GetWorkspace(workspaceName)
212+
if err != nil {
213+
return nil
214+
}
215+
names := make([]string, len(ws.Projects))
216+
for i, p := range ws.Projects {
217+
names[i] = p.Name
218+
}
219+
return names
220+
}

0 commit comments

Comments
 (0)