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) }