From 9a52b5dce1bbab451534bd0d13dab05cde097942 Mon Sep 17 00:00:00 2001 From: thibaud-lclr Date: Tue, 12 May 2026 10:37:46 +0200 Subject: [PATCH] feat(bootstrap): auto-hide commands with no hook configured Commands are now hidden from help and return ErrUnknownCommand when invoked if their hook is nil (and for version, if Version string is also empty). No explicit DisabledCommands needed for MCPs that don't use login/setup/config/etc. Co-Authored-By: Claude Sonnet 4.6 --- bootstrap/bootstrap.go | 37 +++++++++++++++++++++++++++++++++---- bootstrap/bootstrap_test.go | 30 +++++++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index e7f1d03..081abab 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -205,6 +205,11 @@ func normalize(opts Options) Options { } opts.Aliases = normalizeAliases(opts.Aliases, opts.EnableDoctorAlias) opts.AliasDescriptions = normalizeAliasDescriptions(opts.AliasDescriptions, opts.Aliases, opts.EnableDoctorAlias) + for _, cmd := range autoDisabledCommands(opts) { + if !isCommandDisabled(cmd, opts.DisabledCommands) { + opts.DisabledCommands = append(opts.DisabledCommands, cmd) + } + } return opts } @@ -461,14 +466,14 @@ func printHelp(opts Options, command string, args ...[]string) error { return printGlobalHelp(opts) } - if command == CommandConfig { - return printConfigHelp(opts, commandArgs) - } - if isCommandDisabled(command, opts.DisabledCommands) { return fmt.Errorf("%w: %s", ErrUnknownCommand, command) } + if command == CommandConfig { + return printConfigHelp(opts, commandArgs) + } + for _, def := range commands { if def.Name != command { continue @@ -583,6 +588,30 @@ func printGlobalHelp(opts Options) error { return err } +func autoDisabledCommands(opts Options) []string { + h := opts.Hooks + var disabled []string + if h.Setup == nil { + disabled = append(disabled, CommandSetup) + } + if h.Login == nil { + disabled = append(disabled, CommandLogin) + } + if h.MCP == nil { + disabled = append(disabled, CommandMCP) + } + if h.Config == nil && h.ConfigShow == nil && h.ConfigTest == nil && h.ConfigDelete == nil { + disabled = append(disabled, CommandConfig) + } + if h.Update == nil { + disabled = append(disabled, CommandUpdate) + } + if h.Version == nil && strings.TrimSpace(opts.Version) == "" { + disabled = append(disabled, CommandVersion) + } + return disabled +} + func isCommandDisabled(command string, disabled []string) bool { for _, d := range disabled { if d == command { diff --git a/bootstrap/bootstrap_test.go b/bootstrap/bootstrap_test.go index f0fbbeb..9bd642f 100644 --- a/bootstrap/bootstrap_test.go +++ b/bootstrap/bootstrap_test.go @@ -91,11 +91,13 @@ func TestRunReturnsCommandNotConfigured(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer + noop := func(_ context.Context, _ Invocation) error { return nil } err := Run(context.Background(), Options{ BinaryName: "my-mcp", Args: []string{"config"}, Stdout: &stdout, Stderr: &stderr, + Hooks: Hooks{ConfigShow: noop}, }) if !errors.Is(err, ErrSubcommandRequired) { t.Fatalf("Run error = %v, want ErrSubcommandRequired", err) @@ -148,7 +150,7 @@ func TestRunVersionHookOverridesDefault(t *testing.T) { } } -func TestRunRequiresVersionWithoutVersionHook(t *testing.T) { +func TestRunVersionAutoHiddenWithoutHookOrVersionString(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer @@ -158,8 +160,8 @@ func TestRunRequiresVersionWithoutVersionHook(t *testing.T) { Stdout: &stdout, Stderr: &stderr, }) - if !errors.Is(err, ErrVersionRequired) { - t.Fatalf("Run error = %v, want ErrVersionRequired", err) + if !errors.Is(err, ErrUnknownCommand) { + t.Fatalf("Run error = %v, want ErrUnknownCommand", err) } } @@ -167,12 +169,15 @@ func TestRunPrintsGlobalHelp(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer + noop := func(_ context.Context, _ Invocation) error { return nil } err := Run(context.Background(), Options{ BinaryName: "my-mcp", Description: "Binaire MCP de test.", + Version: "v1.2.3", Args: []string{"help"}, Stdout: &stdout, Stderr: &stderr, + Hooks: Hooks{Setup: noop, MCP: noop, ConfigShow: noop, Update: noop}, }) if err != nil { t.Fatalf("Run error = %v", err) @@ -197,12 +202,15 @@ func TestRunPrintsGlobalHelpWhenNoArgs(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer + noop := func(_ context.Context, _ Invocation) error { return nil } err := Run(context.Background(), Options{ BinaryName: "my-mcp", Description: "Binaire MCP de test.", + Version: "v1.2.3", Args: []string{}, Stdout: &stdout, Stderr: &stderr, + Hooks: Hooks{Setup: noop, MCP: noop, ConfigShow: noop, Update: noop}, }) if err != nil { t.Fatalf("Run error = %v", err) @@ -227,11 +235,13 @@ func TestRunPrintsCommandHelp(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer + noop := func(_ context.Context, _ Invocation) error { return nil } err := Run(context.Background(), Options{ BinaryName: "my-mcp", Args: []string{"help", "update"}, Stdout: &stdout, Stderr: &stderr, + Hooks: Hooks{Update: noop}, }) if err != nil { t.Fatalf("Run error = %v", err) @@ -250,11 +260,13 @@ func TestRunPrintsLoginCommandHelp(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer + noop := func(_ context.Context, _ Invocation) error { return nil } err := Run(context.Background(), Options{ BinaryName: "my-mcp", Args: []string{"help", "login"}, Stdout: &stdout, Stderr: &stderr, + Hooks: Hooks{Login: noop}, }) if err != nil { t.Fatalf("Run error = %v", err) @@ -273,11 +285,13 @@ func TestRunPrintsConfigHelp(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer + noop := func(_ context.Context, _ Invocation) error { return nil } err := Run(context.Background(), Options{ BinaryName: "my-mcp", Args: []string{"help", "config"}, Stdout: &stdout, Stderr: &stderr, + Hooks: Hooks{ConfigShow: noop}, }) if err != nil { t.Fatalf("Run error = %v", err) @@ -299,11 +313,13 @@ func TestRunPrintsConfigSubcommandHelp(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer + noop := func(_ context.Context, _ Invocation) error { return nil } err := Run(context.Background(), Options{ BinaryName: "my-mcp", Args: []string{"help", "config", "show"}, Stdout: &stdout, Stderr: &stderr, + Hooks: Hooks{ConfigShow: noop}, }) if err != nil { t.Fatalf("Run error = %v", err) @@ -349,11 +365,13 @@ func TestRunConfigShowReturnsCommandNotConfigured(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer + noop := func(_ context.Context, _ Invocation) error { return nil } err := Run(context.Background(), Options{ BinaryName: "my-mcp", Args: []string{"config", "show"}, Stdout: &stdout, Stderr: &stderr, + Hooks: Hooks{ConfigTest: noop}, }) if !errors.Is(err, ErrCommandNotConfigured) { t.Fatalf("Run error = %v, want ErrCommandNotConfigured", err) @@ -364,11 +382,13 @@ func TestRunConfigReturnsUnknownSubcommand(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer + noop := func(_ context.Context, _ Invocation) error { return nil } err := Run(context.Background(), Options{ BinaryName: "my-mcp", Args: []string{"config", "sync"}, Stdout: &stdout, Stderr: &stderr, + Hooks: Hooks{ConfigShow: noop}, }) if !errors.Is(err, ErrUnknownSubcommand) { t.Fatalf("Run error = %v, want ErrUnknownSubcommand", err) @@ -412,6 +432,7 @@ func TestRunPrintsAliasHelpFromHelpCommand(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer + noop := func(_ context.Context, _ Invocation) error { return nil } err := Run(context.Background(), Options{ BinaryName: "my-mcp", Aliases: map[string][]string{ @@ -420,6 +441,7 @@ func TestRunPrintsAliasHelpFromHelpCommand(t *testing.T) { Args: []string{"help", "doctor"}, Stdout: &stdout, Stderr: &stderr, + Hooks: Hooks{ConfigTest: noop}, }) if err != nil { t.Fatalf("Run error = %v", err) @@ -435,6 +457,7 @@ func TestRunPrintsAliasHelpFromTrailingHelpFlag(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer + noop := func(_ context.Context, _ Invocation) error { return nil } err := Run(context.Background(), Options{ BinaryName: "my-mcp", Aliases: map[string][]string{ @@ -443,6 +466,7 @@ func TestRunPrintsAliasHelpFromTrailingHelpFlag(t *testing.T) { Args: []string{"doctor", "--help"}, Stdout: &stdout, Stderr: &stderr, + Hooks: Hooks{ConfigTest: noop}, }) if err != nil { t.Fatalf("Run error = %v", err)