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>
This commit is contained in:
Eric Turner
2025-08-02 20:21:25 -06:00
parent f0c2137e69
commit 3ddad4b543
3 changed files with 1299 additions and 0 deletions

590
lib/package_manager.sh Executable file
View File

@@ -0,0 +1,590 @@
#!/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!"
}

300
lib/profile_manager.sh Executable file
View File

@@ -0,0 +1,300 @@
#!/bin/bash
# Machine Profile Manager for Dotfiles
# Handles profile detection, selection, and package filtering
PROFILES_CONFIG="$HOME/.dotfiles/machine-profiles.yaml"
PROFILE_FILE="$HOME/.dotfiles/.machine_profile"
PROFILE_LOG="$HOME/.dotfiles/.profile.log"
# Function to log profile operations
profile_log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$PROFILE_LOG"
}
# Function to get current profile
get_current_profile() {
if [[ -f "$PROFILE_FILE" ]]; then
cat "$PROFILE_FILE"
else
echo "dev-lite" # Default fallback
fi
}
# Function to set profile
set_profile() {
local profile="$1"
echo "$profile" > "$PROFILE_FILE"
profile_log "Profile set to: $profile"
}
# Function to detect machine type automatically
detect_machine_profile() {
local server_score=0
local dev_score=0
local personal_score=0
profile_log "Starting automatic profile detection"
# Server detection
if systemctl is-active sshd >/dev/null 2>&1; then
server_score=$((server_score + 30))
profile_log "Server indicator: sshd active (+30)"
fi
if [[ -f /etc/systemd/system/multi-user.target ]]; then
server_score=$((server_score + 20))
profile_log "Server indicator: multi-user target (+20)"
fi
if [[ -n "$SSH_CONNECTION" ]]; then
server_score=$((server_score + 25))
profile_log "Server indicator: SSH connection (+25)"
fi
# Development detection
if command -v code >/dev/null 2>&1 || command -v vim >/dev/null 2>&1 || command -v nvim >/dev/null 2>&1; then
dev_score=$((dev_score + 25))
profile_log "Dev indicator: editor available (+25)"
fi
if [[ -d ~/projects ]] || [[ -d ~/dev ]] || [[ -d ~/src ]]; then
dev_score=$((dev_score + 20))
profile_log "Dev indicator: development directories (+20)"
fi
if command -v node >/dev/null 2>&1 || command -v python3 >/dev/null 2>&1 || command -v docker >/dev/null 2>&1; then
dev_score=$((dev_score + 30))
profile_log "Dev indicator: development tools (+30)"
fi
if [[ -n "$DISPLAY" ]]; then
dev_score=$((dev_score + 15))
profile_log "Dev indicator: GUI environment (+15)"
fi
# Personal machine detection
if [[ -d ~/Desktop ]] || [[ -d ~/Documents ]]; then
personal_score=$((personal_score + 20))
profile_log "Personal indicator: user directories (+20)"
fi
if command -v firefox >/dev/null 2>&1 || command -v chrome >/dev/null 2>&1 || command -v safari >/dev/null 2>&1; then
personal_score=$((personal_score + 25))
profile_log "Personal indicator: browsers available (+25)"
fi
if [[ "$HOME" == *"/Users/"* ]] || [[ "$HOME" == *"/home/eric"* ]]; then
personal_score=$((personal_score + 15))
profile_log "Personal indicator: personal home directory (+15)"
fi
# Determine profile based on scores
profile_log "Scores - Server: $server_score, Dev: $dev_score, Personal: $personal_score"
if [[ $server_score -gt $dev_score ]] && [[ $server_score -gt $personal_score ]]; then
echo "server"
elif [[ $personal_score -gt $dev_score ]] && [[ $personal_score -gt $server_score ]]; then
echo "personal"
elif [[ $dev_score -gt 0 ]]; then
echo "dev"
else
echo "minimal"
fi
}
# Function to prompt user for profile confirmation
prompt_profile_selection() {
local detected_profile="$1"
echo "🤖 Machine Profile Detection"
echo "=============================="
echo "Detected profile: $detected_profile"
echo ""
echo "Available profiles:"
echo " server - Minimal server setup (curl, git, vim, sshuttle, ripgrep, fd)"
echo " dev - Full development setup (all tools + claude, gemini, docker)"
echo " dev-lite - Development without heavy packages (no docker)"
echo " personal - Personal machine (all tools including task-master)"
echo " minimal - Bare bones (curl, git, vim only)"
echo ""
echo "Use detected profile '$detected_profile'? [Y/n/choose]: "
read -r response
case "$response" in
[nN]|[nN][oO])
echo "Available profiles: server, dev, dev-lite, personal, minimal"
echo "Enter profile name: "
read -r selected_profile
if [[ -n "$selected_profile" ]]; then
echo "$selected_profile"
else
echo "$detected_profile"
fi
;;
[cC]|[cC][hH]*)
echo "Available profiles: server, dev, dev-lite, personal, minimal"
echo "Enter profile name: "
read -r selected_profile
if [[ -n "$selected_profile" ]]; then
echo "$selected_profile"
else
echo "$detected_profile"
fi
;;
*)
echo "$detected_profile"
;;
esac
}
# Function to get packages for current profile
get_profile_packages() {
local profile=$(get_current_profile)
local package_type="$1"
case "$profile" in
"server")
case "$package_type" in
"system") echo "curl git vim sshuttle" ;;
"binary") echo "" ;;
"github") echo "ripgrep fd" ;;
"npm") echo "" ;;
"python") echo "" ;;
"zsh") echo "" ;;
"optional") echo "" ;;
esac
;;
"dev")
case "$package_type" in
"system") echo "curl git vim sshuttle" ;;
"binary") echo "claude-code gemini-cli" ;;
"github") echo "fzf bat ripgrep fd delta" ;;
"npm") echo "pm2" ;;
"python") echo "requests" ;;
"zsh") echo "zsh-syntax-highlighting zsh-autosuggestions" ;;
"optional") echo "docker docker-compose" ;;
esac
;;
"dev-lite")
case "$package_type" in
"system") echo "curl git vim sshuttle" ;;
"binary") echo "claude-code gemini-cli" ;;
"github") echo "fzf bat ripgrep fd delta" ;;
"npm") echo "" ;;
"python") echo "requests" ;;
"zsh") echo "zsh-syntax-highlighting zsh-autosuggestions" ;;
"optional") echo "" ;;
esac
;;
"personal")
case "$package_type" in
"system") echo "curl git vim sshuttle" ;;
"binary") echo "claude-code gemini-cli task-master" ;;
"github") echo "fzf bat ripgrep fd delta" ;;
"npm") echo "pm2" ;;
"python") echo "requests" ;;
"zsh") echo "zsh-syntax-highlighting zsh-autosuggestions" ;;
"optional") echo "docker docker-compose" ;;
esac
;;
"minimal")
case "$package_type" in
"system") echo "curl git vim" ;;
"binary") echo "" ;;
"github") echo "" ;;
"npm") echo "" ;;
"python") echo "" ;;
"zsh") echo "" ;;
"optional") echo "" ;;
esac
;;
*)
# Default to dev-lite
case "$package_type" in
"system") echo "curl git vim sshuttle" ;;
"binary") echo "claude-code gemini-cli" ;;
"github") echo "fzf bat ripgrep fd delta" ;;
"npm") echo "" ;;
"python") echo "requests" ;;
"zsh") echo "zsh-syntax-highlighting zsh-autosuggestions" ;;
"optional") echo "" ;;
esac
;;
esac
}
# Function to show current profile status
show_profile_status() {
local current_profile=$(get_current_profile)
echo "🖥️ Machine Profile Status"
echo "========================="
echo "Current profile: $current_profile"
echo ""
case "$current_profile" in
"server")
echo "📋 Server Profile - Minimal setup"
echo " ✅ Essential tools: curl, git, vim, sshuttle"
echo " ✅ Admin tools: ripgrep, fd"
echo " ❌ Development tools: Not installed"
echo " ❌ GUI tools: Not installed"
;;
"dev")
echo "🚀 Development Profile - Full setup"
echo " ✅ Essential tools: All included"
echo " ✅ Development tools: claude, gemini, docker"
echo " ✅ Modern CLI tools: fzf, bat, ripgrep, fd, delta"
echo " ✅ Optional tools: docker, docker-compose"
;;
"dev-lite")
echo "⚡ Development Lite Profile - No heavy packages"
echo " ✅ Essential tools: All included"
echo " ✅ Development tools: claude, gemini (no docker)"
echo " ✅ Modern CLI tools: fzf, bat, ripgrep, fd, delta"
echo " ❌ Heavy tools: docker excluded"
;;
"personal")
echo "🏠 Personal Profile - Everything included"
echo " ✅ Essential tools: All included"
echo " ✅ Development tools: claude, gemini, task-master"
echo " ✅ Modern CLI tools: All included"
echo " ✅ Personal tools: All conveniences"
;;
"minimal")
echo "🔧 Minimal Profile - Bare essentials"
echo " ✅ Core tools: curl, git, vim"
echo " ❌ Everything else: Excluded"
;;
esac
echo ""
echo "💡 Change profile with: dotprofile set <profile>"
echo " Available: server, dev, dev-lite, personal, minimal"
}
# Function to initialize profile (used during installation)
initialize_profile() {
local force_detect=${1:-false}
if [[ ! -f "$PROFILE_FILE" ]] || [[ "$force_detect" == "true" ]]; then
profile_log "Initializing machine profile"
local detected_profile=$(detect_machine_profile)
profile_log "Auto-detected profile: $detected_profile"
# In interactive mode, ask user
if [[ $- == *i* ]] && [[ "$force_detect" != "silent" ]]; then
local selected_profile=$(prompt_profile_selection "$detected_profile")
set_profile "$selected_profile"
profile_log "User selected profile: $selected_profile"
echo "✅ Profile set to: $selected_profile"
else
# Non-interactive or silent mode
set_profile "$detected_profile"
profile_log "Auto-set profile: $detected_profile"
echo "🤖 Auto-detected profile: $detected_profile"
fi
fi
get_current_profile
}

409
lib/update_checker.sh Executable file
View File

@@ -0,0 +1,409 @@
#!/bin/bash
# Package Update Checker for Dotfiles
# Efficiently checks for updates without running expensive update commands
UPDATE_LOG="$HOME/.dotfiles/.update.log"
UPDATE_CACHE="$HOME/.dotfiles/.update_cache"
LAST_CHECK_FILE="$HOME/.dotfiles/.last_update_check"
# Function to log update operations
update_log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$UPDATE_LOG"
}
# Function to check if we should check for updates (avoid too frequent checks)
should_check_updates() {
local current_time=$(date +%s)
local last_check_time=0
local force_check=${1:-false}
if [[ -f "$LAST_CHECK_FILE" ]]; then
last_check_time=$(cat "$LAST_CHECK_FILE")
fi
# If force check, always allow
if [[ "$force_check" == "true" ]]; then
return 0
fi
# Only check if more than 6 hours (21600 seconds) have passed
local time_diff=$((current_time - last_check_time))
if [[ $time_diff -lt 21600 ]]; then
return 1
fi
return 0
}
# Function to check npm packages for updates
check_npm_updates() {
local packages_to_check=()
local updates_available=false
update_log "Checking npm packages for updates"
# Get profile-aware package list
if [[ -f "$HOME/.dotfiles/lib/profile_manager.sh" ]]; then
source "$HOME/.dotfiles/lib/profile_manager.sh"
local binary_packages=$(get_profile_packages "binary")
local npm_packages=$(get_profile_packages "npm")
# Add binary packages that are npm-based
for package in $binary_packages; do
case "$package" in
"claude-code") packages_to_check+=("@anthropic-ai/claude-code") ;;
"gemini-cli") packages_to_check+=("@google/gemini-cli") ;;
"task-master") packages_to_check+=("task-master-ai") ;;
esac
done
# Add npm packages
for package in $npm_packages; do
packages_to_check+=("$package")
done
else
# Fallback list
packages_to_check=("@anthropic-ai/claude-code" "@google/gemini-cli" "task-master-ai" "pm2")
fi
if [[ ${#packages_to_check[@]} -eq 0 ]]; then
echo "No npm packages to check"
return 1
fi
echo "🔍 Checking npm packages for updates..."
# Use npm outdated to check for updates (much faster than npm update)
local outdated_output
if command -v npm >/dev/null 2>&1; then
# npm outdated returns non-zero if packages are outdated, but that's expected
outdated_output=$(npm outdated -g --json 2>/dev/null || true)
if [[ -n "$outdated_output" && "$outdated_output" != "{}" ]]; then
echo "$outdated_output" > "$UPDATE_CACHE.npm"
# Parse and display outdated packages
for package in "${packages_to_check[@]}"; do
if echo "$outdated_output" | grep -q "\"$package\""; then
local current_version=$(echo "$outdated_output" | jq -r ".\"$package\".current // \"unknown\"" 2>/dev/null || echo "unknown")
local wanted_version=$(echo "$outdated_output" | jq -r ".\"$package\".wanted // \"unknown\"" 2>/dev/null || echo "unknown")
local latest_version=$(echo "$outdated_output" | jq -r ".\"$package\".latest // \"unknown\"" 2>/dev/null || echo "unknown")
echo " 📦 $package: $current_version$latest_version"
updates_available=true
update_log "Update available for $package: $current_version$latest_version"
fi
done
else
echo " ✅ All npm packages are up to date"
update_log "All npm packages are up to date"
fi
else
echo " ⚠️ npm not available"
update_log "npm not available for update check"
fi
if [[ "$updates_available" == "true" ]]; then
return 0
else
return 1
fi
}
# Function to check GitHub packages for updates
check_github_updates() {
local updates_available=false
update_log "Checking GitHub packages for updates"
# Get profile-aware GitHub packages
local github_packages
if [[ -f "$HOME/.dotfiles/lib/profile_manager.sh" ]]; then
source "$HOME/.dotfiles/lib/profile_manager.sh"
github_packages=$(get_profile_packages "github")
else
github_packages="fzf bat ripgrep fd delta"
fi
if [[ -z "$github_packages" ]]; then
echo "No GitHub packages to check"
return 1
fi
echo "🐙 Checking GitHub packages for updates..."
for package in $github_packages; do
local repo=""
local current_version=""
local cmd_name="$package"
case "$package" in
"fzf")
repo="junegunn/fzf"
current_version=$(fzf --version 2>/dev/null | cut -d' ' -f1 || echo "unknown")
;;
"bat")
repo="sharkdp/bat"
current_version=$(bat --version 2>/dev/null | grep -o 'bat [0-9.]*' | cut -d' ' -f2 || echo "unknown")
;;
"ripgrep")
repo="BurntSushi/ripgrep"
cmd_name="rg"
current_version=$(rg --version 2>/dev/null | head -1 | grep -o '[0-9.]*' || echo "unknown")
;;
"fd")
repo="sharkdp/fd"
current_version=$(fd --version 2>/dev/null | grep -o '[0-9.]*' || echo "unknown")
;;
"delta")
repo="dandavison/delta"
current_version=$(delta --version 2>/dev/null | grep -o '[0-9.]*' || echo "unknown")
;;
esac
if [[ -n "$repo" ]] && command -v "$cmd_name" >/dev/null 2>&1; then
# Get latest release from GitHub API
local latest_version=$(curl -s "https://api.github.com/repos/$repo/releases/latest" | grep -o '"tag_name": "v*[^"]*"' | cut -d'"' -f4 | sed 's/^v//')
if [[ -n "$latest_version" && "$latest_version" != "$current_version" ]]; then
echo " 📦 $package: $current_version$latest_version"
updates_available=true
update_log "Update available for $package: $current_version$latest_version"
echo "$package|$current_version|$latest_version|$repo" >> "$UPDATE_CACHE.github"
else
echo "$package: $current_version (latest)"
fi
elif [[ -n "$repo" ]]; then
echo " ⚠️ $package not installed"
fi
done
if [[ "$updates_available" == "true" ]]; then
return 0
else
return 1
fi
}
# Function to perform actual updates
update_npm_packages() {
echo "🔄 Updating npm packages..."
if [[ -f "$UPDATE_CACHE.npm" ]]; then
local outdated_output=$(cat "$UPDATE_CACHE.npm")
# Get list of packages that need updating from our profile
local packages_to_update=()
if [[ -f "$HOME/.dotfiles/lib/profile_manager.sh" ]]; then
source "$HOME/.dotfiles/lib/profile_manager.sh"
local binary_packages=$(get_profile_packages "binary")
local npm_packages=$(get_profile_packages "npm")
for package in $binary_packages; do
case "$package" in
"claude-code")
if echo "$outdated_output" | grep -q "@anthropic-ai/claude-code"; then
packages_to_update+=("@anthropic-ai/claude-code")
fi
;;
"gemini-cli")
if echo "$outdated_output" | grep -q "@google/gemini-cli"; then
packages_to_update+=("@google/gemini-cli")
fi
;;
"task-master")
if echo "$outdated_output" | grep -q "task-master-ai"; then
packages_to_update+=("task-master-ai")
fi
;;
esac
done
for package in $npm_packages; do
if echo "$outdated_output" | grep -q "\"$package\""; then
packages_to_update+=("$package")
fi
done
fi
if [[ ${#packages_to_update[@]} -gt 0 ]]; then
echo " Updating: ${packages_to_update[*]}"
npm update -g "${packages_to_update[@]}"
update_log "Updated npm packages: ${packages_to_update[*]}"
else
echo " No packages to update"
fi
# Clean up cache
rm -f "$UPDATE_CACHE.npm"
else
echo " No update information available"
fi
}
# Function to update GitHub packages
update_github_packages() {
echo "🐙 Updating GitHub packages..."
if [[ -f "$UPDATE_CACHE.github" ]]; then
while IFS='|' read -r package current_version latest_version repo; do
echo " Updating $package: $current_version$latest_version"
# Source package manager for installation functions
if [[ -f "$HOME/.dotfiles/lib/package_manager.sh" ]]; then
source "$HOME/.dotfiles/lib/package_manager.sh"
case "$package" in
"fzf")
# Reinstall fzf
if install_github_package "fzf" "$repo" "clone_and_install" "$HOME/.fzf" "" "" "$HOME/.fzf/install --all"; then
echo " ✅ Updated $package"
update_log "Updated $package to $latest_version"
else
echo " ❌ Failed to update $package"
fi
;;
"bat"|"ripgrep"|"fd"|"delta")
local cmd_name="$package"
[[ "$package" == "ripgrep" ]] && cmd_name="rg"
local pattern="x86_64-unknown-linux-gnu"
if [[ "$OSTYPE" == "darwin"* ]]; then
pattern="x86_64-apple-darwin"
fi
if install_github_package "$package" "$repo" "release_binary" "$HOME/.local/bin" "$cmd_name" "$pattern"; then
echo " ✅ Updated $package"
update_log "Updated $package to $latest_version"
else
echo " ❌ Failed to update $package"
fi
;;
esac
fi
done < "$UPDATE_CACHE.github"
# Clean up cache
rm -f "$UPDATE_CACHE.github"
else
echo " No update information available"
fi
}
# Function to show update status
show_update_status() {
echo "🔍 Package Update Status"
echo "======================="
if [[ -f "$LAST_CHECK_FILE" ]]; then
local last_check=$(cat "$LAST_CHECK_FILE")
local last_check_date=$(date -d "@$last_check" 2>/dev/null || echo "Unknown")
echo "Last checked: $last_check_date"
else
echo "Last checked: Never"
fi
echo ""
# Check for cached update information
local updates_pending=false
if [[ -f "$UPDATE_CACHE.npm" ]]; then
echo "📦 npm updates pending:"
local outdated_output=$(cat "$UPDATE_CACHE.npm")
echo "$outdated_output" | jq -r 'to_entries[] | " • \(.key): \(.value.current) → \(.value.latest)"' 2>/dev/null || echo " • Updates available (run dotupdate to see details)"
updates_pending=true
fi
if [[ -f "$UPDATE_CACHE.github" ]]; then
echo "🐙 GitHub updates pending:"
while IFS='|' read -r package current_version latest_version repo; do
echo "$package: $current_version$latest_version"
done < "$UPDATE_CACHE.github"
updates_pending=true
fi
if [[ "$updates_pending" == "false" ]]; then
echo "✅ No pending updates"
echo ""
echo "💡 Run 'dotupdatecheck' to check for new updates"
else
echo ""
echo "💡 Run 'dotupdate' to install pending updates"
fi
}
# Main update check function
check_for_updates() {
local force_check=${1:-false}
local show_output=${2:-true}
if ! should_check_updates "$force_check"; then
if [[ "$show_output" == "true" ]]; then
echo " Skipping update check (checked recently)"
echo " Use 'dotupdatecheck --force' to force check"
fi
return 0
fi
update_log "Starting update check (force: $force_check)"
# Clean old cache files
rm -f "$UPDATE_CACHE.npm" "$UPDATE_CACHE.github"
local npm_updates=false
local github_updates=false
if check_npm_updates; then
npm_updates=true
fi
echo ""
if check_github_updates; then
github_updates=true
fi
# Update last check time
echo "$(date +%s)" > "$LAST_CHECK_FILE"
if [[ "$npm_updates" == "true" || "$github_updates" == "true" ]]; then
echo ""
echo "💡 Updates available! Run 'dotupdate' to install them."
update_log "Updates found and cached"
return 0
else
echo ""
echo "✅ All packages are up to date!"
update_log "No updates available"
return 1
fi
}
# Main update function
update_packages() {
echo "🔄 Updating packages..."
update_log "Starting package updates"
local updated_something=false
if [[ -f "$UPDATE_CACHE.npm" ]]; then
update_npm_packages
updated_something=true
echo ""
fi
if [[ -f "$UPDATE_CACHE.github" ]]; then
update_github_packages
updated_something=true
echo ""
fi
if [[ "$updated_something" == "false" ]]; then
echo "No cached updates found. Run 'dotupdatecheck' first."
return 1
fi
echo "🎉 Package updates completed!"
update_log "Package updates completed"
}