From 3c972fb8284eedfeea93f325b4802738a27fbeec Mon Sep 17 00:00:00 2001 From: darrenhinde <107584450+darrenhinde@users.noreply.github.com> Date: Tue, 18 Nov 2025 23:08:30 +0000 Subject: [PATCH] Fix critical bash 5.x compatibility and add custom install directory support - Fix set -e bug: Change ((variable++)) to variable=$((variable + 1)) to prevent script exit on bash 5.2.21 (Ubuntu 24.04) - Fix collision handling UX: Display menu options correctly by redirecting to stderr - Add custom installation directory support with --install-dir flag and OPENCODE_INSTALL_DIR env var - Add interactive location selection menu (Local/Global/Custom) - Implement cross-platform path handling (Linux/macOS/Windows) with tilde expansion, backslash conversion, and validation - Enhance help documentation with comprehensive examples and platform support information Fixes installation failures on modern Linux systems and enables flexible installation to any directory including ~/.config/opencode --- install.sh | 399 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 326 insertions(+), 73 deletions(-) diff --git a/install.sh b/install.sh index f7b9653..59be0c6 100755 --- a/install.sh +++ b/install.sh @@ -48,7 +48,7 @@ REPO_URL="https://github.com/darrenhinde/OpenAgents" BRANCH="${OPENCODE_BRANCH:-main}" # Allow override via environment variable RAW_URL="https://raw.githubusercontent.com/darrenhinde/OpenAgents/${BRANCH}" REGISTRY_URL="${RAW_URL}/registry.json" -INSTALL_DIR=".opencode" +INSTALL_DIR="${OPENCODE_INSTALL_DIR:-.opencode}" # Allow override via environment variable TEMP_DIR="/tmp/opencode-installer-$$" # Global variables @@ -56,6 +56,7 @@ SELECTED_COMPONENTS=() INSTALL_MODE="" PROFILE="" NON_INTERACTIVE=false +CUSTOM_INSTALL_DIR="" # Set via --install-dir argument ############################################################################# # Utility Functions @@ -91,6 +92,90 @@ print_step() { echo -e "\n${MAGENTA}${BOLD}▶${NC} $1\n" } +############################################################################# +# Path Handling (Cross-Platform) +############################################################################# + +normalize_and_validate_path() { + local input_path="$1" + local normalized_path + + # Handle empty path + if [ -z "$input_path" ]; then + echo "" + return 1 + fi + + # Expand tilde to $HOME (works on Linux, macOS, Windows Git Bash) + if [[ $input_path == ~* ]]; then + normalized_path="${HOME}${input_path:1}" + else + normalized_path="$input_path" + fi + + # Convert backslashes to forward slashes (Windows compatibility) + normalized_path="${normalized_path//\\//}" + + # Remove trailing slashes + normalized_path="${normalized_path%/}" + + # If path is relative, make it absolute based on current directory + if [[ ! "$normalized_path" = /* ]] && [[ ! "$normalized_path" =~ ^[A-Za-z]: ]]; then + normalized_path="$(pwd)/${normalized_path}" + fi + + echo "$normalized_path" + return 0 +} + +validate_install_path() { + local path="$1" + local parent_dir + + # Get parent directory + parent_dir="$(dirname "$path")" + + # Check if parent directory exists + if [ ! -d "$parent_dir" ]; then + print_error "Parent directory does not exist: $parent_dir" + return 1 + fi + + # Check if parent directory is writable + if [ ! -w "$parent_dir" ]; then + print_error "No write permission for directory: $parent_dir" + return 1 + fi + + # If target directory exists, check if it's writable + if [ -d "$path" ] && [ ! -w "$path" ]; then + print_error "No write permission for directory: $path" + return 1 + fi + + return 0 +} + +get_global_install_path() { + # Return platform-appropriate global installation path + case "$PLATFORM" in + macOS) + # macOS: Use XDG standard (consistent with Linux) + echo "${HOME}/.config/opencode" + ;; + Linux) + echo "${HOME}/.config/opencode" + ;; + Windows) + # Windows Git Bash/WSL: Use same as Linux + echo "${HOME}/.config/opencode" + ;; + *) + echo "${HOME}/.config/opencode" + ;; + esac +} + ############################################################################# # Dependency Checks ############################################################################# @@ -243,6 +328,92 @@ check_interactive_mode() { fi } +show_install_location_menu() { + check_interactive_mode + + clear + print_header + + local global_path=$(get_global_install_path) + + echo -e "${BOLD}Choose installation location:${NC}\n" + echo -e " ${GREEN}1) Local${NC} - Install to ${CYAN}.opencode/${NC} in current directory" + echo " (Best for project-specific agents)" + echo "" + echo -e " ${BLUE}2) Global${NC} - Install to ${CYAN}${global_path}${NC}" + echo " (Best for user-wide agents available everywhere)" + echo "" + echo -e " ${MAGENTA}3) Custom${NC} - Enter exact path" + echo " Examples:" + case "$PLATFORM" in + Windows) + echo " ${CYAN}C:/Users/username/my-agents${NC} or ${CYAN}~/my-agents${NC}" + ;; + *) + echo " ${CYAN}/home/username/my-agents${NC} or ${CYAN}~/my-agents${NC}" + ;; + esac + echo "" + echo " 4) Back / Exit" + echo "" + read -p "Enter your choice [1-4]: " location_choice + + case $location_choice in + 1) + INSTALL_DIR=".opencode" + print_success "Installing to local directory: .opencode/" + sleep 1 + ;; + 2) + INSTALL_DIR="$global_path" + print_success "Installing to global directory: $global_path" + sleep 1 + ;; + 3) + echo "" + read -p "Enter installation path: " custom_path + + if [ -z "$custom_path" ]; then + print_error "No path entered" + sleep 2 + show_install_location_menu + return + fi + + local normalized_path=$(normalize_and_validate_path "$custom_path") + + if [ $? -ne 0 ]; then + print_error "Invalid path" + sleep 2 + show_install_location_menu + return + fi + + if ! validate_install_path "$normalized_path"; then + echo "" + read -p "Continue anyway? [y/N]: " continue_choice + if [[ ! $continue_choice =~ ^[Yy] ]]; then + show_install_location_menu + return + fi + fi + + INSTALL_DIR="$normalized_path" + print_success "Installing to custom directory: $INSTALL_DIR" + sleep 1 + ;; + 4) + cleanup_and_exit 0 + ;; + *) + print_error "Invalid choice" + sleep 2 + show_install_location_menu + return + ;; + esac +} + show_main_menu() { check_interactive_mode @@ -486,6 +657,8 @@ show_installation_preview() { echo -e "Mode: ${GREEN}Custom${NC}" fi + echo -e "Installation directory: ${CYAN}${INSTALL_DIR}${NC}" + echo -e "\nComponents to install (${#SELECTED_COMPONENTS[@]} total):\n" # Group by type @@ -592,18 +765,18 @@ show_collision_report() { } get_install_strategy() { - echo -e "${BOLD}How would you like to proceed?${NC}\n" - echo " 1) ${GREEN}Skip existing${NC} - Only install new files, keep all existing files unchanged" - echo " 2) ${YELLOW}Overwrite all${NC} - Replace existing files with new versions (your changes will be lost)" - echo " 3) ${CYAN}Backup & overwrite${NC} - Backup existing files, then install new versions" - echo " 4) ${RED}Cancel${NC} - Exit without making changes" - echo "" + echo -e "${BOLD}How would you like to proceed?${NC}\n" >&2 + echo " 1) ${GREEN}Skip existing${NC} - Only install new files, keep all existing files unchanged" >&2 + echo " 2) ${YELLOW}Overwrite all${NC} - Replace existing files with new versions (your changes will be lost)" >&2 + echo " 3) ${CYAN}Backup & overwrite${NC} - Backup existing files, then install new versions" >&2 + echo " 4) ${RED}Cancel${NC} - Exit without making changes" >&2 + echo "" >&2 read -p "Enter your choice [1-4]: " strategy_choice case $strategy_choice in 1) echo "skip" ;; 2) - echo "" + echo "" >&2 print_warning "This will overwrite existing files. Your changes will be lost!" read -p "Are you sure? Type 'yes' to confirm: " confirm if [ "$confirm" = "yes" ]; then @@ -665,7 +838,7 @@ perform_installation() { local backup_file="${backup_dir}/${file}" mkdir -p "$(dirname "$backup_file")" if cp "$file" "$backup_file" 2>/dev/null; then - ((backup_count++)) + backup_count=$((backup_count + 1)) else print_warning "Failed to backup: $file" fi @@ -701,7 +874,7 @@ perform_installation() { if [ -z "$path" ] || [ "$path" = "null" ]; then print_warning "Could not find path for ${comp}" - ((failed++)) + failed=$((failed + 1)) continue fi @@ -714,7 +887,7 @@ perform_installation() { # Check if file exists and we're in skip mode if [ "$file_existed" = true ] && [ "$install_strategy" = "skip" ]; then print_info "Skipped existing: ${type}:${id}" - ((skipped++)) + skipped=$((skipped + 1)) continue fi @@ -732,10 +905,10 @@ perform_installation() { else print_success "Installed ${type}: ${id}" fi - ((installed++)) + installed=$((installed + 1)) else print_error "Failed to install ${type}: ${id}" - ((failed++)) + failed=$((failed + 1)) fi done @@ -769,15 +942,24 @@ show_post_install() { echo "" print_step "Next Steps" - echo "1. Review the installed components in .opencode/" - echo "2. Copy env.example to .env and configure:" - echo " ${CYAN}cp env.example .env${NC}" - echo "3. Start using OpenCode agents:" + echo "1. Review the installed components in ${CYAN}${INSTALL_DIR}/${NC}" + + # Check if env.example was installed + if [ -f "${INSTALL_DIR}/../env.example" ] || [ -f "env.example" ]; then + echo "2. Copy env.example to .env and configure:" + echo " ${CYAN}cp env.example .env${NC}" + echo "3. Start using OpenCode agents:" + else + echo "2. Start using OpenCode agents:" + fi echo " ${CYAN}opencode${NC}" echo "" + # Show installation location info + print_info "Installation directory: ${CYAN}${INSTALL_DIR}${NC}" + if [ -d "${INSTALL_DIR}.backup."* ] 2>/dev/null; then - print_info "Backup created - you can restore files from .opencode.backup.* if needed" + print_info "Backup created - you can restore files from ${INSTALL_DIR}.backup.* if needed" fi print_info "Documentation: ${REPO_URL}" @@ -832,60 +1014,130 @@ trap 'cleanup_and_exit 1' INT TERM main() { # Parse command line arguments - case "${1:-}" in - essential|--essential) - INSTALL_MODE="profile" - PROFILE="essential" - NON_INTERACTIVE=true - ;; - developer|--developer) - INSTALL_MODE="profile" - PROFILE="developer" - NON_INTERACTIVE=true - ;; - business|--business) - INSTALL_MODE="profile" - PROFILE="business" - NON_INTERACTIVE=true - ;; - full|--full) - INSTALL_MODE="profile" - PROFILE="full" - NON_INTERACTIVE=true - ;; - advanced|--advanced) - INSTALL_MODE="profile" - PROFILE="advanced" - NON_INTERACTIVE=true - ;; - list|--list) - check_dependencies - fetch_registry - list_components - cleanup_and_exit 0 - ;; - --help|-h|help) - print_header - echo "Usage: $0 [OPTIONS]" - echo "" - echo "Options:" - echo " essential, --essential Install essential profile (minimal)" - echo " developer, --developer Install developer profile (code-focused)" - echo " business, --business Install business profile (content-focused)" - echo " full, --full Install full profile (everything except system-builder)" - echo " advanced, --advanced Install advanced profile (complete system)" - echo " list, --list List all available components" - echo " help, --help, -h Show this help message" - echo "" - echo "Examples:" - echo " $0 essential" - echo " $0 --developer" - echo " curl -fsSL https://raw.githubusercontent.com/darrenhinde/OpenAgents/main/install.sh | bash -s essential" - echo "" - echo "Without options, runs in interactive mode" - exit 0 - ;; - esac + while [ $# -gt 0 ]; do + case "$1" in + --install-dir=*) + CUSTOM_INSTALL_DIR="${1#*=}" + shift + ;; + --install-dir) + if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then + CUSTOM_INSTALL_DIR="$2" + shift 2 + else + echo "Error: --install-dir requires a path argument" + exit 1 + fi + ;; + essential|--essential) + INSTALL_MODE="profile" + PROFILE="essential" + NON_INTERACTIVE=true + shift + ;; + developer|--developer) + INSTALL_MODE="profile" + PROFILE="developer" + NON_INTERACTIVE=true + shift + ;; + business|--business) + INSTALL_MODE="profile" + PROFILE="business" + NON_INTERACTIVE=true + shift + ;; + full|--full) + INSTALL_MODE="profile" + PROFILE="full" + NON_INTERACTIVE=true + shift + ;; + advanced|--advanced) + INSTALL_MODE="profile" + PROFILE="advanced" + NON_INTERACTIVE=true + shift + ;; + list|--list) + check_dependencies + fetch_registry + list_components + cleanup_and_exit 0 + ;; + --help|-h|help) + print_header + echo "Usage: $0 [PROFILE] [OPTIONS]" + echo "" + echo -e "${BOLD}Profiles:${NC}" + echo " essential, --essential Minimal setup with core agents" + echo " developer, --developer Code-focused development tools" + echo " business, --business Content and business-focused tools" + echo " full, --full Everything except system-builder" + echo " advanced, --advanced Complete system with all components" + echo "" + echo -e "${BOLD}Options:${NC}" + echo " --install-dir PATH Custom installation directory" + echo " (default: .opencode)" + echo " list, --list List all available components" + echo " help, --help, -h Show this help message" + echo "" + echo -e "${BOLD}Environment Variables:${NC}" + echo " OPENCODE_INSTALL_DIR Installation directory" + echo " OPENCODE_BRANCH Git branch to install from (default: main)" + echo "" + echo -e "${BOLD}Examples:${NC}" + echo "" + echo " ${CYAN}# Interactive mode (choose location and components)${NC}" + echo " $0" + echo "" + echo " ${CYAN}# Quick install with default location (.opencode/)${NC}" + echo " $0 developer" + echo "" + echo " ${CYAN}# Install to global location (Linux/macOS)${NC}" + echo " $0 developer --install-dir ~/.config/opencode" + echo "" + echo " ${CYAN}# Install to global location (Windows Git Bash)${NC}" + echo " $0 developer --install-dir ~/.config/opencode" + echo "" + echo " ${CYAN}# Install to custom location${NC}" + echo " $0 essential --install-dir ~/my-agents" + echo "" + echo " ${CYAN}# Using environment variable${NC}" + echo " export OPENCODE_INSTALL_DIR=~/.config/opencode" + echo " $0 developer" + echo "" + echo " ${CYAN}# Install from URL (non-interactive)${NC}" + echo " curl -fsSL https://raw.githubusercontent.com/darrenhinde/OpenAgents/main/install.sh | bash -s developer" + echo "" + echo -e "${BOLD}Platform Support:${NC}" + echo " ✓ Linux (bash 3.2+)" + echo " ✓ macOS (bash 3.2+)" + echo " ✓ Windows (Git Bash, WSL)" + echo "" + exit 0 + ;; + *) + echo "Unknown option: $1" + echo "Run '$0 --help' for usage information" + exit 1 + ;; + esac + done + + # Apply custom install directory if specified (CLI arg overrides env var) + if [ -n "$CUSTOM_INSTALL_DIR" ]; then + local normalized_path=$(normalize_and_validate_path "$CUSTOM_INSTALL_DIR") + if [ $? -eq 0 ]; then + INSTALL_DIR="$normalized_path" + if ! validate_install_path "$INSTALL_DIR"; then + print_warning "Installation path may have issues, but continuing..." + fi + else + print_error "Invalid installation directory: $CUSTOM_INSTALL_DIR" + exit 1 + fi + fi check_bash_version check_dependencies @@ -901,7 +1153,8 @@ main() { done < "$temp_file" show_installation_preview else - # Interactive mode + # Interactive mode - show location menu first + show_install_location_menu show_main_menu if [ "$INSTALL_MODE" = "profile" ]; then