Files
gastown/internal/doctor/env_check.go
Ryan Snodgrass e1f2bb8b4b feat(ui): import comprehensive UX system from beads
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>
2026-01-09 22:46:06 -08:00

160 lines
4.3 KiB
Go

package doctor
import (
"fmt"
"path/filepath"
"strings"
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/session"
"github.com/steveyegge/gastown/internal/tmux"
)
// SessionEnvReader abstracts tmux session environment access for testing.
type SessionEnvReader interface {
ListSessions() ([]string, error)
GetAllEnvironment(session string) (map[string]string, error)
}
// tmuxEnvReader wraps real tmux operations.
type tmuxEnvReader struct {
t *tmux.Tmux
}
func (r *tmuxEnvReader) ListSessions() ([]string, error) {
return r.t.ListSessions()
}
func (r *tmuxEnvReader) GetAllEnvironment(session string) (map[string]string, error) {
return r.t.GetAllEnvironment(session)
}
// EnvVarsCheck verifies that tmux session environment variables match expected values.
type EnvVarsCheck struct {
BaseCheck
reader SessionEnvReader // nil means use real tmux
}
// NewEnvVarsCheck creates a new env vars check.
func NewEnvVarsCheck() *EnvVarsCheck {
return &EnvVarsCheck{
BaseCheck: BaseCheck{
CheckName: "env-vars",
CheckDescription: "Verify tmux session environment variables match expected values",
CheckCategory: CategoryConfig,
},
}
}
// NewEnvVarsCheckWithReader creates a check with a custom reader (for testing).
func NewEnvVarsCheckWithReader(reader SessionEnvReader) *EnvVarsCheck {
c := NewEnvVarsCheck()
c.reader = reader
return c
}
// Run checks environment variables for all Gas Town sessions.
func (c *EnvVarsCheck) Run(ctx *CheckContext) *CheckResult {
reader := c.reader
if reader == nil {
reader = &tmuxEnvReader{t: tmux.NewTmux()}
}
sessions, err := reader.ListSessions()
if err != nil {
// No tmux server - treat as success (valid when Gas Town is down)
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "No tmux sessions running",
}
}
// Filter to Gas Town sessions only (gt-* and hq-*)
var gtSessions []string
for _, sess := range sessions {
if strings.HasPrefix(sess, "gt-") || strings.HasPrefix(sess, "hq-") {
gtSessions = append(gtSessions, sess)
}
}
if len(gtSessions) == 0 {
// No Gas Town sessions - treat as success (valid when Gas Town is down)
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "No Gas Town sessions running",
}
}
var mismatches []string
checkedCount := 0
for _, sess := range gtSessions {
identity, err := session.ParseSessionName(sess)
if err != nil {
// Skip unparseable sessions
continue
}
// Get expected env vars based on role, emulating real call sites
// Town-level roles use TownRoot for beads, rig-level roles use rig path
var beadsDir string
if identity.Rig != "" {
rigPath := filepath.Join(ctx.TownRoot, identity.Rig)
beadsDir = beads.ResolveBeadsDir(rigPath)
} else {
beadsDir = beads.ResolveBeadsDir(ctx.TownRoot)
}
expected := config.AgentEnv(config.AgentEnvConfig{
Role: string(identity.Role),
Rig: identity.Rig,
AgentName: identity.Name,
TownRoot: ctx.TownRoot,
BeadsDir: beadsDir,
})
// Get actual tmux env vars
actual, err := reader.GetAllEnvironment(sess)
if err != nil {
mismatches = append(mismatches, fmt.Sprintf("%s: could not read env vars: %v", sess, err))
continue
}
checkedCount++
// Compare each expected var
for key, expectedVal := range expected {
actualVal, exists := actual[key]
if !exists {
mismatches = append(mismatches, fmt.Sprintf("%s: missing %s (expected %q)", sess, key, expectedVal))
} else if actualVal != expectedVal {
mismatches = append(mismatches, fmt.Sprintf("%s: %s=%q (expected %q)", sess, key, actualVal, expectedVal))
}
}
}
if len(mismatches) == 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: fmt.Sprintf("All %d session(s) have correct environment variables", checkedCount),
}
}
// Add explanation about needing restart
details := append(mismatches,
"",
"Note: Mismatched session env vars won't affect running Claude until sessions restart.",
)
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: fmt.Sprintf("Found %d env var mismatch(es) across %d session(s)", len(mismatches), checkedCount),
Details: details,
FixHint: "Run 'gt shutdown && gt up' to restart sessions with correct env vars",
}
}