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/)
|
- **Go 1.23+** - [go.dev/dl](https://go.dev/dl/)
|
||||||
- **Git 2.25+** - for worktree support
|
- **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
|
- **tmux 3.0+** - recommended for full experience
|
||||||
- **Claude Code CLI** - [claude.ai/code](https://claude.ai/code)
|
- **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
|
It coordinates agent spawning, work distribution, and communication
|
||||||
across distributed teams of AI agents working on shared codebases.`,
|
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.
|
// Execute runs the root command and returns an exit code.
|
||||||
|
|||||||
Reference in New Issue
Block a user