#!/usr/bin/env bash # lib/restore-state.sh — tracks restore step outcomes and accumulated errors. # # State file: $SSR_STATE_DIR/restore-state (key=value, one per line) # Error log: $SSR_STATE_DIR/restore-errors.log # # Step values: ok | warn | fail | skipped | pending _ssr_rs_state_file() { printf '%s/restore-state' "${SSR_STATE_DIR:-$HOME/.local/state/ssr}" } _ssr_rs_error_file() { printf '%s/restore-errors.log' "${SSR_STATE_DIR:-$HOME/.local/state/ssr}" } # Initialise (or reset) the state file for a new restore run. # $1 = brewfile path used for this run. ssr::rs::init() { local brewfile="${1:-}" local state_dir="${SSR_STATE_DIR:-$HOME/.local/state/ssr}" ssr::ensure_dir "$state_dir" # Archive previous state so `ssr status restore` can show it even after a # new run starts. local sf; sf="$(_ssr_rs_state_file)" [[ -f "$sf" ]] && cp -f "$sf" "${sf}.prev" 2>/dev/null || true # Truncate error log for the new run. : > "$(_ssr_rs_error_file)" # Write fresh state header. printf 'started=%s\nbrewfile=%s\n' "$(ssr::ts)" "$brewfile" > "$sf" } # Set a key in the state file (upsert). ssr::rs::set() { local key="$1" val="$2" local sf; sf="$(_ssr_rs_state_file)" local tmp; tmp="$(mktemp)" grep -v "^${key}=" "$sf" 2>/dev/null > "$tmp" || true printf '%s=%s\n' "$key" "$val" >> "$tmp" mv -f "$tmp" "$sf" } # Get a value from the state file (empty string if absent). ssr::rs::get() { local key="$1" grep "^${key}=" "$(_ssr_rs_state_file)" 2>/dev/null | cut -d= -f2- || true } # Append a timestamped error line to the error log. ssr::rs::error() { printf '[%s] %s\n' "$(ssr::ts)" "$1" >> "$(_ssr_rs_error_file)" } # Run a named restore step. # Usage: ssr::rs::run [args…] # # If --resume is active and the step is already 'ok', it is skipped. # Exit code of the function is captured; non-zero sets step to 'warn' (not # fatal) and logs the error. The outer script keeps running. ssr::rs::run() { local key="$1" desc="$2"; shift 2 local fn="$1"; shift # Resume: skip steps that already completed successfully. if [[ "${SSR_RESTORE_RESUME:-false}" == "true" ]]; then local prev; prev="$(ssr::rs::get "step_${key}")" if [[ "$prev" == "ok" ]]; then ssr::log "SKIP [$desc] (already completed in previous run)" return 0 fi fi ssr::log "STEP [$desc]" ssr::rs::set "step_${key}" "running" local rc=0 "$fn" "$@" || rc=$? if [[ $rc -eq 0 ]]; then ssr::rs::set "step_${key}" "ok" else ssr::rs::set "step_${key}" "warn:rc=$rc" ssr::rs::error "[$desc] exited rc=$rc" ssr::warn "[$desc] finished with errors (rc=$rc) — continuing restore" fi } # Print a formatted restore report to stdout. ssr::rs::report() { local sf; sf="$(_ssr_rs_state_file)" local ef; ef="$(_ssr_rs_error_file)" if [[ ! -f "$sf" ]]; then printf 'No restore state found (%s).\n' "$sf" return 0 fi local started brewfile started="$(grep '^started=' "$sf" | cut -d= -f2-)" brewfile="$(grep '^brewfile=' "$sf" | cut -d= -f2-)" printf '\n%s\n' "── Restore Report ──────────────────────────────" printf ' started : %s\n' "${started:-unknown}" printf ' brewfile : %s\n' "${brewfile:-(unknown)}" printf '\n Steps:\n' local -a step_keys=(brew_install brew_restore mackup_restore macos_defaults prefs_import) local -A step_labels=( [brew_install]="Homebrew install" [brew_restore]="Brew bundle install" [mackup_restore]="Mackup restore" [macos_defaults]="macOS defaults" [prefs_import]="Prefs snapshot import" ) local key val icon for key in "${step_keys[@]}"; do val="$(grep "^step_${key}=" "$sf" 2>/dev/null | cut -d= -f2- || true)" case "$val" in ok) icon="✓" ;; skipped) icon="–" ;; warn*) icon="⚠" ;; fail*) icon="✗" ;; running) icon="…" ;; *) icon="?" ;; esac printf ' %s %-28s %s\n' "$icon" "${step_labels[$key]}" "${val:-(not run)}" done if [[ -s "$ef" ]]; then printf '\n Errors:\n' sed 's/^/ /' "$ef" else printf '\n No errors recorded.\n' fi printf '%s\n\n' "────────────────────────────────────────────────" # Resume hint if any step failed. if grep -qE '^step_[^=]+=(warn|fail)' "$sf" 2>/dev/null; then printf ' To retry failed steps: ssr restore --resume\n\n' fi }