Replace the Docker-aliases-only component with a full install_docker step: docker.io via apt, systemd enable, docker group membership, and aliases. Remove yazi from core packages, terminal configs, aliases, and the repo. Harden install.sh for SSH clients (e.g. Ghostty) that forward unknown TERM values: fall back through xterm-256color, xterm, screen, dumb, and use safe_clear in the interactive menu. Document Ghostty/SSH terminal issues in README, including bash vs zsh ssh() wrappers and verification steps.
596 lines
17 KiB
Bash
Executable File
596 lines
17 KiB
Bash
Executable File
#!/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 "$@"
|