Files
dotfiles/lib/package_manager.sh
Eric Turner 3ddad4b543 feat: add core library functions for package and profile management
- Add package_manager.sh for handling system package installations
- Add profile_manager.sh for managing machine-specific configurations
- Add update_checker.sh for version control and updates

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-02 20:21:25 -06:00

590 lines
22 KiB
Bash
Executable File

#!/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!"
}