diff --git a/lib/package_manager.sh b/lib/package_manager.sh new file mode 100755 index 0000000..016ac40 --- /dev/null +++ b/lib/package_manager.sh @@ -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!" +} \ No newline at end of file diff --git a/lib/profile_manager.sh b/lib/profile_manager.sh new file mode 100755 index 0000000..7b8140c --- /dev/null +++ b/lib/profile_manager.sh @@ -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 " + 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 +} \ No newline at end of file diff --git a/lib/update_checker.sh b/lib/update_checker.sh new file mode 100755 index 0000000..c2e3001 --- /dev/null +++ b/lib/update_checker.sh @@ -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" +} \ No newline at end of file