From 8c4f88ea934f09b2d0734ff3b79cc7f04370e825 Mon Sep 17 00:00:00 2001 From: thibaud-lclr Date: Tue, 14 Apr 2026 16:31:26 +0200 Subject: [PATCH] feat(bootstrap): add command alias expansion --- bootstrap/bootstrap.go | 61 +++++++++++++++++++++++++++- bootstrap/bootstrap_test.go | 79 +++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 3a14226..4d5521b 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -47,6 +47,7 @@ type Options struct { BinaryName string Description string Version string + Aliases map[string][]string Args []string Stdin io.Reader Stdout io.Writer @@ -113,7 +114,7 @@ func Run(ctx context.Context, opts Options) error { return ErrBinaryNameRequired } - command, commandArgs, showHelp := parseArgs(normalized.Args) + command, commandArgs, showHelp := parseArgs(expandAliases(normalized.Args, normalized.Aliases)) if showHelp { return printHelp(normalized, command, commandArgs) } @@ -168,6 +169,7 @@ func normalize(opts Options) Options { } func parseArgs(args []string) (command string, commandArgs []string, showHelp bool) { + args = trimArgs(args) if len(args) == 0 { return "", nil, false } @@ -193,6 +195,63 @@ func parseArgs(args []string) (command string, commandArgs []string, showHelp bo return command, commandArgs, false } +func expandAliases(args []string, aliases map[string][]string) []string { + args = trimArgs(args) + if len(args) == 0 || len(aliases) == 0 { + return args + } + + if args[0] == "help" && len(args) > 1 { + expanded, ok := resolveAlias(aliases, args[1]) + if !ok { + return args + } + + withHelp := make([]string, 0, 1+len(expanded)+len(args[2:])) + withHelp = append(withHelp, "help") + withHelp = append(withHelp, expanded...) + withHelp = append(withHelp, args[2:]...) + return withHelp + } + + expanded, ok := resolveAlias(aliases, args[0]) + if !ok { + return args + } + + withCommand := make([]string, 0, len(expanded)+len(args[1:])) + withCommand = append(withCommand, expanded...) + withCommand = append(withCommand, args[1:]...) + + if len(withCommand) == 0 { + return args + } + + last := strings.TrimSpace(withCommand[len(withCommand)-1]) + if last != "-h" && last != "--help" { + return withCommand + } + + helpArgs := make([]string, 0, len(withCommand)) + helpArgs = append(helpArgs, "help") + helpArgs = append(helpArgs, withCommand[:len(withCommand)-1]...) + return helpArgs +} + +func resolveAlias(aliases map[string][]string, command string) ([]string, bool) { + target, ok := aliases[strings.TrimSpace(command)] + if !ok { + return nil, false + } + + expanded := trimArgs(target) + if len(expanded) == 0 { + return nil, false + } + + return expanded, true +} + func trimArgs(args []string) []string { if len(args) == 0 { return nil diff --git a/bootstrap/bootstrap_test.go b/bootstrap/bootstrap_test.go index d3d34c1..356c76f 100644 --- a/bootstrap/bootstrap_test.go +++ b/bootstrap/bootstrap_test.go @@ -319,3 +319,82 @@ func TestRunConfigReturnsUnknownSubcommand(t *testing.T) { t.Fatalf("Run error = %v, want ErrUnknownSubcommand", err) } } + +func TestRunRoutesAliasToConfigSubcommandHook(t *testing.T) { + var stdout bytes.Buffer + var stderr bytes.Buffer + var got Invocation + + err := Run(context.Background(), Options{ + BinaryName: "my-mcp", + Aliases: map[string][]string{ + "doctor": {CommandConfig, ConfigSubcommandTest}, + }, + Args: []string{"doctor", "--profile", "work"}, + 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", "work"} + if !slices.Equal(got.Args, wantArgs) { + t.Fatalf("invocation args = %v, want %v", got.Args, wantArgs) + } +} + +func TestRunPrintsAliasHelpFromHelpCommand(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}, + }, + Args: []string{"help", "doctor"}, + Stdout: &stdout, + Stderr: &stderr, + }) + if err != nil { + t.Fatalf("Run error = %v", err) + } + + text := stdout.String() + if !strings.Contains(text, "my-mcp config test [args]") { + t.Fatalf("command help output = %q", text) + } +} + +func TestRunPrintsAliasHelpFromTrailingHelpFlag(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}, + }, + Args: []string{"doctor", "--help"}, + Stdout: &stdout, + Stderr: &stderr, + }) + if err != nil { + t.Fatalf("Run error = %v", err) + } + + text := stdout.String() + if !strings.Contains(text, "my-mcp config test [args]") { + t.Fatalf("command help output = %q", text) + } +}