237 lines
6.4 KiB
Go
237 lines
6.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"email-mcp/mcpgen"
|
|
frameworkcli "forge.lclr.dev/AI/mcp-framework/cli"
|
|
frameworkconfig "forge.lclr.dev/AI/mcp-framework/config"
|
|
frameworkmanifest "forge.lclr.dev/AI/mcp-framework/manifest"
|
|
frameworkupdate "forge.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](mcpgen.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(a.resolveDoctorProfileName(profileFlag)),
|
|
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,
|
|
passwordOnlyFieldSpecs(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
|
|
}
|