feat(version): add stale binary detection with startup warning

Add detection for when the installed gt binary is out of date with the
source repository. This helps catch issues where commands fail mysteriously
because the installed binary doesn't have recent fixes.

Changes:
- Add internal/version package with stale binary detection logic
- Add startup warning in PersistentPreRunE when binary is stale
- Add gt doctor check for stale-binary
- Use prefix matching for commit comparison (handles short vs full hash)

The warning is non-blocking and only shows once per shell session via
the GT_STALE_WARNED environment variable.

Resolves: gt-ud912

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
slit
2026-01-09 15:35:25 -08:00
committed by beads/crew/giles
parent b8075a5e06
commit be35b3eaab
6 changed files with 302 additions and 10 deletions

View File

@@ -0,0 +1,83 @@
package doctor
import (
"fmt"
"github.com/steveyegge/gastown/internal/version"
)
// StaleBinaryCheck verifies the installed gt binary is up to date with the repo.
type StaleBinaryCheck struct {
FixableCheck
}
// NewStaleBinaryCheck creates a new stale binary check.
func NewStaleBinaryCheck() *StaleBinaryCheck {
return &StaleBinaryCheck{
FixableCheck: FixableCheck{
BaseCheck: BaseCheck{
CheckName: "stale-binary",
CheckDescription: "Check if gt binary is up to date with repo",
},
},
}
}
// Run checks if the binary is stale.
func (c *StaleBinaryCheck) Run(ctx *CheckContext) *CheckResult {
repoRoot, err := version.GetRepoRoot()
if err != nil {
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "Cannot locate gt source repo (not a development environment)",
Details: []string{err.Error()},
}
}
info := version.CheckStaleBinary(repoRoot)
if info.Error != nil {
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "Cannot determine binary version (dev build?)",
Details: []string{info.Error.Error()},
}
}
if info.IsStale {
msg := fmt.Sprintf("Binary is stale (built from %s, repo at %s)",
version.ShortCommit(info.BinaryCommit), version.ShortCommit(info.RepoCommit))
if info.CommitsBehind > 0 {
msg = fmt.Sprintf("Binary is %d commits behind (built from %s, repo at %s)",
info.CommitsBehind, version.ShortCommit(info.BinaryCommit), version.ShortCommit(info.RepoCommit))
}
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: msg,
FixHint: "Run 'gt install' to rebuild and install",
}
}
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: fmt.Sprintf("Binary is up to date (%s)", version.ShortCommit(info.BinaryCommit)),
}
}
// Fix rebuilds and installs gt.
func (c *StaleBinaryCheck) Fix(ctx *CheckContext) error {
// Note: We don't auto-fix this because:
// 1. It requires building and installing, which takes time
// 2. It modifies system files outside the workspace
// 3. User should explicitly run 'gt install'
return fmt.Errorf("run 'gt install' manually to rebuild")
}
// CanFix returns false - stale binary should be fixed manually.
func (c *StaleBinaryCheck) CanFix() bool {
return false
}