docs: improve CLI help, shell completion, and README
- 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
This commit is contained in:
parent
197e33956a
commit
719429b06e
22 changed files with 973 additions and 119 deletions
239
internal/cmd/daemon_install.go
Normal file
239
internal/cmd/daemon_install.go
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"text/template"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const systemdTemplate = `[Unit]
|
||||
Description=unarr download daemon
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart={{.BinPath}} start
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
User={{.User}}
|
||||
Environment=HOME={{.Home}}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`
|
||||
|
||||
const launchdTemplate = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.torrentclaw.unarr</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{{.BinPath}}</string>
|
||||
<string>start</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>{{.LogDir}}/unarr.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{{.LogDir}}/unarr.err.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
`
|
||||
|
||||
func newDaemonInstallCmdReal() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "install",
|
||||
Short: "Install daemon as a system service (systemd/launchd)",
|
||||
Long: `Install the unarr daemon as a system service so it starts automatically on boot.
|
||||
|
||||
Linux: Creates a systemd user service (~/.config/systemd/user/unarr.service)
|
||||
Enables lingering so the service runs without an active login session.
|
||||
macOS: Creates a launchd user agent (~/Library/LaunchAgents/com.torrentclaw.unarr.plist)
|
||||
|
||||
The service is enabled and started immediately after installation.
|
||||
No sudo or root access is required (uses user-level service managers).`,
|
||||
Example: ` unarr daemon install`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runDaemonInstall()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newDaemonUninstallCmdReal() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "uninstall",
|
||||
Short: "Remove daemon system service",
|
||||
Long: `Stop the daemon and remove the system service created by 'unarr daemon install'.
|
||||
|
||||
Removes the service file and disables automatic startup on boot.`,
|
||||
Example: ` unarr daemon uninstall`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runDaemonUninstall()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type serviceData struct {
|
||||
BinPath string
|
||||
User string
|
||||
Home string
|
||||
LogDir string
|
||||
}
|
||||
|
||||
func runDaemonInstall() error {
|
||||
binPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("find executable: %w", err)
|
||||
}
|
||||
binPath, _ = filepath.EvalSymlinks(binPath)
|
||||
|
||||
home, _ := os.UserHomeDir()
|
||||
user := os.Getenv("USER")
|
||||
if user == "" {
|
||||
user = os.Getenv("USERNAME")
|
||||
}
|
||||
|
||||
data := serviceData{
|
||||
BinPath: binPath,
|
||||
User: user,
|
||||
Home: home,
|
||||
LogDir: filepath.Join(home, ".local", "share", "unarr"),
|
||||
}
|
||||
|
||||
bold := color.New(color.Bold)
|
||||
green := color.New(color.FgGreen)
|
||||
|
||||
fmt.Println()
|
||||
bold.Println(" unarr daemon install")
|
||||
fmt.Println()
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return installSystemd(data, green)
|
||||
case "darwin":
|
||||
return installLaunchd(data, green)
|
||||
default:
|
||||
return fmt.Errorf("service installation not supported on %s yet", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
func installSystemd(data serviceData, green *color.Color) error {
|
||||
// User-level systemd service (no sudo needed)
|
||||
dir := filepath.Join(data.Home, ".config", "systemd", "user")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("create systemd dir: %w", err)
|
||||
}
|
||||
|
||||
path := filepath.Join(dir, "unarr.service")
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create service file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
tmpl := template.Must(template.New("systemd").Parse(systemdTemplate))
|
||||
if err := tmpl.Execute(f, data); err != nil {
|
||||
return fmt.Errorf("write service file: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf(" Created: %s\n", path)
|
||||
|
||||
// Enable and start
|
||||
exec.Command("systemctl", "--user", "daemon-reload").Run()
|
||||
exec.Command("systemctl", "--user", "enable", "unarr").Run()
|
||||
exec.Command("systemctl", "--user", "start", "unarr").Run()
|
||||
|
||||
// Enable lingering so user services run without login session
|
||||
exec.Command("loginctl", "enable-linger", data.User).Run()
|
||||
|
||||
fmt.Println()
|
||||
green.Println(" ✓ Installed and started!")
|
||||
fmt.Println()
|
||||
fmt.Println(" Manage with:")
|
||||
fmt.Println(" systemctl --user status unarr")
|
||||
fmt.Println(" systemctl --user restart unarr")
|
||||
fmt.Println(" journalctl --user -u unarr -f")
|
||||
fmt.Println()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func installLaunchd(data serviceData, green *color.Color) error {
|
||||
os.MkdirAll(data.LogDir, 0o755)
|
||||
|
||||
dir := filepath.Join(data.Home, "Library", "LaunchAgents")
|
||||
os.MkdirAll(dir, 0o755)
|
||||
|
||||
path := filepath.Join(dir, "com.torrentclaw.unarr.plist")
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create plist: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
tmpl := template.Must(template.New("launchd").Parse(launchdTemplate))
|
||||
if err := tmpl.Execute(f, data); err != nil {
|
||||
return fmt.Errorf("write plist: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf(" Created: %s\n", path)
|
||||
|
||||
exec.Command("launchctl", "load", path).Run()
|
||||
|
||||
fmt.Println()
|
||||
green.Println(" ✓ Installed and loaded!")
|
||||
fmt.Println()
|
||||
fmt.Println(" Manage with:")
|
||||
fmt.Println(" launchctl list | grep unarr")
|
||||
fmt.Println(" launchctl unload " + path)
|
||||
fmt.Println(" tail -f " + filepath.Join(data.LogDir, "unarr.log"))
|
||||
fmt.Println()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runDaemonUninstall() error {
|
||||
home, _ := os.UserHomeDir()
|
||||
|
||||
bold := color.New(color.Bold)
|
||||
green := color.New(color.FgGreen)
|
||||
|
||||
fmt.Println()
|
||||
bold.Println(" unarr daemon uninstall")
|
||||
fmt.Println()
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
exec.Command("systemctl", "--user", "stop", "unarr").Run()
|
||||
exec.Command("systemctl", "--user", "disable", "unarr").Run()
|
||||
path := filepath.Join(home, ".config", "systemd", "user", "unarr.service")
|
||||
os.Remove(path)
|
||||
exec.Command("systemctl", "--user", "daemon-reload").Run()
|
||||
green.Printf(" ✓ Removed %s\n", path)
|
||||
|
||||
case "darwin":
|
||||
path := filepath.Join(home, "Library", "LaunchAgents", "com.torrentclaw.unarr.plist")
|
||||
exec.Command("launchctl", "unload", path).Run()
|
||||
os.Remove(path)
|
||||
green.Printf(" ✓ Removed %s\n", path)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("service uninstall not supported on %s yet", runtime.GOOS)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
return nil
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue