email-mcp/internal/cli/doctor.go
2026-04-14 09:04:40 +02:00

234 lines
6.6 KiB
Go

package cli
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
frameworkcli "gitea.lclr.dev/AI/mcp-framework/cli"
frameworkconfig "gitea.lclr.dev/AI/mcp-framework/config"
frameworkmanifest "gitea.lclr.dev/AI/mcp-framework/manifest"
frameworksecretstore "gitea.lclr.dev/AI/mcp-framework/secretstore"
)
func (a *App) runDoctor(ctx context.Context, args []string) error {
profileFlag, err := parseProfileArgs("doctor", args)
if err != nil {
return err
}
if a.configStore == nil {
return fmt.Errorf("config store is not configured")
}
if a.openSecretStore == nil {
return fmt.Errorf("secret store is not configured")
}
if a.newMailService == nil {
return fmt.Errorf("mail service is not configured")
}
report := frameworkcli.RunDoctor(ctx, frameworkcli.DoctorOptions{
ConfigCheck: frameworkcli.NewConfigCheck(frameworkconfig.NewStore[ProfileConfig](binaryName)),
SecretStoreCheck: frameworkcli.SecretStoreAvailabilityCheck(a.frameworkSecretStoreFactory()),
ManifestDir: a.doctorManifestDir(),
ManifestValidator: func(file frameworkmanifest.File, _ string) []string {
if strings.TrimSpace(file.Update.LatestReleaseURL) == "" {
return []string{"update.latest_release_url must not be empty"}
}
return nil
},
ConnectivityCheck: a.doctorConnectivityCheck(profileFlag),
ExtraChecks: []frameworkcli.DoctorCheck{
a.doctorProfileCheck(profileFlag),
a.doctorPasswordCheck(profileFlag),
},
})
if err := frameworkcli.RenderDoctorReport(a.stdout, report); err != nil {
return err
}
if report.HasFailures() {
return fmt.Errorf("doctor checks failed")
}
return nil
}
func (a *App) frameworkSecretStoreFactory() func() (frameworksecretstore.Store, error) {
return func() (frameworksecretstore.Store, error) {
store, err := a.openSecretStore()
if err != nil {
return nil, err
}
return store, nil
}
}
func (a *App) doctorManifestDir() string {
if a.resolveExecutable == nil {
return "."
}
executablePath, err := a.resolveExecutable()
if err != nil {
return "."
}
return filepath.Dir(executablePath)
}
func (a *App) doctorProfileCheck(profileFlag string) frameworkcli.DoctorCheck {
return func(context.Context) frameworkcli.DoctorResult {
cfg, _, err := a.configStore.LoadDefault()
if err != nil {
return frameworkcli.DoctorResult{
Name: "profile",
Status: frameworkcli.DoctorStatusFail,
Summary: "cannot load profile configuration",
Detail: err.Error(),
}
}
profileName := frameworkcli.ResolveProfileName(profileFlag, os.Getenv(defaultProfileEnv), cfg.CurrentProfile)
resolution, err := resolveCredentialFields(cfg.Profiles[profileName], nil, profileFieldSpecs())
if err != nil {
var missingErr *frameworkcli.MissingRequiredValuesError
if errors.As(err, &missingErr) {
return frameworkcli.DoctorResult{
Name: "profile",
Status: frameworkcli.DoctorStatusFail,
Summary: "resolved profile is incomplete",
Detail: fmt.Sprintf("profile %q: missing %s", profileName, strings.Join(missingErr.Fields, ", ")),
}
}
return frameworkcli.DoctorResult{
Name: "profile",
Status: frameworkcli.DoctorStatusFail,
Summary: "cannot resolve profile values",
Detail: err.Error(),
}
}
host, _ := resolution.Get("host")
username, _ := resolution.Get("username")
return frameworkcli.DoctorResult{
Name: "profile",
Status: frameworkcli.DoctorStatusOK,
Summary: "resolved profile is complete",
Detail: fmt.Sprintf(
"profile %q (host: %s, username: %s)",
profileName,
host.Source,
username.Source,
),
}
}
}
func (a *App) doctorPasswordCheck(profileFlag string) frameworkcli.DoctorCheck {
return func(context.Context) frameworkcli.DoctorResult {
profileName := a.resolveDoctorProfileName(profileFlag)
store, err := a.openSecretStore()
if err != nil {
return frameworkcli.DoctorResult{
Name: "password",
Status: frameworkcli.DoctorStatusFail,
Summary: "cannot inspect stored password",
Detail: err.Error(),
}
}
resolution, err := resolveCredentialFields(
ProfileConfig{},
store,
[]frameworkcli.FieldSpec{passwordFieldSpec(profileName)},
)
if err != nil {
var missingErr *frameworkcli.MissingRequiredValuesError
if errors.As(err, &missingErr) {
return frameworkcli.DoctorResult{
Name: "password",
Status: frameworkcli.DoctorStatusFail,
Summary: "stored password is missing",
Detail: fmt.Sprintf(
"set %q or secret %q",
passwordEnv,
passwordSecretName(profileName),
),
}
}
return frameworkcli.DoctorResult{
Name: "password",
Status: frameworkcli.DoctorStatusFail,
Summary: "cannot read stored password",
Detail: err.Error(),
}
}
password, _ := resolution.Get("password")
if password.Source == frameworkcli.SourceEnv {
return frameworkcli.DoctorResult{
Name: "password",
Status: frameworkcli.DoctorStatusOK,
Summary: "password is provided via environment",
Detail: fmt.Sprintf("variable %q", passwordEnv),
}
}
return frameworkcli.DoctorResult{
Name: "password",
Status: frameworkcli.DoctorStatusOK,
Summary: "stored password is present",
Detail: fmt.Sprintf("secret %q", passwordSecretName(profileName)),
}
}
}
func (a *App) doctorConnectivityCheck(profileFlag string) frameworkcli.DoctorCheck {
return func(parent context.Context) frameworkcli.DoctorResult {
ctx, cancel := context.WithTimeout(parent, 35*time.Second)
defer cancel()
cred, err := a.loadCredential(profileFlag)
if err != nil {
return frameworkcli.DoctorResult{
Name: "connectivity",
Status: frameworkcli.DoctorStatusFail,
Summary: "cannot load IMAP credentials",
Detail: err.Error(),
}
}
if _, err := a.newMailService().ListMailboxes(ctx, cred); err != nil {
return frameworkcli.DoctorResult{
Name: "connectivity",
Status: frameworkcli.DoctorStatusFail,
Summary: "IMAP server is unreachable or rejected authentication",
Detail: err.Error(),
}
}
return frameworkcli.DoctorResult{
Name: "connectivity",
Status: frameworkcli.DoctorStatusOK,
Summary: "IMAP server is reachable",
}
}
}
func (a *App) resolveDoctorProfileName(profileFlag string) string {
envProfile := os.Getenv(defaultProfileEnv)
if a.configStore == nil {
return frameworkcli.ResolveProfileName(profileFlag, envProfile, "")
}
cfg, _, err := a.configStore.LoadDefault()
if err != nil {
return frameworkcli.ResolveProfileName(profileFlag, envProfile, "")
}
return frameworkcli.ResolveProfileName(profileFlag, envProfile, cfg.CurrentProfile)
}