unarr/internal/cmd/doctor.go
Deivid Soto 29cf0a0126 feat: initial commit — unarr CLI
Search, inspect, stream, and download torrents from the terminal.
Replaces the entire *arr stack with a single binary.
2026-03-28 11:29:42 +01:00

211 lines
4.8 KiB
Go

package cmd
import (
"context"
"fmt"
"os"
"runtime"
"syscall"
"time"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/torrentclaw/torrentclaw-cli/internal/agent"
"github.com/torrentclaw/torrentclaw-cli/internal/config"
)
func newDoctorCmd() *cobra.Command {
return &cobra.Command{
Use: "doctor",
Short: "Diagnose CLI configuration and connectivity",
Long: "Run diagnostic checks on API connectivity, config validity, disk space, and capabilities.",
RunE: func(cmd *cobra.Command, args []string) error {
return runDoctor()
},
}
}
func runDoctor() error {
bold := color.New(color.Bold)
green := color.New(color.FgGreen)
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
fmt.Println()
bold.Println(" unarr Diagnostics")
fmt.Println()
pass := 0
fail := 0
warn := 0
check := func(name string, fn func() (string, error)) {
msg, err := fn()
if err != nil {
red.Printf(" x %s", name)
if msg != "" {
fmt.Printf(" — %s", msg)
}
fmt.Println()
fail++
} else if msg != "" && msg[0] == '!' {
yellow.Printf(" ! %s", name)
fmt.Printf(" — %s", msg[1:])
fmt.Println()
warn++
} else {
green.Printf(" + %s", name)
if msg != "" {
fmt.Printf(" — %s", msg)
}
fmt.Println()
pass++
}
}
// Config
bold.Println(" Config")
cfg := loadConfig()
check("Config file", func() (string, error) {
path := config.FilePath()
if cfgFile != "" {
path = cfgFile
}
if _, err := os.Stat(path); os.IsNotExist(err) {
return path + " (not found, run unarr setup)", fmt.Errorf("missing")
}
return path, nil
})
check("API key configured", func() (string, error) {
key := apiKeyFlag
if key == "" {
key = cfg.Auth.APIKey
}
if key == "" {
return "run unarr setup to configure", fmt.Errorf("missing")
}
if len(key) > 8 {
return key[:8] + "...", nil
}
return "set", nil
})
fmt.Println()
bold.Println(" Connectivity")
// API connectivity
check("API reachable", func() (string, error) {
client := getClient()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
start := time.Now()
_, err := client.Health(ctx)
elapsed := time.Since(start)
if err != nil {
return cfg.Auth.APIURL, err
}
return fmt.Sprintf("%s (%dms)", cfg.Auth.APIURL, elapsed.Milliseconds()), nil
})
// Agent registration
check("Agent registration", func() (string, error) {
key := apiKeyFlag
if key == "" {
key = cfg.Auth.APIKey
}
if key == "" {
return "no API key", fmt.Errorf("skipped")
}
if cfg.Agent.ID == "" {
return "no agent ID, run unarr setup", fmt.Errorf("not registered")
}
ac := agent.NewClient(cfg.Auth.APIURL, key, "unarr/"+Version)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
resp, err := ac.Register(ctx, agent.RegisterRequest{
AgentID: cfg.Agent.ID,
Name: cfg.Agent.Name,
OS: runtime.GOOS,
Arch: runtime.GOARCH,
Version: Version,
})
if err != nil {
return "", err
}
return fmt.Sprintf("%s (%s) [%s]", resp.User.Name, resp.User.Email, resp.User.Plan), nil
})
fmt.Println()
bold.Println(" Downloads")
check("Download directory", func() (string, error) {
dir := cfg.Download.Dir
if dir == "" {
return "not configured, run unarr setup", fmt.Errorf("missing")
}
fi, err := os.Stat(dir)
if os.IsNotExist(err) {
return dir + " (does not exist)", fmt.Errorf("missing")
}
if !fi.IsDir() {
return dir + " (not a directory)", fmt.Errorf("invalid")
}
return dir, nil
})
check("Download dir writable", func() (string, error) {
dir := cfg.Download.Dir
if dir == "" {
return "", fmt.Errorf("not configured")
}
tmpFile := dir + "/.unarr_write_test"
f, err := os.Create(tmpFile)
if err != nil {
return "", fmt.Errorf("not writable: %w", err)
}
f.Close()
os.Remove(tmpFile)
return "OK", nil
})
check("Disk space", func() (string, error) {
dir := cfg.Download.Dir
if dir == "" {
return "", fmt.Errorf("not configured")
}
var stat syscall.Statfs_t
if err := syscall.Statfs(dir, &stat); err != nil {
return "", err
}
available := int64(stat.Bavail) * int64(stat.Bsize)
gb := float64(available) / (1024 * 1024 * 1024)
msg := fmt.Sprintf("%.1f GB free", gb)
if gb < 10 {
return "!" + msg + " (low)", nil
}
return msg, nil
})
fmt.Println()
bold.Println(" Version")
check("unarr version", func() (string, error) {
return fmt.Sprintf("%s (%s/%s)", Version, runtime.GOOS, runtime.GOARCH), nil
})
// Summary
fmt.Println()
if fail == 0 && warn == 0 {
green.Println(" All checks passed!")
} else if fail == 0 {
yellow.Printf(" %d passed, %d warnings\n", pass, warn)
} else {
red.Printf(" %d passed, %d failed, %d warnings\n", pass, fail, warn)
}
fmt.Println()
return nil
}