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>
189 lines
4.2 KiB
Go
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)
|
|
}
|