From 0e5bfb2d390143ec86810667c1e1169a733abeb5 Mon Sep 17 00:00:00 2001 From: thibaud-lclr Date: Tue, 14 Apr 2026 17:51:28 +0200 Subject: [PATCH] feat(bootstrap): expose doctor alias in help and options --- README.md | 11 ++-- bootstrap/bootstrap.go | 103 ++++++++++++++++++++++++++++++++---- bootstrap/bootstrap_test.go | 84 +++++++++++++++++++++++++++++ 3 files changed, 186 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a713318..f9145a8 100644 --- a/README.md +++ b/README.md @@ -70,9 +70,13 @@ Exemple minimal : ```go func main() { err := bootstrap.Run(context.Background(), bootstrap.Options{ - BinaryName: "my-mcp", - Description: "Client MCP", - Version: version, + BinaryName: "my-mcp", + Description: "Client MCP", + Version: version, + EnableDoctorAlias: true, // expose `doctor` comme alias de `config test` + AliasDescriptions: map[string]string{ + "doctor": "Diagnostiquer la configuration locale.", + }, Hooks: bootstrap.Hooks{ Setup: func(ctx context.Context, inv bootstrap.Invocation) error { 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`). 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` diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 4d5521b..ea2ff2f 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "sort" "strings" ) @@ -15,6 +16,7 @@ const ( CommandConfig = "config" CommandUpdate = "update" CommandVersion = "version" + CommandDoctor = "doctor" ConfigSubcommandShow = "show" ConfigSubcommandTest = "test" @@ -44,15 +46,17 @@ type Hooks struct { } type Options struct { - BinaryName string - Description string - Version string - Aliases map[string][]string - Args []string - Stdin io.Reader - Stdout io.Writer - Stderr io.Writer - Hooks Hooks + BinaryName string + Description string + Version string + Aliases map[string][]string + AliasDescriptions map[string]string + EnableDoctorAlias bool + Args []string + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer + Hooks Hooks } type Invocation struct { @@ -165,9 +169,62 @@ func normalize(opts Options) Options { if opts.Args == nil { opts.Args = os.Args[1:] } + opts.Aliases = normalizeAliases(opts.Aliases, opts.EnableDoctorAlias) + opts.AliasDescriptions = normalizeAliasDescriptions(opts.AliasDescriptions, opts.Aliases, opts.EnableDoctorAlias) 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) { args = trimArgs(args) 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 \n", opts.BinaryName) 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) +} diff --git a/bootstrap/bootstrap_test.go b/bootstrap/bootstrap_test.go index 356c76f..02644e3 100644 --- a/bootstrap/bootstrap_test.go +++ b/bootstrap/bootstrap_test.go @@ -398,3 +398,87 @@ func TestRunPrintsAliasHelpFromTrailingHelpFlag(t *testing.T) { 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) + } +}