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>
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/deps"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/templates"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
@@ -105,6 +106,13 @@ func runInstall(cmd *cobra.Command, args []string) error {
|
||||
style.PrintWarning("Creating HQ inside existing workspace at %s", existingRoot)
|
||||
}
|
||||
|
||||
// Ensure beads (bd) is available before proceeding
|
||||
if !installNoBeads {
|
||||
if err := deps.EnsureBeads(true); err != nil {
|
||||
return fmt.Errorf("beads dependency check failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("%s Creating Gas Town HQ at %s\n\n",
|
||||
style.Bold.Render("🏭"), style.Dim.Render(absPath))
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/deps"
|
||||
"github.com/steveyegge/gastown/internal/git"
|
||||
"github.com/steveyegge/gastown/internal/polecat"
|
||||
"github.com/steveyegge/gastown/internal/refinery"
|
||||
@@ -193,6 +194,11 @@ func runRigAdd(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
gitURL := args[1]
|
||||
|
||||
// Ensure beads (bd) is available before proceeding
|
||||
if err := deps.EnsureBeads(true); err != nil {
|
||||
return fmt.Errorf("beads dependency check failed: %w", err)
|
||||
}
|
||||
|
||||
// Find workspace
|
||||
townRoot, err := workspace.FindFromCwdOrError()
|
||||
if err != nil {
|
||||
|
||||
146
internal/deps/beads.go
Normal file
146
internal/deps/beads.go
Normal file
@@ -0,0 +1,146 @@
|
||||
// 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
|
||||
}
|
||||
61
internal/deps/beads_test.go
Normal file
61
internal/deps/beads_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package deps
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseBeadsVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"bd version 0.43.0 (dev: main@3e1378e122c6)", "0.43.0"},
|
||||
{"bd version 0.43.0", "0.43.0"},
|
||||
{"bd version 1.2.3", "1.2.3"},
|
||||
{"bd version 10.20.30 (release)", "10.20.30"},
|
||||
{"some other output", ""},
|
||||
{"", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := parseBeadsVersion(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("parseBeadsVersion(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareVersions(t *testing.T) {
|
||||
tests := []struct {
|
||||
a, b string
|
||||
expected int
|
||||
}{
|
||||
{"0.43.0", "0.43.0", 0},
|
||||
{"0.43.0", "0.42.0", 1},
|
||||
{"0.42.0", "0.43.0", -1},
|
||||
{"1.0.0", "0.99.99", 1},
|
||||
{"0.43.1", "0.43.0", 1},
|
||||
{"0.43.0", "0.43.1", -1},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := compareVersions(tt.a, tt.b)
|
||||
if result != tt.expected {
|
||||
t.Errorf("compareVersions(%q, %q) = %d, want %d", tt.a, tt.b, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckBeads(t *testing.T) {
|
||||
// This test depends on whether bd is installed in the test environment
|
||||
status, version := CheckBeads()
|
||||
|
||||
// We expect bd to be installed in dev environment
|
||||
if status == BeadsNotFound {
|
||||
t.Skip("bd not installed, skipping integration test")
|
||||
}
|
||||
|
||||
if status == BeadsOK && version == "" {
|
||||
t.Error("CheckBeads returned BeadsOK but empty version")
|
||||
}
|
||||
|
||||
t.Logf("CheckBeads: status=%d, version=%s", status, version)
|
||||
}
|
||||
Reference in New Issue
Block a user