#!/bin/sh # install.sh - bootstrap dotfiles on a fresh Debian or Alpine host. # # What it does: # 1. Detects the OS and package manager (apt on Debian, apk on Alpine). # 2. Installs base tooling (git, curl, wget, zsh, vim) + CLI tools # (htop, fzf, nslookup, nfs client, kubectl when available) for both distros. # 3. Installs oh-my-zsh (unattended) and the zsh-autosuggestions plugin. # 4. Deploys the generic server/ dotfiles. # 5. Installs Vim plugins (clones each plugin declared in .vimrc). # # Usage: # ./install.sh # full bootstrap # ./install.sh --no-zsh # skip oh-my-zsh / shell setup # ./install.sh --chsh # change the current user's login shell to zsh # ./install.sh --dry-run # show what would happen, change nothing # # Run this from inside the cloned dotfiles repo (the dir that contains servers/). # Safe to re-run: every step is idempotent. set -eu # ---------------------------------------------------------------------------- # Helpers # ---------------------------------------------------------------------------- DRY_RUN=0 INSTALL_ZSH=1 CHSH=0 info() { printf '\033[0;34m==>\033[0m %s\n' "$1"; } ok() { printf '\033[0;32m✔\033[0m %s\n' "$1"; } warn() { printf '\033[0;33m!\033[0m %s\n' "$1"; } die() { printf '\033[0;31m✘\033[0m %s\n' "$1" >&2; exit 1; } run() { # Print the command, then run it (unless --dry-run). # We capture the exit status explicitly so that a failing command does NOT # trip the caller's 'set -e' inside 'if run ... ; then' constructs. if [ "$DRY_RUN" -eq 1 ]; then printf ' $ %s\n' "$*" return 0 fi "$@" return $? } usage() { sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//' exit 0 } # ---------------------------------------------------------------------------- # Argument parsing # ---------------------------------------------------------------------------- while [ $# -gt 0 ]; do case "$1" in --no-zsh) INSTALL_ZSH=0; shift ;; --chsh) CHSH=1; shift ;; --dry-run) DRY_RUN=1; shift ;; -h|--help) usage ;; *) die "Unknown argument: $1 (try --help)" ;; esac done # ---------------------------------------------------------------------------- # Locate the repo root (dir that holds this script and servers/) # ---------------------------------------------------------------------------- SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) if [ ! -d "$SCRIPT_DIR/servers" ]; then die "servers/ not found next to this script. Clone the dotfiles repo first, then run install.sh from its root: git clone git@git.ivanch.me:ivanch/dotfiles.git cd dotfiles && ./install.sh" fi # ---------------------------------------------------------------------------- # OS / package-manager detection # ---------------------------------------------------------------------------- detect_os() { OS_RELEASE_FILE="${OS_RELEASE_FILE:-/etc/os-release}" if [ -f "$OS_RELEASE_FILE" ]; then . "$OS_RELEASE_FILE" OS_ID="$ID" else OS_ID="$(uname -s)" fi case "$OS_ID" in alpine) PKG="apk"; MGR_UPDATE="apk update"; MGR_ADD="apk add --no-cache" ;; debian|ubuntu|raspbian) PKG="apt"; MGR_UPDATE="apt-get update"; MGR_ADD="apt-get install -y" ;; *) die "Unsupported OS: $OS_ID (only Debian/Ubuntu and Alpine are supported)" ;; esac info "Detected OS: $OS_ID (package manager: $PKG)" } # ---------------------------------------------------------------------------- # Package installation # ---------------------------------------------------------------------------- install_packages() { info "Installing base packages" # Always-present on both Debian and Alpine main/community repos. ESSENTIAL="git curl wget zsh vim" # Cross-distro CLI tools. Package names differ between distros: # nslookup -> dnsutils (Debian) / bind-tools (Alpine) # fzf -> community repo on Alpine, main on Debian # htop -> main on both # nfs client-> nfs-common (Debian) / nfs-utils (Alpine) [for NFS mounts] if [ "$PKG" = "apk" ]; then CLI_TOOLS="htop fzf bind-tools nfs-utils" else CLI_TOOLS="htop fzf dnsutils nfs-common" fi if [ "$PKG" = "apk" ]; then # On Alpine, vim/fzf/kubectl live in the 'community' repo, which is # commented out by default in /etc/apk/repositories. Enable it (idempotent). info "Ensuring Alpine community repository is enabled" run sed -i -E "s|^#(.*/community)$|\1|" /etc/apk/repositories run $MGR_UPDATE else run $MGR_UPDATE fi info "Installing essential packages" if run $MGR_ADD $ESSENTIAL; then ok "Essential packages installed" else die "Failed to install essential packages ($ESSENTIAL)." fi # CLI tools are best-effort: a missing one must not abort the whole # bootstrap (e.g. some tools simply aren't in the default repos). info "Installing CLI tools (best-effort): $CLI_TOOLS" if run $MGR_ADD $CLI_TOOLS; then ok "CLI tools installed" else warn "Some CLI tools failed to install (non-fatal)." fi # kubectl is NOT in the default Debian/Alpine repos (it needs the # Kubernetes apt repo / a manual install). Install it only if the package # is actually available, so the script stays generic and repo-agnostic. if command -v kubectl >/dev/null 2>&1; then ok "kubectl already present, skipping" else info "Attempting to install kubectl (if available in repos)" if run $MGR_ADD kubectl 2>/dev/null; then ok "kubectl installed" else warn "kubectl not available in default repos; skipping (install manually if needed)." fi fi } # ---------------------------------------------------------------------------- # oh-my-zsh + zsh-autosuggestions # ---------------------------------------------------------------------------- install_oh_my_zsh() { info "Installing oh-my-zsh" if [ -d "$HOME/.oh-my-zsh" ]; then ok "oh-my-zsh already present, skipping" else # --unattended => RUNZSH=no CHSH=no (no prompt, no shell change) # Special-cased (not via run()) so --dry-run never hits the network. if [ "$DRY_RUN" -eq 1 ]; then printf ' $ sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" --unattended\n' else sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended fi ok "oh-my-zsh installed" fi ZSH_CUSTOM_DIR="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}" SUG_DIR="$ZSH_CUSTOM_DIR/plugins/zsh-autosuggestions" if [ -d "$SUG_DIR" ]; then ok "zsh-autosuggestions already present, skipping" else info "Installing zsh-autosuggestions plugin" run git clone --depth=1 https://github.com/zsh-users/zsh-autosuggestions "$SUG_DIR" ok "zsh-autosuggestions installed" fi } # ---------------------------------------------------------------------------- # Deploy the dotfiles for this host # ---------------------------------------------------------------------------- deploy_dotfiles() { SRC_DIR="$SCRIPT_DIR/servers" if [ ! -d "$SRC_DIR" ]; then warn "servers/ not found next to this script; skipping dotfile deploy." return 0 fi info "Deploying generic server dotfiles from servers/" # Copy every file/dir from servers/ into $HOME, preserving structure. # .zshrc/.vimrc/.bashrc land at $HOME; .config/* stays nested. for src in "$SRC_DIR"/* "$SRC_DIR"/.[!.]* "$SRC_DIR"/..?*; do [ -e "$src" ] || continue run cp -Rf "$src" "$HOME/" ok "$(basename "$src") -> $HOME/" done } # ---------------------------------------------------------------------------- # Vim plugins via Vundle # ---------------------------------------------------------------------------- install_vim_plugins() { info "Installing Vim plugins" if [ ! -f "$HOME/.vimrc" ]; then warn "No .vimrc deployed; skipping Vim plugin install." return 0 fi VUNDLE_DIR="$HOME/.vim/bundle/Vundle.vim" if [ ! -d "$VUNDLE_DIR" ]; then info "Cloning Vundle" run git clone --depth=1 https://github.com/VundleVim/Vundle.vim.git "$VUNDLE_DIR" else ok "Vundle already present, skipping" fi # Rather than drive Vundle's interactive :PluginInstall (which hangs when # stdout is not a TTY), clone each plugin declared in .vimrc directly. # This is fully unattended and idempotent. info "Cloning Vim plugins declared in .vimrc" # Extract 'Plugin/Repo' and 'Plugin user/repo' style declarations, # normalise to an https URL, skip the Vundle self-reference. plugins=$(grep -oE "Plugin '[^']+'" "$HOME/.vimrc" \ | sed -E "s/Plugin '//; s/'//" \ | grep -v "^VundleVim/Vundle.vim$") for repo in $plugins; do name=$(basename "$repo") dir="$HOME/.vim/bundle/$name" if [ -d "$dir" ]; then ok "$name already present, skipping" continue fi info "Cloning $repo" if run git clone --depth=1 "https://github.com/$repo.git" "$dir"; then ok "$name installed" else warn "Failed to clone $repo (non-fatal)." fi done } # ---------------------------------------------------------------------------- # Optional: change login shell to zsh # ---------------------------------------------------------------------------- change_shell() { if ! command -v zsh >/dev/null 2>&1; then warn "zsh not found; cannot change shell." return 0 fi if ! command -v chsh >/dev/null 2>&1; then warn "chsh not found on this system; set your login shell manually (e.g. in /etc/passwd)." return 0 fi ZSH_BIN="$(command -v zsh)" cur="$(grep "^$(id -un):" /etc/passwd | cut -d: -f7)" if [ "$cur" = "$ZSH_BIN" ]; then ok "Login shell already $ZSH_BIN" return 0 fi info "Changing login shell to $ZSH_BIN" run chsh -s "$ZSH_BIN" ok "Login shell set to zsh (re-login to apply)" } # ---------------------------------------------------------------------------- # Main # ---------------------------------------------------------------------------- info "Bootstrapping dotfiles from $SCRIPT_DIR" [ "$DRY_RUN" -eq 1 ] && warn "DRY RUN - no changes will be made" detect_os install_packages if [ "$INSTALL_ZSH" -eq 1 ]; then install_oh_my_zsh fi deploy_dotfiles install_vim_plugins if [ "$CHSH" -eq 1 ]; then change_shell fi info "Done. Restart your shell (or re-login) to pick up the new configuration."