From 825633d62341d900d8bc346834d805a8797f1245 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Thu, 9 Apr 2026 13:59:30 +0800 Subject: [PATCH] feat: robust installer with preflight checks, one-command get.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit install.sh: - set -eo pipefail + trap ERR with line number and debug hint - Network connectivity check before any downloads - curl and git preflight: auto-install or die with clear instructions - vim auto-install attempt before dying - sudo availability check (one-time auth, graceful skip when unavailable) - macOS: offer to install Homebrew if missing - Node.js: offer nvm install if missing (with fallback to vim-lsp) - Python3: offer install if missing - ask() reads from /dev/tty — interactive prompts work under curl | bash - safe_download(): verifies file non-empty and not an HTML error page - pkg_install(): unified cross-platform helper (brew/apt/pacman/dnf) - arch_github()/arch_linux_x64(): normalize uname -m (handles aarch64) - Temp files cleaned up via EXIT trap - Symlink creation verified after ln -sf - vim-plug: fallback to git clone if curl fails; verify file non-empty - vim +PlugInstall: warn on failure instead of silent continue - Binary downloads (hadolint, marksman) use named temp files get.sh (new): - One-command bootstrap: curl | bash - Installs git if missing - Clones repo to ~/.vim (or git pull if already present) - exec bash install.sh &2; exit 1; } +step() { echo -e "\n${BOLD}==> $1${NC}"; } + +echo -e "${BOLD}chopsticks — One-command installer${NC}" +echo "----------------------------------" +echo " Repo: $REPO" +echo " Dest: $DEST" + +# ── git ─────────────────────────────────────────────────────────────────────── +step "Checking for git" + +if ! command -v git >/dev/null 2>&1; then + warn "git not found — attempting to install" + if command -v apt-get >/dev/null 2>&1; then sudo apt-get install -y git >/dev/null 2>&1 + elif command -v pacman >/dev/null 2>&1; then sudo pacman -S --noconfirm git >/dev/null 2>&1 + elif command -v dnf >/dev/null 2>&1; then sudo dnf install -y git >/dev/null 2>&1 + elif command -v brew >/dev/null 2>&1; then brew install git >/dev/null 2>&1 + else die "git is required. Install it manually then re-run."; fi + command -v git >/dev/null 2>&1 || die "git install failed. Try: sudo apt install git" +fi +ok "git $(git --version | awk '{print $3}')" + +# ── Clone or update ─────────────────────────────────────────────────────────── +step "Setting up $DEST" + +if [[ -d "$DEST/.git" ]]; then + warn "$DEST already exists — pulling latest changes" + git -C "$DEST" pull --ff-only origin main 2>/dev/null || \ + warn "Could not pull latest — using existing version (run: git -C ~/.vim pull)" + ok "Repository updated" +elif [[ -d "$DEST" ]]; then + die "$HOME/.vim exists but is not a chopsticks git repo. + Back it up first: mv ~/.vim ~/.vim.bak + Then re-run: curl -fsSL https://raw.githubusercontent.com/m1ngsama/chopsticks/main/get.sh | bash" +else + git clone --depth=1 "$REPO" "$DEST" || \ + die "Clone failed — check your network connection" + ok "Cloned to $DEST" +fi + +# ── Run installer ───────────────────────────────────────────────────────────── +step "Running installer" + +cd "$DEST" + +# exec replaces this process with install.sh and reconnects stdin to /dev/tty +# so interactive prompts work correctly even when this script was piped from curl +if [[ -e /dev/tty ]]; then + exec bash install.sh "$@" &2; exit 1; } +die() { echo -e "${RED}[FATAL]${NC} $1" >&2 + echo " Retry with: ./install.sh 2>&1 | tee /tmp/chopsticks-install.log" >&2 + echo " Report issues: https://github.com/m1ngsama/chopsticks/issues" >&2 + exit 1; } step() { echo -e "\n${BOLD}==> $1${NC}"; } +info() { echo " $1"; } # Track results for summary INSTALLED=() @@ -30,52 +35,89 @@ SKIPPED=() FAILED=() # Ask yes/no; returns 0 for yes +# Reads from /dev/tty so interactive prompts work even under: curl | bash ask() { [[ $AUTO_YES -eq 1 ]] && return 0 - read -r -p "$1 [y/N] " reply + if [[ -t 0 ]]; then + read -r -p "$1 [y/N] " reply + elif [[ -e /dev/tty ]]; then + read -r -p "$1 [y/N] " reply -try_install() { - local name="$1"; local check="$2"; shift 2 - if command -v "$check" >/dev/null 2>&1; then - ok "$name (already installed: $(command -v "$check"))" - return 0 +# ── Error trap ──────────────────────────────────────────────────────────────── +on_error() { + local line="${BASH_LINENO[0]}" + echo -e "\n${RED}[FATAL]${NC} Unexpected error at line $line." >&2 + echo " To get a full debug log:" >&2 + echo " ./install.sh 2>&1 | tee /tmp/chopsticks-install.log" >&2 + echo " Report issues: https://github.com/m1ngsama/chopsticks/issues" >&2 +} +trap on_error ERR + +# Cleanup temp files on exit +trap 'rm -f /tmp/chopsticks-hadolint /tmp/chopsticks-marksman 2>/dev/null' EXIT + +# ── Safe download helper ────────────────────────────────────────────────────── +# safe_download +# Returns 1 if download fails or file is empty / HTML error page +safe_download() { + local url="$1" dest="$2" + curl -fsSL --connect-timeout 15 --retry 3 "$url" -o "$dest" 2>/dev/null || return 1 + # Reject empty files + [[ -s "$dest" ]] || { rm -f "$dest"; return 1; } + # Reject HTML error pages (GitHub 404, rate limits, etc.) + if head -c 200 "$dest" 2>/dev/null | grep -qi "/dev/null 2>&1; then - ok "$name" - INSTALLED+=("$name") - else - fail "$name — install failed (run manually: $*)" - FAILED+=("$name") + return 0 +} + +# ── Cross-platform package install helper ───────────────────────────────────── +# pkg_install (pass "" to skip that pkg manager) +pkg_install() { + local brew_pkg="${1:-}" apt_pkg="${2:-}" pac_pkg="${3:-}" dnf_pkg="${4:-}" + if [[ $HAS_BREW -eq 1 && -n "$brew_pkg" ]]; then brew install "$brew_pkg" >/dev/null 2>&1 + elif [[ $HAS_APT -eq 1 && -n "$apt_pkg" && $HAS_SUDO -eq 1 ]]; then sudo apt-get install -y "$apt_pkg" >/dev/null 2>&1 + elif [[ $HAS_PACMAN -eq 1 && -n "$pac_pkg" && $HAS_SUDO -eq 1 ]]; then sudo pacman -S --noconfirm "$pac_pkg" >/dev/null 2>&1 + elif [[ $HAS_DNF -eq 1 && -n "$dnf_pkg" && $HAS_SUDO -eq 1 ]]; then sudo dnf install -y "$dnf_pkg" >/dev/null 2>&1 + else return 1 fi } +# ── CPU architecture normalizer ─────────────────────────────────────────────── +# Normalize uname -m to the naming convention used by GitHub releases +arch_github() { + case "$(uname -m)" in + x86_64) echo "x86_64" ;; + aarch64|arm64) echo "arm64" ;; + armv7l) echo "armv7" ;; + *) echo "$(uname -m)" ;; + esac +} +arch_linux_x64() { + # Returns x64 or arm64 style (used by marksman) + case "$(uname -m)" in + x86_64) echo "x64" ;; + aarch64|arm64) echo "arm64" ;; + *) echo "$(uname -m)" ;; + esac +} + echo -e "${BOLD}chopsticks — Vim Configuration Installer${NC}" echo "----------------------------------------" # ============================================================================ -# Preflight +# 1. OS + Package Manager Detection # ============================================================================ -step "Checking environment" +step "Detecting environment" -[ -f "$SCRIPT_DIR/.vimrc" ] || die ".vimrc not found in $SCRIPT_DIR" - -command -v vim >/dev/null 2>&1 || die "vim not found. - Ubuntu/Debian: sudo apt install vim - Fedora: sudo dnf install vim - macOS: brew install vim" - -VIM_VERSION=$(vim --version | head -n1) -ok "Found $VIM_VERSION" - -vim --version | grep -q 'Vi IMproved 8\|Vi IMproved 9' || \ - warn "Vim 8.0+ recommended for full LSP support." - -# Detect OS OS="unknown" if [[ "$OSTYPE" == darwin* ]]; then OS="macos" @@ -88,89 +130,258 @@ elif [[ -f /etc/arch-release ]]; then fi ok "OS: $OS" -# Detect package managers -HAS_BREW=0; command -v brew >/dev/null 2>&1 && HAS_BREW=1 -HAS_APT=0; command -v apt >/dev/null 2>&1 && HAS_APT=1 -HAS_DNF=0; command -v dnf >/dev/null 2>&1 && HAS_DNF=1 +HAS_BREW=0; command -v brew >/dev/null 2>&1 && HAS_BREW=1 +HAS_APT=0; command -v apt >/dev/null 2>&1 && HAS_APT=1 +HAS_DNF=0; command -v dnf >/dev/null 2>&1 && HAS_DNF=1 HAS_PACMAN=0; command -v pacman >/dev/null 2>&1 && HAS_PACMAN=1 -HAS_NODE=0; command -v node >/dev/null 2>&1 && HAS_NODE=1 && ok "Node.js $(node --version) detected" -HAS_PYTHON=0; command -v python3 >/dev/null 2>&1 && HAS_PYTHON=1 -HAS_PIP=0; command -v pip3 >/dev/null 2>&1 && HAS_PIP=1 -HAS_GO=0; command -v go >/dev/null 2>&1 && HAS_GO=1 && ok "Go $(go version | awk '{print $3}') detected" -# Bootstrap pip3 when python3 exists but pip3 is absent (common on Ubuntu minimal images) +# ── sudo ───────────────────────────────────────────────────────────────────── +HAS_SUDO=0 +if [[ $OS == "macos" ]]; then + # brew handles its own privilege escalation; no sudo needed for system tools + HAS_SUDO=1 +elif sudo -n true 2>/dev/null; then + HAS_SUDO=1 + ok "sudo: available (passwordless)" +elif [[ $AUTO_YES -eq 1 ]]; then + warn "sudo requires a password but running non-interactively (--yes)" + warn "System package installations will be skipped" +else + # Prompt once for password now so later sudo calls don't interrupt flow + warn "Some steps require sudo. Authenticating now..." + if sudo true; then + HAS_SUDO=1 + ok "sudo: authenticated" + else + warn "sudo not available — system package installations will be skipped" + fi +fi + +# ── Network ────────────────────────────────────────────────────────────────── +if curl -fsSL --connect-timeout 5 https://github.com -o /dev/null 2>/dev/null; then + ok "Network: github.com reachable" +else + warn "Network: cannot reach github.com — plugin and binary downloads may fail" + warn "Check your internet connection or proxy settings before continuing" +fi + +# ── Homebrew (macOS) ───────────────────────────────────────────────────────── +if [[ $OS == "macos" && $HAS_BREW -eq 0 ]]; then + warn "Homebrew not found — it is the recommended package manager for macOS" + if ask "Install Homebrew now? (strongly recommended — required for system tools)"; then + info "This may take a few minutes and will prompt for your password..." + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || \ + die "Homebrew installation failed. Install manually: https://brew.sh" + # Source brew for Apple Silicon and Intel paths + for brew_path in /opt/homebrew/bin/brew /usr/local/bin/brew; do + [[ -x "$brew_path" ]] && eval "$("$brew_path" shellenv)" && break + done + command -v brew >/dev/null 2>&1 && HAS_BREW=1 && ok "Homebrew installed" + else + warn "Homebrew skipped — system tools (ripgrep, fzf, etc.) will be unavailable" + fi +fi + +# ── curl ───────────────────────────────────────────────────────────────────── +if ! command -v curl >/dev/null 2>&1; then + warn "curl not found — required to download plugins and tools" + if pkg_install curl curl curl curl 2>/dev/null; then + ok "curl installed" + else + die "curl is required but could not be installed automatically. + Ubuntu/Debian: sudo apt install curl + Arch: sudo pacman -S curl + Fedora: sudo dnf install curl + macOS: brew install curl" + fi +fi + +# ── git ────────────────────────────────────────────────────────────────────── +if ! command -v git >/dev/null 2>&1; then + warn "git not found — required for vim-plug to install plugins" + if pkg_install git git git git 2>/dev/null; then + ok "git installed" + else + die "git is required but could not be installed automatically. + Ubuntu/Debian: sudo apt install git + Arch: sudo pacman -S git + Fedora: sudo dnf install git + macOS: brew install git (or: xcode-select --install)" + fi +fi + +# ── vim ────────────────────────────────────────────────────────────────────── +[ -f "$SCRIPT_DIR/.vimrc" ] || die ".vimrc not found in $SCRIPT_DIR — is this the chopsticks repo?" + +if ! command -v vim >/dev/null 2>&1; then + warn "vim not found — attempting to install" + if pkg_install vim vim vim vim 2>/dev/null; then + ok "vim installed" + else + die "vim not found and could not be installed automatically. + Ubuntu/Debian: sudo apt install vim + Arch: sudo pacman -S vim + Fedora: sudo dnf install vim + macOS: brew install vim" + fi +fi + +VIM_VERSION=$(vim --version | head -n1) +ok "Found: $VIM_VERSION" + +vim --version | grep -q 'Vi IMproved 8\|Vi IMproved 9' || \ + warn "Vim 8.0+ recommended for full async/LSP support — some features may not work" + +# ── Node.js ────────────────────────────────────────────────────────────────── +HAS_NODE=0; command -v node >/dev/null 2>&1 && HAS_NODE=1 + +if [[ $HAS_NODE -eq 0 ]]; then + warn "Node.js not found — CoC LSP and npm-based formatters will be unavailable" + info "Without Node.js, the config falls back to vim-lsp (pure VimScript)." + info "" + info "Install options:" + info " nvm (recommended): curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/HEAD/install.sh | bash" + info " macOS: brew install node" + info " Ubuntu/Debian: curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -" + info " sudo apt-get install -y nodejs" + info " Arch: sudo pacman -S nodejs npm" + info "" + if ask "Install Node.js via nvm now? (recommended — manages multiple Node versions)"; then + info "Fetching latest nvm release..." + NVM_VER=$(curl -fsSL https://api.github.com/repos/nvm-sh/nvm/releases/latest \ + | grep '"tag_name"' | cut -d'"' -f4 2>/dev/null) || NVM_VER="v0.40.1" + [[ -z "$NVM_VER" ]] && NVM_VER="v0.40.1" + info "Installing nvm $NVM_VER + Node.js LTS..." + if curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VER}/install.sh" | bash >/dev/null 2>&1; then + export NVM_DIR="$HOME/.nvm" + # shellcheck disable=SC1091 + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + if command -v nvm >/dev/null 2>&1; then + nvm install --lts >/dev/null 2>&1 && nvm use --lts >/dev/null 2>&1 || true + command -v node >/dev/null 2>&1 && HAS_NODE=1 && ok "Node.js $(node --version) installed via nvm" + fi + fi + if [[ $HAS_NODE -eq 0 ]]; then + warn "nvm install failed — CoC and npm tools will be skipped" + warn "After manually installing Node.js, re-run: ./install.sh" + fi + else + skip "Node.js — config will use vim-lsp fallback (no Node.js required)" + fi +else + ok "Node.js $(node --version) found" +fi + +# ── Python3 ────────────────────────────────────────────────────────────────── +HAS_PYTHON=0; command -v python3 >/dev/null 2>&1 && HAS_PYTHON=1 +HAS_PIP=0; command -v pip3 >/dev/null 2>&1 && HAS_PIP=1 + +if [[ $HAS_PYTHON -eq 0 ]]; then + warn "python3 not found — Python formatters/linters will be unavailable" + if ask "Install Python 3?"; then + if pkg_install python3 python3 python3 python3 2>/dev/null; then + command -v python3 >/dev/null 2>&1 && HAS_PYTHON=1 && ok "Python3 installed" + else + warn "Python3 install failed — Python tools will be skipped" + fi + else + skip "Python3" + fi +fi + +# Bootstrap pip3 when python3 exists but pip3 is absent (common on Ubuntu minimal) if [[ $HAS_PYTHON -eq 1 && $HAS_PIP -eq 0 ]]; then warn "python3 found but pip3 missing — attempting bootstrap" if python3 -m ensurepip --upgrade >/dev/null 2>&1 || \ - (command -v apt-get >/dev/null 2>&1 && sudo apt-get install -y python3-pip >/dev/null 2>&1) || \ - (command -v pacman >/dev/null 2>&1 && sudo pacman -S --noconfirm python-pip >/dev/null 2>&1) || \ - (command -v dnf >/dev/null 2>&1 && sudo dnf install -y python3-pip >/dev/null 2>&1); then + pkg_install python3-pip python3-pip python-pip python3-pip >/dev/null 2>&1; then command -v pip3 >/dev/null 2>&1 && HAS_PIP=1 && ok "pip3 bootstrapped" else warn "pip3 bootstrap failed — Python tools will be skipped" fi fi -[[ $HAS_PIP -eq 1 ]] && ok "Python/pip3 detected" -[[ $HAS_NODE -eq 0 ]] && warn "Node.js not found — JS/TS/Markdown npm tools will be skipped" -[[ $HAS_PIP -eq 0 ]] && warn "pip3 not found — Python tools will be skipped" -[[ $HAS_GO -eq 0 ]] && warn "Go not found — Go tools will be skipped" +[[ $HAS_PIP -eq 1 ]] && ok "Python/pip3 found" +[[ $HAS_PYTHON -eq 1 && $HAS_PIP -eq 0 ]] && warn "pip3 not available — Python tools will be skipped" + +# ── Go ─────────────────────────────────────────────────────────────────────── +HAS_GO=0; command -v go >/dev/null 2>&1 && HAS_GO=1 +[[ $HAS_GO -eq 1 ]] && ok "Go $(go version | awk '{print $3}') found" +[[ $HAS_GO -eq 0 ]] && warn "Go not found — Go tools will be skipped (see https://go.dev/dl/)" # ============================================================================ -# Symlink +# 2. Symlinks # ============================================================================ -step "Setting up ~/.vimrc symlink" +step "Setting up symlinks" if [ -f "$HOME/.vimrc" ] && [ ! -L "$HOME/.vimrc" ]; then TS=$(date +%Y%m%d_%H%M%S) - warn "Backing up existing ~/.vimrc to ~/.vimrc.backup.$TS" + warn "Backing up existing ~/.vimrc → ~/.vimrc.backup.$TS" mv "$HOME/.vimrc" "$HOME/.vimrc.backup.$TS" fi - ln -sf "$SCRIPT_DIR/.vimrc" "$HOME/.vimrc" -ok "~/.vimrc -> $SCRIPT_DIR/.vimrc" +# Verify symlink +[[ -L "$HOME/.vimrc" ]] && ok "~/.vimrc → $SCRIPT_DIR/.vimrc" || die "Failed to create ~/.vimrc symlink" -# CoC settings (marksman markdown LSP + format-on-save config) mkdir -p "$HOME/.vim" COC_CFG="$HOME/.vim/coc-settings.json" if [ -f "$COC_CFG" ] && [ ! -L "$COC_CFG" ]; then TS=$(date +%Y%m%d_%H%M%S) - warn "Backing up existing coc-settings.json to ~/.vim/coc-settings.json.backup.$TS" + warn "Backing up existing coc-settings.json → ~/.vim/coc-settings.json.backup.$TS" mv "$COC_CFG" "$COC_CFG.backup.$TS" fi ln -sf "$SCRIPT_DIR/coc-settings.json" "$COC_CFG" -ok "~/.vim/coc-settings.json -> $SCRIPT_DIR/coc-settings.json" +[[ -L "$COC_CFG" ]] && ok "~/.vim/coc-settings.json → $SCRIPT_DIR/coc-settings.json" || warn "coc-settings.json symlink failed (non-fatal)" # ============================================================================ -# vim-plug + plugins +# 3. vim-plug + Plugins # ============================================================================ step "Installing vim-plug" VIM_PLUG="$HOME/.vim/autoload/plug.vim" if [ ! -f "$VIM_PLUG" ]; then - curl -fLo "$VIM_PLUG" --create-dirs \ - https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim - ok "vim-plug installed" + mkdir -p "$HOME/.vim/autoload" + if safe_download "https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim" "$VIM_PLUG"; then + ok "vim-plug downloaded" + else + # Fallback: git clone + warn "curl download failed — trying git clone fallback" + if git clone --depth=1 https://github.com/junegunn/vim-plug.git /tmp/vim-plug-src 2>/dev/null; then + cp /tmp/vim-plug-src/plug.vim "$VIM_PLUG" && rm -rf /tmp/vim-plug-src + ok "vim-plug installed (via git)" + else + die "vim-plug installation failed. Check your network connection and try again." + fi + fi + [[ -s "$VIM_PLUG" ]] || die "vim-plug file is empty after download — aborting" else ok "vim-plug already present" fi step "Installing Vim plugins" +info "(Vim will open fullscreen to install plugins — screen may go dark for 10-30s, this is normal)" # /dev/null 2>&1; then ok "$name (already installed)" return @@ -180,92 +391,110 @@ if ask "Install system tools (ripgrep, fzf, ctags, shellcheck, hadolint, marksma if eval "$cmd" >/dev/null 2>&1; then installed=1; break; fi done if [[ $installed -eq 1 ]]; then - ok "$name" - INSTALLED+=("$name") + ok "$name"; INSTALLED+=("$name") else - fail "$name — could not install automatically" + fail "$name — could not install automatically (install manually)" FAILED+=("$name") fi } - if [[ $OS == macos ]]; then - command -v brew >/dev/null 2>&1 || { warn "brew not found — skipping system tools"; } - install_sys "ripgrep" rg "brew install ripgrep" - install_sys "fzf" fzf "brew install fzf" - install_sys "universal-ctags" ctags "brew install universal-ctags" - install_sys "shellcheck" shellcheck "brew install shellcheck" - install_sys "hadolint" hadolint "brew install hadolint" - install_sys "marksman" marksman "brew install marksman" + if [[ $OS == "macos" ]]; then + install_sys "ripgrep" rg "brew install ripgrep" + install_sys "fzf" fzf "brew install fzf" + install_sys "universal-ctags" ctags "brew install universal-ctags" + install_sys "shellcheck" shellcheck "brew install shellcheck" + install_sys "hadolint" hadolint "brew install hadolint" + install_sys "marksman" marksman "brew install marksman" + elif [[ $HAS_APT -eq 1 ]]; then - sudo apt-get update -qq - install_sys "ripgrep" rg "sudo apt-get install -y ripgrep" - install_sys "fzf" fzf "sudo apt-get install -y fzf" - install_sys "universal-ctags" ctags "sudo apt-get install -y universal-ctags" - install_sys "shellcheck" shellcheck "sudo apt-get install -y shellcheck" - # hadolint: no apt package, download binary - if ! command -v hadolint >/dev/null 2>&1; then - ARCH=$(uname -m) - [[ "$ARCH" == "x86_64" ]] && HARCH="x86_64" || HARCH="arm64" - HVER=$(curl -s https://api.github.com/repos/hadolint/hadolint/releases/latest \ - | grep '"tag_name"' | cut -d'"' -f4) - if [[ -n "$HVER" ]]; then - curl -fsSL "https://github.com/hadolint/hadolint/releases/download/${HVER}/hadolint-Linux-${HARCH}" \ - -o /tmp/hadolint && chmod +x /tmp/hadolint && sudo mv /tmp/hadolint /usr/local/bin/hadolint - ok "hadolint" - INSTALLED+=("hadolint") - else - warn "hadolint: could not detect latest release, install manually" - SKIPPED+=("hadolint") - fi + if [[ $HAS_SUDO -eq 0 ]]; then + warn "No sudo — skipping apt system tools" + SKIPPED+=("ripgrep" "fzf" "ctags" "shellcheck" "hadolint" "marksman") else - ok "hadolint (already installed)" - fi - # marksman: no apt package, download binary - if ! command -v marksman >/dev/null 2>&1; then - ARCH=$(uname -m) - [[ "$ARCH" == "x86_64" ]] && MARCH="x64" || MARCH="arm64" - MVER=$(curl -s https://api.github.com/repos/artempyanykh/marksman/releases/latest \ - | grep '"tag_name"' | cut -d'"' -f4) - if [[ -n "$MVER" ]]; then - curl -fsSL "https://github.com/artempyanykh/marksman/releases/download/${MVER}/marksman-linux-${MARCH}" \ - -o /tmp/marksman && chmod +x /tmp/marksman && sudo mv /tmp/marksman /usr/local/bin/marksman - ok "marksman" - INSTALLED+=("marksman") + sudo apt-get update -qq + install_sys "ripgrep" rg "sudo apt-get install -y ripgrep" + install_sys "fzf" fzf "sudo apt-get install -y fzf" + install_sys "universal-ctags" ctags "sudo apt-get install -y universal-ctags" + install_sys "shellcheck" shellcheck "sudo apt-get install -y shellcheck" + + # hadolint: no apt package — download binary from GitHub releases + if command -v hadolint >/dev/null 2>&1; then + ok "hadolint (already installed)" else - warn "marksman: could not detect latest release, install manually" - SKIPPED+=("marksman") + HARCH=$(arch_github) + HVER=$(curl -fsSL https://api.github.com/repos/hadolint/hadolint/releases/latest \ + | grep '"tag_name"' | cut -d'"' -f4 2>/dev/null) || HVER="" + if [[ -n "$HVER" ]] && safe_download \ + "https://github.com/hadolint/hadolint/releases/download/${HVER}/hadolint-Linux-${HARCH}" \ + /tmp/chopsticks-hadolint; then + chmod +x /tmp/chopsticks-hadolint && sudo mv /tmp/chopsticks-hadolint /usr/local/bin/hadolint + ok "hadolint"; INSTALLED+=("hadolint") + else + fail "hadolint — download failed (install manually: https://github.com/hadolint/hadolint/releases)" + FAILED+=("hadolint") + fi + fi + + # marksman: no apt package — download binary from GitHub releases + if command -v marksman >/dev/null 2>&1; then + ok "marksman (already installed)" + else + MARCH=$(arch_linux_x64) + MVER=$(curl -fsSL https://api.github.com/repos/artempyanykh/marksman/releases/latest \ + | grep '"tag_name"' | cut -d'"' -f4 2>/dev/null) || MVER="" + if [[ -n "$MVER" ]] && safe_download \ + "https://github.com/artempyanykh/marksman/releases/download/${MVER}/marksman-linux-${MARCH}" \ + /tmp/chopsticks-marksman; then + chmod +x /tmp/chopsticks-marksman && sudo mv /tmp/chopsticks-marksman /usr/local/bin/marksman + ok "marksman"; INSTALLED+=("marksman") + else + fail "marksman — download failed (install manually: https://github.com/artempyanykh/marksman/releases)" + FAILED+=("marksman") + fi fi - else - ok "marksman (already installed)" fi + elif [[ $HAS_PACMAN -eq 1 ]]; then - install_sys "ripgrep" rg "sudo pacman -S --noconfirm ripgrep" - install_sys "fzf" fzf "sudo pacman -S --noconfirm fzf" - install_sys "universal-ctags" ctags "sudo pacman -S --noconfirm ctags" - install_sys "shellcheck" shellcheck "sudo pacman -S --noconfirm shellcheck" - install_sys "hadolint" hadolint "sudo pacman -S --noconfirm hadolint" - install_sys "marksman" marksman "sudo pacman -S --noconfirm marksman" + if [[ $HAS_SUDO -eq 0 ]]; then + warn "No sudo — skipping pacman system tools" + SKIPPED+=("ripgrep" "fzf" "ctags" "shellcheck" "hadolint" "marksman") + else + install_sys "ripgrep" rg "sudo pacman -S --noconfirm ripgrep" + install_sys "fzf" fzf "sudo pacman -S --noconfirm fzf" + install_sys "universal-ctags" ctags "sudo pacman -S --noconfirm ctags" + install_sys "shellcheck" shellcheck "sudo pacman -S --noconfirm shellcheck" + install_sys "hadolint" hadolint "sudo pacman -S --noconfirm hadolint" + install_sys "marksman" marksman "sudo pacman -S --noconfirm marksman" + fi + elif [[ $HAS_DNF -eq 1 ]]; then - install_sys "ripgrep" rg "sudo dnf install -y ripgrep" - install_sys "fzf" fzf "sudo dnf install -y fzf" - install_sys "shellcheck" shellcheck "sudo dnf install -y ShellCheck" - skip "universal-ctags — install manually: sudo dnf install ctags" - SKIPPED+=("ctags") - skip "hadolint — install manually from https://github.com/hadolint/hadolint/releases" - SKIPPED+=("hadolint") - skip "marksman — install manually from https://github.com/artempyanykh/marksman/releases" - SKIPPED+=("marksman") + if [[ $HAS_SUDO -eq 0 ]]; then + warn "No sudo — skipping dnf system tools" + SKIPPED+=("ripgrep" "fzf" "shellcheck" "ctags" "hadolint" "marksman") + else + install_sys "ripgrep" rg "sudo dnf install -y ripgrep" + install_sys "fzf" fzf "sudo dnf install -y fzf" + install_sys "shellcheck" shellcheck "sudo dnf install -y ShellCheck" + skip "universal-ctags — install manually: sudo dnf install ctags" + SKIPPED+=("ctags") + skip "hadolint — install manually: https://github.com/hadolint/hadolint/releases" + SKIPPED+=("hadolint") + skip "marksman — install manually: https://github.com/artempyanykh/marksman/releases" + SKIPPED+=("marksman") + fi + else - warn "Unknown Linux distro — skipping system tools (install manually)" + warn "Unknown distro — skipping system tools (install manually)" SKIPPED+=("ripgrep" "fzf" "ctags" "shellcheck" "hadolint" "marksman") fi + else skip "system tools" SKIPPED+=("ripgrep" "fzf" "ctags" "shellcheck" "hadolint" "marksman") fi # ============================================================================ -# npm tools (prettier, markdownlint-cli, stylelint, eslint, typescript) +# 5. npm tools # ============================================================================ step "npm tools (formatters + linters)" @@ -275,15 +504,12 @@ if [[ $HAS_NODE -eq 1 ]]; then npm_install() { local pkg="$1"; local check="${2:-$1}" if command -v "$check" >/dev/null 2>&1; then - ok "$pkg (already installed)" - return + ok "$pkg (already installed)"; return fi if npm install -g "$pkg" >/dev/null 2>&1; then - ok "$pkg" - INSTALLED+=("$pkg") + ok "$pkg"; INSTALLED+=("$pkg") else - fail "$pkg" - FAILED+=("$pkg") + fail "$pkg"; FAILED+=("$pkg") fi } npm_install prettier @@ -302,7 +528,7 @@ else fi # ============================================================================ -# pip tools (black, isort, flake8, pylint, sqlfluff) +# 6. Python tools # ============================================================================ step "Python tools (formatters + linters)" @@ -312,16 +538,13 @@ if [[ $HAS_PIP -eq 1 ]]; then pip_install() { local pkg="$1"; local check="${2:-$1}" if command -v "$check" >/dev/null 2>&1; then - ok "$pkg (already installed)" - return + ok "$pkg (already installed)"; return fi if pip3 install --quiet "$pkg" 2>/dev/null || \ pip3 install --quiet --break-system-packages "$pkg" 2>/dev/null; then - ok "$pkg" - INSTALLED+=("$pkg") + ok "$pkg"; INSTALLED+=("$pkg") else - fail "$pkg" - FAILED+=("$pkg") + fail "$pkg"; FAILED+=("$pkg") fi } pip_install black @@ -340,62 +563,54 @@ else fi # ============================================================================ -# Go tools (gopls, goimports, staticcheck) +# 7. Go tools # ============================================================================ step "Go tools" if [[ $HAS_GO -eq 1 ]]; then if ask "Install Go tools (gopls, goimports, staticcheck)?"; then - # Go installs binaries to $(go env GOPATH)/bin — add to PATH for this session GOBIN="$(go env GOPATH)/bin" export PATH="$PATH:$GOBIN" go_install() { - local name="$1"; local pkg="$2"; local check="$3" + local name="$1" pkg="$2" check="$3" if command -v "$check" >/dev/null 2>&1 || [[ -x "$GOBIN/$check" ]]; then - ok "$name (already installed)" - return + ok "$name (already installed)"; return fi if go install "$pkg" >/dev/null 2>&1; then - ok "$name" - INSTALLED+=("$name") + ok "$name"; INSTALLED+=("$name") else - fail "$name" - FAILED+=("$name") + fail "$name"; FAILED+=("$name") fi } - go_install gopls "golang.org/x/tools/gopls@latest" gopls - go_install goimports "golang.org/x/tools/cmd/goimports@latest" goimports - go_install staticcheck "honnef.co/go/tools/cmd/staticcheck@latest" staticcheck + go_install gopls "golang.org/x/tools/gopls@latest" gopls + go_install goimports "golang.org/x/tools/cmd/goimports@latest" goimports + go_install staticcheck "honnef.co/go/tools/cmd/staticcheck@latest" staticcheck - # Remind user to add GOPATH/bin to their shell profile - if ! echo "$PATH" | grep -q "$GOBIN"; then + echo "$PATH" | grep -q "$GOBIN" || \ warn "Add Go binaries to PATH: export PATH=\"\$PATH:$GOBIN\"" - fi else skip "Go tools" SKIPPED+=("gopls" "goimports" "staticcheck") fi else - skip "Go tools (go not installed)" + skip "Go tools (go not installed — see https://go.dev/dl/)" SKIPPED+=("gopls" "goimports" "staticcheck") fi # ============================================================================ -# tmux: vim-tmux-navigator integration +# 8. tmux: vim-tmux-navigator integration # ============================================================================ step "tmux: vim-tmux-navigator integration" if command -v tmux >/dev/null 2>&1; then TMUX_CONF="$HOME/.tmux.conf" - # Check if already configured if grep -q 'vim-tmux-navigator' "$TMUX_CONF" 2>/dev/null; then ok "vim-tmux-navigator bindings already present in ~/.tmux.conf" - else - if ask "Append vim-tmux-navigator bindings to ~/.tmux.conf (enables seamless Ctrl+h/j/k/l across vim and tmux)?"; then - cat >> "$TMUX_CONF" << 'TMUXEOF' + elif ask "Append vim-tmux-navigator bindings to ~/.tmux.conf (enables seamless Ctrl+h/j/k/l across vim and tmux)?"; then + cat >> "$TMUX_CONF" << 'TMUXEOF' # vim-tmux-navigator: seamless Ctrl+h/j/k/l navigation between vim and tmux is_vim="ps -o state= -o comm= -t '#{pane_tty}' | grep -iqE '^[^TXZ ]+ +(\S+\/)?g?(view|n?vim?x?)(diff)?$'" @@ -404,15 +619,14 @@ bind-key -n 'C-j' if-shell "$is_vim" 'send-keys C-j' 'select-pane -D' bind-key -n 'C-k' if-shell "$is_vim" 'send-keys C-k' 'select-pane -U' bind-key -n 'C-l' if-shell "$is_vim" 'send-keys C-l' 'select-pane -R' TMUXEOF - ok "vim-tmux-navigator bindings appended to ~/.tmux.conf" - warn "Reload tmux config now: tmux source-file ~/.tmux.conf" - warn "Note: C-l now navigates panes instead of clearing the screen." - warn " To restore clear: add 'bind C-l send-keys C-l' to ~/.tmux.conf" - INSTALLED+=("tmux-navigator-config") - else - skip "tmux navigator config" - SKIPPED+=("tmux-navigator-config") - fi + ok "vim-tmux-navigator bindings appended to ~/.tmux.conf" + warn "Reload tmux config now: tmux source-file ~/.tmux.conf" + warn "Note: C-l now navigates panes instead of clearing the screen." + warn " To restore clear: add 'bind C-l send-keys C-l' to ~/.tmux.conf" + INSTALLED+=("tmux-navigator-config") + else + skip "tmux navigator config" + SKIPPED+=("tmux-navigator-config") fi else skip "tmux not found — skipping navigator config" @@ -420,22 +634,23 @@ else fi # ============================================================================ -# CoC language server extensions +# 9. CoC language server extensions # ============================================================================ step "CoC language server extensions" if [[ $HAS_NODE -eq 1 ]]; then if ask "Install CoC language servers (LSP for all configured languages)?"; then + info "(Downloading CoC extensions via npm — screen may go dark for 1-3 minutes, this is normal)" # Note: coc-marksman doesn't exist on npm — markdown LSP is handled via coc-settings.json vim +'CocInstall -sync coc-json coc-tsserver coc-pyright coc-sh coc-html coc-css coc-yaml coc-go coc-rust-analyzer coc-sql' +qall inside Vim" + info "Install later with :CocInstall inside Vim" fi else - warn "Node.js not found — using vim-lsp fallback (run :LspInstallServer inside Vim)" + warn "Node.js not found — using vim-lsp fallback (run :LspInstallServer inside Vim for each language)" fi # ============================================================================ @@ -458,6 +673,8 @@ fi if [[ ${#FAILED[@]} -gt 0 ]]; then echo -e "\n${RED}Failed (install manually):${NC}" for t in "${FAILED[@]}"; do echo " ! $t"; done + echo "" + echo " To debug failures: ./install.sh 2>&1 | tee /tmp/chopsticks-install.log" fi echo ""