Files
gastown/internal/state/state.go
Sohail Mohammad 81bfe48ed3 feat: Set and Forget - Seamless Gas Town Integration (#255)
Adds shell integration for automatic Gas Town context detection.

Features:
- `gt enable` / `gt disable` - Global on/off switch
- `gt shell install|remove|status` - Shell integration management
- `gt rig quick-add [path]` - One-command project setup
- `gt uninstall` - Clean removal with options
- Shell hook auto-sets GT_TOWN_ROOT/GT_RIG on cd

Implementation:
- XDG-compliant state storage (~/.local/state/gastown/)
- Safe RC file manipulation with block markers
- Environment overrides (GASTOWN_DISABLED/ENABLED)
- Doctor check for global state validation

Co-authored-by: Sohail Mohammad <sohailm25@gmail.com>
2026-01-08 20:25:01 -08:00

189 lines
4.2 KiB
Go

// 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)
}