#!/bin/bash # ABOUTME: Moonarch system health checker — verifies services, configs, packages and paths. # ABOUTME: Shipped as /usr/bin/moonarch-doctor (alias: moondoc) by the moonarch-git package. set -uo pipefail # --- Output helpers --- PASS=0 FAIL=0 WARN=0 pass() { echo -e " \e[1;32m✓\e[0m $*" ((PASS++)) } fail() { echo -e " \e[1;31m✗\e[0m $*" ((FAIL++)) } warn() { echo -e " \e[1;33m⚠\e[0m $*" ((WARN++)) } section() { echo echo -e "\e[1;34m[$1]\e[0m" } # --- Check functions --- check_system_service() { local svc="$1" local type type=$(systemctl show "$svc" --property=Type 2>/dev/null | cut -d= -f2) if systemctl is-enabled "$svc" &>/dev/null; then if systemctl is-active "$svc" &>/dev/null; then pass "$svc (enabled, active)" elif [[ "$type" == "oneshot" ]]; then # Oneshot services are inactive after completion — check if they succeeded local result result=$(systemctl show "$svc" --property=Result 2>/dev/null | cut -d= -f2) if [[ "$result" == "success" ]]; then pass "$svc (enabled, oneshot completed)" else fail "$svc (enabled, oneshot failed: $result)" fi else fail "$svc (enabled, NOT active)" fi else fail "$svc (NOT enabled)" fi } check_user_service() { local svc="$1" if systemctl --user is-enabled "$svc" &>/dev/null; then if systemctl --user is-active "$svc" &>/dev/null; then pass "$svc (enabled, active)" else # User services may be inactive if not in a graphical session warn "$svc (enabled, not active — may need graphical session)" fi else fail "$svc (NOT enabled)" fi } check_config_match() { local deployed="$1" local source="$2" local label="${deployed}" if [[ ! -f "$deployed" ]]; then fail "$label (missing)" return fi if [[ ! -f "$source" ]]; then warn "$label (exists, but no source to compare at $source)" return fi local hash_deployed hash_source hash_deployed=$(sha256sum "$deployed" | cut -d' ' -f1) hash_source=$(sha256sum "$source" | cut -d' ' -f1) if [[ "$hash_deployed" == "$hash_source" ]]; then pass "$label" else warn "$label (differs from moonarch default)" fi } # --- Header --- echo -e "\e[1;34m" echo " Moonarch Doctor" echo -e "\e[0m" # --- 1. Packages --- section "Packages" OFFICIAL="/usr/share/moonarch/official.txt" AUR="/usr/share/moonarch/aur.txt" # Hoist INSTALLED so the AUR block below can use it even if OFFICIAL is absent — # otherwise `set -u` aborts the script when $INSTALLED is referenced unset. INSTALLED=$(pacman -Qq 2>/dev/null) if [[ -f "$OFFICIAL" ]]; then MISSING_OFFICIAL=() while IFS= read -r pkg; do [[ "$pkg" =~ ^[[:space:]]*# ]] && continue [[ -z "${pkg// }" ]] && continue if ! echo "$INSTALLED" | grep -qx "$pkg"; then MISSING_OFFICIAL+=("$pkg") fi done < "$OFFICIAL" if [[ ${#MISSING_OFFICIAL[@]} -eq 0 ]]; then pass "All official packages installed" else fail "Missing official packages: ${MISSING_OFFICIAL[*]}" fi else warn "$OFFICIAL not found (moonarch-git not installed?)" fi if [[ -f "$AUR" ]]; then MISSING_AUR=() while IFS= read -r pkg; do [[ "$pkg" =~ ^[[:space:]]*# ]] && continue [[ -z "${pkg// }" ]] && continue if ! echo "$INSTALLED" | grep -qx "$pkg"; then MISSING_AUR+=("$pkg") fi done < "$AUR" if [[ ${#MISSING_AUR[@]} -eq 0 ]]; then pass "All AUR packages installed" else fail "Missing AUR packages: ${MISSING_AUR[*]}" fi else warn "$AUR not found (moonarch-git not installed?)" fi ORPHANS=$(pacman -Qdtq 2>/dev/null || true) if [[ -z "$ORPHANS" ]]; then pass "No orphaned packages" else ORPHAN_COUNT=$(echo "$ORPHANS" | wc -l) warn "$ORPHAN_COUNT orphaned package(s) (run moonup to clean)" fi # --- 2. System Services --- section "System Services" for svc in NetworkManager bluetooth greetd systemd-timesyncd ufw auto-cpufreq; do check_system_service "$svc" done # Battery conservation service (laptop only) if [[ -f /sys/class/power_supply/BAT0/charge_control_end_threshold ]]; then check_system_service moonarch-batsaver else pass "moonarch-batsaver (skipped — no battery threshold support)" fi # --- 3. User Services --- section "User Services" if [[ $EUID -eq 0 ]]; then warn "Running as root — skipping user service checks" else for svc in kanshi wlsunset stasis walker nautilus cliphist-text cliphist-image; do check_user_service "$svc" done fi # --- 4. Config Files --- section "Config Files" SRC="/usr/share/moonarch" check_config_match "/etc/xdg/foot/foot.ini" "$SRC/foot/foot.ini" check_config_match "/etc/xdg/waybar/style.css" "$SRC/waybar/style.css" check_config_match "/etc/xdg/swaync/config.json" "$SRC/swaync/config.json" check_config_match "/etc/xdg/swaync/style.css" "$SRC/swaync/style.css" check_config_match "/etc/greetd/config.toml" "$SRC/greetd/config.toml" check_config_match "/etc/greetd/niri-greeter.kdl" "$SRC/greetd/niri-greeter.kdl" check_config_match "/etc/moongreet/moongreet.toml" "$SRC/moongreet/moongreet.toml" if [[ -f /etc/zsh/zshrc.moonarch ]]; then pass "/etc/zsh/zshrc.moonarch" else fail "/etc/zsh/zshrc.moonarch (missing)" fi # --- 5. Helper Scripts --- section "Helper Scripts" EXPECTED_SCRIPTS=( moonarch-batsaver-toggle moonarch-btnote moonarch-capsnote moonarch-cpugov moonarch-doctor moonarch-nightlight moonarch-sink-switcher moonarch-update moonarch-vpn moonarch-waybar moonarch-waybar-batsaver moonarch-waybar-cpugov moonarch-waybar-gpustat moonarch-waybar-hidpp moonarch-waybar-nightlight moonarch-waybar-updates ) MISSING_SCRIPTS=() for script in "${EXPECTED_SCRIPTS[@]}"; do if [[ ! -x "/usr/bin/$script" ]]; then MISSING_SCRIPTS+=("$script") fi done if [[ ${#MISSING_SCRIPTS[@]} -eq 0 ]]; then pass "All ${#EXPECTED_SCRIPTS[@]} helper scripts present" else fail "Missing scripts: ${MISSING_SCRIPTS[*]}" fi # Symlinks for link in moonup moondoc; do if [[ -L "/usr/bin/$link" ]]; then pass "$link symlink" else fail "$link symlink (missing)" fi done # --- 6. System Config --- section "System Config" # UFW (check via systemd, no sudo needed) if command -v ufw &>/dev/null; then if systemctl is-active ufw &>/dev/null; then pass "UFW active" else fail "UFW not active" fi else fail "UFW not installed" fi # Pacman moonarch repo if grep -q '^\[moonarch\]' /etc/pacman.conf 2>/dev/null; then pass "Pacman [moonarch] repo configured" else fail "Pacman [moonarch] repo missing from /etc/pacman.conf" fi # Default shell USER_SHELL=$(getent passwd "$USER" | cut -d: -f7) if [[ "$USER_SHELL" == */zsh ]]; then pass "Default shell: zsh" else warn "Default shell: $USER_SHELL (expected zsh)" fi # --- 7. Directories --- section "Directories" if [[ -d /var/lib/moonarch ]]; then DIR_PERMS=$(stat -c '%a' /var/lib/moonarch) DIR_GROUP=$(stat -c '%G' /var/lib/moonarch) if [[ "$DIR_GROUP" == "wheel" ]]; then pass "/var/lib/moonarch/ (group: wheel, mode: $DIR_PERMS)" else warn "/var/lib/moonarch/ (group: $DIR_GROUP, expected wheel)" fi else warn "/var/lib/moonarch/ missing (created on first battery toggle)" fi if [[ -d /usr/share/moonarch ]]; then pass "/usr/share/moonarch/" else fail "/usr/share/moonarch/ missing (moonarch-git not installed?)" fi # --- Summary --- echo TOTAL=$((PASS + FAIL + WARN)) echo -e "\e[1;34m[Summary]\e[0m $TOTAL checks: \e[1;32m$PASS passed\e[0m, \e[1;31m$FAIL failed\e[0m, \e[1;33m$WARN warnings\e[0m" if [[ $FAIL -gt 0 ]]; then exit 1 fi