mcp-framework/bootstrap/bootstrap.go

375 lines
8.5 KiB
Go
Raw Normal View History

package bootstrap
import (
"context"
"errors"
"fmt"
"io"
"os"
"strings"
)
const (
CommandSetup = "setup"
CommandMCP = "mcp"
CommandConfig = "config"
CommandUpdate = "update"
CommandVersion = "version"
ConfigSubcommandShow = "show"
ConfigSubcommandTest = "test"
ConfigSubcommandDelete = "delete"
)
var (
ErrBinaryNameRequired = errors.New("binary name is required")
ErrUnknownCommand = errors.New("unknown command")
ErrUnknownSubcommand = errors.New("unknown subcommand")
ErrCommandNotConfigured = errors.New("command not configured")
ErrVersionRequired = errors.New("version is required when no version hook is configured")
ErrSubcommandRequired = errors.New("subcommand is required")
)
type Handler func(context.Context, Invocation) error
type Hooks struct {
Setup Handler
MCP Handler
Config Handler
ConfigShow Handler
ConfigTest Handler
ConfigDelete Handler
Update Handler
Version Handler
}
type Options struct {
BinaryName string
Description string
Version string
Args []string
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
Hooks Hooks
}
type Invocation struct {
Command string
Args []string
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
}
type commandDef struct {
Name string
Description string
Handler func(Hooks) Handler
}
var commands = []commandDef{
{
Name: CommandSetup,
Description: "Initialiser ou mettre a jour la configuration locale.",
Handler: func(h Hooks) Handler {
return h.Setup
},
},
{
Name: CommandMCP,
Description: "Executer la logique MCP principale du binaire.",
Handler: func(h Hooks) Handler {
return h.MCP
},
},
{
Name: CommandConfig,
Description: "Inspecter ou modifier la configuration.",
Handler: func(h Hooks) Handler {
return h.Config
},
},
{
Name: CommandUpdate,
Description: "Declencher le flux d'auto-update.",
Handler: func(h Hooks) Handler {
return h.Update
},
},
{
Name: CommandVersion,
Description: "Afficher la version du binaire.",
Handler: func(h Hooks) Handler {
return h.Version
},
},
}
func Run(ctx context.Context, opts Options) error {
normalized := normalize(opts)
if strings.TrimSpace(normalized.BinaryName) == "" {
return ErrBinaryNameRequired
}
command, commandArgs, showHelp := parseArgs(normalized.Args)
if showHelp {
return printHelp(normalized, command, commandArgs)
}
if command == "" {
return printHelp(normalized, "")
}
if command == CommandConfig {
return runConfigCommand(ctx, normalized, commandArgs)
}
handler, known := resolveHandler(command, normalized.Hooks)
if !known {
return fmt.Errorf("%w: %s", ErrUnknownCommand, command)
}
if handler == nil {
if command == CommandVersion {
if strings.TrimSpace(normalized.Version) == "" {
return ErrVersionRequired
}
_, err := fmt.Fprintln(normalized.Stdout, normalized.Version)
return err
}
return fmt.Errorf("%w: %s", ErrCommandNotConfigured, command)
}
return handler(ctx, Invocation{
Command: command,
Args: commandArgs,
Stdin: normalized.Stdin,
Stdout: normalized.Stdout,
Stderr: normalized.Stderr,
})
}
func normalize(opts Options) Options {
if opts.Stdin == nil {
opts.Stdin = os.Stdin
}
if opts.Stdout == nil {
opts.Stdout = os.Stdout
}
if opts.Stderr == nil {
opts.Stderr = os.Stderr
}
if opts.Args == nil {
opts.Args = os.Args[1:]
}
return opts
}
func parseArgs(args []string) (command string, commandArgs []string, showHelp bool) {
if len(args) == 0 {
return "", nil, false
}
first := strings.TrimSpace(args[0])
switch first {
case "help", "-h", "--help":
if len(args) > 1 {
return strings.TrimSpace(args[1]), trimArgs(args[2:]), true
}
return "", nil, true
}
command = first
commandArgs = trimArgs(args[1:])
if len(commandArgs) > 0 {
last := strings.TrimSpace(commandArgs[len(commandArgs)-1])
if last == "-h" || last == "--help" {
return command, commandArgs[:len(commandArgs)-1], true
}
}
return command, commandArgs, false
}
func trimArgs(args []string) []string {
if len(args) == 0 {
return nil
}
result := make([]string, 0, len(args))
for _, arg := range args {
trimmed := strings.TrimSpace(arg)
if trimmed == "" {
continue
}
result = append(result, trimmed)
}
return result
}
func runConfigCommand(ctx context.Context, opts Options, args []string) error {
if len(args) == 0 {
return fmt.Errorf("%w: %s requires one of: %s, %s", ErrSubcommandRequired, CommandConfig, ConfigSubcommandShow, ConfigSubcommandTest)
}
subcommand := strings.TrimSpace(args[0])
subcommandArgs := args[1:]
handler, known := resolveConfigHandler(opts.Hooks, subcommand)
if !known {
if opts.Hooks.Config != nil {
return opts.Hooks.Config(ctx, Invocation{
Command: CommandConfig,
Args: args,
Stdin: opts.Stdin,
Stdout: opts.Stdout,
Stderr: opts.Stderr,
})
}
return fmt.Errorf("%w: %s %s", ErrUnknownSubcommand, CommandConfig, subcommand)
}
if handler == nil {
if opts.Hooks.Config != nil {
return opts.Hooks.Config(ctx, Invocation{
Command: fmt.Sprintf("%s %s", CommandConfig, subcommand),
Args: subcommandArgs,
Stdin: opts.Stdin,
Stdout: opts.Stdout,
Stderr: opts.Stderr,
})
}
return fmt.Errorf("%w: %s %s", ErrCommandNotConfigured, CommandConfig, subcommand)
}
return handler(ctx, Invocation{
Command: fmt.Sprintf("%s %s", CommandConfig, subcommand),
Args: subcommandArgs,
Stdin: opts.Stdin,
Stdout: opts.Stdout,
Stderr: opts.Stderr,
})
}
func resolveConfigHandler(hooks Hooks, subcommand string) (Handler, bool) {
switch subcommand {
case ConfigSubcommandShow:
return hooks.ConfigShow, true
case ConfigSubcommandTest:
return hooks.ConfigTest, true
case ConfigSubcommandDelete:
return hooks.ConfigDelete, true
default:
return nil, false
}
}
func resolveHandler(command string, hooks Hooks) (Handler, bool) {
for _, def := range commands {
if def.Name == command {
return def.Handler(hooks), true
}
}
return nil, false
}
func printHelp(opts Options, command string, args ...[]string) error {
var commandArgs []string
if len(args) > 0 {
commandArgs = args[0]
}
if command == "" {
return printGlobalHelp(opts)
}
if command == CommandConfig {
return printConfigHelp(opts, commandArgs)
}
for _, def := range commands {
if def.Name != command {
continue
}
_, err := fmt.Fprintf(
opts.Stdout,
"Usage:\n %s %s [args]\n\n%s\n",
opts.BinaryName,
def.Name,
def.Description,
)
return err
}
return fmt.Errorf("%w: %s", ErrUnknownCommand, command)
}
func printConfigHelp(opts Options, args []string) error {
if len(args) == 0 {
_, err := fmt.Fprintf(
opts.Stdout,
"Usage:\n %s config <subcommand> [args]\n\nSubcommandes:\n %-7s Afficher la configuration résolue et la provenance des valeurs.\n %-7s Vérifier la configuration et la connectivité.\n %-7s Supprimer un profil local (optionnel).\n",
opts.BinaryName,
ConfigSubcommandShow,
ConfigSubcommandTest,
ConfigSubcommandDelete,
)
return err
}
switch args[0] {
case ConfigSubcommandShow:
_, err := fmt.Fprintf(
opts.Stdout,
"Usage:\n %s config %s [args]\n\nAfficher la configuration résolue et l'origine des valeurs.\n",
opts.BinaryName,
ConfigSubcommandShow,
)
return err
case ConfigSubcommandTest:
_, err := fmt.Fprintf(
opts.Stdout,
"Usage:\n %s config %s [args]\n\nTester la configuration résolue et la connectivité associée.\n",
opts.BinaryName,
ConfigSubcommandTest,
)
return err
case ConfigSubcommandDelete:
_, err := fmt.Fprintf(
opts.Stdout,
"Usage:\n %s config %s [args]\n\nSupprimer un profil local de configuration.\n",
opts.BinaryName,
ConfigSubcommandDelete,
)
return err
default:
return fmt.Errorf("%w: %s %s", ErrUnknownSubcommand, CommandConfig, args[0])
}
}
func printGlobalHelp(opts Options) error {
if strings.TrimSpace(opts.Description) != "" {
if _, err := fmt.Fprintf(opts.Stdout, "%s\n\n", opts.Description); err != nil {
return err
}
}
if _, err := fmt.Fprintf(opts.Stdout, "Usage:\n %s <command> [args]\n\n", opts.BinaryName); err != nil {
return err
}
if _, err := fmt.Fprintln(opts.Stdout, "Commandes communes:"); err != nil {
return err
}
for _, def := range commands {
if _, err := fmt.Fprintf(opts.Stdout, " %-7s %s\n", def.Name, def.Description); err != nil {
return err
}
}
_, err := fmt.Fprintf(opts.Stdout, "\nAide detaillee: %s help <command>\n", opts.BinaryName)
return err
}