#!/usr/bin/env bash set -euo pipefail # --------------------------------------------------------------------------- # Server CLI Tools — interactive installer # Clone this repo to any Ubuntu/Debian server and run: ./install.sh # --------------------------------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONFIGS="$SCRIPT_DIR/configs" BACKUP_DIR="$HOME/.server_cli_tools_bkp/$(date +%Y%m%d-%H%M%S)" # SSH from Ghostty and other clients forwards TERM values (e.g. xterm-ghostty) # that remote servers often lack in terminfo — breaks clear/tput and colors. if ! infocmp "${TERM:-}" &>/dev/null; then for _term_fallback in xterm-256color xterm screen dumb; do if infocmp "$_term_fallback" &>/dev/null; then export TERM="$_term_fallback" break fi done unset _term_fallback fi # Colors (matching the dotfiles convention) e_err() { echo -e "\e[1;31m$1\e[0m"; } e_info() { echo -e "\e[1;34m$1\e[0m"; } e_succ() { echo -e "\e[1;32m$1\e[0m"; } e_warn() { echo -e "\e[1;38;5;208m$1\e[0m"; } INSTALLED=() SKIPPED=() # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- backup_file() { local src="$1" if [ -e "$src" ]; then local rel="${src#$HOME/}" local dest="$BACKUP_DIR/$rel" mkdir -p "$(dirname "$dest")" cp -a "$src" "$dest" e_info " Backed up $src" fi } copy_config() { local src="$1" dest="$2" backup_file "$dest" mkdir -p "$(dirname "$dest")" cp -a "$src" "$dest" } copy_dir() { local src="$1" dest="$2" if [ -d "$dest" ]; then backup_file "$dest" fi mkdir -p "$dest" cp -a "$src"/. "$dest"/ } command_exists() { command -v "$1" >/dev/null 2>&1 } safe_clear() { clear 2>/dev/null || printf '\033[H\033[2J' } check_os() { if [ ! -f /etc/os-release ]; then e_err "Cannot detect OS. This script supports Ubuntu/Debian only." exit 1 fi . /etc/os-release case "$ID" in ubuntu|debian) ;; *) e_warn "Detected $ID — this script is designed for Ubuntu/Debian." e_warn "Package installation may not work. Configs will still be copied." ;; esac } apt_install() { local packages=("$@") local to_install=() for pkg in "${packages[@]}"; do if dpkg -s "$pkg" &>/dev/null; then e_info " $pkg is already installed" else to_install+=("$pkg") fi done if [ ${#to_install[@]} -gt 0 ]; then e_info " Installing: ${to_install[*]}" sudo apt-get install -y "${to_install[@]}" fi } # --------------------------------------------------------------------------- # Component installers # --------------------------------------------------------------------------- install_core_packages() { e_info "\n[1/6] Core packages" sudo apt-get update -qq apt_install curl vim bat fzf htop tmux zsh ripgrep fd-find zoxide # bat is installed as 'batcat' on Debian/Ubuntu if command_exists batcat && ! command_exists bat; then sudo ln -sf /usr/bin/batcat /usr/local/bin/bat e_info " Symlinked batcat -> bat" fi # eza — not in all distro repos, try apt first, fall back to cargo/binary if ! command_exists eza; then if apt-cache show eza &>/dev/null; then apt_install eza else e_warn " eza not in apt repos. Install manually: https://github.com/eza-community/eza" fi else e_info " eza is already installed" fi # git-delta if ! command_exists delta; then if apt-cache show git-delta &>/dev/null; then apt_install git-delta else e_warn " git-delta not in apt repos. Install from: https://github.com/dandavison/delta/releases" fi else e_info " delta is already installed" fi INSTALLED+=("core-packages") } install_zsh() { e_info "\n[2/6] Zsh + Oh-My-Zsh" if ! command_exists zsh; then apt_install zsh fi # Oh-My-Zsh if [ ! -d "$HOME/.oh-my-zsh" ]; then e_info " Installing Oh-My-Zsh..." RUNZSH=no CHSH=no sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended else e_info " Oh-My-Zsh already installed" fi # Plugins local zsh_custom="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}" if [ ! -d "$zsh_custom/plugins/zsh-autosuggestions" ]; then e_info " Cloning zsh-autosuggestions..." git clone --quiet https://github.com/zsh-users/zsh-autosuggestions "$zsh_custom/plugins/zsh-autosuggestions" else e_info " zsh-autosuggestions already installed" fi if [ ! -d "$zsh_custom/plugins/zsh-syntax-highlighting" ]; then e_info " Cloning zsh-syntax-highlighting..." git clone --quiet https://github.com/zsh-users/zsh-syntax-highlighting.git "$zsh_custom/plugins/zsh-syntax-highlighting" else e_info " zsh-syntax-highlighting already installed" fi # Theme mkdir -p "$HOME/.config/zsh" copy_config "$CONFIGS/zsh/zhann-elvis.zsh-theme" "$HOME/.config/zsh/zhann-elvis.zsh-theme" e_info " Installed zhann-elvis.zsh-theme" # .zshrc copy_config "$CONFIGS/zshrc" "$HOME/.zshrc" e_info " Installed .zshrc" # Change default shell to zsh (if not already) if [ "$(basename "$SHELL")" != "zsh" ]; then e_info " Changing default shell to zsh..." if command_exists chsh; then chsh -s "$(which zsh)" || e_warn " chsh failed — change shell manually: chsh -s \$(which zsh)" fi fi INSTALLED+=("zsh") } install_aliases() { e_info "\n[3/6] Aliases + custom tools" copy_dir "$CONFIGS/aliases" "$HOME/.config/aliases" chmod +x "$HOME/.config/aliases"/.aliases "$HOME/.config/aliases"/.functions.sh 2>/dev/null || true e_info " Installed aliases to ~/.config/aliases/" copy_dir "$CONFIGS/custom_tools" "$HOME/.config/custom_tools" chmod +x "$HOME/.config/custom_tools"/*.sh 2>/dev/null || true e_info " Installed custom tools to ~/.config/custom_tools/" INSTALLED+=("aliases" "custom-tools") } install_terminal_configs() { e_info "\n[4/6] Terminal configs (bat)" mkdir -p "$HOME/.config/bat" copy_config "$CONFIGS/bat/config" "$HOME/.config/bat/config" e_info " Installed bat config (theme: base16)" INSTALLED+=("terminal-configs") } install_docker() { e_info "\n[5/6] Docker" sudo apt-get update -qq apt_install docker.io if command_exists systemctl; then sudo systemctl enable --now docker 2>/dev/null \ || e_warn " Could not enable docker service — start manually: sudo systemctl start docker" fi if id -nG "$USER" 2>/dev/null | grep -qw docker; then e_info " $USER is already in the docker group" else e_info " Adding $USER to the docker group..." sudo usermod -aG docker "$USER" e_warn " Log out and back in (or run: newgrp docker) for group membership to apply" fi mkdir -p "$HOME/.config/aliases" copy_config "$CONFIGS/aliases/.docker_aliases" "$HOME/.config/aliases/.docker_aliases" e_info " Installed Docker aliases" INSTALLED+=("docker") } install_git_config() { e_info "\n[6/6] Git config (delta pager)" if ! command_exists delta; then e_warn " git-delta not installed — config will be ready when delta is available" fi local gitconfig="$HOME/.gitconfig" # Only inject delta config if not already present if [ -f "$gitconfig" ] && grep -q '\[delta\]' "$gitconfig" 2>/dev/null; then e_info " Delta config already present in ~/.gitconfig" else e_info " Adding delta pager config to ~/.gitconfig" cat >> "$gitconfig" << 'DELTA' [core] pager = delta [interactive] diffFilter = delta --color-only [delta] side-by-side = true navigate = true dark = true [merge] conflictStyle = zdiff3 DELTA fi INSTALLED+=("git-config") } # --------------------------------------------------------------------------- # Uninstall # --------------------------------------------------------------------------- uninstall() { e_warn "This will remove configs installed by Server CLI Tools." e_warn "Installed packages (apt) will NOT be removed." echo "" printf "Continue? [y/N] " read -r confirm case "$confirm" in y|Y) ;; *) echo "Aborted."; exit 0 ;; esac local removed=() # Aliases + custom tools if [ -d "$HOME/.config/aliases" ]; then rm -rf "$HOME/.config/aliases" removed+=("aliases") fi if [ -d "$HOME/.config/custom_tools" ]; then rm -rf "$HOME/.config/custom_tools" removed+=("custom-tools") fi # Terminal configs if [ -f "$HOME/.config/bat/config" ]; then rm -f "$HOME/.config/bat/config" removed+=("bat-config") fi # Zsh theme + zshrc if [ -f "$HOME/.config/zsh/zhann-elvis.zsh-theme" ]; then rm -f "$HOME/.config/zsh/zhann-elvis.zsh-theme" removed+=("zsh-theme") fi if [ -f "$HOME/.zshrc" ]; then rm -f "$HOME/.zshrc" removed+=("zshrc") fi # Oh-My-Zsh plugins (only the ones we installed) local zsh_custom="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}" if [ -d "$zsh_custom/plugins/zsh-autosuggestions" ]; then rm -rf "$zsh_custom/plugins/zsh-autosuggestions" removed+=("zsh-autosuggestions") fi if [ -d "$zsh_custom/plugins/zsh-syntax-highlighting" ]; then rm -rf "$zsh_custom/plugins/zsh-syntax-highlighting" removed+=("zsh-syntax-highlighting") fi # Git config — strip delta block local gitconfig="$HOME/.gitconfig" if [ -f "$gitconfig" ] && grep -q '\[delta\]' "$gitconfig" 2>/dev/null; then local tmp tmp=$(mktemp) awk ' /^\[core\]/ { if (skip==0) { peek=1; hold=$0; next } } peek==1 { if ($0 ~ /pager *= *delta/) { skip=1; next } else { print hold; peek=0 } } /^\[interactive\]/ && skip { next } /^\[delta\]/ { skip=1; next } /^\[merge\]/ && skip { skip_merge=1; next } skip_merge==1 { skip_merge=0; skip=0; next } /^\[/ { skip=0 } skip { next } { print } ' "$gitconfig" > "$tmp" mv "$tmp" "$gitconfig" removed+=("git-delta-config") fi echo "" if [ ${#removed[@]} -gt 0 ]; then e_succ "Removed:" for item in "${removed[@]}"; do echo " - $item" done else e_info "Nothing to remove." fi # Offer to restore backups local bkp_base="$HOME/.server_cli_tools_bkp" if [ -d "$bkp_base" ]; then echo "" local latest latest=$(ls -1t "$bkp_base" 2>/dev/null | head -n1) if [ -n "$latest" ]; then e_info "Backup found: $bkp_base/$latest" printf "Restore from this backup? [y/N] " read -r restore case "$restore" in y|Y) cp -a "$bkp_base/$latest"/. "$HOME"/ e_succ "Restored from $bkp_base/$latest" ;; *) e_info "Skipping restore." ;; esac fi fi echo "" } # --------------------------------------------------------------------------- # Menu # --------------------------------------------------------------------------- COMPONENTS=( "Core packages (vim, bat, eza, fzf, htop, tmux, rg, fd, zoxide, delta)" "Zsh + Oh-My-Zsh (shell, plugins, theme)" "Aliases + custom tools" "Terminal configs (bat)" "Docker (engine + aliases)" "Git config (delta pager)" ) INSTALLERS=( install_core_packages install_zsh install_aliases install_terminal_configs install_docker install_git_config ) show_menu() { local count=${#COMPONENTS[@]} local selected=() for ((i = 0; i < count; i++)); do selected+=(1) done while true; do safe_clear echo "" e_info "Server CLI Tools — Installer" echo "=============================" echo "" echo "Select components (enter number to toggle, a=all, n=none):" echo "" for ((i = 0; i < count; i++)); do local mark=" " if [ "${selected[$i]}" -eq 1 ]; then mark=" x " fi printf " [%s] %d) %s\n" "$mark" "$((i + 1))" "${COMPONENTS[$i]}" done echo "" echo " [a] Select all [n] Select none [Enter] Install [q] Quit" echo "" printf " > " read -r choice case "$choice" in q|Q) echo "Aborted." exit 0 ;; a|A) for ((i = 0; i < count; i++)); do selected[$i]=1 done ;; n|N) for ((i = 0; i < count; i++)); do selected[$i]=0 done ;; "") # Run selected installers local any=0 for ((i = 0; i < count; i++)); do if [ "${selected[$i]}" -eq 1 ]; then any=1 fi done if [ "$any" -eq 0 ]; then e_warn "Nothing selected!" sleep 1 continue fi echo "" for ((i = 0; i < count; i++)); do if [ "${selected[$i]}" -eq 1 ]; then ${INSTALLERS[$i]} else SKIPPED+=("${COMPONENTS[$i]}") fi done return ;; *) if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "$count" ]; then local idx=$((choice - 1)) if [ "${selected[$idx]}" -eq 1 ]; then selected[$idx]=0 else selected[$idx]=1 fi fi ;; esac done } # --------------------------------------------------------------------------- # Summary # --------------------------------------------------------------------------- print_summary() { echo "" echo "===========================================" e_succ " Installation complete!" echo "===========================================" if [ ${#INSTALLED[@]} -gt 0 ]; then echo "" e_succ "Installed:" for item in "${INSTALLED[@]}"; do echo " + $item" done fi if [ ${#SKIPPED[@]} -gt 0 ]; then echo "" e_warn "Skipped:" for item in "${SKIPPED[@]}"; do echo " - $item" done fi if [ -d "$BACKUP_DIR" ]; then echo "" e_info "Backups saved to: $BACKUP_DIR" fi echo "" e_info "To apply changes, start a new shell or run:" echo " exec zsh" echo "" e_info "Type 'h' to see all available aliases." echo "" } # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- usage() { echo "Usage: $0 [OPTIONS]" echo "" echo "Options:" echo " --all Install all components (non-interactive)" echo " --update Pull latest changes and re-run installer" echo " --update --all Pull latest and install all (non-interactive)" echo " --uninstall Remove installed configs (keeps packages)" echo " --help Show this help message" echo "" echo "Without options, an interactive menu is shown." } main() { case "${1:-}" in --help|-h) usage exit 0 ;; --uninstall) uninstall exit 0 ;; --update) e_info "Pulling latest changes..." git -C "$SCRIPT_DIR" pull || { e_err "git pull failed"; exit 1; } echo "" check_os if [ "${2:-}" = "--all" ] || [ "${2:-}" = "-a" ]; then e_info "Server CLI Tools — installing all components" for installer in "${INSTALLERS[@]}"; do $installer done else show_menu fi print_summary ;; --all|-a) check_os e_info "Server CLI Tools — installing all components" for installer in "${INSTALLERS[@]}"; do $installer done print_summary ;; "") check_os show_menu print_summary ;; *) e_err "Unknown option: $1" usage exit 1 ;; esac } main "$@"