feat(cli): adopt rc4 declarative setup engine
This commit is contained in:
parent
8c79db73d7
commit
1da92a8240
4 changed files with 131 additions and 3 deletions
2
go.mod
2
go.mod
|
|
@ -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
4
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue