Files
gastown/internal/deps/beads.go
mayor f3b7640563 feat(deps): Auto-install beads (bd) when missing (GHI #22)
- Add internal/deps package for dependency management
- Check for bd before gt install and gt rig add
- Auto-install bd via go install if missing
- Version check warns if bd is too old (min: 0.43.0)

Closes #22

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 12:23:55 -08:00

147 lines
3.8 KiB
Go

// Package deps manages external dependencies for Gas Town.
package deps
import (
"fmt"
"os/exec"
"regexp"
"strconv"
"strings"
)
// MinBeadsVersion is the minimum compatible beads version for this Gas Town release.
// Update this when Gas Town requires new beads features.
const MinBeadsVersion = "0.43.0"
// BeadsInstallPath is the go install path for beads.
const BeadsInstallPath = "github.com/steveyegge/beads/cmd/bd@latest"
// BeadsStatus represents the state of the beads installation.
type BeadsStatus int
const (
BeadsOK BeadsStatus = iota // bd found, version compatible
BeadsNotFound // bd not in PATH
BeadsTooOld // bd found but version too old
BeadsUnknown // bd found but couldn't parse version
)
// CheckBeads checks if bd is installed and compatible.
// Returns status and the installed version (if found).
func CheckBeads() (BeadsStatus, string) {
// Check if bd exists in PATH
path, err := exec.LookPath("bd")
if err != nil {
return BeadsNotFound, ""
}
_ = path // bd found
// Get version
cmd := exec.Command("bd", "version")
output, err := cmd.Output()
if err != nil {
return BeadsUnknown, ""
}
version := parseBeadsVersion(string(output))
if version == "" {
return BeadsUnknown, ""
}
// Compare versions
if compareVersions(version, MinBeadsVersion) < 0 {
return BeadsTooOld, version
}
return BeadsOK, version
}
// EnsureBeads checks for bd and installs it if missing or outdated.
// Returns nil if bd is available and compatible.
// If autoInstall is true, will attempt to install bd when missing.
func EnsureBeads(autoInstall bool) error {
status, version := CheckBeads()
switch status {
case BeadsOK:
return nil
case BeadsNotFound:
if !autoInstall {
return fmt.Errorf("beads (bd) not found in PATH\n\nInstall with: go install %s", BeadsInstallPath)
}
return installBeads()
case BeadsTooOld:
return fmt.Errorf("beads version %s is too old (minimum: %s)\n\nUpgrade with: go install %s",
version, MinBeadsVersion, BeadsInstallPath)
case BeadsUnknown:
// Found bd but couldn't determine version - proceed with warning
return nil
}
return nil
}
// installBeads runs go install to install the latest beads.
func installBeads() error {
fmt.Printf(" beads (bd) not found. Installing...\n")
cmd := exec.Command("go", "install", BeadsInstallPath)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to install beads: %s\n%s", err, string(output))
}
// Verify installation
status, version := CheckBeads()
if status == BeadsNotFound {
return fmt.Errorf("beads installed but not in PATH - ensure $GOPATH/bin is in your PATH")
}
if status == BeadsTooOld {
return fmt.Errorf("installed beads %s but minimum required is %s", version, MinBeadsVersion)
}
fmt.Printf(" ✓ Installed beads %s\n", version)
return nil
}
// parseBeadsVersion extracts version from "bd version X.Y.Z ..." output.
func parseBeadsVersion(output string) string {
// Match patterns like "bd version 0.43.0" or "bd version 0.43.0 (dev: ...)"
re := regexp.MustCompile(`bd version (\d+\.\d+\.\d+)`)
matches := re.FindStringSubmatch(output)
if len(matches) >= 2 {
return matches[1]
}
return ""
}
// compareVersions compares two semver strings.
// Returns -1 if a < b, 0 if a == b, 1 if a > b.
func compareVersions(a, b string) int {
aParts := parseVersion(a)
bParts := parseVersion(b)
for i := 0; i < 3; i++ {
if aParts[i] < bParts[i] {
return -1
}
if aParts[i] > bParts[i] {
return 1
}
}
return 0
}
// parseVersion parses "X.Y.Z" into [3]int.
func parseVersion(v string) [3]int {
var parts [3]int
split := strings.Split(v, ".")
for i := 0; i < 3 && i < len(split); i++ {
parts[i], _ = strconv.Atoi(split[i])
}
return parts
}