Merge pull request #311 from rsnodgrass/feat/ux-system-import

feat(ui): import comprehensive UX system from beads
This commit is contained in:
Steve Yegge
2026-01-09 23:13:58 -08:00
committed by GitHub
45 changed files with 1400 additions and 75 deletions
+1
View File
@@ -28,6 +28,7 @@ func NewAgentBeadsCheck() *AgentBeadsCheck {
BaseCheck: BaseCheck{
CheckName: "agent-beads-exist",
CheckDescription: "Verify agent beads exist for all agents",
CheckCategory: CategoryRig,
},
},
}
+1
View File
@@ -20,6 +20,7 @@ func NewBdDaemonCheck() *BdDaemonCheck {
BaseCheck: BaseCheck{
CheckName: "bd-daemon",
CheckDescription: "Check if bd (beads) daemon is running",
CheckCategory: CategoryInfrastructure,
},
},
}
+3
View File
@@ -26,6 +26,7 @@ func NewBeadsDatabaseCheck() *BeadsDatabaseCheck {
BaseCheck: BaseCheck{
CheckName: "beads-database",
CheckDescription: "Verify beads database is properly initialized",
CheckCategory: CategoryConfig,
},
},
}
@@ -176,6 +177,7 @@ func NewPrefixConflictCheck() *PrefixConflictCheck {
BaseCheck: BaseCheck{
CheckName: "prefix-conflict",
CheckDescription: "Check for duplicate beads prefixes across rigs",
CheckCategory: CategoryConfig,
},
}
}
@@ -243,6 +245,7 @@ func NewPrefixMismatchCheck() *PrefixMismatchCheck {
BaseCheck: BaseCheck{
CheckName: "prefix-mismatch",
CheckDescription: "Check for prefix mismatches between rigs.json and routes.jsonl",
CheckCategory: CategoryConfig,
},
},
}
+1
View File
@@ -20,6 +20,7 @@ func NewBootHealthCheck() *BootHealthCheck {
BaseCheck: BaseCheck{
CheckName: "boot-health",
CheckDescription: "Check Boot watchdog health (the vet checks on the dog)",
CheckCategory: CategoryInfrastructure,
},
}
}
+3
View File
@@ -23,6 +23,7 @@ func NewBranchCheck() *BranchCheck {
BaseCheck: BaseCheck{
CheckName: "persistent-role-branches",
CheckDescription: "Detect persistent roles not on main branch",
CheckCategory: CategoryCleanup,
},
},
}
@@ -213,6 +214,7 @@ func NewBeadsSyncOrphanCheck() *BeadsSyncOrphanCheck {
BaseCheck: BaseCheck{
CheckName: "beads-sync-orphans",
CheckDescription: "Detect orphaned code on beads-sync branch",
CheckCategory: CategoryCleanup,
},
}
}
@@ -338,6 +340,7 @@ func NewCloneDivergenceCheck() *CloneDivergenceCheck {
BaseCheck: BaseCheck{
CheckName: "clone-divergence",
CheckDescription: "Detect emergency divergence between git clones",
CheckCategory: CategoryCleanup,
},
}
}
+1
View File
@@ -50,6 +50,7 @@ func NewClaudeSettingsCheck() *ClaudeSettingsCheck {
BaseCheck: BaseCheck{
CheckName: "claude-settings",
CheckDescription: "Verify Claude settings.json files match expected templates",
CheckCategory: CategoryConfig,
},
},
}
+1
View File
@@ -22,6 +22,7 @@ func NewCommandsCheck() *CommandsCheck {
BaseCheck: BaseCheck{
CheckName: "commands-provisioned",
CheckDescription: "Check .claude/commands/ is provisioned at town level",
CheckCategory: CategoryConfig,
},
},
}
+5
View File
@@ -24,6 +24,7 @@ func NewSettingsCheck() *SettingsCheck {
BaseCheck: BaseCheck{
CheckName: "rig-settings",
CheckDescription: "Check that rigs have settings/ directory",
CheckCategory: CategoryConfig,
},
},
}
@@ -105,6 +106,7 @@ func NewRuntimeGitignoreCheck() *RuntimeGitignoreCheck {
BaseCheck: BaseCheck{
CheckName: "runtime-gitignore",
CheckDescription: "Check that .runtime/ directories are gitignored",
CheckCategory: CategoryConfig,
},
}
}
@@ -194,6 +196,7 @@ func NewLegacyGastownCheck() *LegacyGastownCheck {
BaseCheck: BaseCheck{
CheckName: "legacy-gastown",
CheckDescription: "Check for old .gastown/ directories that should be migrated",
CheckCategory: CategoryConfig,
},
},
}
@@ -281,6 +284,7 @@ func NewSessionHookCheck() *SessionHookCheck {
BaseCheck: BaseCheck{
CheckName: "session-hooks",
CheckDescription: "Check that settings.json hooks use session-start.sh or --hook flag",
CheckCategory: CategoryConfig,
},
}
}
@@ -549,6 +553,7 @@ func NewCustomTypesCheck() *CustomTypesCheck {
BaseCheck: BaseCheck{
CheckName: "beads-custom-types",
CheckDescription: "Check that Gas Town custom types are registered with beads",
CheckCategory: CategoryConfig,
},
},
}
+1
View File
@@ -31,6 +31,7 @@ func NewCrashReportCheck() *CrashReportCheck {
BaseCheck: BaseCheck{
CheckName: "crash-reports",
CheckDescription: "Check for recent macOS crash reports (tmux, Claude)",
CheckCategory: CategoryCleanup,
},
}
}
+2
View File
@@ -32,6 +32,7 @@ func NewCrewStateCheck() *CrewStateCheck {
BaseCheck: BaseCheck{
CheckName: "crew-state",
CheckDescription: "Validate crew worker state.json files",
CheckCategory: CategoryCleanup,
},
},
}
@@ -239,6 +240,7 @@ func NewCrewWorktreeCheck() *CrewWorktreeCheck {
BaseCheck: BaseCheck{
CheckName: "crew-worktrees",
CheckDescription: "Detect stale cross-rig worktrees in crew directories",
CheckCategory: CategoryCleanup,
},
},
}
+1
View File
@@ -20,6 +20,7 @@ func NewDaemonCheck() *DaemonCheck {
BaseCheck: BaseCheck{
CheckName: "daemon",
CheckDescription: "Check if Gas Town daemon is running",
CheckCategory: CategoryInfrastructure,
},
},
}
+23
View File
@@ -27,6 +27,11 @@ func (d *Doctor) Checks() []Check {
return d.checks
}
// categoryGetter interface for checks that provide a category
type categoryGetter interface {
Category() string
}
// Run executes all registered checks and returns a report.
func (d *Doctor) Run(ctx *CheckContext) *Report {
report := NewReport()
@@ -37,6 +42,10 @@ func (d *Doctor) Run(ctx *CheckContext) *Report {
if result.Name == "" {
result.Name = check.Name()
}
// Set category from check if available
if cg, ok := check.(categoryGetter); ok && result.Category == "" {
result.Category = cg.Category()
}
report.Add(result)
}
@@ -53,6 +62,10 @@ func (d *Doctor) Fix(ctx *CheckContext) *Report {
if result.Name == "" {
result.Name = check.Name()
}
// Set category from check if available
if cg, ok := check.(categoryGetter); ok && result.Category == "" {
result.Category = cg.Category()
}
// Attempt fix if check failed and is fixable
if result.Status != StatusOK && check.CanFix() {
@@ -63,6 +76,10 @@ func (d *Doctor) Fix(ctx *CheckContext) *Report {
if result.Name == "" {
result.Name = check.Name()
}
// Set category again after re-run
if cg, ok := check.(categoryGetter); ok && result.Category == "" {
result.Category = cg.Category()
}
// Update message to indicate fix was applied
if result.Status == StatusOK {
result.Message = result.Message + " (fixed)"
@@ -84,6 +101,12 @@ func (d *Doctor) Fix(ctx *CheckContext) *Report {
type BaseCheck struct {
CheckName string
CheckDescription string
CheckCategory string // Category for grouping (e.g., CategoryCore)
}
// Category returns the check's category for grouping in output.
func (b *BaseCheck) Category() string {
return b.CheckCategory
}
// Name returns the check name.
+6 -2
View File
@@ -219,8 +219,12 @@ func TestReport_Print(t *testing.T) {
if !bytes.Contains(buf.Bytes(), []byte("TestCheck")) {
t.Error("Output should contain check name")
}
if !bytes.Contains(buf.Bytes(), []byte("2 checks")) {
t.Error("Output should contain summary")
// New summary format: "✓ N passed ⚠ N warnings ✖ N failed"
if !bytes.Contains(buf.Bytes(), []byte("1 passed")) {
t.Error("Output should contain summary with passed count")
}
if !bytes.Contains(buf.Bytes(), []byte("1 warnings")) {
t.Error("Output should contain summary with warnings count")
}
}
+1
View File
@@ -42,6 +42,7 @@ func NewEnvVarsCheck() *EnvVarsCheck {
BaseCheck: BaseCheck{
CheckName: "env-vars",
CheckDescription: "Verify tmux session environment variables match expected values",
CheckCategory: CategoryConfig,
},
}
}
+1
View File
@@ -21,6 +21,7 @@ func NewFormulaCheck() *FormulaCheck {
BaseCheck: BaseCheck{
CheckName: "formulas",
CheckDescription: "Check embedded formulas are up-to-date",
CheckCategory: CategoryConfig,
},
},
}
+1
View File
@@ -21,6 +21,7 @@ func NewGlobalStateCheck() *GlobalStateCheck {
BaseCheck: BaseCheck{
CheckName: "global-state",
CheckDescription: "Validates Gas Town global state and shell integration",
CheckCategory: CategoryCore,
},
}
}
+3
View File
@@ -32,6 +32,7 @@ func NewHookAttachmentValidCheck() *HookAttachmentValidCheck {
BaseCheck: BaseCheck{
CheckName: "hook-attachment-valid",
CheckDescription: "Verify attached molecules exist and are not closed",
CheckCategory: CategoryHooks,
},
},
}
@@ -207,6 +208,7 @@ func NewHookSingletonCheck() *HookSingletonCheck {
BaseCheck: BaseCheck{
CheckName: "hook-singleton",
CheckDescription: "Ensure each agent has at most one handoff bead",
CheckCategory: CategoryHooks,
},
},
}
@@ -346,6 +348,7 @@ func NewOrphanedAttachmentsCheck() *OrphanedAttachmentsCheck {
BaseCheck: BaseCheck{
CheckName: "orphaned-attachments",
CheckDescription: "Detect handoff beads for non-existent agents",
CheckCategory: CategoryHooks,
},
}
}
+10 -10
View File
@@ -9,19 +9,19 @@ import (
)
// IdentityCollisionCheck checks for agent identity collisions and stale locks.
type IdentityCollisionCheck struct{}
type IdentityCollisionCheck struct {
BaseCheck
}
// NewIdentityCollisionCheck creates a new identity collision check.
func NewIdentityCollisionCheck() *IdentityCollisionCheck {
return &IdentityCollisionCheck{}
}
func (c *IdentityCollisionCheck) Name() string {
return "identity-collision"
}
func (c *IdentityCollisionCheck) Description() string {
return "Check for agent identity collisions and stale locks"
return &IdentityCollisionCheck{
BaseCheck: BaseCheck{
CheckName: "identity-collision",
CheckDescription: "Check for agent identity collisions and stale locks",
CheckCategory: CategoryInfrastructure,
},
}
}
func (c *IdentityCollisionCheck) CanFix() bool {
+1
View File
@@ -27,6 +27,7 @@ func NewLifecycleHygieneCheck() *LifecycleHygieneCheck {
BaseCheck: BaseCheck{
CheckName: "lifecycle-hygiene",
CheckDescription: "Check for stale lifecycle messages",
CheckCategory: CategoryConfig,
},
},
}
+2
View File
@@ -27,6 +27,7 @@ func NewOrphanSessionCheck() *OrphanSessionCheck {
BaseCheck: BaseCheck{
CheckName: "orphan-sessions",
CheckDescription: "Detect orphaned tmux sessions",
CheckCategory: CategoryCleanup,
},
},
}
@@ -244,6 +245,7 @@ func NewOrphanProcessCheck() *OrphanProcessCheck {
BaseCheck: BaseCheck{
CheckName: "orphan-processes",
CheckDescription: "Detect runtime processes outside tmux",
CheckCategory: CategoryCleanup,
},
}
}
+5
View File
@@ -28,6 +28,7 @@ func NewPatrolMoleculesExistCheck() *PatrolMoleculesExistCheck {
BaseCheck: BaseCheck{
CheckName: "patrol-molecules-exist",
CheckDescription: "Check if patrol molecules exist for each rig",
CheckCategory: CategoryPatrol,
},
},
}
@@ -155,6 +156,7 @@ func NewPatrolHooksWiredCheck() *PatrolHooksWiredCheck {
BaseCheck: BaseCheck{
CheckName: "patrol-hooks-wired",
CheckDescription: "Check if hooks trigger patrol execution",
CheckCategory: CategoryPatrol,
},
},
}
@@ -229,6 +231,7 @@ func NewPatrolNotStuckCheck() *PatrolNotStuckCheck {
BaseCheck: BaseCheck{
CheckName: "patrol-not-stuck",
CheckDescription: "Check for stuck patrol wisps (>1h in_progress)",
CheckCategory: CategoryPatrol,
},
stuckThreshold: DefaultStuckThreshold,
}
@@ -351,6 +354,7 @@ func NewPatrolPluginsAccessibleCheck() *PatrolPluginsAccessibleCheck {
BaseCheck: BaseCheck{
CheckName: "patrol-plugins-accessible",
CheckDescription: "Check if plugin directories exist and are readable",
CheckCategory: CategoryPatrol,
},
},
}
@@ -420,6 +424,7 @@ func NewPatrolRolesHavePromptsCheck() *PatrolRolesHavePromptsCheck {
BaseCheck: BaseCheck{
CheckName: "patrol-roles-have-prompts",
CheckDescription: "Check if internal/templates/roles/*.md.tmpl exist for each patrol role",
CheckCategory: CategoryPatrol,
},
},
}
@@ -21,6 +21,7 @@ func NewPreCheckoutHookCheck() *PreCheckoutHookCheck {
BaseCheck: BaseCheck{
CheckName: "pre-checkout-hook",
CheckDescription: "Verify pre-checkout hook prevents branch switches",
CheckCategory: CategoryHooks,
},
},
}
@@ -42,6 +42,7 @@ func NewRepoFingerprintCheck() *RepoFingerprintCheck {
BaseCheck: BaseCheck{
CheckName: "repo-fingerprint",
CheckDescription: "Verify beads database has valid repository fingerprint",
CheckCategory: CategoryInfrastructure,
},
},
}
+1
View File
@@ -23,6 +23,7 @@ func NewRigBeadsCheck() *RigBeadsCheck {
BaseCheck: BaseCheck{
CheckName: "rig-beads-exist",
CheckDescription: "Verify rig identity beads exist for all rigs",
CheckCategory: CategoryRig,
},
},
}
+10
View File
@@ -25,6 +25,7 @@ func NewRigIsGitRepoCheck() *RigIsGitRepoCheck {
BaseCheck: BaseCheck{
CheckName: "rig-is-git-repo",
CheckDescription: "Verify rig has a valid mayor/rig git clone",
CheckCategory: CategoryRig,
},
}
}
@@ -99,6 +100,7 @@ func NewGitExcludeConfiguredCheck() *GitExcludeConfiguredCheck {
BaseCheck: BaseCheck{
CheckName: "git-exclude-configured",
CheckDescription: "Check .git/info/exclude has Gas Town directories",
CheckCategory: CategoryRig,
},
},
}
@@ -249,6 +251,7 @@ func NewHooksPathConfiguredCheck() *HooksPathConfiguredCheck {
BaseCheck: BaseCheck{
CheckName: "hooks-path-configured",
CheckDescription: "Check core.hooksPath is set for all clones",
CheckCategory: CategoryRig,
},
},
}
@@ -371,6 +374,7 @@ func NewWitnessExistsCheck() *WitnessExistsCheck {
BaseCheck: BaseCheck{
CheckName: "witness-exists",
CheckDescription: "Verify witness/ directory structure exists",
CheckCategory: CategoryRig,
},
},
}
@@ -477,6 +481,7 @@ func NewRefineryExistsCheck() *RefineryExistsCheck {
BaseCheck: BaseCheck{
CheckName: "refinery-exists",
CheckDescription: "Verify refinery/ directory structure exists",
CheckCategory: CategoryRig,
},
},
}
@@ -582,6 +587,7 @@ func NewMayorCloneExistsCheck() *MayorCloneExistsCheck {
BaseCheck: BaseCheck{
CheckName: "mayor-clone-exists",
CheckDescription: "Verify mayor/rig/ git clone exists",
CheckCategory: CategoryRig,
},
},
}
@@ -664,6 +670,7 @@ func NewPolecatClonesValidCheck() *PolecatClonesValidCheck {
BaseCheck: BaseCheck{
CheckName: "polecat-clones-valid",
CheckDescription: "Verify polecat directories are valid git clones",
CheckCategory: CategoryRig,
},
}
}
@@ -798,6 +805,7 @@ func NewBeadsConfigValidCheck() *BeadsConfigValidCheck {
BaseCheck: BaseCheck{
CheckName: "beads-config-valid",
CheckDescription: "Verify beads configuration if .beads/ exists",
CheckCategory: CategoryRig,
},
},
}
@@ -893,6 +901,7 @@ func NewBeadsRedirectCheck() *BeadsRedirectCheck {
BaseCheck: BaseCheck{
CheckName: "beads-redirect",
CheckDescription: "Verify rig-level beads redirect for tracked beads",
CheckCategory: CategoryRig,
},
},
}
@@ -1105,6 +1114,7 @@ func NewBareRepoRefspecCheck() *BareRepoRefspecCheck {
BaseCheck: BaseCheck{
CheckName: "bare-repo-refspec",
CheckDescription: "Verify bare repo has correct refspec for worktrees",
CheckCategory: CategoryRig,
},
},
}
+1
View File
@@ -23,6 +23,7 @@ func NewRoutesCheck() *RoutesCheck {
BaseCheck: BaseCheck{
CheckName: "routes-config",
CheckDescription: "Check beads routing configuration",
CheckCategory: CategoryConfig,
},
},
}
+1
View File
@@ -26,6 +26,7 @@ func NewSparseCheckoutCheck() *SparseCheckoutCheck {
BaseCheck: BaseCheck{
CheckName: "sparse-checkout",
CheckDescription: "Verify sparse checkout excludes Claude context files (.claude/, CLAUDE.md, etc.)",
CheckCategory: CategoryRig,
},
},
}
+1
View File
@@ -18,6 +18,7 @@ func NewStaleBinaryCheck() *StaleBinaryCheck {
BaseCheck: BaseCheck{
CheckName: "stale-binary",
CheckDescription: "Check if gt binary is up to date with repo",
CheckCategory: CategoryInfrastructure,
},
},
}
+1
View File
@@ -20,6 +20,7 @@ func NewThemeCheck() *ThemeCheck {
BaseCheck: BaseCheck{
CheckName: "themes",
CheckDescription: "Check tmux session theme configuration",
CheckCategory: CategoryConfig,
},
},
}
+1
View File
@@ -23,6 +23,7 @@ func NewLinkedPaneCheck() *LinkedPaneCheck {
BaseCheck: BaseCheck{
CheckName: "linked-panes",
CheckDescription: "Detect tmux sessions sharing panes (causes crosstalk)",
CheckCategory: CategoryInfrastructure,
},
},
}
+1
View File
@@ -20,6 +20,7 @@ func NewTownGitCheck() *TownGitCheck {
BaseCheck: BaseCheck{
CheckName: "town-git",
CheckDescription: "Verify town root is under version control",
CheckCategory: CategoryCore,
},
}
}
@@ -21,6 +21,7 @@ func NewTownRootBranchCheck() *TownRootBranchCheck {
BaseCheck: BaseCheck{
CheckName: "town-root-branch",
CheckDescription: "Verify town root is on main branch",
CheckCategory: CategoryCore,
},
},
}
+136 -40
View File
@@ -4,12 +4,34 @@ package doctor
import (
"fmt"
"io"
"strings"
"slices"
"time"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/ui"
)
// Category constants for grouping checks
const (
CategoryCore = "Core"
CategoryInfrastructure = "Infrastructure"
CategoryRig = "Rig"
CategoryPatrol = "Patrol"
CategoryConfig = "Configuration"
CategoryCleanup = "Cleanup"
CategoryHooks = "Hooks"
)
// CategoryOrder defines the display order for categories
var CategoryOrder = []string{
CategoryCore,
CategoryInfrastructure,
CategoryRig,
CategoryPatrol,
CategoryConfig,
CategoryCleanup,
CategoryHooks,
}
// CheckStatus represents the result status of a health check.
type CheckStatus int
@@ -55,11 +77,12 @@ func (ctx *CheckContext) RigPath() string {
// CheckResult represents the outcome of a health check.
type CheckResult struct {
Name string // Check name
Status CheckStatus // Result status
Message string // Primary result message
Details []string // Additional information
FixHint string // Suggestion if not auto-fixable
Name string // Check name
Status CheckStatus // Result status
Message string // Primary result message
Details []string // Additional information
FixHint string // Suggestion if not auto-fixable
Category string // Category for grouping (e.g., CategoryCore)
}
// Check defines the interface for a health check.
@@ -135,59 +158,132 @@ func (r *Report) IsHealthy() bool {
}
// Print outputs the report to the given writer.
// Matches bd doctor UX: grouped by category, semantic icons, warnings section.
func (r *Report) Print(w io.Writer, verbose bool) {
// Print individual check results
// Print header with version placeholder (caller should set via PrintWithVersion)
_, _ = fmt.Fprintln(w)
// Group checks by category
checksByCategory := make(map[string][]*CheckResult)
for _, check := range r.Checks {
r.printCheck(w, check, verbose)
cat := check.Category
if cat == "" {
cat = "Other"
}
checksByCategory[cat] = append(checksByCategory[cat], check)
}
// Print summary (output errors non-actionable)
_, _ = fmt.Fprintln(w)
// Track warnings/errors for summary section
var warnings []*CheckResult
// Print checks by category in defined order
for _, category := range CategoryOrder {
checks, exists := checksByCategory[category]
if !exists || len(checks) == 0 {
continue
}
// Print category header
_, _ = fmt.Fprintln(w, ui.RenderCategory(category))
// Print each check in this category
for _, check := range checks {
r.printCheck(w, check, verbose)
if check.Status != StatusOK {
warnings = append(warnings, check)
}
}
_, _ = fmt.Fprintln(w)
}
// Print any checks without a category
if otherChecks, exists := checksByCategory["Other"]; exists && len(otherChecks) > 0 {
_, _ = fmt.Fprintln(w, ui.RenderCategory("Other"))
for _, check := range otherChecks {
r.printCheck(w, check, verbose)
if check.Status != StatusOK {
warnings = append(warnings, check)
}
}
_, _ = fmt.Fprintln(w)
}
// Print separator and summary
_, _ = fmt.Fprintln(w, ui.RenderSeparator())
r.printSummary(w)
// Print warnings/errors section with fixes
r.printWarningsSection(w, warnings)
}
// printCheck outputs a single check result (output errors non-actionable).
// printCheck outputs a single check result with semantic styling.
func (r *Report) printCheck(w io.Writer, check *CheckResult, verbose bool) {
var prefix string
var statusIcon string
switch check.Status {
case StatusOK:
prefix = style.SuccessPrefix
statusIcon = ui.RenderPassIcon()
case StatusWarning:
prefix = style.WarningPrefix
statusIcon = ui.RenderWarnIcon()
case StatusError:
prefix = style.ErrorPrefix
statusIcon = ui.RenderFailIcon()
}
_, _ = fmt.Fprintf(w, "%s %s: %s\n", prefix, check.Name, check.Message)
// Print check line: icon + name + muted message
_, _ = fmt.Fprintf(w, " %s %s", statusIcon, check.Name)
if check.Message != "" {
_, _ = fmt.Fprintf(w, "%s", ui.RenderMuted(" "+check.Message))
}
_, _ = fmt.Fprintln(w)
// Print details in verbose mode or for non-OK results
// Print details in verbose mode or for non-OK results (with tree connector)
if len(check.Details) > 0 && (verbose || check.Status != StatusOK) {
for _, detail := range check.Details {
_, _ = fmt.Fprintf(w, " %s\n", detail)
_, _ = fmt.Fprintf(w, " %s%s\n", ui.MutedStyle.Render(ui.TreeLast), ui.RenderMuted(detail))
}
}
// Print fix hint for errors/warnings
if check.FixHint != "" && check.Status != StatusOK {
_, _ = fmt.Fprintf(w, " %s %s\n", style.ArrowPrefix, check.FixHint)
}
}
// printSummary outputs the summary line (output errors non-actionable).
// printSummary outputs the summary line with semantic icons.
func (r *Report) printSummary(w io.Writer) {
parts := []string{
fmt.Sprintf("%d checks", r.Summary.Total),
}
if r.Summary.OK > 0 {
parts = append(parts, style.Success.Render(fmt.Sprintf("%d passed", r.Summary.OK)))
}
if r.Summary.Warnings > 0 {
parts = append(parts, style.Warning.Render(fmt.Sprintf("%d warnings", r.Summary.Warnings)))
}
if r.Summary.Errors > 0 {
parts = append(parts, style.Error.Render(fmt.Sprintf("%d errors", r.Summary.Errors)))
}
_, _ = fmt.Fprintln(w, strings.Join(parts, ", "))
summary := fmt.Sprintf("%s %d passed %s %d warnings %s %d failed",
ui.RenderPassIcon(), r.Summary.OK,
ui.RenderWarnIcon(), r.Summary.Warnings,
ui.RenderFailIcon(), r.Summary.Errors,
)
_, _ = fmt.Fprintln(w, summary)
}
// printWarningsSection outputs numbered warnings/errors sorted by severity.
func (r *Report) printWarningsSection(w io.Writer, warnings []*CheckResult) {
if len(warnings) == 0 {
_, _ = fmt.Fprintln(w)
_, _ = fmt.Fprintln(w, ui.RenderPass(ui.IconPass+" All checks passed"))
return
}
_, _ = fmt.Fprintln(w)
_, _ = fmt.Fprintln(w, ui.RenderWarn(ui.IconWarn+" WARNINGS"))
// Sort by severity: errors first, then warnings
slices.SortStableFunc(warnings, func(a, b *CheckResult) int {
if a.Status == StatusError && b.Status != StatusError {
return -1
}
if a.Status != StatusError && b.Status == StatusError {
return 1
}
return 0
})
for i, check := range warnings {
line := fmt.Sprintf("%s: %s", check.Name, check.Message)
if check.Status == StatusError {
_, _ = fmt.Fprintf(w, " %s %s %s\n", ui.RenderFailIcon(), ui.RenderFail(fmt.Sprintf("%d.", i+1)), ui.RenderFail(line))
} else {
_, _ = fmt.Fprintf(w, " %s %s %s\n", ui.RenderWarnIcon(), ui.RenderWarn(fmt.Sprintf("%d.", i+1)), line)
}
if check.FixHint != "" {
_, _ = fmt.Fprintf(w, " %s%s\n", ui.MutedStyle.Render(ui.TreeLast), check.FixHint)
}
}
}
+1
View File
@@ -28,6 +28,7 @@ func NewWispGCCheck() *WispGCCheck {
BaseCheck: BaseCheck{
CheckName: "wisp-gc",
CheckDescription: "Detect and clean orphaned wisps (>1h old)",
CheckCategory: CategoryCleanup,
},
},
threshold: 1 * time.Hour,
+5
View File
@@ -18,6 +18,7 @@ func NewTownConfigExistsCheck() *TownConfigExistsCheck {
BaseCheck: BaseCheck{
CheckName: "town-config-exists",
CheckDescription: "Check that mayor/town.json exists",
CheckCategory: CategoryCore,
},
}
}
@@ -53,6 +54,7 @@ func NewTownConfigValidCheck() *TownConfigValidCheck {
BaseCheck: BaseCheck{
CheckName: "town-config-valid",
CheckDescription: "Check that mayor/town.json is valid with required fields",
CheckCategory: CategoryCore,
},
}
}
@@ -130,6 +132,7 @@ func NewRigsRegistryExistsCheck() *RigsRegistryExistsCheck {
BaseCheck: BaseCheck{
CheckName: "rigs-registry-exists",
CheckDescription: "Check that mayor/rigs.json exists",
CheckCategory: CategoryCore,
},
},
}
@@ -188,6 +191,7 @@ func NewRigsRegistryValidCheck() *RigsRegistryValidCheck {
BaseCheck: BaseCheck{
CheckName: "rigs-registry-valid",
CheckDescription: "Check that registered rigs exist on disk",
CheckCategory: CategoryCore,
},
},
}
@@ -320,6 +324,7 @@ func NewMayorExistsCheck() *MayorExistsCheck {
BaseCheck: BaseCheck{
CheckName: "mayor-exists",
CheckDescription: "Check that mayor/ directory exists with required files",
CheckCategory: CategoryCore,
},
}
}