package state import ( "encoding/json" "fmt" "os" "path/filepath" "regexp" ) // State represents the local ship configuration type State struct { DefaultHost string `json:"default_host,omitempty"` Hosts map[string]*Host `json:"hosts"` } // Host represents configuration for a single VPS type Host struct { BaseDomain string `json:"base_domain,omitempty"` GitSetup bool `json:"git_setup,omitempty"` } var validName = regexp.MustCompile(`^[a-z][a-z0-9-]{0,62}$`) // ValidateName checks that a name is safe for use in shell commands, // file paths, systemd units, and DNS labels. func ValidateName(name string) error { if !validName.MatchString(name) { return fmt.Errorf("invalid name %q: must start with a lowercase letter, contain only lowercase letters, digits, and hyphens, and be 1-63 characters", name) } return nil } // Load reads state from ~/.config/ship/state.json func Load() (*State, error) { path := statePath() if _, err := os.Stat(path); os.IsNotExist(err) { return &State{ Hosts: make(map[string]*Host), }, nil } data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to read state file: %w", err) } var state State if err := json.Unmarshal(data, &state); err != nil { return nil, fmt.Errorf("failed to parse state file: %w", err) } if state.Hosts == nil { state.Hosts = make(map[string]*Host) } return &state, nil } // Save writes state to ~/.config/ship/state.json func (s *State) Save() error { path := statePath() dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("failed to create config directory: %w", err) } data, err := json.MarshalIndent(s, "", " ") if err != nil { return fmt.Errorf("failed to marshal state: %w", err) } if err := os.WriteFile(path, data, 0600); err != nil { return fmt.Errorf("failed to write state file: %w", err) } return nil } // GetHost returns the host config, creating it if it doesn't exist func (s *State) GetHost(host string) *Host { if s.Hosts[host] == nil { s.Hosts[host] = &Host{} } return s.Hosts[host] } // GetDefaultHost returns the default host, or empty string if not set func (s *State) GetDefaultHost() string { return s.DefaultHost } // SetDefaultHost sets the default host func (s *State) SetDefaultHost(host string) { s.DefaultHost = host } func statePath() string { home, err := os.UserHomeDir() if err != nil { return ".ship-state.json" } return filepath.Join(home, ".config", "ship", "state.json") }