Scaffold ipswap: tray-driven Windows IP preset switcher
Implements the design in claude.md as a building skeleton: pure Go, no cgo, cross-compiles to a single Windows .exe from Linux. Architecture follows the spec's deliberate split. The fast path is native — tray icon, grouped submenus, a Win32 MessageBox showing a live before/after diff, then netsh. The slow path is an embedded web editor served on a random loopback port and opened in the default browser. Two decisions worth recording: Reads use GetAdaptersAddresses, writes use netsh. The spec left the enumeration mechanism open; parsing `netsh show config` breaks on a non-English Windows because the output is localised. DNS static-vs-DHCP origin is not exposed by that API, so it comes from one registry read. The netsh command plan is built in portable code. That puts the delete-every- existing-address step — the one that stops secondary addresses leaking across switches — under test without needing a Windows box. The editor requires the session token in a header for mutations, not just the cookie: any page in the browser can make it POST to 127.0.0.1 with the cookie attached, but it cannot set a header. The updater refuses to install a release that publishes no SHA256. Not yet done: no group picker for export (the API supports it), no single-instance guard, and internal/server/web/app.css is reconstructed from the description in claude.md rather than the canonical apointless.css. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Version is a parsed semantic version. Only what a release-tag comparison
|
||||
// needs is modelled: major.minor.patch plus an optional pre-release, which
|
||||
// sorts before the same version without one.
|
||||
type Version struct {
|
||||
Major, Minor, Patch int
|
||||
Pre string
|
||||
}
|
||||
|
||||
// ParseVersion accepts "v1.2.0", "1.2.0", "1.2", "1" and "1.2.0-rc1". Build
|
||||
// metadata after "+" is ignored, as semver says it must be for ordering.
|
||||
func ParseVersion(s string) (Version, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
s = strings.TrimPrefix(s, "v")
|
||||
if s == "" {
|
||||
return Version{}, fmt.Errorf("empty version")
|
||||
}
|
||||
if i := strings.IndexByte(s, '+'); i >= 0 {
|
||||
s = s[:i]
|
||||
}
|
||||
|
||||
var v Version
|
||||
if i := strings.IndexByte(s, '-'); i >= 0 {
|
||||
v.Pre = s[i+1:]
|
||||
s = s[:i]
|
||||
}
|
||||
|
||||
parts := strings.Split(s, ".")
|
||||
if len(parts) > 3 {
|
||||
return Version{}, fmt.Errorf("%q has too many components", s)
|
||||
}
|
||||
dst := []*int{&v.Major, &v.Minor, &v.Patch}
|
||||
for i, p := range parts {
|
||||
n, err := strconv.Atoi(p)
|
||||
if err != nil || n < 0 {
|
||||
return Version{}, fmt.Errorf("%q is not a version number", s)
|
||||
}
|
||||
*dst[i] = n
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Compare returns -1, 0 or 1 as v sorts before, equal to, or after other.
|
||||
func (v Version) Compare(other Version) int {
|
||||
for _, pair := range [][2]int{
|
||||
{v.Major, other.Major},
|
||||
{v.Minor, other.Minor},
|
||||
{v.Patch, other.Patch},
|
||||
} {
|
||||
if pair[0] != pair[1] {
|
||||
if pair[0] < pair[1] {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// A pre-release sorts before the release it leads up to.
|
||||
switch {
|
||||
case v.Pre == "" && other.Pre == "":
|
||||
return 0
|
||||
case v.Pre == "":
|
||||
return 1
|
||||
case other.Pre == "":
|
||||
return -1
|
||||
case v.Pre < other.Pre:
|
||||
return -1
|
||||
case v.Pre > other.Pre:
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (v Version) String() string {
|
||||
s := fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
|
||||
if v.Pre != "" {
|
||||
s += "-" + v.Pre
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// IsNewer reports whether candidate is a strictly later version than current.
|
||||
// An unparseable version on either side means "no update", because offering a
|
||||
// download on the strength of a tag we could not read is worse than silence.
|
||||
func IsNewer(candidate, current string) bool {
|
||||
c, err := ParseVersion(candidate)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
cur, err := ParseVersion(current)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return c.Compare(cur) > 0
|
||||
}
|
||||
Reference in New Issue
Block a user