email-mcp/internal/cli/setup.go

147 lines
3.4 KiB
Go
Raw Normal View History

package cli
import (
"bufio"
"context"
"fmt"
"io"
"os"
"strings"
2026-04-13 16:01:28 +00:00
2026-05-11 09:16:37 +00:00
"email-mcp/mcpgen"
frameworkcli "forge.lclr.dev/AI/mcp-framework/cli"
"email-mcp/internal/secretstore"
)
2026-04-13 16:01:28 +00:00
type InteractiveConfigPrompter struct {
reader *bufio.Reader
stdinFile *os.File
output io.Writer
}
2026-04-13 16:01:28 +00:00
func NewInteractiveConfigPrompter(input io.Reader, output io.Writer) *InteractiveConfigPrompter {
if input == nil {
input = strings.NewReader("")
}
if output == nil {
output = io.Discard
}
2026-04-13 16:01:28 +00:00
prompter := &InteractiveConfigPrompter{
reader: bufio.NewReader(input),
output: output,
}
2026-04-13 16:01:28 +00:00
if file, ok := input.(*os.File); ok {
prompter.stdinFile = file
}
return prompter
}
2026-04-13 16:01:28 +00:00
func (p *InteractiveConfigPrompter) PromptCredential(_ context.Context, existing secretstore.Credential, hasStoredPassword bool) (secretstore.Credential, error) {
if p.stdinFile != nil {
return p.promptCredentialWithSetupEngine(existing, hasStoredPassword)
}
2026-04-13 16:01:28 +00:00
host, err := frameworkcli.PromptLine(p.reader, p.output, "IMAP host", existing.Host)
if err != nil {
return secretstore.Credential{}, err
}
2026-04-13 16:01:28 +00:00
username, err := frameworkcli.PromptLine(p.reader, p.output, "Username", existing.Username)
if err != nil {
return secretstore.Credential{}, err
}
2026-04-13 16:01:28 +00:00
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
}
2026-04-13 16:01:28 +00:00
return cred, nil
}
func (p *InteractiveConfigPrompter) promptCredentialWithSetupEngine(existing secretstore.Credential, hasStoredPassword bool) (secretstore.Credential, error) {
password := ""
if hasStoredPassword {
password = existing.Password
}
2026-05-11 09:16:37 +00:00
fields := mcpgen.SetupFields(map[string]string{"password": password})
for i := range fields {
switch fields[i].Name {
case "host":
fields[i].Default = existing.Host
case "username":
fields[i].Default = existing.Username
}
}
result, err := frameworkcli.RunSetup(frameworkcli.SetupOptions{
Stdin: p.stdinFile,
Stdout: p.output,
2026-05-11 09:16:37 +00:00
Fields: fields,
})
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
}
2026-04-13 16:01:28 +00:00
func (p *InteractiveConfigPrompter) promptPassword(storedPassword string, hasStoredPassword bool) (string, error) {
if p.stdinFile != nil {
return frameworkcli.PromptSecret(p.stdinFile, p.output, "Password", hasStoredPassword, storedPassword)
}
2026-04-13 16:01:28 +00:00
if hasStoredPassword {
fmt.Fprint(p.output, "Password [stored, leave blank to keep]: ")
} else {
fmt.Fprint(p.output, "Password: ")
}
2026-04-13 16:01:28 +00:00
line, err := p.reader.ReadString('\n')
if err != nil && err != io.EOF {
2026-04-13 16:01:28 +00:00
return "", err
}
2026-04-13 16:01:28 +00:00
password := strings.TrimSpace(line)
if password == "" && hasStoredPassword {
return storedPassword, nil
}
2026-04-13 16:01:28 +00:00
return password, nil
}