Files
beads/internal/configfile/configfile.go
Steve Yegge 1f2a79dfce 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>
2025-11-23 17:09:17 -08:00

97 lines
2.3 KiB
Go

package configfile
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
const ConfigFileName = "metadata.json"
type Config struct {
Database string `json:"database"`
JSONLExport string `json:"jsonl_export,omitempty"`
LastBdVersion string `json:"last_bd_version,omitempty"`
}
func DefaultConfig() *Config {
return &Config{
Database: "beads.db",
JSONLExport: "issues.jsonl",
}
}
func ConfigPath(beadsDir string) string {
return filepath.Join(beadsDir, ConfigFileName)
}
func Load(beadsDir string) (*Config, error) {
configPath := ConfigPath(beadsDir)
data, err := os.ReadFile(configPath) // #nosec G304 - controlled path from config
if os.IsNotExist(err) {
// Try legacy config.json location (migration path)
legacyPath := filepath.Join(beadsDir, "config.json")
data, err = os.ReadFile(legacyPath) // #nosec G304 - controlled path from config
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("reading legacy config: %w", err)
}
// Migrate: parse legacy config, save as metadata.json, remove old file
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parsing legacy config: %w", err)
}
// Save to new location
if err := cfg.Save(beadsDir); err != nil {
return nil, fmt.Errorf("migrating config to metadata.json: %w", err)
}
// Remove legacy file (best effort)
_ = os.Remove(legacyPath)
return &cfg, nil
}
if err != nil {
return nil, fmt.Errorf("reading config: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parsing config: %w", err)
}
return &cfg, nil
}
func (c *Config) Save(beadsDir string) error {
configPath := ConfigPath(beadsDir)
data, err := json.MarshalIndent(c, "", " ")
if err != nil {
return fmt.Errorf("marshaling config: %w", err)
}
if err := os.WriteFile(configPath, data, 0600); err != nil {
return fmt.Errorf("writing config: %w", err)
}
return nil
}
func (c *Config) DatabasePath(beadsDir string) string {
return filepath.Join(beadsDir, c.Database)
}
func (c *Config) JSONLPath(beadsDir string) string {
if c.JSONLExport == "" {
return filepath.Join(beadsDir, "issues.jsonl")
}
return filepath.Join(beadsDir, c.JSONLExport)
}