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 <noreply@anthropic.com>
This commit is contained in:
parent
4a7248cfa9
commit
9a52b5dce1
2 changed files with 60 additions and 7 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue