diff --git a/internal/cmd/beads_version.go b/internal/cmd/beads_version.go index 3cdac27f..b604faa6 100644 --- a/internal/cmd/beads_version.go +++ b/internal/cmd/beads_version.go @@ -2,11 +2,14 @@ package cmd import ( + "context" "fmt" "os/exec" "regexp" "strconv" "strings" + "sync" + "time" ) // MinBeadsVersion is the minimum required beads version for Gas Town. @@ -84,10 +87,19 @@ func (v beadsVersion) compare(other beadsVersion) int { return 0 } +// Pre-compiled regex for beads version parsing +var beadsVersionRe = regexp.MustCompile(`bd version (\d+\.\d+(?:\.\d+)?(?:-\w+)?)`) + func getBeadsVersion() (string, error) { - cmd := exec.Command("bd", "version") + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "bd", "version") output, err := cmd.Output() if err != nil { + if ctx.Err() == context.DeadlineExceeded { + return "", fmt.Errorf("bd version check timed out") + } if exitErr, ok := err.(*exec.ExitError); ok { return "", fmt.Errorf("bd version failed: %s", string(exitErr.Stderr)) } @@ -96,8 +108,7 @@ func getBeadsVersion() (string, error) { // 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)) + matches := beadsVersionRe.FindStringSubmatch(string(output)) if len(matches) < 2 { return "", fmt.Errorf("could not parse beads version from: %s", strings.TrimSpace(string(output))) } @@ -105,9 +116,22 @@ func getBeadsVersion() (string, error) { return matches[1], nil } +var ( + cachedVersionCheckResult error + versionCheckOnce sync.Once +) + // 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. +// The check is performed only once per process execution. func CheckBeadsVersion() error { + versionCheckOnce.Do(func() { + cachedVersionCheckResult = checkBeadsVersionInternal() + }) + return cachedVersionCheckResult +} + +func checkBeadsVersionInternal() error { installedStr, err := getBeadsVersion() if err != nil { return fmt.Errorf("cannot verify beads version: %w", err) diff --git a/internal/cmd/hook.go b/internal/cmd/hook.go index 20e3d0ab..2b091ee5 100644 --- a/internal/cmd/hook.go +++ b/internal/cmd/hook.go @@ -233,8 +233,10 @@ func runHook(_ *cobra.Command, args []string) error { fmt.Printf(" Use 'gt handoff' to restart with this work\n") fmt.Printf(" Use 'gt hook' to see hook status\n") - // Log hook event to activity feed - _ = events.LogFeed(events.TypeHook, agentID, events.HookPayload(beadID)) + // Log hook event to activity feed (non-fatal) + if err := events.LogFeed(events.TypeHook, agentID, events.HookPayload(beadID)); err != nil { + fmt.Fprintf(os.Stderr, "%s Warning: failed to log hook event: %v\n", style.Dim.Render("⚠"), err) + } return nil } diff --git a/internal/cmd/mail_queue.go b/internal/cmd/mail_queue.go index 887e689e..0ad4a141 100644 --- a/internal/cmd/mail_queue.go +++ b/internal/cmd/mail_queue.go @@ -35,7 +35,7 @@ func runMailClaim(cmd *cobra.Command, args []string) error { } queueCfg, ok := cfg.Queues[queueName] - if !ok { + if !ok || queueCfg == nil { return fmt.Errorf("unknown queue: %s", queueName) } diff --git a/internal/cmd/migrate_agents_test.go b/internal/cmd/migrate_agents_test.go index 33d28cc4..e60ee277 100644 --- a/internal/cmd/migrate_agents_test.go +++ b/internal/cmd/migrate_agents_test.go @@ -20,7 +20,7 @@ func TestMigrationResultStatus(t *testing.T) { Status: "migrated", Message: "successfully migrated", }, - wantIcon: "✓", + wantIcon: " ✓", }, { name: "would migrate shows checkmark", @@ -30,7 +30,7 @@ func TestMigrationResultStatus(t *testing.T) { Status: "would migrate", Message: "would copy state from gt-mayor", }, - wantIcon: "✓", + wantIcon: " ✓", }, { name: "skipped shows empty circle", @@ -40,7 +40,7 @@ func TestMigrationResultStatus(t *testing.T) { Status: "skipped", Message: "already exists", }, - wantIcon: "⊘", + wantIcon: " ⊘", }, { name: "error shows X", @@ -50,7 +50,7 @@ func TestMigrationResultStatus(t *testing.T) { Status: "error", Message: "failed to create", }, - wantIcon: "✗", + wantIcon: " ✗", }, } @@ -59,11 +59,11 @@ func TestMigrationResultStatus(t *testing.T) { var icon string switch tt.result.Status { case "migrated", "would migrate": - icon = "✓" + icon = " ✓" case "skipped": - icon = "⊘" + icon = " ⊘" case "error": - icon = "✗" + icon = " ✗" } if icon != tt.wantIcon { t.Errorf("icon for status %q = %q, want %q", tt.result.Status, icon, tt.wantIcon)