1ad8de32f2
Adds a small, well-tested package that: - Queries /api/v1/repos/root/drover-go/releases/latest (404 = no updates, not an error). - Compares the published tag against the running Version using golang.org/x/mod/semver, so v0.1.0-rc.2 < v0.1.0. "dev" or any semver-invalid current version is treated as "always update". - Downloads the windows-amd64 asset + SHA256SUMS.txt, verifies the sha256 of the binary against its line in the sums file (tolerates the asterisk binary-mode prefix), and atomically swaps the running exe via github.com/minio/selfupdate. - Uses a 15s connect timeout with no overall request deadline, so large asset downloads aren't truncated. - Reports progress via an optional callback. Public surface: Source interface + ForgejoSource implementation, CheckForUpdate, ApplyUpdate, SetVersion. No GUI/cobra/Wails imports in the package, so the same code is reusable from the CLI, the Windows service, and the future tray UI. Wires the package into "drover update" / "drover update --check-only" in cmd/drover/main.go. --check-only exits 0 whether or not an update is available; only network/sha/apply errors are non-zero. Tests cover CheckForUpdate (table-driven incl. semver pre-release ordering, dev fallthrough, source errors), parseSHA256Sums (text and binary modes, CRLF, malformed lines, missing entries), ForgejoSource.Latest (httptest with canned JSON, 404, 500, missing asset, missing SHA256SUMS), and downloadAndVerify (success, sha mismatch, HTTP 404, context cancellation). All run with -race. Smoke-tested manually: built drover.exe and "drover update --check-only" against git.okcu.io prints "No updates available" and exits 0 (no releases yet). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
124 lines
3.3 KiB
Go
124 lines
3.3 KiB
Go
// Command drover is the entry point for the Discord proxy CLI.
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"git.okcu.io/root/drover-go/internal/updater"
|
|
)
|
|
|
|
// Build-time variables, populated via -ldflags "-X main.Version=... -X main.Commit=... -X main.BuildDate=...".
|
|
var (
|
|
Version = "dev"
|
|
Commit = "dev"
|
|
BuildDate = "dev"
|
|
)
|
|
|
|
// configPath is the path to the TOML config file, set via the --config global flag.
|
|
// Reserved for use in later phases.
|
|
var configPath string
|
|
|
|
func main() {
|
|
// Inject our build version so the updater package can stamp it on the
|
|
// User-Agent header it sends to git.okcu.io.
|
|
updater.SetVersion(Version)
|
|
|
|
if err := newRootCmd().Execute(); err != nil {
|
|
// Cobra already prints the error; just exit non-zero.
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func newRootCmd() *cobra.Command {
|
|
root := &cobra.Command{
|
|
Use: "drover",
|
|
Short: "Discord proxy via SOCKS5 + WinDivert",
|
|
Version: fmt.Sprintf("%s (commit %s, built %s)", Version, Commit, BuildDate),
|
|
SilenceUsage: true,
|
|
SilenceErrors: false,
|
|
}
|
|
|
|
// Custom version template: "drover-go vX.Y.Z (commit abc1234, built 2026-05-01)".
|
|
root.SetVersionTemplate(fmt.Sprintf("drover-go v%s (commit %s, built %s)\n", Version, Commit, BuildDate))
|
|
|
|
root.PersistentFlags().StringVar(&configPath, "config", "", "path to TOML config file (reserved)")
|
|
|
|
root.AddCommand(newCheckCmd())
|
|
root.AddCommand(newUpdateCmd())
|
|
root.AddCommand(newServiceCmd())
|
|
|
|
return root
|
|
}
|
|
|
|
func newCheckCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "check",
|
|
Short: "Run the 7-step proxy diagnostic",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
fmt.Fprintln(cmd.OutOrStdout(), "TODO: 7-step diagnostic")
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func newUpdateCmd() *cobra.Command {
|
|
var checkOnly bool
|
|
cmd := &cobra.Command{
|
|
Use: "update",
|
|
Short: "Self-update via the Forgejo Releases API",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
ctx := cmd.Context()
|
|
out := cmd.OutOrStdout()
|
|
|
|
src := updater.NewForgejoSource("git.okcu.io", "root", "drover-go", "windows-amd64.exe")
|
|
rel, hasUpdate, err := updater.CheckForUpdate(ctx, src, Version)
|
|
if err != nil {
|
|
return fmt.Errorf("check for update: %w", err)
|
|
}
|
|
if !hasUpdate {
|
|
fmt.Fprintln(out, "No updates available")
|
|
return nil
|
|
}
|
|
fmt.Fprintf(out, "Update available: %s (current v%s)\n", rel.TagName, Version)
|
|
if checkOnly {
|
|
return nil
|
|
}
|
|
|
|
fmt.Fprintln(out, "Downloading...")
|
|
if err := updater.ApplyUpdate(ctx, rel, func(d, t int64) {
|
|
if t > 0 {
|
|
fmt.Fprintf(out, "\r%d/%d bytes", d, t)
|
|
}
|
|
}); err != nil {
|
|
return fmt.Errorf("apply update: %w", err)
|
|
}
|
|
fmt.Fprintln(out, "\nUpdate applied. Restart drover.")
|
|
return nil
|
|
},
|
|
}
|
|
cmd.Flags().BoolVar(&checkOnly, "check-only", false, "only check for an update, do not apply")
|
|
return cmd
|
|
}
|
|
|
|
func newServiceCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "service",
|
|
Short: "Manage the drover Windows service",
|
|
}
|
|
for _, name := range []string{"install", "uninstall", "start", "stop"} {
|
|
name := name
|
|
cmd.AddCommand(&cobra.Command{
|
|
Use: name,
|
|
Short: fmt.Sprintf("%s the drover Windows service", name),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
fmt.Fprintf(cmd.OutOrStdout(), "TODO: service %s\n", name)
|
|
return nil
|
|
},
|
|
})
|
|
}
|
|
return cmd
|
|
}
|