package settings import ( "encoding/json" "errors" "fmt" "os" "path/filepath" "time" ) // Stable IDs for seeded built-in actions. const ( BuiltinActionUptimeID = "00000000-0000-4000-8000-000000000001" BuiltinActionDiskUsageID = "00000000-0000-4000-8000-000000000002" BuiltinActionSysInfoID = "00000000-0000-4000-8000-000000000003" ) // DefaultBuiltinActions returns the read-only seed actions. func DefaultBuiltinActions() []Action { now := time.Now().UTC() return []Action{ { ID: BuiltinActionUptimeID, Name: "Uptime", Description: "Show how long the target system has been running.", Kind: ActionKindShell, Body: "uptime", Env: []ActionEnvVar{}, Builtin: true, CreatedAt: now, UpdatedAt: now, }, { ID: BuiltinActionDiskUsageID, Name: "Disk usage", Description: "Show filesystem disk space usage on the target.", Kind: ActionKindShell, Body: "df -h", Env: []ActionEnvVar{}, Builtin: true, CreatedAt: now, UpdatedAt: now, }, { ID: BuiltinActionSysInfoID, Name: "System info", Description: "Print kernel and OS release details from the target.", Kind: ActionKindScript, Body: `uname -a if [ -f /etc/os-release ]; then . /etc/os-release echo "OS: ${NAME:-unknown} ${VERSION:-}" fi`, Env: []ActionEnvVar{}, Builtin: true, CreatedAt: now, UpdatedAt: now, }, } } // LoadActions reads and parses actions.json from dir. func LoadActions(dir string) (ActionStore, error) { path := filepath.Join(dir, ActionsFileName) payload, err := os.ReadFile(path) if err != nil { return ActionStore{}, err } var store ActionStore if err := json.Unmarshal(payload, &store); err != nil { return ActionStore{}, fmt.Errorf("parse actions: %w", err) } if store.Actions == nil { store.Actions = []Action{} } for index := range store.Actions { if store.Actions[index].Env == nil { store.Actions[index].Env = []ActionEnvVar{} } } return store, nil } // LoadActionsOrSeed returns seeded builtins (and persists them) when actions.json is missing. func LoadActionsOrSeed(dir string) (ActionStore, error) { store, err := LoadActions(dir) if err != nil { if errors.Is(err, os.ErrNotExist) { seeded := ActionStore{Actions: DefaultBuiltinActions()} if saveErr := SaveActions(dir, seeded); saveErr != nil { return ActionStore{}, saveErr } return seeded, nil } return ActionStore{}, err } return store, nil } // SaveActions writes actions.json to dir with mode 0640. func SaveActions(dir string, store ActionStore) error { if err := ensureConfigDir(dir); err != nil { return err } if store.Actions == nil { store.Actions = []Action{} } for index := range store.Actions { if store.Actions[index].Env == nil { store.Actions[index].Env = []ActionEnvVar{} } } payload, err := json.MarshalIndent(store, "", " ") if err != nil { return fmt.Errorf("encode actions: %w", err) } payload = append(payload, '\n') path := filepath.Join(dir, ActionsFileName) if err := os.WriteFile(path, payload, 0o640); err != nil { return fmt.Errorf("write actions: %w", err) } return nil }