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"` // Deletions configuration DeletionsRetentionDays int `json:"deletions_retention_days,omitempty"` // 0 means use default (7 days) } func DefaultConfig() *Config { return &Config{ Database: "beads.db", JSONLExport: "issues.jsonl", // Canonical name (bd-6xd) } } 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) } // DefaultDeletionsRetentionDays is the default retention period for deletion records. const DefaultDeletionsRetentionDays = 7 // GetDeletionsRetentionDays returns the configured retention days, or the default if not set. func (c *Config) GetDeletionsRetentionDays() int { if c.DeletionsRetentionDays <= 0 { return DefaultDeletionsRetentionDays } return c.DeletionsRetentionDays }