package cli import ( "context" "errors" "fmt" "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" frameworksecretstore "gitea.lclr.dev/AI/mcp-framework/secretstore" 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.frameworkSecretStoreFactory()), ManifestDir: a.doctorManifestDir(), ManifestValidator: func(file frameworkmanifest.File, _ string) []string { return validateManifestUpdate(file, metadata.BinaryName) }, 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 := a.resolveProfileName(profileFlag, 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 { 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 }