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
|
```go
|
||||||
func main() {
|
func main() {
|
||||||
err := bootstrap.Run(context.Background(), bootstrap.Options{
|
err := bootstrap.Run(context.Background(), bootstrap.Options{
|
||||||
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`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -44,15 +46,17 @@ type Hooks struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
BinaryName string
|
BinaryName string
|
||||||
Description string
|
Description string
|
||||||
Version string
|
Version string
|
||||||
Aliases map[string][]string
|
Aliases map[string][]string
|
||||||
Args []string
|
AliasDescriptions map[string]string
|
||||||
Stdin io.Reader
|
EnableDoctorAlias bool
|
||||||
Stdout io.Writer
|
Args []string
|
||||||
Stderr io.Writer
|
Stdin io.Reader
|
||||||
Hooks Hooks
|
Stdout io.Writer
|
||||||
|
Stderr io.Writer
|
||||||
|
Hooks Hooks
|
||||||
}
|
}
|
||||||
|
|
||||||
type Invocation struct {
|
type Invocation struct {
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue