feat(cli): adopt rc4 declarative setup engine

This commit is contained in:
thibaud-leclere 2026-04-14 10:13:39 +02:00
parent 8c79db73d7
commit 1da92a8240
4 changed files with 131 additions and 3 deletions

2
go.mod
View file

@ -3,7 +3,7 @@ module email-mcp
go 1.25.0
require (
gitea.lclr.dev/AI/mcp-framework v1.2.0-rc3
gitea.lclr.dev/AI/mcp-framework v1.2.0-rc4
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

4
go.sum
View file

@ -1,5 +1,5 @@
gitea.lclr.dev/AI/mcp-framework v1.2.0-rc3 h1:pbG3eFQbBBVZDlNMA1MY3ZYocVGiZT0z95dHOUbSJYQ=
gitea.lclr.dev/AI/mcp-framework v1.2.0-rc3/go.mod h1:kUVMrL3/UBYgjOsW7sJCs3V0pO0qoJJMpIpueoTsoA4=
gitea.lclr.dev/AI/mcp-framework v1.2.0-rc4 h1:gnnYBvnbhD/u3IRfJs6hzTbMU7kf1/KD1oUWIRTyjO8=
gitea.lclr.dev/AI/mcp-framework v1.2.0-rc4/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=

View file

@ -39,6 +39,10 @@ func NewInteractiveConfigPrompter(input io.Reader, output io.Writer) *Interactiv
}
func (p *InteractiveConfigPrompter) PromptCredential(_ context.Context, existing secretstore.Credential, hasStoredPassword bool) (secretstore.Credential, error) {
if p.stdinFile != nil {
return p.promptCredentialWithSetupEngine(existing, hasStoredPassword)
}
host, err := frameworkcli.PromptLine(p.reader, p.output, "IMAP host", existing.Host)
if err != nil {
return secretstore.Credential{}, err
@ -66,6 +70,68 @@ func (p *InteractiveConfigPrompter) PromptCredential(_ context.Context, existing
return cred, nil
}
func (p *InteractiveConfigPrompter) promptCredentialWithSetupEngine(existing secretstore.Credential, hasStoredPassword bool) (secretstore.Credential, error) {
password := ""
if hasStoredPassword {
password = existing.Password
}
result, err := frameworkcli.RunSetup(frameworkcli.SetupOptions{
Stdin: p.stdinFile,
Stdout: p.output,
Fields: []frameworkcli.SetupField{
{
Name: "host",
Label: "IMAP host",
Type: frameworkcli.SetupFieldString,
Required: true,
Default: existing.Host,
},
{
Name: "username",
Label: "Username",
Type: frameworkcli.SetupFieldString,
Required: true,
Default: existing.Username,
},
{
Name: "password",
Label: "Password",
Type: frameworkcli.SetupFieldSecret,
Required: true,
ExistingSecret: password,
},
},
})
if err != nil {
return secretstore.Credential{}, err
}
host, ok := result.Get("host")
if !ok {
return secretstore.Credential{}, fmt.Errorf("setup result is missing host")
}
username, ok := result.Get("username")
if !ok {
return secretstore.Credential{}, fmt.Errorf("setup result is missing username")
}
secret, ok := result.Get("password")
if !ok {
return secretstore.Credential{}, fmt.Errorf("setup result is missing password")
}
cred := secretstore.Credential{
Host: host.String,
Username: username.String,
Password: secret.String,
}
if err := cred.Validate(); err != nil {
return secretstore.Credential{}, err
}
return cred, nil
}
func (p *InteractiveConfigPrompter) promptPassword(storedPassword string, hasStoredPassword bool) (string, error) {
if p.stdinFile != nil {
return frameworkcli.PromptSecret(p.stdinFile, p.output, "Password", hasStoredPassword, storedPassword)

View file

@ -3,6 +3,7 @@ package cli
import (
"bytes"
"context"
"os"
"strings"
"testing"
@ -48,3 +49,64 @@ func TestInteractiveConfigPrompterPromptCredentialKeepsStoredPassword(t *testing
t.Fatalf("unexpected prompts: %q", got)
}
}
func TestInteractiveConfigPrompterPromptCredentialUsesSetupEngineWithFileInput(t *testing.T) {
input := setupInputFile(t, "imap.example.com\nalice\nsecret\n")
output := &bytes.Buffer{}
prompter := NewInteractiveConfigPrompter(input, output)
cred, err := prompter.PromptCredential(context.Background(), secretstore.Credential{}, false)
if err != nil {
t.Fatalf("PromptCredential returned error: %v", err)
}
if cred.Host != "imap.example.com" || cred.Username != "alice" || cred.Password != "secret" {
t.Fatalf("unexpected credential: %#v", cred)
}
if got := output.String(); got != "IMAP host: Username: Password: " {
t.Fatalf("unexpected prompts: %q", got)
}
}
func TestInteractiveConfigPrompterPromptCredentialKeepsStoredPasswordWithSetupEngine(t *testing.T) {
input := setupInputFile(t, "imap.example.com\nalice\n\n")
output := &bytes.Buffer{}
prompter := NewInteractiveConfigPrompter(input, output)
cred, err := prompter.PromptCredential(context.Background(), secretstore.Credential{
Host: "imap.example.com",
Username: "alice",
Password: "stored-secret",
}, true)
if err != nil {
t.Fatalf("PromptCredential returned error: %v", err)
}
if cred.Password != "stored-secret" {
t.Fatalf("expected stored password to be preserved, got %q", cred.Password)
}
if got := output.String(); !strings.Contains(got, "Password [stored, leave blank to keep]: ") {
t.Fatalf("unexpected prompts: %q", got)
}
}
func setupInputFile(t *testing.T, content string) *os.File {
t.Helper()
input, err := os.CreateTemp(t.TempDir(), "setup-input-*.txt")
if err != nil {
t.Fatalf("CreateTemp returned error: %v", err)
}
t.Cleanup(func() {
_ = input.Close()
})
if _, err := input.WriteString(content); err != nil {
t.Fatalf("WriteString returned error: %v", err)
}
if _, err := input.Seek(0, 0); err != nil {
t.Fatalf("Seek returned error: %v", err)
}
return input
}