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 [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 [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 \n", opts.BinaryName) return err }