feat: restore state/resume/report + single Spotlight index after mas installs

lib/brew.sh:
  Set MAS_NO_AUTO_INDEX=1 before brew bundle install to suppress the
  per-app mdimport call that mas otherwise runs after each App Store
  installation. Run one mdimport /Applications batch pass afterwards.

lib/restore-state.sh (new):
  Lightweight key=value state file (~/.local/state/ssr/restore-state).
  ssr::rs::init / ssr::rs::run / ssr::rs::set/get / ssr::rs::error /
  ssr::rs::report — tracks step outcomes and accumulated errors.

bin/ssr-restore:
  --resume flag: skip steps already marked ok in state file.
  Each step (brew_install, brew_restore, mackup_restore, macos_defaults,
  prefs_import) wrapped in ssr::rs::run; errors accumulate without abort.
  Formatted report printed at end of every run.

bin/ssr-status:
  ssr status restore — full restore step report + error log
  ssr status backup  — Brewfile lines, unmanaged apps, prefs snapshot
  ssr status         — general view now shows one-line last-restore
                       summary and hint to run 'ssr status restore'

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-12 09:39:48 +02:00
parent cf7f61e95d
commit 634487b9d8
4 changed files with 348 additions and 58 deletions
+144
View File
@@ -0,0 +1,144 @@
#!/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 <step_key> <description> <function> [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
}