2026-04-13 20:33:10 +00:00
|
|
|
package cli
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
2026-04-14 15:09:46 +00:00
|
|
|
"os"
|
2026-04-13 20:33:10 +00:00
|
|
|
"time"
|
|
|
|
|
|
2026-05-11 09:16:37 +00:00
|
|
|
"email-mcp/mcpgen"
|
|
|
|
|
frameworkcli "forge.lclr.dev/AI/mcp-framework/cli"
|
|
|
|
|
frameworkconfig "forge.lclr.dev/AI/mcp-framework/config"
|
2026-04-13 20:33:10 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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{
|
2026-05-13 09:39:39 +00:00
|
|
|
ConfigCheck: frameworkcli.NewConfigCheck(frameworkconfig.NewStore[ProfileConfig](mcpgen.BinaryName)),
|
|
|
|
|
SecretStoreCheck: frameworkcli.SecretStoreAvailabilityCheck(a.openSecretStore),
|
2026-04-13 20:33:10 +00:00
|
|
|
ConnectivityCheck: a.doctorConnectivityCheck(profileFlag),
|
|
|
|
|
ExtraChecks: []frameworkcli.DoctorCheck{
|
2026-04-14 15:09:46 +00:00
|
|
|
a.doctorRequiredProfileFieldsCheck(profileFlag),
|
2026-04-13 20:33:10 +00:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if err := frameworkcli.RenderDoctorReport(a.stdout, report); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if report.HasFailures() {
|
|
|
|
|
return fmt.Errorf("doctor checks failed")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 15:09:46 +00:00
|
|
|
func (a *App) doctorRequiredProfileFieldsCheck(profileFlag string) frameworkcli.DoctorCheck {
|
|
|
|
|
var (
|
|
|
|
|
profileValues map[string]string
|
|
|
|
|
loadErr error
|
|
|
|
|
)
|
2026-04-13 20:33:10 +00:00
|
|
|
|
2026-04-14 15:09:46 +00:00
|
|
|
check := frameworkcli.RequiredResolvedFieldsCheck(frameworkcli.ResolveOptions{
|
2026-05-11 09:16:37 +00:00
|
|
|
Fields: profileFieldSpecs(a.resolveDoctorProfileName(profileFlag)),
|
2026-04-14 15:09:46 +00:00
|
|
|
Lookup: frameworkcli.ResolveLookup(frameworkcli.ResolveLookupOptions{
|
|
|
|
|
Env: frameworkcli.EnvLookup(os.LookupEnv),
|
|
|
|
|
Config: func(key string) (string, bool, error) {
|
|
|
|
|
if loadErr != nil {
|
|
|
|
|
return "", false, loadErr
|
2026-04-14 07:04:40 +00:00
|
|
|
}
|
2026-04-14 15:09:46 +00:00
|
|
|
if profileValues == nil {
|
|
|
|
|
cfg, _, err := a.configStore.LoadDefault()
|
|
|
|
|
if err != nil {
|
|
|
|
|
loadErr = err
|
|
|
|
|
return "", false, loadErr
|
|
|
|
|
}
|
2026-04-13 20:33:10 +00:00
|
|
|
|
2026-04-14 15:09:46 +00:00
|
|
|
profileName := a.resolveProfileName(profileFlag, cfg.CurrentProfile)
|
|
|
|
|
profile := cfg.Profiles[profileName]
|
|
|
|
|
profileValues = map[string]string{
|
|
|
|
|
"host": profile.Host,
|
|
|
|
|
"username": profile.Username,
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-13 20:33:10 +00:00
|
|
|
|
2026-04-14 15:09:46 +00:00
|
|
|
return frameworkcli.MapLookup(profileValues)(key)
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
})
|
2026-04-14 07:04:40 +00:00
|
|
|
|
2026-04-14 15:09:46 +00:00
|
|
|
return func(ctx context.Context) frameworkcli.DoctorResult {
|
|
|
|
|
result := check(ctx)
|
|
|
|
|
result.Name = "profile"
|
|
|
|
|
return result
|
2026-04-13 20:33:10 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
if a.configStore == nil {
|
2026-04-14 10:48:36 +00:00
|
|
|
return a.resolveProfileName(profileFlag, "")
|
2026-04-13 20:33:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cfg, _, err := a.configStore.LoadDefault()
|
|
|
|
|
if err != nil {
|
2026-04-14 10:48:36 +00:00
|
|
|
return a.resolveProfileName(profileFlag, "")
|
2026-04-13 20:33:10 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-14 10:48:36 +00:00
|
|
|
return a.resolveProfileName(profileFlag, cfg.CurrentProfile)
|
2026-04-13 20:33:10 +00:00
|
|
|
}
|
2026-04-14 13:54:17 +00:00
|
|
|
|