Files
Nomarchy/pkgs/nomarchy-doctor/nomarchy-doctor.sh
Bernardo Magri bfb80cb60d
All checks were successful
Check / eval (push) Successful in 2m56s
feat(state): validation + friendly errors on both write and eval paths (item 11)
'Never has to master Nix' includes the error messages. The tool grows
validate_state (appearance schema = hard errors; settings.* and
unknown keys = warnings so menu writers and newer schemas keep
working), a read-only `validate` subcommand, and validation BEFORE
every write — an invalid set/apply is refused with the on-disk file
untouched. JSON syntax errors now say line, column, and fix. theme.nix
enforces the same schema at eval time (on the post-defaults state, so
sparse/older files still evaluate) and throws a message naming each
field, its got-value, and the fix paths — the hand-edit escape hatch
no longer ends in a Nix stack trace. Doctor's JSON fix-text points at
validate.

V1: new checks.theme-sync-validate fixture corpus green (good state,
trailing comma, bad hex, bad type, unknown key, refused set with
byte-identical file); negative mkFlake eval of a broken scratch
downstream shows the friendly throw; good-path template-home builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 20:44:08 +01:00

139 lines
5.9 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# nomarchy-doctor — one-shot, READ-ONLY health check: the things that
# actually break user machines, as a pass/fail sheet where every
# failure prints the one command that fixes it. It never changes
# anything itself. Exit 0 = no failures (warnings allowed), 1 = at
# least one ✖.
flake="${NOMARCHY_PATH:-$HOME/.nomarchy}"
fails=0
warns=0
grn=$'\033[32m'; red=$'\033[31m'; ylw=$'\033[33m'; dim=$'\033[2m'; rst=$'\033[0m'
ok() { printf ' %b✔%b %s\n' "$grn" "$rst" "$1"; }
bad() { printf ' %b✖%b %s\n' "$red" "$rst" "$1"
printf ' %bfix: %s%b\n' "$dim" "$2" "$rst"
fails=$((fails + 1)); }
warn() { printf ' %b●%b %s\n' "$ylw" "$rst" "$1"
if [ $# -gt 1 ]; then printf ' %b%s%b\n' "$dim" "$2" "$rst"; fi
warns=$((warns + 1)); }
skip() { printf ' %b %s%b\n' "$dim" "$1" "$rst"; }
printf 'nomarchy-doctor — %s\n\n' "$(date '+%Y-%m-%d %H:%M')"
# ── systemd units ────────────────────────────────────────────────────
mapfile -t sysfailed < <(systemctl --failed --no-legend --plain 2>/dev/null | awk '{print $1}')
if [ "${#sysfailed[@]}" -eq 0 ]; then
ok "no failed system units"
else
bad "failed system unit(s): ${sysfailed[*]}" \
"journalctl -b -u <unit> (read why; sys-rebuild after fixing)"
fi
# A user bus only exists inside a session (not over bare SSH/root).
if [ -n "$(systemctl --user is-system-running 2>/dev/null || true)" ]; then
mapfile -t usrfailed < <(systemctl --user --failed --no-legend --plain 2>/dev/null | awk '{print $1}')
if [ "${#usrfailed[@]}" -eq 0 ]; then
ok "no failed user units"
else
bad "failed user unit(s): ${usrfailed[*]}" \
"journalctl --user -b -u <unit>"
fi
else
skip "user units (no user session bus here)"
fi
# ── disk space ───────────────────────────────────────────────────────
# Only real on-disk filesystems; skips the tmpfs/9p/overlay mounts of
# live systems and test VMs (where usage numbers mean nothing).
seen=""
for mp in / /boot /nix; do
[ -d "$mp" ] || continue
line=$(df -PT "$mp" 2>/dev/null | awk 'NR==2 {gsub("%","",$6); print $1, $2, $6, $5}')
[ -n "$line" ] || continue
read -r dev fstype use availkb <<<"$line"
case "$fstype" in
ext2|ext3|ext4|btrfs|xfs|vfat|f2fs|zfs) ;;
*) continue ;;
esac
case " $seen " in *" $dev "*) continue ;; esac
seen="$seen $dev"
avail=$(numfmt --to=iec $((availkb * 1024)))
if [ "$use" -ge 90 ]; then
bad "$mp is ${use}% full (${avail} free)" \
"sudo nix-collect-garbage --delete-older-than 14d (then sys-rebuild to prune old boot entries)"
else
ok "$mp has space (${use}% used, ${avail} free)"
fi
done
# ── the flake + state file ───────────────────────────────────────────
if [ -f "$flake/theme-state.json" ]; then
if jq empty "$flake/theme-state.json" 2>/dev/null; then
ok "theme-state.json parses"
else
bad "theme-state.json is not valid JSON (rebuilds will fail)" \
"nomarchy-theme-sync validate (names the spot; fix it, or re-apply a theme)"
fi
if git -C "$flake" ls-files --error-unmatch theme-state.json >/dev/null 2>&1; then
ok "theme-state.json is git-tracked"
else
bad "theme-state.json is NOT git-tracked (flake evaluation can't see it)" \
"git -C $flake add theme-state.json"
fi
else
skip "theme-state.json (no flake checkout at $flake)"
fi
if [ -d "$flake/.git" ]; then
dirty=$(git -C "$flake" status --porcelain 2>/dev/null | wc -l)
if [ "$dirty" -gt 0 ]; then
warn "$dirty uncommitted change(s) in $flake (normal — theme writes land there)" \
"commit when happy: git -C $flake add -A && git -C $flake commit -m settings"
else
ok "flake checkout is clean"
fi
behind=$(git -C "$flake" rev-list --count 'HEAD..@{u}' 2>/dev/null || echo 0)
if [ "$behind" -gt 0 ]; then
warn "flake is $behind commit(s) behind its upstream" "git -C $flake pull, then sys-rebuild + home-update"
fi
fi
# ── generation age ───────────────────────────────────────────────────
# The profile SYMLINK's own mtime is the generation's creation time
# (store paths themselves are all epoch-1).
link=$(readlink /nix/var/nix/profiles/system 2>/dev/null || true)
if [ -n "$link" ] && [ -e "/nix/var/nix/profiles/$link" ]; then
ts=$(stat -c %Y "/nix/var/nix/profiles/$link" 2>/dev/null || echo 0)
days=$(( ($(date +%s) - ts) / 86400 ))
if [ "$days" -gt 30 ]; then
warn "last system rebuild was $days days ago" \
"sys-update when convenient (security fixes arrive with input bumps)"
else
ok "system generation is $days day(s) old"
fi
else
skip "system generation age (no system profile)"
fi
# ── snapper timeline (when enabled) ──────────────────────────────────
if [ -n "$(systemctl list-unit-files snapper-timeline.timer --no-legend --plain 2>/dev/null)" ]; then
if systemctl is-active --quiet snapper-timeline.timer; then
ok "snapper timeline snapshots are running"
else
bad "snapper is enabled but the timeline timer is not active" \
"systemctl status snapper-timeline.timer (then journalctl -u snapper-timeline.service)"
fi
else
skip "snapper (not enabled on this machine)"
fi
# ── verdict ──────────────────────────────────────────────────────────
echo
if [ "$fails" -eq 0 ]; then
printf '%b✔ healthy%b — %d warning(s)\n' "$grn" "$rst" "$warns"
exit 0
else
printf '%b✖ %d problem(s)%b, %d warning(s) — each ✖ above shows its fix\n' "$red" "$fails" "$rst" "$warns"
exit 1
fi