# 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 (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 " 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)" \ "jq . $flake/theme-state.json (shows the bad 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