feat: Add built-in version tracking to bd (bd-loka)

Implement automatic bd version tracking and upgrade awareness:

- Add LastBdVersion field to Config struct in metadata.json
- Auto-update version on every bd command in PersistentPreRun
- Add 'bd upgrade' command with status/review/ack subcommands
- Show upgrade notifications on 'bd ready' and 'bd list'
- Non-intrusive: only shows once per session, skipped for JSON output

The system tracks version changes automatically and helps users stay
aware of bd upgrades without manual intervention. Notifications are
graceful - failures don't break commands.

Example output on bd ready after upgrade:
  🔄 bd upgraded from v0.22.0 to v0.24.2 since last use
  💡 Run 'bd upgrade review' to see what changed

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-11-23 17:09:17 -08:00
parent 83e2221acd
commit 1f2a79dfce
7 changed files with 348 additions and 3 deletions

View File

@@ -301,6 +301,9 @@ var listCmd = &cobra.Command{
return
}
// Show upgrade notification if needed (bd-loka)
maybeShowUpgradeNotification()
var issues []*types.Issue
if err := json.Unmarshal(resp.Data, &issues); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
@@ -400,6 +403,9 @@ var listCmd = &cobra.Command{
return
}
// Show upgrade notification if needed (bd-loka)
maybeShowUpgradeNotification()
// Load labels in bulk for display
issueIDs := make([]string, len(issues))
for i, issue := range issues {

View File

@@ -83,6 +83,11 @@ var (
// Auto-import state
autoImportEnabled = true // Can be disabled with --no-auto-import
// Version upgrade tracking (bd-loka)
versionUpgradeDetected = false // Set to true if bd version changed since last run
previousVersion = "" // The last bd version user had (empty = first run or unknown)
upgradeAcknowledged = false // Set to true after showing upgrade notification once per session
)
var (
@@ -293,6 +298,10 @@ var rootCmd = &cobra.Command{
}
}
// Track bd version changes (bd-loka)
// Best-effort tracking - failures are silent
trackBdVersion()
// Initialize daemon status
socketPath := getSocketPath()
daemonStatus = DaemonStatus{

View File

@@ -75,6 +75,10 @@ var readyCmd = &cobra.Command{
outputJSON(issues)
return
}
// Show upgrade notification if needed (bd-loka)
maybeShowUpgradeNotification()
if len(issues) == 0 {
yellow := color.New(color.FgYellow).SprintFunc()
fmt.Printf("\n%s No ready work found (all issues have blocking dependencies)\n\n",
@@ -131,6 +135,9 @@ var readyCmd = &cobra.Command{
outputJSON(issues)
return
}
// Show upgrade notification if needed (bd-loka)
maybeShowUpgradeNotification()
if len(issues) == 0 {
yellow := color.New(color.FgYellow).SprintFunc()
fmt.Printf("\n%s No ready work found (all issues have blocking dependencies)\n\n",

215
cmd/bd/upgrade.go Normal file
View File

@@ -0,0 +1,215 @@
package main
import (
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/beads"
"github.com/steveyegge/beads/internal/configfile"
)
var upgradeCmd = &cobra.Command{
Use: "upgrade",
Short: "Check and manage bd version upgrades",
Long: `Commands for checking bd version upgrades and reviewing changes.
The upgrade command helps you stay aware of bd version changes:
- bd upgrade status: Check if bd version changed since last use
- bd upgrade review: Show what's new since your last version
- bd upgrade ack: Acknowledge the current version
Version tracking is automatic - bd updates metadata.json on every run.`,
}
var upgradeStatusCmd = &cobra.Command{
Use: "status",
Short: "Check if bd version has changed",
Long: `Check if bd has been upgraded since you last used it.
This command uses the version tracking that happens automatically
at startup to detect if bd was upgraded.
Examples:
bd upgrade status
bd upgrade status --json`,
Run: func(cmd *cobra.Command, args []string) {
// Use in-memory state from trackBdVersion() which runs in PersistentPreRun
if jsonOutput {
result := map[string]interface{}{
"upgraded": versionUpgradeDetected,
"current_version": Version,
}
if versionUpgradeDetected {
result["previous_version"] = previousVersion
result["changes_available"] = len(getVersionsSince(previousVersion)) > 0
}
outputJSON(result)
return
}
// Human-readable output
if versionUpgradeDetected {
fmt.Printf("✨ bd upgraded from v%s to v%s\n", previousVersion, Version)
newVersions := getVersionsSince(previousVersion)
if len(newVersions) > 0 {
fmt.Printf(" %d version%s with changes available\n",
len(newVersions),
pluralize(len(newVersions)))
fmt.Println()
fmt.Println("Run 'bd upgrade review' to see what changed")
}
} else if previousVersion == "" {
fmt.Printf("bd version: v%s (first run or version tracking just enabled)\n", Version)
} else {
fmt.Printf("bd version: v%s (no upgrade detected)\n", Version)
}
},
}
var upgradeReviewCmd = &cobra.Command{
Use: "review",
Short: "Review changes since last bd version",
Long: `Show what's new in bd since the last version you used.
Unlike 'bd info --whats-new' which shows the last 3 versions,
this command shows ALL changes since your specific last version.
If you're upgrading from an old version, you'll see the complete
changelog of everything that changed since then.
Examples:
bd upgrade review
bd upgrade review --json`,
Run: func(cmd *cobra.Command, args []string) {
// Use in-memory state from trackBdVersion() which runs in PersistentPreRun
lastVersion := previousVersion
if lastVersion == "" {
fmt.Println("No previous version recorded")
fmt.Println("Run 'bd info --whats-new' to see recent changes")
return
}
if !versionUpgradeDetected {
fmt.Printf("You're already on v%s (no upgrade detected)\n", Version)
fmt.Println("Run 'bd info --whats-new' to see recent changes")
return
}
newVersions := getVersionsSince(lastVersion)
if jsonOutput {
outputJSON(map[string]interface{}{
"current_version": Version,
"previous_version": lastVersion,
"new_versions": newVersions,
})
return
}
// Human-readable output
fmt.Printf("\n🔄 Upgraded from v%s to v%s\n", lastVersion, Version)
fmt.Println(strings.Repeat("=", 60))
fmt.Println()
if len(newVersions) == 0 {
fmt.Printf("v%s is newer than v%s but not in changelog\n", Version, lastVersion)
fmt.Println("Run 'bd info --whats-new' to see recent documented changes")
return
}
for _, vc := range newVersions {
versionMarker := ""
if vc.Version == Version {
versionMarker = " ← current"
}
fmt.Printf("## v%s (%s)%s\n\n", vc.Version, vc.Date, versionMarker)
for _, change := range vc.Changes {
fmt.Printf(" • %s\n", change)
}
fmt.Println()
}
fmt.Println("💡 Run 'bd upgrade ack' to mark this version as seen")
fmt.Println()
},
}
var upgradeAckCmd = &cobra.Command{
Use: "ack",
Short: "Acknowledge the current bd version",
Long: `Mark the current bd version as acknowledged.
This updates metadata.json to record that you've seen the current
version. Mainly useful after reviewing upgrade changes to suppress
future upgrade notifications.
Note: Version tracking happens automatically, so you don't need to
run this command unless you want to explicitly mark acknowledgement.
Examples:
bd upgrade ack
bd upgrade ack --json`,
Run: func(cmd *cobra.Command, args []string) {
beadsDir := beads.FindBeadsDir()
if beadsDir == "" {
fmt.Println("Error: No .beads directory found")
return
}
cfg, err := configfile.Load(beadsDir)
if err != nil {
fmt.Printf("Error loading metadata.json: %v\n", err)
return
}
if cfg == nil {
cfg = configfile.DefaultConfig()
}
previousVersion := cfg.LastBdVersion
cfg.LastBdVersion = Version
if err := cfg.Save(beadsDir); err != nil {
fmt.Printf("Error saving metadata.json: %v\n", err)
return
}
// Mark as acknowledged in current session
upgradeAcknowledged = true
versionUpgradeDetected = false
if jsonOutput {
outputJSON(map[string]interface{}{
"acknowledged": true,
"current_version": Version,
"previous_version": previousVersion,
})
return
}
if previousVersion == Version {
fmt.Printf("✓ Already on v%s\n", Version)
} else if previousVersion == "" {
fmt.Printf("✓ Acknowledged bd v%s\n", Version)
} else {
fmt.Printf("✓ Acknowledged upgrade from v%s to v%s\n", previousVersion, Version)
}
},
}
func pluralize(count int) string {
if count == 1 {
return ""
}
return "s"
}
func init() {
upgradeCmd.AddCommand(upgradeStatusCmd)
upgradeCmd.AddCommand(upgradeReviewCmd)
upgradeCmd.AddCommand(upgradeAckCmd)
rootCmd.AddCommand(upgradeCmd)
}

103
cmd/bd/version_tracking.go Normal file
View File

@@ -0,0 +1,103 @@
package main
import (
"fmt"
"github.com/steveyegge/beads/internal/beads"
"github.com/steveyegge/beads/internal/configfile"
)
// trackBdVersion checks if bd version has changed since last run and updates metadata.json.
// This function is best-effort - failures are silent to avoid disrupting commands.
// Sets global variables versionUpgradeDetected and previousVersion if upgrade detected.
//
// bd-loka: Built-in version tracking for upgrade awareness
func trackBdVersion() {
// Find the beads directory
beadsDir := beads.FindBeadsDir()
if beadsDir == "" {
// No .beads directory found - this is fine (e.g., bd init, bd version, etc.)
return
}
// Load current config
cfg, err := configfile.Load(beadsDir)
if err != nil {
// Silent failure - config might not exist yet
return
}
if cfg == nil {
// No config file yet - create one with current version
cfg = configfile.DefaultConfig()
cfg.LastBdVersion = Version
_ = cfg.Save(beadsDir) // Best effort
return
}
// Check if version changed
if cfg.LastBdVersion != "" && cfg.LastBdVersion != Version {
// Version upgrade detected!
versionUpgradeDetected = true
previousVersion = cfg.LastBdVersion
}
// Update metadata.json with current version (best effort)
// Only write if version actually changed to minimize I/O
if cfg.LastBdVersion != Version {
cfg.LastBdVersion = Version
_ = cfg.Save(beadsDir) // Silent failure is fine
}
}
// getVersionsSince returns all version changes since the given version.
// If sinceVersion is empty, returns all known versions.
// Returns changes in chronological order (oldest first).
func getVersionsSince(sinceVersion string) []VersionChange {
if sinceVersion == "" {
// Return all versions
return versionChanges
}
// Find the index of sinceVersion
startIdx := -1
for i, vc := range versionChanges {
if vc.Version == sinceVersion {
startIdx = i
break
}
}
if startIdx == -1 {
// sinceVersion not found in our changelog - return all versions
// (user might be upgrading from a very old version)
return versionChanges
}
// Return versions after sinceVersion (don't include sinceVersion itself)
if startIdx+1 < len(versionChanges) {
return versionChanges[startIdx+1:]
}
// No new versions
return []VersionChange{}
}
// maybeShowUpgradeNotification displays a one-time upgrade notification if version changed.
// This is called by commands like 'bd ready' and 'bd list' to inform users of upgrades.
// Returns true if notification was shown.
func maybeShowUpgradeNotification() bool {
// Only show if upgrade detected and not yet acknowledged
if !versionUpgradeDetected || upgradeAcknowledged {
return false
}
// Mark as acknowledged so we only show once per session
upgradeAcknowledged = true
// Display notification
fmt.Printf("🔄 bd upgraded from v%s to v%s since last use\n", previousVersion, Version)
fmt.Println("💡 Run 'bd upgrade review' to see what changed")
fmt.Println()
return true
}