feat(bootstrap): expose doctor alias in help and options
This commit is contained in:
parent
3d8a7dc84d
commit
0e5bfb2d39
3 changed files with 186 additions and 12 deletions
11
README.md
11
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`
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <command>\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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue