#!/bin/bash # Package Management Library for Dotfiles # Handles installation and checking of various package types # Global variables PACKAGES_CONFIG="$HOME/.dotfiles/packages.yaml" PACKAGE_LOG="$HOME/.dotfiles/.package.log" # Function to log package operations pkg_log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$PACKAGE_LOG" } # Function to detect OS detect_os() { if [[ "$OSTYPE" == "linux-gnu"* ]]; then if command -v lsb_release >/dev/null 2>&1; then lsb_release -si | tr '[:upper:]' '[:lower:]' elif [[ -f /etc/debian_version ]]; then echo "debian" elif [[ -f /etc/redhat-release ]]; then if grep -q "Fedora" /etc/redhat-release; then echo "fedora" elif grep -q "CentOS" /etc/redhat-release; then echo "centos" else echo "rhel" fi else echo "linux" fi elif [[ "$OSTYPE" == "darwin"* ]]; then echo "macos" else echo "unknown" fi } # Function to parse YAML (basic implementation) parse_yaml() { local yaml_file="$1" local section="$2" if [[ ! -f "$yaml_file" ]]; then pkg_log "ERROR: Package config file not found: $yaml_file" return 1 fi # Extract section from YAML (basic implementation) awk -v section="$section" ' /^[a-zA-Z_][a-zA-Z0-9_]*:/ { current_section = substr($1, 1, length($1)-1) in_section = (current_section == section) next } /^ - name:/ && in_section { name = substr($3, 1) getline; desc = substr($0, match($0, /: "/) + 3, length($0) - match($0, /: "/) - 3) getline; check = substr($0, match($0, /: "/) + 3, length($0) - match($0, /: "/) - 3) print name "|" desc "|" check } ' "$yaml_file" } # Function to check if a package is installed is_package_installed() { local check_command="$1" eval "$check_command" >/dev/null 2>&1 } # Function to install system packages install_system_package() { local package_name="$1" local os=$(detect_os) pkg_log "Installing system package: $package_name for OS: $os" case "$os" in "debian"|"ubuntu") case "$package_name" in "sshuttle") sudo apt update && sudo apt install -y sshuttle ;; "curl") sudo apt update && sudo apt install -y curl ;; "git") sudo apt update && sudo apt install -y git ;; "vim") sudo apt update && sudo apt install -y vim ;; "docker") curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh && rm get-docker.sh ;; "docker-compose") sudo apt update && sudo apt install -y docker-compose ;; *) pkg_log "Unknown system package: $package_name" return 1 ;; esac ;; "fedora") case "$package_name" in "sshuttle"|"curl"|"git"|"vim"|"docker"|"docker-compose") sudo dnf install -y "$package_name" ;; *) pkg_log "Unknown system package: $package_name" return 1 ;; esac ;; "centos"|"rhel") case "$package_name" in "sshuttle"|"curl"|"git"|"vim"|"docker"|"docker-compose") sudo yum install -y "$package_name" ;; *) pkg_log "Unknown system package: $package_name" return 1 ;; esac ;; "macos") if ! command -v brew >/dev/null 2>&1; then pkg_log "Homebrew not installed. Installing..." /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" fi case "$package_name" in "sshuttle"|"curl"|"git"|"vim"|"docker-compose") brew install "$package_name" ;; "docker") brew install --cask docker ;; *) pkg_log "Unknown system package: $package_name" return 1 ;; esac ;; *) pkg_log "Unsupported OS: $os" return 1 ;; esac } # Function to install GitHub packages install_github_package() { local package_name="$1" local repo="$2" local install_method="$3" local install_path="$4" local binary_name="$5" local asset_pattern="$6" local post_install="$7" pkg_log "Installing GitHub package: $package_name from $repo" # Create install directory if it doesn't exist mkdir -p "$install_path" case "$install_method" in "clone_and_install") echo " Cloning repository..." if [[ -d "$install_path" ]]; then rm -rf "$install_path" fi if git clone "https://github.com/$repo.git" "$install_path"; then if [[ -n "$post_install" ]]; then echo " Running post-install script..." cd "$install_path" && eval "$post_install" fi return 0 else pkg_log "Failed to clone repository: $repo" return 1 fi ;; "release_binary") echo " Downloading latest release..." local latest_url=$(curl -s "https://api.github.com/repos/$repo/releases/latest" | grep -o "https://github.com/$repo/releases/download/[^\"]*$asset_pattern[^\"]*") if [[ -n "$latest_url" ]]; then local temp_file="/tmp/${binary_name}_download" if curl -L "$latest_url" -o "$temp_file"; then # Handle different archive types if [[ "$latest_url" == *.tar.gz ]]; then tar -xzf "$temp_file" -C /tmp/ find /tmp -name "$binary_name" -type f -executable | head -1 | xargs -I {} mv {} "$install_path/$binary_name" elif [[ "$latest_url" == *.zip ]]; then unzip -q "$temp_file" -d /tmp/ find /tmp -name "$binary_name" -type f -executable | head -1 | xargs -I {} mv {} "$install_path/$binary_name" else # Assume it's a direct binary mv "$temp_file" "$install_path/$binary_name" fi chmod +x "$install_path/$binary_name" rm -f "$temp_file" # Clean up extracted files find /tmp -name "*$binary_name*" -type d -exec rm -rf {} + 2>/dev/null || true return 0 else pkg_log "Failed to download: $latest_url" return 1 fi else pkg_log "Could not find release for: $repo with pattern: $asset_pattern" return 1 fi ;; *) pkg_log "Unknown install method: $install_method" return 1 ;; esac } # Function to install binary packages install_binary_package() { local package_name="$1" local install_type="$2" local extra_params="$3" pkg_log "Installing binary package: $package_name" case "$install_type" in "script") case "$package_name" in "claude-code") echo "Installing Claude Code CLI..." if [[ "$OSTYPE" == "linux-gnu"* ]] || [[ "$OSTYPE" == "darwin"* ]]; then curl -fsSL https://storage.googleapis.com/anthropic-artifacts/claude-code/install.sh | bash else pkg_log "Unsupported OS for Claude Code CLI" return 1 fi ;; *) pkg_log "Unknown script package: $package_name" return 1 ;; esac ;; "npm") case "$package_name" in "claude-code") echo "Installing Claude Code CLI..." if command -v npm >/dev/null 2>&1; then npm install -g @anthropic-ai/claude-code if [[ $? -eq 0 ]]; then echo "✅ Claude Code installed successfully via npm" echo "💡 Run 'claude doctor' to verify installation" return 0 else echo "⚠️ npm installation failed, trying binary installation..." if [[ "$OSTYPE" == "linux-gnu"* ]] || [[ "$OSTYPE" == "darwin"* ]]; then curl -fsSL https://claude.ai/install.sh | bash else pkg_log "Unsupported OS for Claude Code CLI" return 1 fi fi else echo "npm not found, trying binary installation..." if [[ "$OSTYPE" == "linux-gnu"* ]] || [[ "$OSTYPE" == "darwin"* ]]; then curl -fsSL https://claude.ai/install.sh | bash else pkg_log "npm and binary installation not available" return 1 fi fi ;; "gemini-cli") echo "Installing Gemini CLI..." local installed=false # Try npm first (requires Node.js 20+) if command -v npm >/dev/null 2>&1; then echo " Trying npm installation..." if npm install -g @google/gemini-cli; then echo "✅ Gemini CLI installed successfully via npm" installed=true else echo "⚠️ npm global installation failed" fi fi # Try homebrew on macOS if npm failed if [[ "$installed" != "true" ]] && [[ "$OSTYPE" == "darwin"* ]] && command -v brew >/dev/null 2>&1; then echo " Trying Homebrew installation..." if brew install gemini-cli; then echo "✅ Gemini CLI installed successfully via Homebrew" installed=true else echo "⚠️ Homebrew installation failed" fi fi # Inform about npx option if all else fails if [[ "$installed" != "true" ]]; then echo "❌ Global installation failed" echo "💡 You can still use Gemini CLI with: npx https://github.com/google-gemini/gemini-cli" pkg_log "Gemini CLI global installation failed, npx option available" return 1 fi ;; *) pkg_log "Unknown npm package: $package_name" return 1 ;; esac ;; "github") # This would need to parse more details from the config echo "⚠️ GitHub installation for $package_name needs configuration" pkg_log "GitHub installation not yet implemented for: $package_name" return 1 ;; *) pkg_log "Unknown install type: $install_type" return 1 ;; esac } # Function to install Python packages install_python_package() { local package_name="$1" pkg_log "Installing Python package: $package_name" if command -v pip3 >/dev/null 2>&1; then pip3 install "$package_name" elif command -v pip >/dev/null 2>&1; then pip install "$package_name" else pkg_log "pip not found. Cannot install Python package: $package_name" return 1 fi } # Function to install npm packages install_npm_package() { local package_name="$1" pkg_log "Installing npm package: $package_name" if command -v npm >/dev/null 2>&1; then npm install -g "$package_name" else pkg_log "npm not found. Cannot install npm package: $package_name" return 1 fi } # Function to install zsh plugins install_zsh_plugin() { local plugin_name="$1" if [[ ! -d "$HOME/.oh-my-zsh" ]]; then pkg_log "Oh My Zsh not installed. Skipping zsh plugin: $plugin_name" return 1 fi pkg_log "Installing zsh plugin: $plugin_name" case "$plugin_name" in "zsh-syntax-highlighting") if [[ ! -d "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting" ]]; then git clone https://github.com/zsh-users/zsh-syntax-highlighting.git "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting" fi ;; "zsh-autosuggestions") if [[ ! -d "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-autosuggestions" ]]; then git clone https://github.com/zsh-users/zsh-autosuggestions "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-autosuggestions" fi ;; *) pkg_log "Unknown zsh plugin: $plugin_name" return 1 ;; esac } # Function to check and install all packages install_all_packages() { local skip_optional=${1:-false} # Source profile manager if available if [[ -f "$HOME/.dotfiles/lib/profile_manager.sh" ]]; then source "$HOME/.dotfiles/lib/profile_manager.sh" local current_profile=$(get_current_profile) pkg_log "Starting package installation for profile: $current_profile" echo "🔧 Checking and installing packages for profile: $current_profile" else pkg_log "Starting package installation process (no profile system)" echo "🔧 Checking and installing packages..." fi # System packages (profile-aware) echo "📦 Checking system packages..." local system_packages if command -v get_profile_packages >/dev/null 2>&1; then system_packages=$(get_profile_packages "system") else system_packages="curl git vim sshuttle" # Fallback fi for package in $system_packages; do case "$package" in "sshuttle") check_cmd="which sshuttle" ;; "curl") check_cmd="which curl" ;; "git") check_cmd="which git" ;; "vim") check_cmd="which vim" ;; "docker") check_cmd="which docker" ;; "docker-compose") check_cmd="which docker-compose" ;; esac if ! is_package_installed "$check_cmd"; then echo " Installing $package..." if install_system_package "$package"; then echo " ✅ $package installed successfully" else echo " ❌ Failed to install $package" fi else echo " ✅ $package already installed" fi done # Binary packages (profile-aware) echo "⚡ Checking binary packages..." local binary_packages if command -v get_profile_packages >/dev/null 2>&1; then binary_packages=$(get_profile_packages "binary") else binary_packages="claude-code gemini-cli" # Fallback fi for package in $binary_packages; do case "$package" in "claude-code") if ! is_package_installed "which claude"; then echo " Installing claude-code..." if install_binary_package "claude-code" "npm"; then echo " ✅ claude-code installed successfully" else echo " ⚠️ Failed to install claude-code" fi else echo " ✅ claude-code already installed" fi ;; "gemini-cli") if ! is_package_installed "which gemini"; then echo " Installing gemini-cli..." if install_binary_package "gemini-cli" "npm"; then echo " ✅ gemini-cli installed successfully" else echo " ⚠️ Failed to install gemini-cli" fi else echo " ✅ gemini-cli already installed" fi ;; "task-master") if ! is_package_installed "which task-master"; then echo " Installing task-master..." if command -v npm >/dev/null 2>&1; then if npm install -g task-master-ai; then echo " ✅ task-master installed successfully" echo " 💡 Initialize with: task-master init" else echo " ⚠️ Failed to install task-master" fi else echo " ⚠️ npm not found. Cannot install task-master" fi else echo " ✅ task-master already installed" fi ;; esac done # GitHub packages (profile-aware) echo "🐙 Checking GitHub packages..." local github_packages if command -v get_profile_packages >/dev/null 2>&1; then github_packages=$(get_profile_packages "github") else github_packages="fzf bat ripgrep fd delta" # Fallback fi for package in $github_packages; do case "$package" in "fzf") if ! is_package_installed "which fzf"; then echo " Installing fzf..." if install_github_package "fzf" "junegunn/fzf" "clone_and_install" "$HOME/.fzf" "" "" "$HOME/.fzf/install --all"; then echo " ✅ fzf installed successfully" else echo " ⚠️ Failed to install fzf" fi else echo " ✅ fzf already installed" fi ;; "bat"|"ripgrep"|"fd"|"delta") local cmd_name="$package" [[ "$package" == "ripgrep" ]] && cmd_name="rg" if ! is_package_installed "which $cmd_name"; then echo " Installing $package..." local pattern="x86_64-unknown-linux-gnu" if [[ "$OSTYPE" == "darwin"* ]]; then pattern="x86_64-apple-darwin" fi local repo="" case "$package" in "bat") repo="sharkdp/bat" ;; "ripgrep") repo="BurntSushi/ripgrep" ;; "fd") repo="sharkdp/fd" ;; "delta") repo="dandavison/delta" ;; esac if install_github_package "$package" "$repo" "release_binary" "$HOME/.local/bin" "$cmd_name" "$pattern"; then echo " ✅ $package installed successfully" else echo " ⚠️ Failed to install $package" fi else echo " ✅ $package already installed" fi ;; esac done # Zsh plugins (only if zsh is being used) if [[ -d "$HOME/.oh-my-zsh" ]]; then echo "🎨 Checking zsh plugins..." for plugin in zsh-syntax-highlighting zsh-autosuggestions; do case "$plugin" in "zsh-syntax-highlighting") check_path="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting" ;; "zsh-autosuggestions") check_path="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-autosuggestions" ;; esac if [[ ! -d "$check_path" ]]; then echo " Installing $plugin..." if install_zsh_plugin "$plugin"; then echo " ✅ $plugin installed successfully" else echo " ❌ Failed to install $plugin" fi else echo " ✅ $plugin already installed" fi done fi # Optional packages if [[ "$skip_optional" != "true" ]]; then echo "🔧 Checking optional packages..." for package in docker docker-compose; do case "$package" in "docker") check_cmd="which docker" ;; "docker-compose") check_cmd="which docker-compose" ;; esac if ! is_package_installed "$check_cmd"; then echo " Installing optional package $package..." if install_system_package "$package"; then echo " ✅ $package installed successfully" else echo " ⚠️ Failed to install optional package $package" fi else echo " ✅ $package already installed" fi done fi pkg_log "Package installation process completed" echo "🎉 Package check and installation completed!" }