- Add command groups (Getting Started, Search, Downloads, Daemon, System) - Add shell completion command (bash, zsh, fish, powershell) - Add flag completions for --type, --quality, --sort, --lang, --genre, --country, --method, --player - Improve Long descriptions and Examples for all commands - Split doctor disk check into platform-specific files (Unix/Windows) - Validate infoHash length before truncating (prevent panic) - Fix references to non-existent 'unarr daemon start' command - Move stats command to System & Diagnostics group - Rewrite README with complete documentation, correct config format (toml not yaml), all commands, shell completion section
212 lines
4.8 KiB
Go
212 lines
4.8 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"runtime"
|
|
"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 configuration and connectivity",
|
|
Long: `Run diagnostic checks to verify that unarr is correctly configured.
|
|
|
|
Checks performed:
|
|
- Config file exists and is readable
|
|
- API key is configured
|
|
- API server is reachable (with latency)
|
|
- Agent is registered with the server
|
|
- Download directory exists and is writable
|
|
- Disk space is sufficient (warns below 10 GB)
|
|
- Current unarr version
|
|
|
|
Use this command to troubleshoot connection issues or verify setup.`,
|
|
Example: ` unarr doctor`,
|
|
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")
|
|
}
|
|
return checkDiskSpace(dir)
|
|
})
|
|
|
|
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
|
|
}
|