From 9522b5ab6d63b15ee06616590da2d5eaaf12d51f Mon Sep 17 00:00:00 2001 From: benbenbang Date: Wed, 14 Oct 2026 14:27:37 +0800 Subject: [PATCH] feat(cli): add scope flag completion for mcp install Register a Cobra flag completion function for the `--scope` flag on the `mcp install` commands. Claude subcommands suggest `user`, `local`, and `project`, while Codex offers only `user`, with prefix matching and no filename suggestions. Add tests covering the new completion behavior and document shell completion setup and the scope suggestions in the CLI usage guide. --- docs/usage.md | 14 ++++++++++ internal/cli/mcp_completion_test.go | 40 +++++++++++++++++++++++++++++ internal/cli/mcp_install.go | 14 ++++++++++ 3 files changed, 68 insertions(+) create mode 100644 internal/cli/mcp_completion_test.go diff --git a/docs/usage.md b/docs/usage.md index d990b81..39cd2a3 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -4,6 +4,20 @@ The examples assume `jevwise` is installed on PATH. For a source build, use `bin/jevwise` instead. +## Shell completion + +Enable completion in your current Zsh session (after installing/upgrading Jevwise): + +```sh +autoload -Uz compinit +compinit +source <(jevwise completion zsh) +``` + +- For Bash, use `source <(jevwise completion bash)` instead. Fish and PowerShell scripts are available through `jevwise completion fish` and `jevwise completion powershell`. +- `jevwise mcp install claude --scope ` suggests `user`, `local`, `project`; Codex suggests only `user`. Prefixes and `--scope=prefix` also work, without filename suggestions. +- Completion reads no decision configuration or credentials and never invokes a client installer. Use a binary containing the new completion callback; source/worktree changes do not update an installed binary automatically. + ## Decisions ```sh diff --git a/internal/cli/mcp_completion_test.go b/internal/cli/mcp_completion_test.go new file mode 100644 index 0000000..124b9ca --- /dev/null +++ b/internal/cli/mcp_completion_test.go @@ -0,0 +1,40 @@ +package cli + +import ( + "context" + "strings" + "testing" +) + +func TestMCPInstallScopeCompletionIsClientSpecificAndLazy(t *testing.T) { + decisionEnvironment(t) + for _, tt := range []struct { + args []string + want string + }{ + {[]string{"mcp", "install", "claude", "--scope", ""}, "user\nlocal\nproject\n:4\n"}, + {[]string{"mcp", "install", "codex", "--scope", ""}, "user\n:4\n"}, + {[]string{"mcp", "install", "--scope", ""}, "user\nlocal\nproject\n:4\n"}, + {[]string{"mcp", "install", "claude", "--scope", "l"}, "local\n:4\n"}, + {[]string{"mcp", "install", "claude", "--scope=pr"}, "project\n:4\n"}, + {[]string{"mcp", "install", "codex", "--scope=u"}, "user\n:4\n"}, + {[]string{"mcp", "install", "codex", "--scope", "l"}, ":4\n"}, + {[]string{"mcp", "install", "claude", "--scope", "private-prefix"}, ":4\n"}, + } { + for _, hidden := range []string{"__complete", "__completeNoDesc"} { + root := installerRoot(t, func(context.Context, string, ...string) ([]byte, error) { + t.Fatal("completion invoked a client") + return nil, nil + }) + // Completion must remain available with unusable runtime settings. + args := []string{hidden, "--debug", "--config", "/missing", "--timeout", "invalid", "--api-key", "private-key"} + out, stderr, err := execute(root, append(args, tt.args...)...) + if err != nil || out != tt.want || !strings.Contains(stderr, "ShellCompDirectiveNoFileComp") { + t.Fatal("scope completion candidates or no-file directive changed", tt.args, err) + } + if strings.Contains(stderr, "mcp.install.register") || strings.Contains(out+stderr, "private") { + t.Fatal("completion ran installation or exposed input") + } + } + } +} diff --git a/internal/cli/mcp_install.go b/internal/cli/mcp_install.go index 2cc94b4..660bfb8 100644 --- a/internal/cli/mcp_install.go +++ b/internal/cli/mcp_install.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "io" + "strings" "time" "github.com/bitbrew-dev/jevwise/internal/debuglog" @@ -24,6 +25,19 @@ func newMCPInstall(run agentCommand) *cobra.Command { RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Help() }} cmd.PersistentFlags().StringVar(&address, "listen", "127.0.0.1:8080", "Server's literal loopback IP:port (same as mcp --listen)") cmd.PersistentFlags().StringVar(&scope, "scope", "user", "Client settings scope: user; Claude also supports local or project") + _ = cmd.RegisterFlagCompletionFunc("scope", func(cmd *cobra.Command, _ []string, prefix string) ([]string, cobra.ShellCompDirective) { + choices := []string{"user"} + if cmd.Name() != "codex" { + choices = append(choices, "local", "project") + } + var matches []string + for _, choice := range choices { + if strings.HasPrefix(choice, prefix) { + matches = append(matches, choice) + } + } + return matches, cobra.ShellCompDirectiveNoFileComp + }) for _, client := range []string{"codex", "claude"} { cmd.AddCommand(&cobra.Command{Use: client, Short: "Register Jevwise with " + client, Args: noArgs, RunE: func(cmd *cobra.Command, _ []string) error {