package bootstrap import ( "context" "errors" "fmt" "io" "os" "sort" "strings" ) const ( CommandSetup = "setup" CommandLogin = "login" CommandMCP = "mcp" CommandConfig = "config" CommandUpdate = "update" CommandVersion = "version" CommandDoctor = "doctor" bitwardenDebugEnvName = "MCP_FRAMEWORK_BITWARDEN_DEBUG" 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 Login Handler MCP Handler Config Handler ConfigShow Handler ConfigTest Handler ConfigDelete Handler Update Handler Version Handler } type Options struct { BinaryName string Description string Version string Aliases map[string][]string AliasDescriptions map[string]string EnableDoctorAlias bool DisabledCommands []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: CommandLogin, Description: "Authentifier et deverrouiller Bitwarden pour persister BW_SESSION.", Handler: func(h Hooks) Handler { return h.Login }, }, { 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 } resolvedArgs := expandAliases(normalized.Args, normalized.Aliases) resolvedArgs, debugEnabled := extractGlobalDebugFlag(resolvedArgs) if debugEnabled { _ = os.Setenv(bitwardenDebugEnvName, "1") } command, commandArgs, showHelp := parseArgs(resolvedArgs) if showHelp { return printHelp(normalized, command, commandArgs) } if command == "" { return printHelp(normalized, "") } if isCommandDisabled(command, normalized.DisabledCommands) { return fmt.Errorf("%w: %s", ErrUnknownCommand, command) } 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 { switch command { case CommandVersion: if strings.TrimSpace(normalized.Version) == "" { return ErrVersionRequired } _, err := fmt.Fprintln(normalized.Stdout, normalized.Version) return err default: 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:] } opts.Aliases = normalizeAliases(opts.Aliases, opts.EnableDoctorAlias) opts.AliasDescriptions = normalizeAliasDescriptions(opts.AliasDescriptions, opts.Aliases, opts.EnableDoctorAlias) for _, cmd := range autoDisabledCommands(opts) { if !isCommandDisabled(cmd, opts.DisabledCommands) { opts.DisabledCommands = append(opts.DisabledCommands, cmd) } } 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 { 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 extractGlobalDebugFlag(args []string) ([]string, bool) { args = trimArgs(args) if len(args) == 0 { return nil, false } filtered := make([]string, 0, len(args)) debugEnabled := false for _, arg := range args { if strings.TrimSpace(arg) == "--debug" { debugEnabled = true continue } filtered = append(filtered, arg) } return filtered, debugEnabled } 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 } 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, %s", ErrSubcommandRequired, CommandConfig, ConfigSubcommandShow, ConfigSubcommandTest, ConfigSubcommandDelete, ) } 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 isCommandDisabled(command, opts.DisabledCommands) { return fmt.Errorf("%w: %s", ErrUnknownCommand, command) } 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 isCommandDisabled(def.Name, opts.DisabledCommands) { continue } if _, err := fmt.Fprintf(opts.Stdout, " %-7s %s\n", def.Name, def.Description); err != nil { return err } } 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 } } } if _, err := fmt.Fprintln(opts.Stdout, "\nOptions globales:"); err != nil { return err } if _, err := fmt.Fprintln(opts.Stdout, " --debug Active le debug des appels Bitwarden."); err != nil { return err } _, err := fmt.Fprintf(opts.Stdout, "\nAide detaillee: %s help \n", opts.BinaryName) return err } func autoDisabledCommands(opts Options) []string { h := opts.Hooks var disabled []string if h.Setup == nil { disabled = append(disabled, CommandSetup) } if h.Login == nil { disabled = append(disabled, CommandLogin) } if h.MCP == nil { disabled = append(disabled, CommandMCP) } if h.Config == nil && h.ConfigShow == nil && h.ConfigTest == nil && h.ConfigDelete == nil { disabled = append(disabled, CommandConfig) } if h.Update == nil { disabled = append(disabled, CommandUpdate) } if h.Version == nil && strings.TrimSpace(opts.Version) == "" { disabled = append(disabled, CommandVersion) } return disabled } func isCommandDisabled(command string, disabled []string) bool { for _, d := range disabled { if d == command { return true } } return false } 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) }