// Package state persists a per-run log used only for resume hints in the UI. // Correctness always comes from live Detect() calls. package state import ( "encoding/json" "fmt" "os" "path/filepath" "sort" "time" ) // RunRecord is the JSON document written to runs/.json. type RunRecord struct { StartedAt time.Time `json:"started_at"` EndedAt time.Time `json:"ended_at"` Modules []ModuleRecord `json:"modules"` } // ModuleRecord captures how one module fared. type ModuleRecord struct { ID string `json:"id"` Status string `json:"status"` // succeeded|failed|skipped|planned Duration string `json:"duration"` Error string `json:"error,omitempty"` At time.Time `json:"at"` } // Store is a file-backed run log. type Store struct { Root string // e.g. ~/.local/state/archduino } // DefaultStore returns a Store rooted at $XDG_STATE_HOME/archduino or // ~/.local/state/archduino. func DefaultStore() (*Store, error) { if xdg := os.Getenv("XDG_STATE_HOME"); xdg != "" { return &Store{Root: filepath.Join(xdg, "archduino")}, nil } home, err := os.UserHomeDir() if err != nil { return nil, err } return &Store{Root: filepath.Join(home, ".local", "state", "archduino")}, nil } // Save writes record as runs/.json and updates last.json. Both paths are // created if missing. Old runs beyond `keep` are deleted. func (s *Store) Save(rec RunRecord, keep int) error { if keep <= 0 { keep = 10 } runsDir := filepath.Join(s.Root, "runs") if err := os.MkdirAll(runsDir, 0o755); err != nil { return err } name := rec.StartedAt.UTC().Format("20060102T150405Z") + ".json" path := filepath.Join(runsDir, name) data, err := json.MarshalIndent(rec, "", " ") if err != nil { return err } if err := os.WriteFile(path, data, 0o644); err != nil { return err } if err := os.WriteFile(filepath.Join(s.Root, "last.json"), data, 0o644); err != nil { return err } return rotate(runsDir, keep) } // LastRun reads last.json if present. func (s *Store) LastRun() (RunRecord, bool, error) { data, err := os.ReadFile(filepath.Join(s.Root, "last.json")) if err != nil { if os.IsNotExist(err) { return RunRecord{}, false, nil } return RunRecord{}, false, err } var rec RunRecord if err := json.Unmarshal(data, &rec); err != nil { return RunRecord{}, false, fmt.Errorf("parse last.json: %w", err) } return rec, true, nil } func rotate(dir string, keep int) error { entries, err := os.ReadDir(dir) if err != nil { return err } names := make([]string, 0, len(entries)) for _, e := range entries { if !e.IsDir() { names = append(names, e.Name()) } } if len(names) <= keep { return nil } sort.Strings(names) // lexicographic == chronological (UTC stamp) for _, n := range names[:len(names)-keep] { _ = os.Remove(filepath.Join(dir, n)) } return nil }