211 lines
6 KiB
Go
211 lines
6 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"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)
|
|
profile, ok := cfg.Profiles[profileName]
|
|
if !ok {
|
|
return frameworkcli.DoctorResult{
|
|
Name: "profile",
|
|
Status: frameworkcli.DoctorStatusFail,
|
|
Summary: "resolved profile is missing",
|
|
Detail: fmt.Sprintf("profile %q", profileName),
|
|
}
|
|
}
|
|
|
|
var issues []string
|
|
if strings.TrimSpace(profile.Host) == "" {
|
|
issues = append(issues, "host is empty")
|
|
}
|
|
if strings.TrimSpace(profile.Username) == "" {
|
|
issues = append(issues, "username is empty")
|
|
}
|
|
if len(issues) > 0 {
|
|
return frameworkcli.DoctorResult{
|
|
Name: "profile",
|
|
Status: frameworkcli.DoctorStatusFail,
|
|
Summary: "resolved profile is incomplete",
|
|
Detail: fmt.Sprintf("profile %q: %s", profileName, strings.Join(issues, "; ")),
|
|
}
|
|
}
|
|
|
|
return frameworkcli.DoctorResult{
|
|
Name: "profile",
|
|
Status: frameworkcli.DoctorStatusOK,
|
|
Summary: "resolved profile is complete",
|
|
Detail: fmt.Sprintf("profile %q", profileName),
|
|
}
|
|
}
|
|
}
|
|
|
|
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(),
|
|
}
|
|
}
|
|
|
|
_, hasPassword, err := loadStoredPassword(store, profileName)
|
|
if err != nil {
|
|
return frameworkcli.DoctorResult{
|
|
Name: "password",
|
|
Status: frameworkcli.DoctorStatusFail,
|
|
Summary: "cannot read stored password",
|
|
Detail: err.Error(),
|
|
}
|
|
}
|
|
if !hasPassword {
|
|
return frameworkcli.DoctorResult{
|
|
Name: "password",
|
|
Status: frameworkcli.DoctorStatusFail,
|
|
Summary: "stored password is missing",
|
|
Detail: fmt.Sprintf("secret %q", passwordSecretName(profileName)),
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|