Files
welcome-to-codevalet-as-a-c…/scripts/doctor.sh
FanaticPythoner (Nathan Trudeau) e27c8a2bd6 Rewrite onboarding prose to a neutral voice
2026-04-26 12:26:19 -04:00

222 lines
8.3 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Verify prerequisites for every recipe. For each missing item, prints the
# EXACT command to install it on the detected platform. Exits non-zero if
# any hard requirement is missing; warns but continues for PATH hygiene
# and the web-browser check (headless systems can still use login-headless).
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
# shellcheck disable=SC1091
. "$here/common.sh"
# ---------------------------------------------------------------------------
# Platform detection
# ---------------------------------------------------------------------------
uname_s="$(uname -s 2>/dev/null || echo unknown)"
pm="" # one of: apt dnf pacman zypper brew ""
case "$uname_s" in
Linux)
if [ -r /etc/os-release ]; then
# shellcheck disable=SC1091
. /etc/os-release
id_all=" ${ID:-} ${ID_LIKE:-} "
case "$id_all" in
*' debian '*|*' ubuntu '*) pm=apt ;;
*' fedora '*|*' rhel '*|*' centos '*) pm=dnf ;;
*' arch '*) pm=pacman ;;
*' opensuse '*|*' suse '*) pm=zypper ;;
esac
fi
if [ -z "$pm" ]; then
for candidate in apt-get dnf pacman zypper; do
if command -v "$candidate" >/dev/null 2>&1; then
case "$candidate" in
apt-get) pm=apt ;;
dnf) pm=dnf ;;
pacman) pm=pacman ;;
zypper) pm=zypper ;;
esac
break
fi
done
fi
;;
Darwin)
pm=brew
;;
esac
# ---------------------------------------------------------------------------
# Render a package-install command for the current platform.
# pkg_cmd <apt> <dnf> <pacman> <brew> <upstream-url>
# ---------------------------------------------------------------------------
pkg_cmd() {
local apt_pkg="$1" dnf_pkg="$2" pac_pkg="$3" brew_pkg="$4" url="$5"
case "$pm" in
apt) echo "sudo apt-get update && sudo apt-get install -y $apt_pkg" ;;
dnf) echo "sudo dnf install -y $dnf_pkg" ;;
pacman) echo "sudo pacman -S --noconfirm $pac_pkg" ;;
zypper) echo "sudo zypper install -y $dnf_pkg" ;;
brew) echo "brew install $brew_pkg" ;;
*) echo "# install manually from: $url" ;;
esac
}
# ---------------------------------------------------------------------------
# Accumulators
# ---------------------------------------------------------------------------
missing=0
fix_commands=()
record_miss() {
local name="$1" detail="$2" fix="$3"
printf ' %s %-12s -> %s\n' "$(_fc_tag "$_FC_RED" miss)" "$name" "$detail"
printf ' fix: %s\n' "$fix"
fix_commands+=("$fix")
missing=$((missing + 1))
}
check_cmd() {
local cmd="$1" fix="$2"
if command -v "$cmd" >/dev/null 2>&1; then
printf ' %s %-12s -> %s\n' "$(_fc_tag "$_FC_GREEN" ok)" "$cmd" "$(command -v "$cmd")"
else
record_miss "$cmd" "not found on PATH" "$fix"
fi
}
info 'checking prerequisites...'
# ---------------------------------------------------------------------------
# Required tools
# ---------------------------------------------------------------------------
check_cmd git "$(pkg_cmd git git git git https://git-scm.com/)"
check_cmd bash "$(pkg_cmd bash bash bash bash https://www.gnu.org/software/bash/)"
check_cmd curl "$(pkg_cmd curl curl curl curl https://curl.se/)"
check_cmd install "$(pkg_cmd coreutils coreutils coreutils coreutils https://www.gnu.org/software/coreutils/)"
check_cmd python3 "$(pkg_cmd python3 python3 python python3 https://www.python.org/)"
# just and uv do not ship in most package managers, so prefer the upstream
# installers (no sudo, land binaries in ~/.local/bin / ~/.cargo/bin).
check_cmd just 'curl --proto "=https" --tlsv1.2 -LsSf https://just.systems/install.sh | bash -s -- --to "$HOME/.local/bin"'
check_cmd uv 'curl -LsSf https://astral.sh/uv/install.sh | sh'
# ---------------------------------------------------------------------------
# Web browser (warning only: headless is supported via login-headless)
# ---------------------------------------------------------------------------
if command -v xdg-open >/dev/null 2>&1 || command -v open >/dev/null 2>&1; then
printf ' %s %-12s -> (detected)\n' "$(_fc_tag "$_FC_GREEN" ok)" 'web browser'
else
printf ' %s %-12s -> no xdg-open/open on PATH.\n' "$(_fc_tag "$_FC_YELLOW" warn)" 'web browser'
printf ' headless mode: just login-headless\n'
case "$pm" in
apt) printf ' otherwise install: sudo apt-get install -y xdg-utils\n' ;;
dnf) printf ' otherwise install: sudo dnf install -y xdg-utils\n' ;;
pacman) printf ' otherwise install: sudo pacman -S --noconfirm xdg-utils\n' ;;
esac
fi
# ---------------------------------------------------------------------------
# Python 3.11+ : accepted via system python3 OR uv-managed interpreter
# ---------------------------------------------------------------------------
py_ok=0
py_via=""
if command -v python3 >/dev/null 2>&1; then
if python3 - <<'PY' >/dev/null 2>&1
import sys
sys.exit(0 if sys.version_info >= (3, 11) else 1)
PY
then
py_ok=1
py_via="$(python3 -V 2>&1)"
fi
fi
if [ "$py_ok" -eq 0 ] && command -v uv >/dev/null 2>&1; then
if uv_py_path="$(uv python find '>=3.11' 2>/dev/null)" && [ -n "$uv_py_path" ]; then
py_ok=1
py_via="$uv_py_path (via uv)"
fi
fi
if [ "$py_ok" -eq 1 ]; then
printf ' %s %-12s -> %s\n' "$(_fc_tag "$_FC_GREEN" ok)" 'python>=3.11' "$py_via"
else
detail='python3 missing'
if command -v python3 >/dev/null 2>&1; then
detail="python3 is $(python3 -V 2>&1 | awk '{print $2}'); need 3.11+"
fi
# Primary fix: uv-managed Python. Works on every platform, no sudo, and
# the orchestrator (a uv project) will resolve it automatically via
# `uv run` / `uv sync`.
py_fix='uv python install 3.11'
# Per-distro alternative (commented so users can copy one or the other):
case "$pm" in
apt)
py_alt='sudo add-apt-repository -y ppa:deadsnakes/ppa && sudo apt-get update && sudo apt-get install -y python3.11 python3.11-venv'
;;
dnf)
py_alt='sudo dnf install -y python3.11'
;;
pacman)
py_alt='sudo pacman -S --noconfirm python'
;;
zypper)
py_alt='sudo zypper install -y python311'
;;
brew)
py_alt='brew install python@3.11'
;;
*)
py_alt=''
;;
esac
printf ' %s %-12s -> %s\n' "$(_fc_tag "$_FC_RED" miss)" 'python>=3.11' "$detail"
printf ' fix: %s\n' "$py_fix"
if [ -n "$py_alt" ]; then
printf ' alt: %s\n' "$py_alt"
fi
fix_commands+=("$py_fix")
missing=$((missing + 1))
fi
# ---------------------------------------------------------------------------
# PATH hygiene: warning, not a hard miss.
# ---------------------------------------------------------------------------
case ":$PATH:" in
*":$LOCAL_BIN:"*)
printf ' %s %-12s -> %s is on PATH\n' "$(_fc_tag "$_FC_GREEN" ok)" 'PATH' "$LOCAL_BIN"
;;
*)
rc='~/.bashrc'
[ -n "${ZSH_VERSION:-}" ] && rc='~/.zshrc'
printf ' %s %-12s -> %s is NOT on PATH.\n' "$(_fc_tag "$_FC_YELLOW" warn)" 'PATH' "$LOCAL_BIN"
# shellcheck disable=SC2016
printf " fix: echo 'export PATH=\"%s:\$PATH\"' >> %s && exec \$SHELL -l\n" "$LOCAL_BIN" "$rc"
;;
esac
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
if [ "$missing" -gt 0 ]; then
printf '\n'
warn "$missing prerequisite(s) missing. Run the following to fix them:"
printf '\n'
# De-duplicate while preserving order.
seen=''
for c in "${fix_commands[@]}"; do
case "$seen" in
*"<<$c>>"*) continue ;;
esac
seen="$seen<<$c>>"
printf ' %s\n' "$c"
done
printf '\n'
warn 're-run: just doctor'
exit 1
fi
info 'all prerequisites present'