diff --git a/README.md b/README.md index d76f502..14cd589 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ Le binaire s’appuie maintenant sur [`mcp-framework`](../mcp-framework) pour : - `email-mcp mcp` : lance le serveur MCP sur `stdin/stdout` - `email-mcp doctor` : diagnostique la configuration locale, le wallet, le manifeste et l’accès IMAP - `email-mcp update` : met à jour le binaire courant depuis la dernière release +- `email-mcp version` : affiche la version du binaire + +La commande `email-mcp help` (ou `-h` / `--help`) affiche l’aide globale. ## Outils MCP diff --git a/go.mod b/go.mod index 484961e..041c674 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module email-mcp go 1.25.0 require ( - gitea.lclr.dev/AI/mcp-framework v1.2.0-rc1 + gitea.lclr.dev/AI/mcp-framework v1.2.0-rc2 github.com/emersion/go-imap/v2 v2.0.0-beta.8 github.com/emersion/go-message v0.18.2 github.com/godbus/dbus/v5 v5.2.2 diff --git a/go.sum b/go.sum index 805d846..c2399c5 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -gitea.lclr.dev/AI/mcp-framework v1.2.0-rc1 h1:S4YZ7G9b5RI7DmgTTEzWJKsWFf9Z1guPjxyajkcNLI0= -gitea.lclr.dev/AI/mcp-framework v1.2.0-rc1/go.mod h1:kUVMrL3/UBYgjOsW7sJCs3V0pO0qoJJMpIpueoTsoA4= +gitea.lclr.dev/AI/mcp-framework v1.2.0-rc2 h1:nzeW1JkGPV/+Hhhtdy7EWWeDQNjt36qMeVQjJYmGCQE= +gitea.lclr.dev/AI/mcp-framework v1.2.0-rc2/go.mod h1:kUVMrL3/UBYgjOsW7sJCs3V0pO0qoJJMpIpueoTsoA4= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= diff --git a/internal/cli/app.go b/internal/cli/app.go index 8dffd95..1e10017 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -10,6 +10,7 @@ import ( "path/filepath" "strings" + frameworkbootstrap "gitea.lclr.dev/AI/mcp-framework/bootstrap" frameworkcli "gitea.lclr.dev/AI/mcp-framework/cli" frameworkconfig "gitea.lclr.dev/AI/mcp-framework/config" frameworkmanifest "gitea.lclr.dev/AI/mcp-framework/manifest" @@ -23,6 +24,7 @@ import ( const ( binaryName = "email-mcp" defaultProfileEnv = "EMAIL_MCP_PROFILE" + binaryDescription = "Local MCP server to read an IMAP mailbox." ) type MCPRunner interface { @@ -112,22 +114,119 @@ func NewAppWithDependencies( } func (a *App) Run(args []string) error { - if len(args) == 0 { - return fmt.Errorf("usage: email-mcp ") + if isDoctorHelpCommand(args) { + return a.printDoctorHelp() } - switch args[0] { - case "config", "setup": - return a.runConfig(context.Background(), args[0], args[1:]) - case "mcp": - return a.runMCP(context.Background(), args[1:]) - case "doctor": + if len(args) > 0 && strings.TrimSpace(args[0]) == "doctor" { return a.runDoctor(context.Background(), args[1:]) - case "update": - return a.runUpdate(context.Background(), args[1:]) - default: - return fmt.Errorf("unknown command: %s", args[0]) } + + if isGlobalHelpCommand(args) { + return a.printGlobalHelp() + } + + return a.runBootstrap(context.Background(), args) +} + +func (a *App) runBootstrap(ctx context.Context, args []string) error { + return frameworkbootstrap.Run(ctx, frameworkbootstrap.Options{ + BinaryName: binaryName, + Description: binaryDescription, + Version: a.version, + Args: args, + Stdin: a.stdin, + Stdout: a.stdout, + Stderr: a.stderr, + Hooks: frameworkbootstrap.Hooks{ + Setup: func(ctx context.Context, inv frameworkbootstrap.Invocation) error { + return a.runConfig(ctx, frameworkbootstrap.CommandSetup, inv.Args) + }, + MCP: func(ctx context.Context, inv frameworkbootstrap.Invocation) error { + return a.runMCP(ctx, inv.Args) + }, + Config: func(ctx context.Context, inv frameworkbootstrap.Invocation) error { + return a.runConfig(ctx, frameworkbootstrap.CommandConfig, inv.Args) + }, + Update: func(ctx context.Context, inv frameworkbootstrap.Invocation) error { + return a.runUpdate(ctx, inv.Args) + }, + }, + }) +} + +func isGlobalHelpCommand(args []string) bool { + if len(args) == 0 { + return true + } + if len(args) != 1 { + return false + } + + switch strings.TrimSpace(args[0]) { + case "help", "-h", "--help": + return true + default: + return false + } +} + +func isDoctorHelpCommand(args []string) bool { + if len(args) != 2 { + return false + } + + first := strings.TrimSpace(args[0]) + second := strings.TrimSpace(args[1]) + + if first == "help" && second == "doctor" { + return true + } + if first == "doctor" && (second == "-h" || second == "--help") { + return true + } + return false +} + +func (a *App) printGlobalHelp() error { + if _, err := fmt.Fprintf(a.stdout, "%s\n\n", binaryDescription); err != nil { + return err + } + if _, err := fmt.Fprintf(a.stdout, "Usage:\n %s [args]\n\n", binaryName); err != nil { + return err + } + if _, err := fmt.Fprintln(a.stdout, "Common commands:"); err != nil { + return err + } + + commands := []struct { + name string + description string + }{ + {name: "setup", description: "Initialize or update local configuration."}, + {name: "mcp", description: "Run the MCP server over stdio."}, + {name: "config", description: "Inspect or update configuration."}, + {name: "doctor", description: "Run local diagnostics."}, + {name: "update", description: "Run the self-update flow."}, + {name: "version", description: "Print the binary version."}, + } + for _, command := range commands { + if _, err := fmt.Fprintf(a.stdout, " %-7s %s\n", command.name, command.description); err != nil { + return err + } + } + + _, err := fmt.Fprintf(a.stdout, "\nDetailed help: %s help \n", binaryName) + return err +} + +func (a *App) printDoctorHelp() error { + _, err := fmt.Fprintf( + a.stdout, + "Usage:\n %s doctor [--profile NAME]\n\nRun local diagnostics for config, wallet, manifest, and IMAP connectivity.\n", + binaryName, + ) + return err } func (a *App) runConfig(ctx context.Context, command string, args []string) error { diff --git a/internal/cli/app_test.go b/internal/cli/app_test.go index 8fa6e95..d7b2952 100644 --- a/internal/cli/app_test.go +++ b/internal/cli/app_test.go @@ -142,17 +142,42 @@ func TestAppRunRejectsUnknownCommand(t *testing.T) { } func TestAppRunShowsUsageWhenNoArgsProvided(t *testing.T) { - app := NewAppWithDependencies(nil, nil, nil, nil, nil, nil, nil, nil, nil, &bytes.Buffer{}, "dev") + output := &bytes.Buffer{} + app := NewAppWithDependencies(nil, nil, nil, nil, nil, nil, nil, nil, output, &bytes.Buffer{}, "dev") - err := app.Run(nil) - if err == nil { - t.Fatal("expected error for missing command") + if err := app.Run(nil); err != nil { + t.Fatalf("expected help to be rendered, got error %v", err) } - if !strings.Contains(err.Error(), "usage:") { - t.Fatalf("expected usage text in error, got %q", err.Error()) + + text := output.String() + for _, snippet := range []string{"Usage:", "doctor", "version"} { + if !strings.Contains(text, snippet) { + t.Fatalf("help output missing %q: %q", snippet, text) + } } - if !strings.Contains(err.Error(), "doctor") { - t.Fatalf("expected usage to mention doctor, got %q", err.Error()) +} + +func TestAppRunVersionPrintsBuildVersion(t *testing.T) { + output := &bytes.Buffer{} + app := NewAppWithDependencies(nil, nil, nil, nil, nil, nil, nil, nil, output, &bytes.Buffer{}, "v1.2.3") + + if err := app.Run([]string{"version"}); err != nil { + t.Fatalf("version returned error: %v", err) + } + if got := output.String(); got != "v1.2.3\n" { + t.Fatalf("version output = %q, want %q", got, "v1.2.3\n") + } +} + +func TestAppRunDoctorHelp(t *testing.T) { + output := &bytes.Buffer{} + app := NewAppWithDependencies(nil, nil, nil, nil, nil, nil, nil, nil, output, &bytes.Buffer{}, "dev") + + if err := app.Run([]string{"doctor", "--help"}); err != nil { + t.Fatalf("doctor help returned error: %v", err) + } + if got := output.String(); !strings.Contains(got, "email-mcp doctor [--profile NAME]") { + t.Fatalf("unexpected doctor help output: %q", got) } }