email-mcp/internal/cli/setup.go
2026-04-14 10:13:39 +02:00

157 lines
3.7 KiB
Go

package cli
import (
"bufio"
"context"
"fmt"
"io"
"os"
"strings"
frameworkcli "gitea.lclr.dev/AI/mcp-framework/cli"
"email-mcp/internal/secretstore"
)
type InteractiveConfigPrompter struct {
reader *bufio.Reader
stdinFile *os.File
output io.Writer
}
func NewInteractiveConfigPrompter(input io.Reader, output io.Writer) *InteractiveConfigPrompter {
if input == nil {
input = strings.NewReader("")
}
if output == nil {
output = io.Discard
}
prompter := &InteractiveConfigPrompter{
reader: bufio.NewReader(input),
output: output,
}
if file, ok := input.(*os.File); ok {
prompter.stdinFile = file
}
return prompter
}
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
}
username, err := frameworkcli.PromptLine(p.reader, p.output, "Username", existing.Username)
if err != nil {
return secretstore.Credential{}, err
}
password, err := p.promptPassword(existing.Password, hasStoredPassword)
if err != nil {
return secretstore.Credential{}, err
}
cred := secretstore.Credential{
Host: host,
Username: username,
Password: password,
}
if err := cred.Validate(); err != nil {
return secretstore.Credential{}, err
}
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)
}
if hasStoredPassword {
fmt.Fprint(p.output, "Password [stored, leave blank to keep]: ")
} else {
fmt.Fprint(p.output, "Password: ")
}
line, err := p.reader.ReadString('\n')
if err != nil && err != io.EOF {
return "", err
}
password := strings.TrimSpace(line)
if password == "" && hasStoredPassword {
return storedPassword, nil
}
return password, nil
}