// Package state manages the persistent xetup deployment state used to resume // after a system reboot. The state file is written before each reboot and // deleted once all deployment steps have completed. package state import ( "encoding/json" "os" "path/filepath" "time" "git.xetup.x9.cz/x9/xetup/internal/config" ) // StepResult records the outcome of a single deployment step. // Results are accumulated across reboot rounds so the final summary // can show every step regardless of which round it ran in. type StepResult struct { StepID string `json:"stepID"` Num string `json:"num"` Name string `json:"name"` Status string `json:"status"` // "OK", "ERROR", "SKIPPED" Elapsed time.Duration `json:"elapsed"` } // State holds everything xetup needs to resume after a reboot. type State struct { Config config.Config `json:"config"` ConfigPath string `json:"configPath"` // path to original config.json LogFile string `json:"logFile"` PendingSteps []string `json:"pendingSteps"` // step IDs to run next, in canonical order Results []StepResult `json:"results"` // accumulated across all rounds } // Load reads the state file. Returns a non-nil error when the file is absent. func Load() (*State, error) { data, err := os.ReadFile(statePath()) if err != nil { return nil, err } var s State if err := json.Unmarshal(data, &s); err != nil { return nil, err } return &s, nil } // Save writes the state file, creating parent directories as needed. func Save(s *State) error { p := statePath() if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil { return err } data, err := json.MarshalIndent(s, "", " ") if err != nil { return err } return os.WriteFile(p, data, 0644) } // Delete removes the state file. Silently ignores not-found. func Delete() error { err := os.Remove(statePath()) if os.IsNotExist(err) { return nil } return err } // Exists reports whether a state file is present on disk. func Exists() bool { _, err := os.Stat(statePath()) return err == nil }