feat(bootstrap): expose doctor alias in help and options

This commit is contained in:
thibaud-lclr 2026-04-14 17:51:28 +02:00
parent 3d8a7dc84d
commit 0e5bfb2d39
3 changed files with 186 additions and 12 deletions

View file

@ -73,6 +73,10 @@ func main() {
BinaryName: "my-mcp", BinaryName: "my-mcp",
Description: "Client MCP", Description: "Client MCP",
Version: version, Version: version,
EnableDoctorAlias: true, // expose `doctor` comme alias de `config test`
AliasDescriptions: map[string]string{
"doctor": "Diagnostiquer la configuration locale.",
},
Hooks: bootstrap.Hooks{ Hooks: bootstrap.Hooks{
Setup: func(ctx context.Context, inv bootstrap.Invocation) error { Setup: func(ctx context.Context, inv bootstrap.Invocation) error {
return runSetup(ctx, inv.Args) return runSetup(ctx, inv.Args)
@ -102,6 +106,7 @@ automatiquement `Options.Version`.
Sans arguments, `bootstrap.Run` affiche l'aide globale (pas de lancement implicite de `mcp`). Sans arguments, `bootstrap.Run` affiche l'aide globale (pas de lancement implicite de `mcp`).
La commande `config` impose une sous-commande (`show`, `test`, et optionnellement `delete`). La commande `config` impose une sous-commande (`show`, `test`, et optionnellement `delete`).
Les alias déclarés (ou `EnableDoctorAlias`) sont affichés dans l'aide globale.
## Manifeste `mcp.toml` ## Manifeste `mcp.toml`

View file

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"sort"
"strings" "strings"
) )
@ -15,6 +16,7 @@ const (
CommandConfig = "config" CommandConfig = "config"
CommandUpdate = "update" CommandUpdate = "update"
CommandVersion = "version" CommandVersion = "version"
CommandDoctor = "doctor"
ConfigSubcommandShow = "show" ConfigSubcommandShow = "show"
ConfigSubcommandTest = "test" ConfigSubcommandTest = "test"
@ -48,6 +50,8 @@ type Options struct {
Description string Description string
Version string Version string
Aliases map[string][]string Aliases map[string][]string
AliasDescriptions map[string]string
EnableDoctorAlias bool
Args []string Args []string
Stdin io.Reader Stdin io.Reader
Stdout io.Writer Stdout io.Writer
@ -165,9 +169,62 @@ func normalize(opts Options) Options {
if opts.Args == nil { if opts.Args == nil {
opts.Args = os.Args[1:] opts.Args = os.Args[1:]
} }
opts.Aliases = normalizeAliases(opts.Aliases, opts.EnableDoctorAlias)
opts.AliasDescriptions = normalizeAliasDescriptions(opts.AliasDescriptions, opts.Aliases, opts.EnableDoctorAlias)
return opts return opts
} }
func normalizeAliases(aliases map[string][]string, enableDoctorAlias bool) map[string][]string {
normalized := make(map[string][]string, len(aliases)+1)
for name, target := range aliases {
trimmedName := strings.TrimSpace(name)
trimmedTarget := trimArgs(target)
if trimmedName == "" || len(trimmedTarget) == 0 {
continue
}
normalized[trimmedName] = trimmedTarget
}
if enableDoctorAlias {
if _, ok := normalized[CommandDoctor]; !ok {
normalized[CommandDoctor] = []string{CommandConfig, ConfigSubcommandTest}
}
}
if len(normalized) == 0 {
return nil
}
return normalized
}
func normalizeAliasDescriptions(descriptions map[string]string, aliases map[string][]string, enableDoctorAlias bool) map[string]string {
normalized := make(map[string]string, len(descriptions)+1)
for name, description := range descriptions {
trimmedName := strings.TrimSpace(name)
trimmedDescription := strings.TrimSpace(description)
if trimmedName == "" || trimmedDescription == "" {
continue
}
if _, ok := aliases[trimmedName]; !ok {
continue
}
normalized[trimmedName] = trimmedDescription
}
if enableDoctorAlias {
if _, ok := aliases[CommandDoctor]; ok {
if _, defined := normalized[CommandDoctor]; !defined {
normalized[CommandDoctor] = "Diagnostiquer la configuration locale."
}
}
}
if len(normalized) == 0 {
return nil
}
return normalized
}
func parseArgs(args []string) (command string, commandArgs []string, showHelp bool) { func parseArgs(args []string) (command string, commandArgs []string, showHelp bool) {
args = trimArgs(args) args = trimArgs(args)
if len(args) == 0 { if len(args) == 0 {
@ -435,6 +492,34 @@ func printGlobalHelp(opts Options) error {
} }
} }
if len(opts.Aliases) > 0 {
if _, err := fmt.Fprintln(opts.Stdout, "\nAlias:"); err != nil {
return err
}
names := make([]string, 0, len(opts.Aliases))
for name := range opts.Aliases {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
target := strings.Join(opts.Aliases[name], " ")
description := aliasDescription(opts.AliasDescriptions, name, target)
if _, err := fmt.Fprintf(opts.Stdout, " %-7s %s\n", name, description); err != nil {
return err
}
}
}
_, err := fmt.Fprintf(opts.Stdout, "\nAide detaillee: %s help <command>\n", opts.BinaryName) _, err := fmt.Fprintf(opts.Stdout, "\nAide detaillee: %s help <command>\n", opts.BinaryName)
return err return err
} }
func aliasDescription(descriptions map[string]string, name, target string) string {
description := strings.TrimSpace(descriptions[name])
if description == "" {
return fmt.Sprintf("Alias de %q.", target)
}
return fmt.Sprintf("%s (alias de %q).", description, target)
}

View file

@ -398,3 +398,87 @@ func TestRunPrintsAliasHelpFromTrailingHelpFlag(t *testing.T) {
t.Fatalf("command help output = %q", text) t.Fatalf("command help output = %q", text)
} }
} }
func TestRunPrintsAliasesInGlobalHelp(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
Aliases: map[string][]string{
"doctor": {CommandConfig, ConfigSubcommandTest},
},
AliasDescriptions: map[string]string{
"doctor": "Diagnostiquer la configuration locale.",
},
Args: []string{"help"},
Stdout: &stdout,
Stderr: &stderr,
})
if err != nil {
t.Fatalf("Run error = %v", err)
}
text := stdout.String()
if !strings.Contains(text, "Alias:") {
t.Fatalf("global help output missing alias section: %q", text)
}
if !strings.Contains(text, `doctor Diagnostiquer la configuration locale. (alias de "config test").`) {
t.Fatalf("global help output missing doctor alias details: %q", text)
}
}
func TestRunRoutesDoctorAliasWhenEnabled(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
var got Invocation
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
EnableDoctorAlias: true,
Args: []string{"doctor", "--profile", "prod"},
Stdout: &stdout,
Stderr: &stderr,
Hooks: Hooks{
ConfigTest: func(_ context.Context, inv Invocation) error {
got = inv
return nil
},
},
})
if err != nil {
t.Fatalf("Run error = %v", err)
}
if got.Command != "config test" {
t.Fatalf("invocation command = %q, want %q", got.Command, "config test")
}
wantArgs := []string{"--profile", "prod"}
if !slices.Equal(got.Args, wantArgs) {
t.Fatalf("invocation args = %v, want %v", got.Args, wantArgs)
}
}
func TestRunPrintsDoctorAliasInGlobalHelpWhenEnabled(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
err := Run(context.Background(), Options{
BinaryName: "my-mcp",
EnableDoctorAlias: true,
Args: []string{"help"},
Stdout: &stdout,
Stderr: &stderr,
})
if err != nil {
t.Fatalf("Run error = %v", err)
}
text := stdout.String()
if !strings.Contains(text, "Alias:") {
t.Fatalf("global help output missing alias section: %q", text)
}
if !strings.Contains(text, `doctor Diagnostiquer la configuration locale. (alias de "config test").`) {
t.Fatalf("global help output missing default doctor alias details: %q", text)
}
}