// ABOUTME: Global state management for Gas Town enable/disable toggle. // ABOUTME: Uses XDG-compliant paths for per-machine state storage. package state import ( "encoding/json" "os" "path/filepath" "time" "github.com/google/uuid" ) // State represents the global Gas Town state. type State struct { Enabled bool `json:"enabled"` Version string `json:"version"` MachineID string `json:"machine_id"` InstalledAt time.Time `json:"installed_at"` UpdatedAt time.Time `json:"updated_at"` ShellIntegration string `json:"shell_integration,omitempty"` LastDoctorRun time.Time `json:"last_doctor_run,omitempty"` } // StateDir returns the XDG-compliant state directory. // Uses ~/.local/state/gastown/ (per XDG Base Directory Specification). func StateDir() string { // Check XDG_STATE_HOME first if xdg := os.Getenv("XDG_STATE_HOME"); xdg != "" { return filepath.Join(xdg, "gastown") } home, _ := os.UserHomeDir() return filepath.Join(home, ".local", "state", "gastown") } // ConfigDir returns the XDG-compliant config directory. // Uses ~/.config/gastown/ func ConfigDir() string { if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" { return filepath.Join(xdg, "gastown") } home, _ := os.UserHomeDir() return filepath.Join(home, ".config", "gastown") } // CacheDir returns the XDG-compliant cache directory. // Uses ~/.cache/gastown/ func CacheDir() string { if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" { return filepath.Join(xdg, "gastown") } home, _ := os.UserHomeDir() return filepath.Join(home, ".cache", "gastown") } // StatePath returns the path to state.json. func StatePath() string { return filepath.Join(StateDir(), "state.json") } // IsEnabled checks if Gas Town is globally enabled. // Priority: env override > state file > default (false) func IsEnabled() bool { // Environment overrides take priority if os.Getenv("GASTOWN_DISABLED") == "1" { return false } if os.Getenv("GASTOWN_ENABLED") == "1" { return true } // Check state file state, err := Load() if err != nil { return false // Default to disabled if state unreadable } return state.Enabled } // Load reads the state from disk. func Load() (*State, error) { data, err := os.ReadFile(StatePath()) if os.IsNotExist(err) { return nil, err } if err != nil { return nil, err } var state State if err := json.Unmarshal(data, &state); err != nil { return nil, err } return &state, nil } // Save writes the state to disk atomically. func Save(s *State) error { dir := StateDir() if err := os.MkdirAll(dir, 0755); err != nil { return err } s.UpdatedAt = time.Now() data, err := json.MarshalIndent(s, "", " ") if err != nil { return err } // Atomic write via temp file tmp := StatePath() + ".tmp" if err := os.WriteFile(tmp, data, 0600); err != nil { return err } return os.Rename(tmp, StatePath()) } // Enable enables Gas Town globally. func Enable(version string) error { s, err := Load() if err != nil { // Create new state s = &State{ InstalledAt: time.Now(), MachineID: generateMachineID(), } } s.Enabled = true s.Version = version return Save(s) } // Disable disables Gas Town globally. func Disable() error { s, err := Load() if err != nil { // Nothing to disable, create disabled state s = &State{ InstalledAt: time.Now(), MachineID: generateMachineID(), Enabled: false, } return Save(s) } s.Enabled = false return Save(s) } // generateMachineID creates a unique machine identifier. func generateMachineID() string { return uuid.New().String()[:8] } // GetMachineID returns the machine ID, creating one if needed. func GetMachineID() string { s, err := Load() if err != nil || s.MachineID == "" { return generateMachineID() } return s.MachineID } // SetShellIntegration records which shell integration is installed. func SetShellIntegration(shell string) error { s, err := Load() if err != nil { s = &State{ InstalledAt: time.Now(), MachineID: generateMachineID(), } } s.ShellIntegration = shell return Save(s) } // RecordDoctorRun records when doctor was last run. func RecordDoctorRun() error { s, err := Load() if err != nil { return err } s.LastDoctorRun = time.Now() return Save(s) }