#!/usr/bin/env bash # catppuccin.sh — Full Catppuccin palette for Bash (truecolour, all flavours) # Usage: # CATPPUCCIN_FLAVOUR=MOCHA ./catppuccin.sh # # or in another script: # source ./catppuccin.sh; info "Kia ora" # dotfiles by GertGerber set -euo pipefail set -o errtrace # Ensure we are running under bash (macOS defaults to zsh) if [ -z "${BASH_VERSION:-}" ]; then printf 'This script must be run with bash.\n' >&2 exit 1 fi # --------------------------------------- # Catppuccin for Bash # --------------------------------------- # ----- Config ----- FLAVOUR="${CATPPUCCIN_FLAVOUR:-LATTE}" # LATTE | FRAPPE | MACCHIATO | MOCHA # ----- Portable helpers ----- uc() { printf '%s' "$1" | tr '[:lower:]' '[:upper:]'; } # Bash 3.2-safe uppercase ensure_path_prefix() { # Prepend path segment only if not already present case ":$PATH:" in *":$1:"*) : ;; *) PATH="$1:$PATH"; export PATH ;; esac } # ----- Colour/ANSI helpers ----- RESET="\033[0m"; BOLD="\033[1m"; DIM="\033[2m"; ITALIC="\033[3m"; UNDERLINE="\033[4m" fg() { printf "\033[38;2;%sm" "$(get_rgb "$1")"; } # fg ROSEWATER bg() { printf "\033[48;2;%sm" "$(get_rgb "$1")"; } # bg BASE off(){ printf "%b" "$RESET"; } # Respect NO_COLOR (https://no-color.org/) if [ -n "${NO_COLOR:-}" ]; then RESET=""; BOLD=""; DIM=""; ITALIC=""; UNDERLINE="" fg(){ :; } bg(){ :; } fi # Coloured logging info() { printf "%b[INFO]%b %s\n" "$(fg BLUE)" "$RESET" "$*"; } warn() { printf "%b[WARN]%b %s\n" "$(fg YELLOW)" "$RESET" "$*"; } error() { printf "%b[ERROR]%b %s\n" "$(fg RED)" "$RESET" "$*"; } ok() { printf "%b[ OK ]%b %s\n" "$(fg GREEN)" "$RESET" "$*"; } # Trap for concise failure diagnostics (line + command + exit) trap 'ec=$?; error "Failed at ${BASH_SOURCE[0]:-stdin}:${LINENO}: ${BASH_COMMAND:-} (exit $ec)"; exit "$ec"' ERR # ----- Palette lookup (RGB; expects uppercase colour names) ----- get_rgb() { local key flavour key="$(uc "$1")" # e.g., rosewater → ROSEWATER flavour="$(uc "$FLAVOUR")" # e.g., mocha → MOCHA case "${flavour}:$key" in # ===================== LATTE ===================== LATTE:ROSEWATER) echo "220;138;120" ;; LATTE:FLAMINGO) echo "221;120;120" ;; LATTE:PINK) echo "234;118;203" ;; LATTE:MAUVE) echo "136;57;239" ;; LATTE:RED) echo "210;15;57" ;; LATTE:MAROON) echo "230;69;83" ;; LATTE:PEACH) echo "254;100;11" ;; LATTE:YELLOW) echo "223;142;29" ;; LATTE:GREEN) echo "64;160;43" ;; LATTE:TEAL) echo "23;146;153" ;; LATTE:SKY) echo "4;165;229" ;; LATTE:SAPPHIRE) echo "32;159;181" ;; LATTE:BLUE) echo "30;102;245" ;; LATTE:LAVENDER) echo "114;135;253" ;; LATTE:TEXT) echo "76;79;105" ;; LATTE:SUBTEXT1) echo "92;95;119" ;; LATTE:SUBTEXT0) echo "108;111;133" ;; LATTE:OVERLAY2) echo "124;127;147" ;; LATTE:OVERLAY1) echo "140;143;161" ;; LATTE:OVERLAY0) echo "156;160;176" ;; LATTE:SURFACE2) echo "172;176;190" ;; LATTE:SURFACE1) echo "188;192;204" ;; LATTE:SURFACE0) echo "204;208;218" ;; LATTE:BASE) echo "239;241;245" ;; LATTE:MANTLE) echo "230;233;239" ;; LATTE:CRUST) echo "220;224;232" ;; # ===================== FRAPPE ==================== FRAPPE:ROSEWATER) echo "242;213;207" ;; FRAPPE:FLAMINGO) echo "238;190;190" ;; FRAPPE:PINK) echo "244;184;228" ;; FRAPPE:MAUVE) echo "202;158;230" ;; FRAPPE:RED) echo "231;130;132" ;; FRAPPE:MAROON) echo "234;153;156" ;; FRAPPE:PEACH) echo "239;159;118" ;; FRAPPE:YELLOW) echo "229;200;144" ;; FRAPPE:GREEN) echo "166;209;137" ;; FRAPPE:TEAL) echo "129;200;190" ;; FRAPPE:SKY) echo "153;209;219" ;; FRAPPE:SAPPHIRE) echo "133;193;220" ;; FRAPPE:BLUE) echo "140;170;238" ;; FRAPPE:LAVENDER) echo "186;187;241" ;; FRAPPE:TEXT) echo "198;208;245" ;; FRAPPE:SUBTEXT1) echo "181;191;226" ;; FRAPPE:SUBTEXT0) echo "165;173;206" ;; FRAPPE:OVERLAY2) echo "148;156;187" ;; FRAPPE:OVERLAY1) echo "131;139;167" ;; FRAPPE:OVERLAY0) echo "115;121;148" ;; FRAPPE:SURFACE2) echo "98;104;128" ;; FRAPPE:SURFACE1) echo "81;87;109" ;; FRAPPE:SURFACE0) echo "65;69;89" ;; FRAPPE:BASE) echo "48;52;70" ;; FRAPPE:MANTLE) echo "41;44;60" ;; FRAPPE:CRUST) echo "35;38;52" ;; # ==================== MACCHIATO ================== MACCHIATO:ROSEWATER) echo "244;219;214" ;; MACCHIATO:FLAMINGO) echo "240;198;198" ;; MACCHIATO:PINK) echo "245;189;230" ;; MACCHIATO:MAUVE) echo "198;160;246" ;; MACCHIATO:RED) echo "237;135;150" ;; MACCHIATO:MAROON) echo "238;153;160" ;; MACCHIATO:PEACH) echo "245;169;127" ;; MACCHIATO:YELLOW) echo "238;212;159" ;; MACCHIATO:GREEN) echo "166;218;149" ;; MACCHIATO:TEAL) echo "139;213;202" ;; MACCHIATO:SKY) echo "145;215;227" ;; MACCHIATO:SAPPHIRE) echo "125;196;228" ;; MACCHIATO:BLUE) echo "138;173;244" ;; MACCHIATO:LAVENDER) echo "183;189;248" ;; MACCHIATO:TEXT) echo "202;211;245" ;; MACCHIATO:SUBTEXT1) echo "184;192;224" ;; MACCHIATO:SUBTEXT0) echo "165;173;203" ;; MACCHIATO:OVERLAY2) echo "147;154;183" ;; MACCHIATO:OVERLAY1) echo "128;135;162" ;; MACCHIATO:OVERLAY0) echo "110;115;141" ;; MACCHIATO:SURFACE2) echo "91;96;120" ;; MACCHIATO:SURFACE1) echo "73;77;100" ;; MACCHIATO:SURFACE0) echo "54;58;79" ;; MACCHIATO:BASE) echo "36;39;58" ;; MACCHIATO:MANTLE) echo "30;32;48" ;; MACCHIATO:CRUST) echo "24;25;38" ;; # ====================== MOCHA ==================== MOCHA:ROSEWATER) echo "245;224;220" ;; MOCHA:FLAMINGO) echo "242;205;205" ;; MOCHA:PINK) echo "245;194;231" ;; MOCHA:MAUVE) echo "203;166;247" ;; MOCHA:RED) echo "243;139;168" ;; MOCHA:MAROON) echo "235;160;172" ;; MOCHA:PEACH) echo "250;179;135" ;; MOCHA:YELLOW) echo "249;226;175" ;; MOCHA:GREEN) echo "166;227;161" ;; MOCHA:TEAL) echo "148;226;213" ;; MOCHA:SKY) echo "137;220;235" ;; MOCHA:SAPPHIRE) echo "116;199;236" ;; MOCHA:BLUE) echo "137;180;250" ;; MOCHA:LAVENDER) echo "180;190;254" ;; MOCHA:TEXT) echo "205;214;244" ;; MOCHA:SUBTEXT1) echo "186;194;222" ;; MOCHA:SUBTEXT0) echo "166;173;200" ;; MOCHA:OVERLAY2) echo "147;153;178" ;; MOCHA:OVERLAY1) echo "127;132;156" ;; MOCHA:OVERLAY0) echo "108;112;134" ;; MOCHA:SURFACE2) echo "88;91;112" ;; MOCHA:SURFACE1) echo "69;71;90" ;; MOCHA:SURFACE0) echo "49;50;68" ;; MOCHA:BASE) echo "30;30;46" ;; MOCHA:MANTLE) echo "24;24;37" ;; MOCHA:CRUST) echo "17;17;27" ;; # ----------------- Unknowns --------------------- * ) echo "128;128;128" ;; # neutral grey if unknown esac } # --------------------------------------- # Print banner # --------------------------------------- print_banner() { printf '\n' printf '%b ██████╗ ███████╗██████╗ ████████╗ ██████╗ ███████╗██████╗ ██████╗ ███████╗██████╗ %b Dotfiles Installer %b\n' "$(fg RED)" "$(fg PEACH)" "$RESET" printf '%b ██╔════╝ ██╔════╝██╔══██╗╚══██╔══╝ ██╔════╝ ██╔════╝██╔══██╗██╔══██╗██╔════╝██╔══██╗%b by GertGerber %b\n' "$(fg RED)" "$(fg BLUE)" "$RESET" printf '%b ██║ ███╗█████╗ ██████╔╝ ██║ ██║ ███╗█████╗ ██████╔╝██████╔╝█████╗ ██████╔╝%b Setup your dev environment %b\n' "$(fg RED)" "$(fg MAUVE)" "$RESET" printf '%b ██║ ██║██╔══╝ ██╔══██╗ ██║ ██║ ██║██╔══╝ ██╔══██╗██╔══██╗██╔══╝ ██╔══██╗%b Enjoy coding! 🚀%b\n' "$(fg RED)" "$(fg YELLOW)" "$RESET" printf '%b ╚██████╔╝███████╗██║ ██║ ██║ ╚██████╔╝███████╗██║ ██║██████╔╝███████╗██║ ██║%b Have fun with roles! 🎉%b\n' "$(fg RED)" "$(fg GREEN)" "$RESET" printf '%b ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝%b\n' "$(fg RED)" "$RESET" printf '\n' printf '%b ██████╗ ██████╗ ████████╗███████╗██╗██╗ ███████╗███████╗%b\n' "$(fg BLUE)" "$RESET" printf '%b ██╔══██╗██╔═══██╗╚══██╔══╝██╔════╝██║██║ ██╔════╝██╔════╝%b\n' "$(fg BLUE)" "$RESET" printf '%b ██║ ██║██║ ██║ ██║ █████╗ ██║██║ █████╗ ███████╗%b\n' "$(fg BLUE)" "$RESET" printf '%b ██║ ██║██║ ██║ ██║ ██╔══╝ ██║██║ ██╔══╝ ╚════██║%b\n' "$(fg BLUE)" "$RESET" printf '%b ██████╔╝╚██████╔╝ ██║ ██║ ██║███████╗███████╗███████║%b\n' "$(fg BLUE)" "$RESET" printf '%b ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝%b\n' "$(fg BLUE)" "$RESET" printf '\n' } print_banner # --------------------------------------- # Detect OS, version, and tag # --------------------------------------- detect_os() { local uname_s name version ps exe out token token="unknown" if command -v uname >/dev/null 2>&1; then uname_s="$(uname -s)" case "$uname_s" in Linux*) if [ -r /etc/os-release ]; then # shellcheck disable=SC1091 . /etc/os-release name="${NAME:-Linux}" version="${VERSION:-$(uname -r)}" dotfiles_os="$name" dotfiles_os_version="$name $version" dotfiles_os_tag="${ID:-linux}-${VERSION_ID:-$(uname -r)}" else dotfiles_os="Linux" dotfiles_os_version="Linux $(uname -r)" dotfiles_os_tag="linux-$(uname -r)" fi # Detect WSL if grep -qi 'microsoft' /proc/sys/kernel/osrelease 2>/dev/null; then dotfiles_os="Windows (WSL)" dotfiles_os_version="Windows (WSL) $(uname -r)" dotfiles_os_tag="windows-wsl-$(uname -r)" token="wsl" else token="linux" fi ;; Darwin*) version="$(sw_vers -productVersion 2>/dev/null || uname -r)" dotfiles_os="macOS" dotfiles_os_version="macOS $version" dotfiles_os_tag="macos-$version" token="macos" ;; FreeBSD*) version="$(uname -r)" dotfiles_os="FreeBSD" dotfiles_os_version="FreeBSD $version" dotfiles_os_tag="freebsd-$version" token="freebsd" ;; CYGWIN*|MINGW*|MSYS*) version="$(uname -r)" dotfiles_os="Windows (POSIX layer)" dotfiles_os_version="Windows (POSIX layer) $version" dotfiles_os_tag="windows-posix-$version" token="windows-posix" ;; *) dotfiles_os="$uname_s" dotfiles_os_version="Unknown Unix-like ($uname_s $(uname -r))" dotfiles_os_tag="unknown-$(uname -r)" token="unknown" ;; esac else # No uname — likely native Windows ps="" for exe in powershell.exe powershell pwsh; do if command -v "$exe" >/dev/null 2>&1; then ps="$exe"; break; fi done if [ -n "$ps" ]; then out="$("$ps" -NoLogo -NoProfile -Command \ "(Get-CimInstance Win32_OperatingSystem | Select-Object -First 1 Caption, Version | ForEach-Object { ""$($_.Caption) $($_.Version)"" })" \ 2>/dev/null || true)" dotfiles_os="Windows" dotfiles_os_version="${out:-Windows}" dotfiles_os_tag="windows-$(printf '%s\n' "$out" | awk '{print $NF}')" token="windows" elif command -v ver >/dev/null 2>&1; then out="$(ver)" dotfiles_os="Windows" dotfiles_os_version="$out" dotfiles_os_tag="windows" token="windows" else dotfiles_os="Unknown" dotfiles_os_version="Unknown" dotfiles_os_tag="unknown" token="unknown" fi fi export dotfiles_os dotfiles_os_version dotfiles_os_tag # Human-friendly log → stderr printf 'Detected OS: %s\n' "$dotfiles_os_version" >&2 # Machine-friendly token → stdout echo "$token" } # --------------------------------------- # Helpers # --------------------------------------- SUDO="" if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then SUDO="sudo" fi has_cmd() { command -v "$1" >/dev/null 2>&1; } ensure_dir() { [ -d "$1" ] || ${SUDO} mkdir -p "$1"; } # Append a line once to a file (idempotent) _append_once() { # Usage: _append_once "" local _file _line _file="$1"; shift _line="$*" ensure_dir "$(dirname -- "$_file")" touch -- "$_file" if ! grep -Fqx -- "$_line" "$_file"; then # add a newline only if file is non-empty and missing a trailing newline if [ -s "$_file" ] && [ "$(tail -c1 "$_file" 2>/dev/null; printf x)" != $'x' ]; then printf '\n' >>"$$_file" fi printf '%s\n' "$_line" >>"$$_file" info "Added to $_file: $_line" else info "Already present in $_file" fi } # The above is still wrong—let's provide the clean, final implementation: unset -f _append_once _append_once() { local _file _line _file="$1"; shift _line="$*" ensure_dir "$(dirname "$_file")" : > /dev/null # keep set -e happy even if no-op touch "$_file" if ! grep -Fqx -- "$_line" "$_file"; then printf '\n%s\n' "$_line" >>"$._file" fi } # --------------------------------------- # pipx completions (current user) # --------------------------------------- enable_pipx_completion() { # Configure shell completions for pipx using argcomplete. # Safe to run multiple times. if ! command -v pipx >/dev/null 2>&1; then warn "pipx not found; skipping completion setup." return 0 fi # Try to ensure register-python-argcomplete is available for the user. if ! command -v register-python-argcomplete >/dev/null 2>&1; then warn "register-python-argcomplete not found; attempting user install of argcomplete…" python3 -m pip install --user argcomplete >/dev/null 2>&1 || true fi local shell rcfile shell="${SHELL##*/}" case "$shell" in bash) rcfile="$HOME/.bashrc" _append_once "$rcfile" 'eval "$(register-python-argcomplete pipx)"' ;; zsh) rcfile="$HOME/.zshrc" _append_once "$rcfile" 'autoload -U bashcompinit' _append_once "$rcfile" 'bashcompinit' _append_once "$rcfile" 'eval "$(register-python-argcomplete pipx)"' ;; fish) # Fish uses a one-time file, not rc eval. local compdir="$HOME/.config/fish/completions" local compfile="$compdir/pipx.fish" mkdir -p "$compdir" if [ ! -s "$compfile" ]; then if command -v register-python-argcomplete >/dev/null 2>&1; then register-python-argcomplete --shell fish pipx >"$compfile" ok "Installed fish completion: $compfile" else warn "register-python-argcomplete not available; cannot write $compfile" fi else info "Fish completion already present: $compfile" fi ;; tcsh|csh) # Prefer .tcshrc if it exists, otherwise fall back to .cshrc if [ -f "$HOME/.tcshrc" ]; then rcfile="$HOME/.tcshrc" else rcfile="$HOME/.cshrc" fi _append_once "$rcfile" 'eval `register-python-argcomplete --shell tcsh pipx`' ;; *) warn "Unsupported or unknown shell '$SHELL' — skipping pipx completion setup." return 0 ;; esac ok "pipx completions configured for ${shell}. Restart your shell to activate." } # --------------------------------------- # Dotfiles sync (clone/update) # --------------------------------------- DOTFILES_REPO="${DOTFILES_REPO:-https://github.com/GertGerber/dotfiles}" DOTFILES_DIR="${DOTFILES_DIR:-$HOME/.dotfiles}" sync_dotfiles() { if ! has_cmd git; then warn "git not found; skipping dotfiles sync." return 0 fi # If target exists but is not a git repo, back it up if [ -e "$DOTFILES_DIR" ] && [ ! -d "$DOTFILES_DIR/.git" ]; then local bak="${DOTFILES_DIR}.bak.$(date +%Y%m%d-%H%M%S)" warn "$DOTFILES_DIR exists but is not a git repo; backing up to $bak" mv -f "$DOTFILES_DIR" "$bak" fi if [ -d "$DOTFILES_DIR/.git" ]; then info "Updating dotfiles in $DOTFILES_DIR" ( cd "$DOTFILES_DIR" # Ensure correct remote if git remote get-url origin >/dev/null 2>&1; then git remote set-url origin "$DOTFILES_REPO" >/dev/null 2>&1 || true else git remote add origin "$DOTFILES_REPO" >/dev/null 2>&1 || true fi git fetch --all --prune # Determine default branch from origin (fallback to main/current) local def_branch def_branch="$(git remote show origin 2>/dev/null | sed -n 's/.*HEAD branch: //p')" [ -n "$def_branch" ] || def_branch="main" # Rebase with autostash to minimise conflicts git checkout "$def_branch" >/dev/null 2>&1 || true git pull --rebase --autostash origin "$def_branch" || { warn "Rebase pull failed; trying a regular pull…" git pull origin "$def_branch" || true } # Submodules (if any) git submodule sync --recursive >/dev/null 2>&1 || true git submodule update --init --recursive >/dev/null 2>&1 || true ) ok "Dotfiles updated." else info "Cloning dotfiles into $DOTFILES_DIR" git clone --depth 1 "$DOTFILES_REPO" "$DOTFILES_DIR" ( cd "$DOTFILES_DIR" git submodule update --init --recursive >/dev/null 2>&1 || true ) ok "Dotfiles cloned." fi } # --------------------------------------- # Ansible Galaxy: install collections # --------------------------------------- install_galaxy_collections() { # Ensure ansible-galaxy is reachable (pipx shim or system) if ! has_cmd ansible-galaxy; then ensure_path_prefix "$HOME/.local/bin" if ! has_cmd ansible-galaxy; then warn "ansible-galaxy not found; skipping Galaxy install." return 0 fi fi # Resolve requirements file safely under 'set -u' local default_req req default_req="${DOTFILES_DIR:-$HOME/.dotfiles}/requirements/ansible.yml" req="${GALAXY_REQUIREMENTS:-$default_req}" if [ ! -f "$req" ]; then # Fallbacks: script directory, then CWD local script_dir script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" if [ -f "$script_dir/requirements/ansible.yml" ]; then req="$script_dir/requirements/ansible.yml" elif [ -f "$PWD/requirements/ansible.yml" ]; then req="$PWD/requirements/ansible.yml" else warn "Galaxy requirements file not found (tried $default_req, script dir, CWD)." return 0 fi fi info "Installing Ansible Galaxy collections from: $req" # Use a user-writable path (matches Ansible's default search path) local target="$HOME/.ansible/collections" mkdir -p "$target" # Optional behaviour toggles local -a extra_flags=() [ "${GALAXY_UPGRADE:-1}" = "1" ] && extra_flags+=(--upgrade) [ "${GALAXY_FORCE:-0}" = "1" ] && extra_flags+=(--force-with-deps) if ansible-galaxy collection install -r "$req" --collections-path "$target" "${extra_flags[@]}"; then ok "Galaxy collections installed to $target" else warn "Install failed; retrying with --force-with-deps…" if ansible-galaxy collection install -r "$req" --collections-path "$target" --force-with-deps "${extra_flags[@]/--force-with-deps}"; then ok "Galaxy collections installed (forced) to $target" else error "Galaxy collection install failed. Check network/proxy and the file at: $req" fi fi info "Collections now visible in Ansible paths; default includes $HOME/.ansible/collections." } # --------------------------------------- # Run Ansible playbook # --------------------------------------- # NOTE: Requires ansible-playbook in PATH (pipx or system) # bin/dotfiles run_ansible() { local play_dir="${DOTFILES_DIR:-$HOME/.dotfiles}/ansible" if command -v ansible-playbook >/dev/null 2>&1 && [ -f "$play_dir/site.yml" ]; then info "Running Ansible playbook…" local ask_become=() if [ "$(id -u)" -ne 0 ]; then ask_become=(-K); fi ansible-playbook -i "$play_dir/inventory/hosts.yml" "$play_dir/site.yml" "${ask_become[@]}" else info "Ansible playbook not found; skipping." fi } # --------------------------------------- # Per-OS installers # --------------------------------------- # NOTE: Avoid upgrading system pip. Use OS packages + pipx for Ansible. setup_macos() { info "macOS: updating system & installing tools" # Command Line Tools (safe if already installed) if ! xcode-select -p >/dev/null 2>&1; then info "Installing Xcode Command Line Tools…" xcode-select --install || true fi # Homebrew if ! has_cmd brew; then warn "Homebrew not found; installing…" /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" # ensure brew in PATH for current session if [ -d /opt/homebrew/bin ]; then eval "$(/opt/homebrew/bin/brew shellenv)"; fi if [ -d /usr/local/bin ]; then eval "$(/usr/local/bin/brew shellenv)"; fi fi brew update brew upgrade || true brew install git gh python pipx pipx ensurepath ensure_path_prefix "$HOME/.local/bin" pipx install --include-deps ansible || pipx upgrade ansible || true enable_pipx_completion } setup_debian_like() { info "Debian/Ubuntu: apt update & base packages" ${SUDO} apt-get update -y ${SUDO} apt-get upgrade -y ${SUDO} apt-get install -y \ git curl ca-certificates gnupg lsb-release software-properties-common \ build-essential python3 python3-pip python3-venv pipx \ libffi-dev libssl-dev # GitHub CLI if ! has_cmd gh; then ${SUDO} apt-get install -y gh || true if ! has_cmd gh; then warn "Enabling official GitHub CLI repo…" curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ | ${SUDO} tee /usr/share/keyrings/githubcli-archive-keyring.gpg >/dev/null ${SUDO} chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ | ${SUDO} tee /etc/apt/sources.list.d/github-cli.list >/dev/null ${SUDO} apt-get update -y && ${SUDO} apt-get install -y gh fi fi pipx ensurepath ensure_path_prefix "$HOME/.local/bin" pipx install --include-deps ansible || pipx upgrade ansible || true enable_pipx_completion } setup_fedora_like() { info "Fedora/RHEL-like: dnf update & base packages" ${SUDO} dnf -y upgrade --refresh || true ${SUDO} dnf -y install git gh python3 python3-pip python3-virtualenv pipx \ gcc gcc-c++ make openssl-devel libffi-devel ${SUDO} python3 -m pip install --upgrade pip pipx ensurepath ensure_path_prefix "$HOME/.local/bin" pipx install --include-deps ansible || pipx upgrade ansible || true enable_pipx_completion } setup_arch() { info "Arch: pacman -Syu & packages" ${SUDO} pacman -Syu --noconfirm ${SUDO} pacman -S --noconfirm git github-cli python python-pip python-virtualenv pipx base-devel libffi openssl python -m pip install --upgrade --user pip || true pipx ensurepath ensure_path_prefix "$HOME/.local/bin" pipx install --include-deps ansible || pipx upgrade ansible || true enable_pipx_completion } setup_opensuse() { info "openSUSE: zypper refresh & packages" ${SUDO} zypper --non-interactive refresh ${SUDO} zypper --non-interactive update || true ${SUDO} zypper --non-interactive install \ git gh python3 python3-pip python3-virtualenv python3-pipx \ gcc-c++ make libopenssl-devel libffi-devel ${SUDO} python3 -m pip install --upgrade pip pipx ensurepath ensure_path_prefix "$HOME/.local/bin" pipx install --include-deps ansible || pipx upgrade ansible || true enable_pipx_completion } setup_freebsd() { info "FreeBSD: pkg update & packages" ${SUDO} pkg update -f || true ${SUDO} pkg upgrade -y || true ${SUDO} pkg install -y git gh python3 py39-virtualenv py39-pip pipx || \ ${SUDO} pkg install -y git gh python311 py311-virtualenv py311-pip pipx || true if has_cmd python3; then ${SUDO} python3 -m pip install --upgrade pip || true fi pipx ensurepath || true ensure_path_prefix "$HOME/.local/bin" pipx install --include-deps ansible || pipx upgrade ansible || true enable_pipx_completion } setup_wsl() { info "WSL detected → using Debian/Ubuntu path by default" setup_debian_like } setup_windows_posix() { warn "Windows POSIX layer (Cygwin/MSYS) detected." warn "Package managers differ; install via your environment’s manager (e.g., pacman for MSYS2)." if has_cmd pacman; then ${SUDO} pacman -Syu --noconfirm ${SUDO} pacman -S --noconfirm git github-cli python python-pip base-devel python -m pip install --upgrade --user pip || true python -m pip install --user pipx || true pipx ensurepath || true ensure_path_prefix "$HOME/.local/bin" pipx install --include-deps ansible || pipx upgrade ansible || true enable_pipx_completion else error "Unsupported POSIX-on-Windows environment. Install git/gh/python/pipx manually, then: pipx install ansible" fi } # --------------------------------------- # Install 'dotfiles' alias or shim # --------------------------------------- install_dotfiles_alias() { local target="$HOME/.dotfiles/bin/dotfiles" if [ ! -x "$target" ]; then warn "dotfiles launcher not found or not executable at $target; skipping alias." return 0 fi case "${SHELL##*/}" in bash) _append_once "$HOME/.bashrc" "alias dotfiles='$target'" ok "Alias 'dotfiles' added to $HOME/.bashrc" ;; zsh) _append_once "$HOME/.zshrc" "alias dotfiles='$target'" ok "Alias 'dotfiles' added to $HOME/.zshrc" ;; fish) # Prefer a function so arguments pass through cleanly local fdir="$HOME/.config/fish/functions" local ffile="$fdir/dotfiles.fish" mkdir -p "$fdir" if [ -f "$ffile" ] && grep -Fq -- "$target" "$ffile"; then info "Fish function already present: $ffile" else printf 'function dotfiles\n %s $argv\nend\n' "$target" >"$ffile" ok "Fish function installed: $ffile" fi ;; tcsh|csh) local rcfile if [ -f "$HOME/.tcshrc" ]; then rcfile="$HOME/.tcshrc"; else rcfile="$HOME/.cshrc"; fi _append_once "$rcfile" "alias dotfiles '$target'" ok "Alias 'dotfiles' added to ${rcfile##*/}" ;; *) # Shell-agnostic fallback: create a shim on PATH ensure_dir "$HOME/.local/bin" ln -sf "$target" "$HOME/.local/bin/dotfiles" # If you have ensure_path_prefix, use it; otherwise export PATH safely if command -v ensure_path_prefix >/dev/null 2>&1; then ensure_path_prefix "$HOME/.local/bin" else case ":$PATH:" in *":$HOME/.local/bin:"*) : ;; *) PATH="$HOME/.local/bin:$PATH"; export PATH ;; esac fi ok "Shim installed at ~/.local/bin/dotfiles" ;; esac } # --------------------------------------- # Dispatcher # --------------------------------------- bootstrap_host() { os="$(detect_os)" case "$os" in macos) setup_macos ;; linux) if [ -r /etc/os-release ]; then # shellcheck disable=SC1091 . /etc/os-release case "${ID_LIKE:-$ID}" in *debian*|debian|ubuntu) setup_debian_like ;; *rhel*|*fedora*|fedora|rhel|centos|rocky|almalinux) setup_fedora_like ;; *suse*|sles|opensuse*) setup_opensuse ;; *arch*|arch) setup_arch ;; *) warn "Unknown Linux family (${ID:-unknown}); defaulting to Debian-like"; setup_debian_like ;; esac else warn "/etc/os-release not found; defaulting to Debian-like" setup_debian_like fi ;; wsl) setup_wsl ;; windows-posix) setup_windows_posix ;; freebsd) setup_freebsd ;; windows) error "Native Windows shell detected. Run this from PowerShell with winget/choco, or install WSL and rerun." return 1 ;; *) error "Unsupported/unknown OS token '$os'. Exiting." return 1 ;; esac info "Verifying installs…" for c in git gh python3 pipx; do if has_cmd "$c"; then info "✔ $c found: $("$c" --version 2>/dev/null | head -n1)" else warn "✖ $c not found in PATH" fi done if has_cmd ansible; then info "✔ ansible found: $(ansible --version 2>/dev/null | head -n1)" else ensure_path_prefix "$HOME/.local/bin" if has_cmd ansible; then info "✔ ansible found (after PATH update): $(ansible --version 2>/dev/null | head -n1)" else error "Ansible not found; check pipx PATH (~/.local/bin) and re-run: pipx install --include-deps ansible" fi fi # Finally, clone/update your dotfiles sync_dotfiles # Install Galaxy collections defined in requirements/ansible.yml install_galaxy_collections # Make 'dotfiles' available as a command/alias install_dotfiles_alias # (if you run Ansible here, keep it after the alias setup) run_ansible } # If this file is sourced by another script, the caller can invoke bootstrap_host explicitly. # If executed directly, run bootstrap_host. if [[ "${BASH_SOURCE[0]:-$0}" == "$0" ]]; then bootstrap_host fi