2026-04-14 06:24:24 +00:00
package bootstrap
import (
"context"
"errors"
"fmt"
"io"
"os"
"strings"
)
const (
CommandSetup = "setup"
CommandMCP = "mcp"
CommandConfig = "config"
CommandUpdate = "update"
CommandVersion = "version"
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
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 {
BinaryName string
Description string
Version string
2026-04-14 14:31:26 +00:00
Aliases map [ string ] [ ] string
2026-04-14 06:24:24 +00:00
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
}
2026-04-14 14:31:26 +00:00
command , commandArgs , showHelp := parseArgs ( expandAliases ( normalized . Args , normalized . Aliases ) )
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-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 {
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 ) {
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-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-04-14 08:52:36 +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 {
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
}