Import beads' UX design system into gastown: - Add internal/ui/ package with Ayu theme colors and semantic styling - styles.go: AdaptiveColor definitions for light/dark mode - terminal.go: TTY detection, NO_COLOR/CLICOLOR support - markdown.go: Glamour rendering with agent mode bypass - pager.go: Smart paging with GT_PAGER support - Add colorized help output (internal/cmd/help.go) - Group headers in accent color - Command names styled for scannability - Flag types and defaults muted - Add gt thanks command (internal/cmd/thanks.go) - Contributor display with same logic as bd thanks - Styled with Ayu theme colors - Update gt doctor to match bd doctor UX - Category grouping (Core, Infrastructure, Rig, Patrol, etc.) - Semantic icons (✓ ⚠ ✖) with Ayu colors - Tree connectors for detail lines - Summary line with pass/warn/fail counts - Warnings section at end with numbered issues - Migrate existing styles to use ui package - internal/style/style.go uses ui.ColorPass etc. - internal/tui/feed/styles.go uses ui package colors Co-Authored-By: SageOx <ox@sageox.ai>
201 lines
5.5 KiB
Go
201 lines
5.5 KiB
Go
package doctor
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/steveyegge/gastown/internal/beads"
|
|
"github.com/steveyegge/gastown/internal/daemon"
|
|
)
|
|
|
|
// bdDoctorResult represents the JSON output from bd doctor --json.
|
|
type bdDoctorResult struct {
|
|
Checks []bdDoctorCheck `json:"checks"`
|
|
}
|
|
|
|
// bdDoctorCheck represents a single check result from bd doctor.
|
|
type bdDoctorCheck struct {
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
Message string `json:"message"`
|
|
Detail string `json:"detail,omitempty"`
|
|
Fix string `json:"fix,omitempty"`
|
|
}
|
|
|
|
// RepoFingerprintCheck verifies that beads databases have valid repository fingerprints.
|
|
// A missing or mismatched fingerprint can cause daemon startup failures and sync issues.
|
|
type RepoFingerprintCheck struct {
|
|
FixableCheck
|
|
needsMigration bool // Cached during Run for use in Fix
|
|
beadsDir string // Beads directory that needs migration
|
|
}
|
|
|
|
// NewRepoFingerprintCheck creates a new repo fingerprint check.
|
|
func NewRepoFingerprintCheck() *RepoFingerprintCheck {
|
|
return &RepoFingerprintCheck{
|
|
FixableCheck: FixableCheck{
|
|
BaseCheck: BaseCheck{
|
|
CheckName: "repo-fingerprint",
|
|
CheckDescription: "Verify beads database has valid repository fingerprint",
|
|
CheckCategory: CategoryInfrastructure,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Run checks if beads databases have valid repo fingerprints.
|
|
func (c *RepoFingerprintCheck) Run(ctx *CheckContext) *CheckResult {
|
|
// Reset cached state
|
|
c.needsMigration = false
|
|
c.beadsDir = ""
|
|
|
|
// Check town-level beads
|
|
townBeadsDir := filepath.Join(ctx.TownRoot, ".beads")
|
|
if _, err := os.Stat(townBeadsDir); err == nil {
|
|
result := c.checkBeadsDir(filepath.Dir(townBeadsDir), "town")
|
|
if result.Status != StatusOK {
|
|
return result
|
|
}
|
|
}
|
|
|
|
// Check rig-level beads if specified
|
|
if ctx.RigName != "" {
|
|
rigBeadsDir := beads.ResolveBeadsDir(ctx.RigPath())
|
|
if _, err := os.Stat(rigBeadsDir); err == nil {
|
|
result := c.checkBeadsDir(filepath.Dir(rigBeadsDir), "rig "+ctx.RigName)
|
|
if result.Status != StatusOK {
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusOK,
|
|
Message: "Repository fingerprints verified",
|
|
}
|
|
}
|
|
|
|
// checkBeadsDir checks a single beads directory for repo fingerprint using bd doctor.
|
|
func (c *RepoFingerprintCheck) checkBeadsDir(workDir, location string) *CheckResult {
|
|
// Run bd doctor --json to get fingerprint status
|
|
cmd := exec.Command("bd", "doctor", "--json")
|
|
cmd.Dir = workDir
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
// bd doctor exits with non-zero if there are warnings, so ignore exit code
|
|
_ = cmd.Run()
|
|
|
|
// Parse JSON output
|
|
var result bdDoctorResult
|
|
if err := json.Unmarshal(stdout.Bytes(), &result); err != nil {
|
|
// If we can't parse bd doctor output, skip this check
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusOK,
|
|
Message: fmt.Sprintf("Skipped %s (bd doctor unavailable)", location),
|
|
}
|
|
}
|
|
|
|
// Find the Repo Fingerprint check
|
|
for _, check := range result.Checks {
|
|
if check.Name == "Repo Fingerprint" {
|
|
switch check.Status {
|
|
case "ok":
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusOK,
|
|
Message: fmt.Sprintf("Fingerprint verified in %s (%s)", location, check.Message),
|
|
}
|
|
case "warning":
|
|
c.needsMigration = true
|
|
c.beadsDir = filepath.Join(workDir, ".beads")
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusWarning,
|
|
Message: fmt.Sprintf("Fingerprint issue in %s: %s", location, check.Message),
|
|
Details: func() []string {
|
|
if check.Detail != "" {
|
|
return []string{check.Detail}
|
|
}
|
|
return nil
|
|
}(),
|
|
FixHint: "Run 'gt doctor --fix' or 'bd migrate --update-repo-id'",
|
|
}
|
|
case "error":
|
|
c.needsMigration = true
|
|
c.beadsDir = filepath.Join(workDir, ".beads")
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusError,
|
|
Message: fmt.Sprintf("Fingerprint error in %s: %s", location, check.Message),
|
|
Details: func() []string {
|
|
if check.Detail != "" {
|
|
return []string{check.Detail}
|
|
}
|
|
return nil
|
|
}(),
|
|
FixHint: "Run 'gt doctor --fix' or 'bd migrate --update-repo-id'",
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fingerprint check not found in bd doctor output - skip
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusOK,
|
|
Message: fmt.Sprintf("Fingerprint check not applicable for %s", location),
|
|
}
|
|
}
|
|
|
|
// Fix runs bd migrate --update-repo-id and restarts the daemon.
|
|
func (c *RepoFingerprintCheck) Fix(ctx *CheckContext) error {
|
|
if !c.needsMigration || c.beadsDir == "" {
|
|
return nil
|
|
}
|
|
|
|
// Run bd migrate --update-repo-id
|
|
cmd := exec.Command("bd", "migrate", "--update-repo-id")
|
|
cmd.Dir = filepath.Dir(c.beadsDir) // Parent of .beads directory
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("bd migrate --update-repo-id failed: %v: %s", err, stderr.String())
|
|
}
|
|
|
|
// Restart daemon if running
|
|
running, _, err := daemon.IsRunning(ctx.TownRoot)
|
|
if err == nil && running {
|
|
// Stop daemon
|
|
stopCmd := exec.Command("gt", "daemon", "stop")
|
|
stopCmd.Dir = ctx.TownRoot
|
|
_ = stopCmd.Run() // Ignore errors
|
|
|
|
// Wait a moment
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
// Start daemon
|
|
startCmd := exec.Command("gt", "daemon", "run")
|
|
startCmd.Dir = ctx.TownRoot
|
|
startCmd.Stdin = nil
|
|
startCmd.Stdout = nil
|
|
startCmd.Stderr = nil
|
|
if err := startCmd.Start(); err != nil {
|
|
return fmt.Errorf("failed to restart daemon: %w", err)
|
|
}
|
|
|
|
// Wait for daemon to initialize
|
|
time.Sleep(300 * time.Millisecond)
|
|
}
|
|
|
|
return nil
|
|
}
|