diff --git a/scripts/moonarch-doctor b/scripts/moonarch-doctor new file mode 100755 index 0000000..6025a0d --- /dev/null +++ b/scripts/moonarch-doctor @@ -0,0 +1,318 @@ +#!/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" + +if [[ -f "$OFFICIAL" ]]; then + INSTALLED=$(pacman -Qq 2>/dev/null) + 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 stasis 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 + +# Paru PKGBUILD repo +if grep -q '\[moonarch-pkgbuilds\]' /etc/paru.conf 2>/dev/null; then + pass "Paru [moonarch-pkgbuilds] repo configured" +else + fail "Paru [moonarch-pkgbuilds] repo missing from /etc/paru.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