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:
2026-08-20 13:29:35 +10:00
commit 23dcfb393f
46 changed files with 5782 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
//go:build !windows
package netcfg
import (
"errors"
"fmt"
"gitea.apointless.space/bsncubed/ipswap/internal/preset"
)
// ErrElevationRequired keeps the non-Windows build type-compatible so the rest
// of the tree compiles and `go vet ./...` is useful during development on
// Linux. Nothing here touches a real adapter.
var ErrElevationRequired = errors.New("this operation requires an elevated process")
// ErrUnsupported is returned by every write path off Windows.
var ErrUnsupported = errors.New("adapter configuration is only supported on Windows")
// Stub is a Manager that reports a plausible fixture instead of touching the
// host. It exists so the tray, the editor server and the confirmation text can
// be exercised on the development machine.
type Stub struct{}
// New returns the platform Manager.
func New() Manager { return Stub{} }
func (Stub) Adapters() ([]Adapter, error) {
return []Adapter{
{Name: "Ethernet", Description: "Stub adapter (non-Windows build)", Up: true},
{Name: "Wi-Fi", Description: "Stub adapter (non-Windows build)", Up: false},
}, nil
}
func (Stub) Current(adapter string) (LiveConfig, error) {
return LiveConfig{
Adapter: adapter,
DHCP: true,
Addresses: []preset.Address{{Address: "192.168.1.87", Prefix: 24}},
Gateways: []string{"192.168.1.1"},
DNS: []string{"192.168.1.1"},
DNSFromDHCP: true,
}, nil
}
func (Stub) Apply(plan []Command) error {
return fmt.Errorf("%w (plan had %d commands)", ErrUnsupported, len(plan))
}