feat(formula): add checksum-based auto-update for embedded formulas
Adds infrastructure to automatically update embedded formulas when the binary is upgraded, while preserving user customizations. Changes: - Add CheckFormulaHealth() to detect outdated/modified/missing formulas - Add UpdateFormulas() to safely update formulas via gt doctor --fix - Track installed formula checksums in .beads/formulas/.installed.json - Add FormulaCheck to gt doctor with auto-fix capability - Compute checksums at runtime from embedded files (no build-time manifest) Update scenarios: - Outdated (embedded changed, user unchanged): Update automatically - Modified (user customized): Skip with warning - Missing (user deleted): Reinstall with message - New (never installed): Install 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
e124402b7b
commit
da2d71c3fe
123
internal/doctor/formula_check.go
Normal file
123
internal/doctor/formula_check.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/formula"
|
||||
)
|
||||
|
||||
// FormulaCheck verifies that embedded formulas are up-to-date.
|
||||
// It detects outdated formulas (binary updated), missing formulas (user deleted),
|
||||
// and modified formulas (user customized). Can auto-fix outdated and missing.
|
||||
type FormulaCheck struct {
|
||||
FixableCheck
|
||||
}
|
||||
|
||||
// NewFormulaCheck creates a new formula check.
|
||||
func NewFormulaCheck() *FormulaCheck {
|
||||
return &FormulaCheck{
|
||||
FixableCheck: FixableCheck{
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "formulas",
|
||||
CheckDescription: "Check embedded formulas are up-to-date",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Run checks if formulas need updating.
|
||||
func (c *FormulaCheck) Run(ctx *CheckContext) *CheckResult {
|
||||
report, err := formula.CheckFormulaHealth(ctx.TownRoot)
|
||||
if err != nil {
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusWarning,
|
||||
Message: fmt.Sprintf("Could not check formulas: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
// All good
|
||||
if report.Outdated == 0 && report.Missing == 0 && report.Modified == 0 && report.New == 0 {
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusOK,
|
||||
Message: fmt.Sprintf("%d formulas up-to-date", report.OK),
|
||||
}
|
||||
}
|
||||
|
||||
// Build details
|
||||
var details []string
|
||||
var needsFix bool
|
||||
|
||||
for _, f := range report.Formulas {
|
||||
switch f.Status {
|
||||
case "outdated":
|
||||
details = append(details, fmt.Sprintf(" %s: update available", f.Name))
|
||||
needsFix = true
|
||||
case "missing":
|
||||
details = append(details, fmt.Sprintf(" %s: missing (will reinstall)", f.Name))
|
||||
needsFix = true
|
||||
case "modified":
|
||||
details = append(details, fmt.Sprintf(" %s: locally modified (skipping)", f.Name))
|
||||
case "new":
|
||||
details = append(details, fmt.Sprintf(" %s: new formula available", f.Name))
|
||||
needsFix = true
|
||||
}
|
||||
}
|
||||
|
||||
// Determine status
|
||||
status := StatusOK
|
||||
if needsFix {
|
||||
status = StatusWarning
|
||||
}
|
||||
|
||||
// Build message
|
||||
var parts []string
|
||||
if report.Outdated > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%d outdated", report.Outdated))
|
||||
}
|
||||
if report.Missing > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%d missing", report.Missing))
|
||||
}
|
||||
if report.New > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%d new", report.New))
|
||||
}
|
||||
if report.Modified > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%d modified", report.Modified))
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("Formulas: %s", strings.Join(parts, ", "))
|
||||
|
||||
result := &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: status,
|
||||
Message: message,
|
||||
Details: details,
|
||||
}
|
||||
|
||||
if needsFix {
|
||||
result.FixHint = "Run 'gt doctor --fix' to update formulas"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Fix updates outdated and missing formulas.
|
||||
func (c *FormulaCheck) Fix(ctx *CheckContext) error {
|
||||
updated, skipped, reinstalled, err := formula.UpdateFormulas(ctx.TownRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Log what was done (caller will re-run check to show new status)
|
||||
if updated > 0 || reinstalled > 0 || skipped > 0 {
|
||||
// The doctor framework will re-run the check after fix
|
||||
// so we don't need to log here
|
||||
_ = updated
|
||||
_ = reinstalled
|
||||
_ = skipped
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user