package cli import ( "context" "errors" "fmt" "os" "path/filepath" "runtime" "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" frameworkupdate "gitea.lclr.dev/AI/mcp-framework/update" ) 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") } metadata := a.runtimeMetadata() report := frameworkcli.RunDoctor(ctx, frameworkcli.DoctorOptions{ ConfigCheck: frameworkcli.NewConfigCheck(frameworkconfig.NewStore[ProfileConfig](binaryName)), SecretStoreCheck: frameworkcli.SecretStoreAvailabilityCheck(a.openSecretStore), ManifestDir: a.doctorManifestDir(), ManifestValidator: func(file frameworkmanifest.File, _ string) []string { return validateManifestUpdate(file, metadata.BinaryName) }, ConnectivityCheck: a.doctorConnectivityCheck(profileFlag), ExtraChecks: []frameworkcli.DoctorCheck{ a.doctorRequiredProfileFieldsCheck(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) doctorManifestDir() string { if a.resolveExecutable == nil { return "." } executablePath, err := a.resolveExecutable() if err != nil { return "." } return filepath.Dir(executablePath) } func (a *App) doctorRequiredProfileFieldsCheck(profileFlag string) frameworkcli.DoctorCheck { var ( profileValues map[string]string loadErr error ) check := frameworkcli.RequiredResolvedFieldsCheck(frameworkcli.ResolveOptions{ Fields: profileFieldSpecs(), Lookup: frameworkcli.ResolveLookup(frameworkcli.ResolveLookupOptions{ Env: frameworkcli.EnvLookup(os.LookupEnv), Config: func(key string) (string, bool, error) { if loadErr != nil { return "", false, loadErr } if profileValues == nil { cfg, _, err := a.configStore.LoadDefault() if err != nil { loadErr = err return "", false, loadErr } profileName := a.resolveProfileName(profileFlag, cfg.CurrentProfile) profile := cfg.Profiles[profileName] profileValues = map[string]string{ "host": profile.Host, "username": profile.Username, } } return frameworkcli.MapLookup(profileValues)(key) }, }), }) return func(ctx context.Context) frameworkcli.DoctorResult { result := check(ctx) result.Name = "profile" return result } } 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 { if a.configStore == nil { return a.resolveProfileName(profileFlag, "") } cfg, _, err := a.configStore.LoadDefault() if err != nil { return a.resolveProfileName(profileFlag, "") } return a.resolveProfileName(profileFlag, cfg.CurrentProfile) } func validateManifestUpdate(file frameworkmanifest.File, runtimeBinaryName string) []string { source := file.Update.ReleaseSource() issues := make([]string, 0, 2) if _, err := frameworkupdate.ResolveLatestReleaseURL("", source); err != nil { issues = append(issues, err.Error()) } binary := strings.TrimSpace(runtimeBinaryName) if binary == "" { binary = strings.TrimSpace(file.BinaryName) } if binary == "" { binary = binaryName } if _, err := frameworkupdate.AssetNameWithTemplate(binary, runtime.GOOS, runtime.GOARCH, source.AssetNameTemplate); err != nil { issues = append(issues, err.Error()) } return issues }