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
This commit is contained in:
darrenhinde 2025-11-18 23:08:30 +00:00
parent ca323285b2
commit 3c972fb828

View file

@ -48,7 +48,7 @@ REPO_URL="https://github.com/darrenhinde/OpenAgents"
BRANCH="${OPENCODE_BRANCH:-main}" # Allow override via environment variable BRANCH="${OPENCODE_BRANCH:-main}" # Allow override via environment variable
RAW_URL="https://raw.githubusercontent.com/darrenhinde/OpenAgents/${BRANCH}" RAW_URL="https://raw.githubusercontent.com/darrenhinde/OpenAgents/${BRANCH}"
REGISTRY_URL="${RAW_URL}/registry.json" 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-$$" TEMP_DIR="/tmp/opencode-installer-$$"
# Global variables # Global variables
@ -56,6 +56,7 @@ SELECTED_COMPONENTS=()
INSTALL_MODE="" INSTALL_MODE=""
PROFILE="" PROFILE=""
NON_INTERACTIVE=false NON_INTERACTIVE=false
CUSTOM_INSTALL_DIR="" # Set via --install-dir argument
############################################################################# #############################################################################
# Utility Functions # Utility Functions
@ -91,6 +92,90 @@ print_step() {
echo -e "\n${MAGENTA}${BOLD}▶${NC} $1\n" 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 # Dependency Checks
############################################################################# #############################################################################
@ -243,6 +328,92 @@ check_interactive_mode() {
fi 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() { show_main_menu() {
check_interactive_mode check_interactive_mode
@ -486,6 +657,8 @@ show_installation_preview() {
echo -e "Mode: ${GREEN}Custom${NC}" echo -e "Mode: ${GREEN}Custom${NC}"
fi fi
echo -e "Installation directory: ${CYAN}${INSTALL_DIR}${NC}"
echo -e "\nComponents to install (${#SELECTED_COMPONENTS[@]} total):\n" echo -e "\nComponents to install (${#SELECTED_COMPONENTS[@]} total):\n"
# Group by type # Group by type
@ -592,18 +765,18 @@ show_collision_report() {
} }
get_install_strategy() { get_install_strategy() {
echo -e "${BOLD}How would you like to proceed?${NC}\n" 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" 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)" 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" echo " 3) ${CYAN}Backup & overwrite${NC} - Backup existing files, then install new versions" >&2
echo " 4) ${RED}Cancel${NC} - Exit without making changes" echo " 4) ${RED}Cancel${NC} - Exit without making changes" >&2
echo "" echo "" >&2
read -p "Enter your choice [1-4]: " strategy_choice read -p "Enter your choice [1-4]: " strategy_choice
case $strategy_choice in case $strategy_choice in
1) echo "skip" ;; 1) echo "skip" ;;
2) 2)
echo "" echo "" >&2
print_warning "This will overwrite existing files. Your changes will be lost!" print_warning "This will overwrite existing files. Your changes will be lost!"
read -p "Are you sure? Type 'yes' to confirm: " confirm read -p "Are you sure? Type 'yes' to confirm: " confirm
if [ "$confirm" = "yes" ]; then if [ "$confirm" = "yes" ]; then
@ -665,7 +838,7 @@ perform_installation() {
local backup_file="${backup_dir}/${file}" local backup_file="${backup_dir}/${file}"
mkdir -p "$(dirname "$backup_file")" mkdir -p "$(dirname "$backup_file")"
if cp "$file" "$backup_file" 2>/dev/null; then if cp "$file" "$backup_file" 2>/dev/null; then
((backup_count++)) backup_count=$((backup_count + 1))
else else
print_warning "Failed to backup: $file" print_warning "Failed to backup: $file"
fi fi
@ -701,7 +874,7 @@ perform_installation() {
if [ -z "$path" ] || [ "$path" = "null" ]; then if [ -z "$path" ] || [ "$path" = "null" ]; then
print_warning "Could not find path for ${comp}" print_warning "Could not find path for ${comp}"
((failed++)) failed=$((failed + 1))
continue continue
fi fi
@ -714,7 +887,7 @@ perform_installation() {
# Check if file exists and we're in skip mode # Check if file exists and we're in skip mode
if [ "$file_existed" = true ] && [ "$install_strategy" = "skip" ]; then if [ "$file_existed" = true ] && [ "$install_strategy" = "skip" ]; then
print_info "Skipped existing: ${type}:${id}" print_info "Skipped existing: ${type}:${id}"
((skipped++)) skipped=$((skipped + 1))
continue continue
fi fi
@ -732,10 +905,10 @@ perform_installation() {
else else
print_success "Installed ${type}: ${id}" print_success "Installed ${type}: ${id}"
fi fi
((installed++)) installed=$((installed + 1))
else else
print_error "Failed to install ${type}: ${id}" print_error "Failed to install ${type}: ${id}"
((failed++)) failed=$((failed + 1))
fi fi
done done
@ -769,15 +942,24 @@ show_post_install() {
echo "" echo ""
print_step "Next Steps" print_step "Next Steps"
echo "1. Review the installed components in .opencode/" echo "1. Review the installed components in ${CYAN}${INSTALL_DIR}/${NC}"
echo "2. Copy env.example to .env and configure:"
echo " ${CYAN}cp env.example .env${NC}" # Check if env.example was installed
echo "3. Start using OpenCode agents:" 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 " ${CYAN}opencode${NC}"
echo "" echo ""
# Show installation location info
print_info "Installation directory: ${CYAN}${INSTALL_DIR}${NC}"
if [ -d "${INSTALL_DIR}.backup."* ] 2>/dev/null; then 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 fi
print_info "Documentation: ${REPO_URL}" print_info "Documentation: ${REPO_URL}"
@ -832,60 +1014,130 @@ trap 'cleanup_and_exit 1' INT TERM
main() { main() {
# Parse command line arguments # Parse command line arguments
case "${1:-}" in while [ $# -gt 0 ]; do
essential|--essential) case "$1" in
INSTALL_MODE="profile" --install-dir=*)
PROFILE="essential" CUSTOM_INSTALL_DIR="${1#*=}"
NON_INTERACTIVE=true shift
;; ;;
developer|--developer) --install-dir)
INSTALL_MODE="profile" if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then
PROFILE="developer" CUSTOM_INSTALL_DIR="$2"
NON_INTERACTIVE=true shift 2
;; else
business|--business) echo "Error: --install-dir requires a path argument"
INSTALL_MODE="profile" exit 1
PROFILE="business" fi
NON_INTERACTIVE=true ;;
;; essential|--essential)
full|--full) INSTALL_MODE="profile"
INSTALL_MODE="profile" PROFILE="essential"
PROFILE="full" NON_INTERACTIVE=true
NON_INTERACTIVE=true shift
;; ;;
advanced|--advanced) developer|--developer)
INSTALL_MODE="profile" INSTALL_MODE="profile"
PROFILE="advanced" PROFILE="developer"
NON_INTERACTIVE=true NON_INTERACTIVE=true
;; shift
list|--list) ;;
check_dependencies business|--business)
fetch_registry INSTALL_MODE="profile"
list_components PROFILE="business"
cleanup_and_exit 0 NON_INTERACTIVE=true
;; shift
--help|-h|help) ;;
print_header full|--full)
echo "Usage: $0 [OPTIONS]" INSTALL_MODE="profile"
echo "" PROFILE="full"
echo "Options:" NON_INTERACTIVE=true
echo " essential, --essential Install essential profile (minimal)" shift
echo " developer, --developer Install developer profile (code-focused)" ;;
echo " business, --business Install business profile (content-focused)" advanced|--advanced)
echo " full, --full Install full profile (everything except system-builder)" INSTALL_MODE="profile"
echo " advanced, --advanced Install advanced profile (complete system)" PROFILE="advanced"
echo " list, --list List all available components" NON_INTERACTIVE=true
echo " help, --help, -h Show this help message" shift
echo "" ;;
echo "Examples:" list|--list)
echo " $0 essential" check_dependencies
echo " $0 --developer" fetch_registry
echo " curl -fsSL https://raw.githubusercontent.com/darrenhinde/OpenAgents/main/install.sh | bash -s essential" list_components
echo "" cleanup_and_exit 0
echo "Without options, runs in interactive mode" ;;
exit 0 --help|-h|help)
;; print_header
esac 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_bash_version
check_dependencies check_dependencies
@ -901,7 +1153,8 @@ main() {
done < "$temp_file" done < "$temp_file"
show_installation_preview show_installation_preview
else else
# Interactive mode # Interactive mode - show location menu first
show_install_location_menu
show_main_menu show_main_menu
if [ "$INSTALL_MODE" = "profile" ]; then if [ "$INSTALL_MODE" = "profile" ]; then