2026-04-14 06:24:24 +00:00
package bootstrap
import (
"context"
"errors"
"fmt"
"io"
"os"
2026-04-14 15:51:28 +00:00
"sort"
2026-04-14 06:24:24 +00:00
"strings"
)
const (
CommandSetup = "setup"
2026-04-20 10:38:58 +00:00
CommandLogin = "login"
2026-04-14 06:24:24 +00:00
CommandMCP = "mcp"
CommandConfig = "config"
CommandUpdate = "update"
CommandVersion = "version"
2026-04-14 15:51:28 +00:00
CommandDoctor = "doctor"
2026-04-14 08:52:36 +00:00
2026-04-20 09:36:07 +00:00
bitwardenDebugEnvName = "MCP_FRAMEWORK_BITWARDEN_DEBUG"
2026-04-14 08:52:36 +00:00
ConfigSubcommandShow = "show"
ConfigSubcommandTest = "test"
ConfigSubcommandDelete = "delete"
2026-04-14 06:24:24 +00:00
)
var (
ErrBinaryNameRequired = errors . New ( "binary name is required" )
ErrUnknownCommand = errors . New ( "unknown command" )
2026-04-14 08:52:36 +00:00
ErrUnknownSubcommand = errors . New ( "unknown subcommand" )
2026-04-14 06:24:24 +00:00
ErrCommandNotConfigured = errors . New ( "command not configured" )
ErrVersionRequired = errors . New ( "version is required when no version hook is configured" )
2026-04-14 08:52:36 +00:00
ErrSubcommandRequired = errors . New ( "subcommand is required" )
2026-04-14 06:24:24 +00:00
)
type Handler func ( context . Context , Invocation ) error
type Hooks struct {
2026-04-14 08:52:36 +00:00
Setup Handler
2026-04-20 10:38:58 +00:00
Login Handler
2026-04-14 08:52:36 +00:00
MCP Handler
Config Handler
ConfigShow Handler
ConfigTest Handler
ConfigDelete Handler
Update Handler
Version Handler
2026-04-14 06:24:24 +00:00
}
type Options struct {
2026-05-12 08:30:11 +00:00
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
2026-04-14 06:24:24 +00:00
}
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
} ,
} ,
2026-04-20 10:38:58 +00:00
{
Name : CommandLogin ,
Description : "Authentifier et deverrouiller Bitwarden pour persister BW_SESSION." ,
Handler : func ( h Hooks ) Handler {
return h . Login
} ,
} ,
2026-04-14 06:24:24 +00:00
{
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
}
2026-04-20 09:36:07 +00:00
resolvedArgs := expandAliases ( normalized . Args , normalized . Aliases )
resolvedArgs , debugEnabled := extractGlobalDebugFlag ( resolvedArgs )
if debugEnabled {
_ = os . Setenv ( bitwardenDebugEnvName , "1" )
}
command , commandArgs , showHelp := parseArgs ( resolvedArgs )
2026-04-14 06:24:24 +00:00
if showHelp {
2026-04-14 08:52:36 +00:00
return printHelp ( normalized , command , commandArgs )
2026-04-14 06:24:24 +00:00
}
if command == "" {
return printHelp ( normalized , "" )
}
2026-05-12 08:30:11 +00:00
if isCommandDisabled ( command , normalized . DisabledCommands ) {
return fmt . Errorf ( "%w: %s" , ErrUnknownCommand , command )
}
2026-04-14 08:52:36 +00:00
if command == CommandConfig {
return runConfigCommand ( ctx , normalized , commandArgs )
}
2026-04-14 06:24:24 +00:00
handler , known := resolveHandler ( command , normalized . Hooks )
if ! known {
return fmt . Errorf ( "%w: %s" , ErrUnknownCommand , command )
}
if handler == nil {
2026-05-11 09:37:35 +00:00
switch command {
case CommandVersion :
2026-04-14 06:24:24 +00:00
if strings . TrimSpace ( normalized . Version ) == "" {
return ErrVersionRequired
}
_ , err := fmt . Fprintln ( normalized . Stdout , normalized . Version )
return err
2026-05-11 09:37:35 +00:00
default :
return fmt . Errorf ( "%w: %s" , ErrCommandNotConfigured , command )
2026-04-14 06:24:24 +00:00
}
}
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 : ]
}
2026-04-14 15:51:28 +00:00
opts . Aliases = normalizeAliases ( opts . Aliases , opts . EnableDoctorAlias )
opts . AliasDescriptions = normalizeAliasDescriptions ( opts . AliasDescriptions , opts . Aliases , opts . EnableDoctorAlias )
2026-05-12 08:37:46 +00:00
for _ , cmd := range autoDisabledCommands ( opts ) {
if ! isCommandDisabled ( cmd , opts . DisabledCommands ) {
opts . DisabledCommands = append ( opts . DisabledCommands , cmd )
}
}
2026-04-14 06:24:24 +00:00
return opts
}
2026-04-14 15:51:28 +00:00
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
}
2026-04-14 06:24:24 +00:00
func parseArgs ( args [ ] string ) ( command string , commandArgs [ ] string , showHelp bool ) {
2026-04-14 14:31:26 +00:00
args = trimArgs ( args )
2026-04-14 06:24:24 +00:00
if len ( args ) == 0 {
return "" , nil , false
}
first := strings . TrimSpace ( args [ 0 ] )
switch first {
case "help" , "-h" , "--help" :
if len ( args ) > 1 {
2026-04-14 08:52:36 +00:00
return strings . TrimSpace ( args [ 1 ] ) , trimArgs ( args [ 2 : ] ) , true
2026-04-14 06:24:24 +00:00
}
return "" , nil , true
}
command = first
2026-04-14 08:52:36 +00:00
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
2026-04-14 06:24:24 +00:00
}
}
return command , commandArgs , false
}
2026-04-20 09:36:07 +00:00
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
}
2026-04-14 14:31:26 +00:00
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
}
2026-04-14 08:52:36 +00:00
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 {
2026-04-14 11:55:00 +00:00
return fmt . Errorf (
"%w: %s requires one of: %s, %s, %s" ,
ErrSubcommandRequired ,
CommandConfig ,
ConfigSubcommandShow ,
ConfigSubcommandTest ,
ConfigSubcommandDelete ,
)
2026-04-14 08:52:36 +00:00
}
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
}
}
2026-04-14 06:24:24 +00:00
func resolveHandler ( command string , hooks Hooks ) ( Handler , bool ) {
for _ , def := range commands {
if def . Name == command {
return def . Handler ( hooks ) , true
}
}
return nil , false
}
2026-04-14 08:52:36 +00:00
func printHelp ( opts Options , command string , args ... [ ] string ) error {
var commandArgs [ ] string
if len ( args ) > 0 {
commandArgs = args [ 0 ]
}
2026-04-14 06:24:24 +00:00
if command == "" {
return printGlobalHelp ( opts )
}
2026-05-12 08:30:11 +00:00
if isCommandDisabled ( command , opts . DisabledCommands ) {
return fmt . Errorf ( "%w: %s" , ErrUnknownCommand , command )
}
2026-05-12 08:37:46 +00:00
if command == CommandConfig {
return printConfigHelp ( opts , commandArgs )
}
2026-04-14 06:24:24 +00:00
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 )
}
2026-04-14 08:52:36 +00:00
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 ] )
}
}
2026-04-14 06:24:24 +00:00
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 {
2026-05-12 08:30:11 +00:00
if isCommandDisabled ( def . Name , opts . DisabledCommands ) {
continue
}
2026-04-14 06:24:24 +00:00
if _ , err := fmt . Fprintf ( opts . Stdout , " %-7s %s\n" , def . Name , def . Description ) ; err != nil {
return err
}
}
2026-04-14 15:51:28 +00:00
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
}
}
}
2026-04-20 09:36:07 +00:00
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
}
2026-04-14 06:24:24 +00:00
_ , err := fmt . Fprintf ( opts . Stdout , "\nAide detaillee: %s help <command>\n" , opts . BinaryName )
return err
}
2026-04-14 15:51:28 +00:00
2026-05-12 08:37:46 +00:00
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
}
2026-05-12 08:30:11 +00:00
func isCommandDisabled ( command string , disabled [ ] string ) bool {
for _ , d := range disabled {
if d == command {
return true
}
}
return false
}
2026-04-14 15:51:28 +00:00
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 )
}