feat(bootstrap): auto-hide commands with no hook configured
All checks were successful
CI / test (push) Successful in 11s
Release / release (push) Successful in 5s

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 <noreply@anthropic.com>
This commit is contained in:
thibaud-lclr 2026-05-12 10:37:46 +02:00
parent 4a7248cfa9
commit 9a52b5dce1
2 changed files with 60 additions and 7 deletions

View file

@ -205,6 +205,11 @@ func normalize(opts Options) Options {
} }
opts.Aliases = normalizeAliases(opts.Aliases, opts.EnableDoctorAlias) opts.Aliases = normalizeAliases(opts.Aliases, opts.EnableDoctorAlias)
opts.AliasDescriptions = normalizeAliasDescriptions(opts.AliasDescriptions, 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 return opts
} }
@ -461,14 +466,14 @@ func printHelp(opts Options, command string, args ...[]string) error {
return printGlobalHelp(opts) return printGlobalHelp(opts)
} }
if command == CommandConfig {
return printConfigHelp(opts, commandArgs)
}
if isCommandDisabled(command, opts.DisabledCommands) { if isCommandDisabled(command, opts.DisabledCommands) {
return fmt.Errorf("%w: %s", ErrUnknownCommand, command) return fmt.Errorf("%w: %s", ErrUnknownCommand, command)
} }
if command == CommandConfig {
return printConfigHelp(opts, commandArgs)
}
for _, def := range commands { for _, def := range commands {
if def.Name != command { if def.Name != command {
continue continue
@ -583,6 +588,30 @@ func printGlobalHelp(opts Options) error {
return err 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 { func isCommandDisabled(command string, disabled []string) bool {
for _, d := range disabled { for _, d := range disabled {
if d == command { if d == command {

View file

@ -91,11 +91,13 @@ func TestRunReturnsCommandNotConfigured(t *testing.T) {
var stdout bytes.Buffer var stdout bytes.Buffer
var stderr bytes.Buffer var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{ err := Run(context.Background(), Options{
BinaryName: "my-mcp", BinaryName: "my-mcp",
Args: []string{"config"}, Args: []string{"config"},
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, Stderr: &stderr,
Hooks: Hooks{ConfigShow: noop},
}) })
if !errors.Is(err, ErrSubcommandRequired) { if !errors.Is(err, ErrSubcommandRequired) {
t.Fatalf("Run error = %v, want ErrSubcommandRequired", err) 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 stdout bytes.Buffer
var stderr bytes.Buffer var stderr bytes.Buffer
@ -158,8 +160,8 @@ func TestRunRequiresVersionWithoutVersionHook(t *testing.T) {
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, Stderr: &stderr,
}) })
if !errors.Is(err, ErrVersionRequired) { if !errors.Is(err, ErrUnknownCommand) {
t.Fatalf("Run error = %v, want ErrVersionRequired", err) t.Fatalf("Run error = %v, want ErrUnknownCommand", err)
} }
} }
@ -167,12 +169,15 @@ func TestRunPrintsGlobalHelp(t *testing.T) {
var stdout bytes.Buffer var stdout bytes.Buffer
var stderr bytes.Buffer var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{ err := Run(context.Background(), Options{
BinaryName: "my-mcp", BinaryName: "my-mcp",
Description: "Binaire MCP de test.", Description: "Binaire MCP de test.",
Version: "v1.2.3",
Args: []string{"help"}, Args: []string{"help"},
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, Stderr: &stderr,
Hooks: Hooks{Setup: noop, MCP: noop, ConfigShow: noop, Update: noop},
}) })
if err != nil { if err != nil {
t.Fatalf("Run error = %v", err) t.Fatalf("Run error = %v", err)
@ -197,12 +202,15 @@ func TestRunPrintsGlobalHelpWhenNoArgs(t *testing.T) {
var stdout bytes.Buffer var stdout bytes.Buffer
var stderr bytes.Buffer var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{ err := Run(context.Background(), Options{
BinaryName: "my-mcp", BinaryName: "my-mcp",
Description: "Binaire MCP de test.", Description: "Binaire MCP de test.",
Version: "v1.2.3",
Args: []string{}, Args: []string{},
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, Stderr: &stderr,
Hooks: Hooks{Setup: noop, MCP: noop, ConfigShow: noop, Update: noop},
}) })
if err != nil { if err != nil {
t.Fatalf("Run error = %v", err) t.Fatalf("Run error = %v", err)
@ -227,11 +235,13 @@ func TestRunPrintsCommandHelp(t *testing.T) {
var stdout bytes.Buffer var stdout bytes.Buffer
var stderr bytes.Buffer var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{ err := Run(context.Background(), Options{
BinaryName: "my-mcp", BinaryName: "my-mcp",
Args: []string{"help", "update"}, Args: []string{"help", "update"},
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, Stderr: &stderr,
Hooks: Hooks{Update: noop},
}) })
if err != nil { if err != nil {
t.Fatalf("Run error = %v", err) t.Fatalf("Run error = %v", err)
@ -250,11 +260,13 @@ func TestRunPrintsLoginCommandHelp(t *testing.T) {
var stdout bytes.Buffer var stdout bytes.Buffer
var stderr bytes.Buffer var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{ err := Run(context.Background(), Options{
BinaryName: "my-mcp", BinaryName: "my-mcp",
Args: []string{"help", "login"}, Args: []string{"help", "login"},
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, Stderr: &stderr,
Hooks: Hooks{Login: noop},
}) })
if err != nil { if err != nil {
t.Fatalf("Run error = %v", err) t.Fatalf("Run error = %v", err)
@ -273,11 +285,13 @@ func TestRunPrintsConfigHelp(t *testing.T) {
var stdout bytes.Buffer var stdout bytes.Buffer
var stderr bytes.Buffer var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{ err := Run(context.Background(), Options{
BinaryName: "my-mcp", BinaryName: "my-mcp",
Args: []string{"help", "config"}, Args: []string{"help", "config"},
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, Stderr: &stderr,
Hooks: Hooks{ConfigShow: noop},
}) })
if err != nil { if err != nil {
t.Fatalf("Run error = %v", err) t.Fatalf("Run error = %v", err)
@ -299,11 +313,13 @@ func TestRunPrintsConfigSubcommandHelp(t *testing.T) {
var stdout bytes.Buffer var stdout bytes.Buffer
var stderr bytes.Buffer var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{ err := Run(context.Background(), Options{
BinaryName: "my-mcp", BinaryName: "my-mcp",
Args: []string{"help", "config", "show"}, Args: []string{"help", "config", "show"},
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, Stderr: &stderr,
Hooks: Hooks{ConfigShow: noop},
}) })
if err != nil { if err != nil {
t.Fatalf("Run error = %v", err) t.Fatalf("Run error = %v", err)
@ -349,11 +365,13 @@ func TestRunConfigShowReturnsCommandNotConfigured(t *testing.T) {
var stdout bytes.Buffer var stdout bytes.Buffer
var stderr bytes.Buffer var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{ err := Run(context.Background(), Options{
BinaryName: "my-mcp", BinaryName: "my-mcp",
Args: []string{"config", "show"}, Args: []string{"config", "show"},
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, Stderr: &stderr,
Hooks: Hooks{ConfigTest: noop},
}) })
if !errors.Is(err, ErrCommandNotConfigured) { if !errors.Is(err, ErrCommandNotConfigured) {
t.Fatalf("Run error = %v, want ErrCommandNotConfigured", err) t.Fatalf("Run error = %v, want ErrCommandNotConfigured", err)
@ -364,11 +382,13 @@ func TestRunConfigReturnsUnknownSubcommand(t *testing.T) {
var stdout bytes.Buffer var stdout bytes.Buffer
var stderr bytes.Buffer var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{ err := Run(context.Background(), Options{
BinaryName: "my-mcp", BinaryName: "my-mcp",
Args: []string{"config", "sync"}, Args: []string{"config", "sync"},
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, Stderr: &stderr,
Hooks: Hooks{ConfigShow: noop},
}) })
if !errors.Is(err, ErrUnknownSubcommand) { if !errors.Is(err, ErrUnknownSubcommand) {
t.Fatalf("Run error = %v, want ErrUnknownSubcommand", err) t.Fatalf("Run error = %v, want ErrUnknownSubcommand", err)
@ -412,6 +432,7 @@ func TestRunPrintsAliasHelpFromHelpCommand(t *testing.T) {
var stdout bytes.Buffer var stdout bytes.Buffer
var stderr bytes.Buffer var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{ err := Run(context.Background(), Options{
BinaryName: "my-mcp", BinaryName: "my-mcp",
Aliases: map[string][]string{ Aliases: map[string][]string{
@ -420,6 +441,7 @@ func TestRunPrintsAliasHelpFromHelpCommand(t *testing.T) {
Args: []string{"help", "doctor"}, Args: []string{"help", "doctor"},
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, Stderr: &stderr,
Hooks: Hooks{ConfigTest: noop},
}) })
if err != nil { if err != nil {
t.Fatalf("Run error = %v", err) t.Fatalf("Run error = %v", err)
@ -435,6 +457,7 @@ func TestRunPrintsAliasHelpFromTrailingHelpFlag(t *testing.T) {
var stdout bytes.Buffer var stdout bytes.Buffer
var stderr bytes.Buffer var stderr bytes.Buffer
noop := func(_ context.Context, _ Invocation) error { return nil }
err := Run(context.Background(), Options{ err := Run(context.Background(), Options{
BinaryName: "my-mcp", BinaryName: "my-mcp",
Aliases: map[string][]string{ Aliases: map[string][]string{
@ -443,6 +466,7 @@ func TestRunPrintsAliasHelpFromTrailingHelpFlag(t *testing.T) {
Args: []string{"doctor", "--help"}, Args: []string{"doctor", "--help"},
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, Stderr: &stderr,
Hooks: Hooks{ConfigTest: noop},
}) })
if err != nil { if err != nil {
t.Fatalf("Run error = %v", err) t.Fatalf("Run error = %v", err)