Files
drover-go/cmd/drover/main.go
T
root 1ad8de32f2 Implement internal/updater: selfupdate via Forgejo Releases API
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>
2026-05-01 00:20:24 +03:00

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
}