feat(deps): add minimum beads version check (gt-im3fl)
Add version check that enforces beads >= 0.44.0 at CLI startup, required for custom type support (bd-i54l). Commands like version, help, and completion bypass the check. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -83,7 +83,7 @@ Git-backed issue tracking system that stores work state as structured data.
|
||||
|
||||
- **Go 1.23+** - [go.dev/dl](https://go.dev/dl/)
|
||||
- **Git 2.25+** - for worktree support
|
||||
- **beads (bd)** - [github.com/steveyegge/beads](https://github.com/steveyegge/beads)
|
||||
- **beads (bd) 0.44.0+** - [github.com/steveyegge/beads](https://github.com/steveyegge/beads) (required for custom type support)
|
||||
- **tmux 3.0+** - recommended for full experience
|
||||
- **Claude Code CLI** - [claude.ai/code](https://claude.ai/code)
|
||||
|
||||
|
||||
134
internal/cmd/beads_version.go
Normal file
134
internal/cmd/beads_version.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// Package cmd provides CLI commands for the gt tool.
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MinBeadsVersion is the minimum required beads version for Gas Town.
|
||||
// This version must include custom type support (bd-i54l).
|
||||
const MinBeadsVersion = "0.44.0"
|
||||
|
||||
// beadsVersion represents a parsed semantic version.
|
||||
type beadsVersion struct {
|
||||
major int
|
||||
minor int
|
||||
patch int
|
||||
}
|
||||
|
||||
// parseBeadsVersion parses a version string like "0.44.0" into components.
|
||||
func parseBeadsVersion(v string) (beadsVersion, error) {
|
||||
// Strip leading 'v' if present
|
||||
v = strings.TrimPrefix(v, "v")
|
||||
|
||||
// Split on dots
|
||||
parts := strings.Split(v, ".")
|
||||
if len(parts) < 2 {
|
||||
return beadsVersion{}, fmt.Errorf("invalid version format: %s", v)
|
||||
}
|
||||
|
||||
major, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return beadsVersion{}, fmt.Errorf("invalid major version: %s", parts[0])
|
||||
}
|
||||
|
||||
minor, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return beadsVersion{}, fmt.Errorf("invalid minor version: %s", parts[1])
|
||||
}
|
||||
|
||||
patch := 0
|
||||
if len(parts) >= 3 {
|
||||
// Handle versions like "0.44.0-dev" - take only numeric prefix
|
||||
patchStr := parts[2]
|
||||
if idx := strings.IndexFunc(patchStr, func(r rune) bool {
|
||||
return r < '0' || r > '9'
|
||||
}); idx != -1 {
|
||||
patchStr = patchStr[:idx]
|
||||
}
|
||||
if patchStr != "" {
|
||||
patch, err = strconv.Atoi(patchStr)
|
||||
if err != nil {
|
||||
return beadsVersion{}, fmt.Errorf("invalid patch version: %s", parts[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return beadsVersion{major: major, minor: minor, patch: patch}, nil
|
||||
}
|
||||
|
||||
// compare returns -1 if v < other, 0 if equal, 1 if v > other.
|
||||
func (v beadsVersion) compare(other beadsVersion) int {
|
||||
if v.major != other.major {
|
||||
if v.major < other.major {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
if v.minor != other.minor {
|
||||
if v.minor < other.minor {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
if v.patch != other.patch {
|
||||
if v.patch < other.patch {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// getBeadsVersion executes `bd --version` and parses the output.
|
||||
// Returns the version string (e.g., "0.44.0") or error.
|
||||
func getBeadsVersion() (string, error) {
|
||||
cmd := exec.Command("bd", "--version")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return "", fmt.Errorf("bd --version failed: %s", string(exitErr.Stderr))
|
||||
}
|
||||
return "", fmt.Errorf("failed to run bd: %w (is beads installed?)", err)
|
||||
}
|
||||
|
||||
// Parse output like "bd version 0.44.0 (dev)"
|
||||
// or "bd version 0.44.0"
|
||||
re := regexp.MustCompile(`bd version (\d+\.\d+(?:\.\d+)?(?:-\w+)?)`)
|
||||
matches := re.FindStringSubmatch(string(output))
|
||||
if len(matches) < 2 {
|
||||
return "", fmt.Errorf("could not parse beads version from: %s", strings.TrimSpace(string(output)))
|
||||
}
|
||||
|
||||
return matches[1], nil
|
||||
}
|
||||
|
||||
// CheckBeadsVersion verifies that the installed beads version meets the minimum requirement.
|
||||
// Returns nil if the version is sufficient, or an error with details if not.
|
||||
func CheckBeadsVersion() error {
|
||||
installedStr, err := getBeadsVersion()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot verify beads version: %w", err)
|
||||
}
|
||||
|
||||
installed, err := parseBeadsVersion(installedStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse installed beads version %q: %w", installedStr, err)
|
||||
}
|
||||
|
||||
required, err := parseBeadsVersion(MinBeadsVersion)
|
||||
if err != nil {
|
||||
// This would be a bug in our code
|
||||
return fmt.Errorf("cannot parse required beads version %q: %w", MinBeadsVersion, err)
|
||||
}
|
||||
|
||||
if installed.compare(required) < 0 {
|
||||
return fmt.Errorf("beads version %s is required, but %s is installed\n\nPlease upgrade beads: go install github.com/steveyegge/beads/cmd/bd@latest", MinBeadsVersion, installedStr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
68
internal/cmd/beads_version_test.go
Normal file
68
internal/cmd/beads_version_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package cmd
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseBeadsVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want beadsVersion
|
||||
wantErr bool
|
||||
}{
|
||||
{"0.44.0", beadsVersion{0, 44, 0}, false},
|
||||
{"1.2.3", beadsVersion{1, 2, 3}, false},
|
||||
{"0.44.0-dev", beadsVersion{0, 44, 0}, false},
|
||||
{"v0.44.0", beadsVersion{0, 44, 0}, false},
|
||||
{"0.44", beadsVersion{0, 44, 0}, false},
|
||||
{"10.20.30", beadsVersion{10, 20, 30}, false},
|
||||
{"invalid", beadsVersion{}, true},
|
||||
{"", beadsVersion{}, true},
|
||||
{"a.b.c", beadsVersion{}, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
got, err := parseBeadsVersion(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseBeadsVersion(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !tt.wantErr && got != tt.want {
|
||||
t.Errorf("parseBeadsVersion(%q) = %+v, want %+v", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBeadsVersionCompare(t *testing.T) {
|
||||
tests := []struct {
|
||||
v1 string
|
||||
v2 string
|
||||
want int
|
||||
}{
|
||||
{"0.44.0", "0.44.0", 0},
|
||||
{"0.44.0", "0.43.0", 1},
|
||||
{"0.43.0", "0.44.0", -1},
|
||||
{"1.0.0", "0.99.99", 1},
|
||||
{"0.44.1", "0.44.0", 1},
|
||||
{"0.44.0", "0.44.1", -1},
|
||||
{"1.2.3", "1.2.3", 0},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.v1+"_vs_"+tt.v2, func(t *testing.T) {
|
||||
v1, err := parseBeadsVersion(tt.v1)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse v1 %q: %v", tt.v1, err)
|
||||
}
|
||||
v2, err := parseBeadsVersion(tt.v2)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse v2 %q: %v", tt.v2, err)
|
||||
}
|
||||
|
||||
got := v1.compare(v2)
|
||||
if got != tt.want {
|
||||
t.Errorf("(%s).compare(%s) = %d, want %d", tt.v1, tt.v2, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,30 @@ var rootCmd = &cobra.Command{
|
||||
|
||||
It coordinates agent spawning, work distribution, and communication
|
||||
across distributed teams of AI agents working on shared codebases.`,
|
||||
PersistentPreRunE: checkBeadsDependency,
|
||||
}
|
||||
|
||||
// Commands that don't require beads to be installed/checked.
|
||||
// These are basic utility commands that should work without beads.
|
||||
var beadsExemptCommands = map[string]bool{
|
||||
"version": true,
|
||||
"help": true,
|
||||
"completion": true,
|
||||
}
|
||||
|
||||
// checkBeadsDependency verifies beads meets minimum version requirements.
|
||||
// Skips check for exempt commands (version, help, completion).
|
||||
func checkBeadsDependency(cmd *cobra.Command, args []string) error {
|
||||
// Get the root command name being run
|
||||
cmdName := cmd.Name()
|
||||
|
||||
// Skip check for exempt commands
|
||||
if beadsExemptCommands[cmdName] {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check beads version
|
||||
return CheckBeadsVersion()
|
||||
}
|
||||
|
||||
// Execute runs the root command and returns an exit code.
|
||||
|
||||
Reference in New Issue
Block a user