146 lines
3.4 KiB
Go
146 lines
3.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"email-mcp/mcpgen"
|
|
frameworkcli "forge.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
|
|
}
|
|
|
|
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,
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|