| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500 |
- #!/usr/bin/env bash
- #############################################################################
- # OpenAgents Control Installer
- # Interactive installer for OpenCode agents, commands, tools, and plugins
- #
- # Compatible with:
- # - macOS (bash 3.2+)
- # - Linux (bash 3.2+)
- # - Windows (Git Bash, WSL)
- #############################################################################
- set -e
- # Detect platform
- PLATFORM="$(uname -s)"
- case "$PLATFORM" in
- Linux*) PLATFORM="Linux";;
- Darwin*) PLATFORM="macOS";;
- CYGWIN*|MINGW*|MSYS*) PLATFORM="Windows";;
- *) PLATFORM="Unknown";;
- esac
- # Colors for output (disable on Windows if not supported)
- if [ "$PLATFORM" = "Windows" ] && [ -z "$WT_SESSION" ] && [ -z "$ConEmuPID" ]; then
- # Basic Windows terminal without color support
- RED=''
- GREEN=''
- YELLOW=''
- BLUE=''
- MAGENTA=''
- CYAN=''
- BOLD=''
- NC=''
- else
- RED='\033[0;31m'
- GREEN='\033[0;32m'
- YELLOW='\033[1;33m'
- BLUE='\033[0;34m'
- MAGENTA='\033[0;35m'
- CYAN='\033[0;36m'
- BOLD='\033[1m'
- NC='\033[0m' # No Color
- fi
- # Configuration
- REPO_URL="https://github.com/darrenhinde/OpenAgentsControl"
- BRANCH="${OPENCODE_BRANCH:-main}" # Allow override via environment variable
- RAW_URL="https://raw.githubusercontent.com/darrenhinde/OpenAgentsControl/${BRANCH}"
- # Registry URL - supports local fallback for development
- # Priority: 1) REGISTRY_URL env var, 2) Local registry.json, 3) Remote GitHub
- if [ -n "$REGISTRY_URL" ]; then
- # Use explicitly set REGISTRY_URL (for testing)
- :
- elif [ -f "./registry.json" ]; then
- # Use local registry.json if it exists (for development)
- REGISTRY_URL="file://$(pwd)/registry.json"
- else
- # Default to remote GitHub registry
- REGISTRY_URL="${RAW_URL}/registry.json"
- fi
- INSTALL_DIR="${OPENCODE_INSTALL_DIR:-.opencode}" # Allow override via environment variable
- TEMP_DIR="/tmp/opencode-installer-$$"
- # Cleanup temp directory on exit (success or failure)
- trap 'rm -rf "$TEMP_DIR" 2>/dev/null || true' EXIT INT TERM
- # Global variables
- SELECTED_COMPONENTS=()
- INSTALL_MODE=""
- PROFILE=""
- NON_INTERACTIVE=false
- CUSTOM_INSTALL_DIR="" # Set via --install-dir argument
- #############################################################################
- # Utility Functions
- #############################################################################
- jq_exec() {
- local output
- output=$(jq -r "$@")
- local ret=$?
- printf "%s\n" "$output" | tr -d '\r'
- return $ret
- }
- print_header() {
- echo -e "${CYAN}${BOLD}"
- echo "╔════════════════════════════════════════════════════════════════╗"
- echo "║ ║"
- echo "║ OpenAgents Control Installer v1.0.0 ║"
- echo "║ ║"
- echo "╚════════════════════════════════════════════════════════════════╝"
- echo -e "${NC}"
- }
- print_success() {
- echo -e "${GREEN}✓${NC} $1"
- }
- print_error() {
- echo -e "${RED}✗${NC} $1"
- }
- print_info() {
- echo -e "${BLUE}ℹ${NC} $1"
- }
- print_warning() {
- echo -e "${YELLOW}⚠${NC} $1"
- }
- 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
- #############################################################################
- check_bash_version() {
- # Check bash version (need 3.2+)
- local bash_version="${BASH_VERSION%%.*}"
- if [ "$bash_version" -lt 3 ]; then
- echo "Error: This script requires Bash 3.2 or higher"
- echo "Current version: $BASH_VERSION"
- echo ""
- echo "Please upgrade bash or use a different shell:"
- echo " macOS: brew install bash"
- echo " Linux: Use your package manager to update bash"
- echo " Windows: Use Git Bash or WSL"
- exit 1
- fi
- }
- check_dependencies() {
- print_step "Checking dependencies..."
-
- local missing_deps=()
-
- if ! command -v curl &> /dev/null; then
- missing_deps+=("curl")
- fi
-
- if ! command -v jq &> /dev/null; then
- missing_deps+=("jq")
- fi
-
- if [ ${#missing_deps[@]} -ne 0 ]; then
- print_error "Missing required dependencies: ${missing_deps[*]}"
- echo ""
- echo "Please install them:"
- case "$PLATFORM" in
- macOS)
- echo " brew install ${missing_deps[*]}"
- ;;
- Linux)
- echo " Ubuntu/Debian: sudo apt-get install ${missing_deps[*]}"
- echo " Fedora/RHEL: sudo dnf install ${missing_deps[*]}"
- echo " Arch: sudo pacman -S ${missing_deps[*]}"
- ;;
- Windows)
- echo " Git Bash: Install via https://git-scm.com/"
- echo " WSL: sudo apt-get install ${missing_deps[*]}"
- echo " Scoop: scoop install ${missing_deps[*]}"
- ;;
- *)
- echo " Use your package manager to install: ${missing_deps[*]}"
- ;;
- esac
- exit 1
- fi
-
- print_success "All dependencies found"
- }
- #############################################################################
- # Registry Functions
- #############################################################################
- fetch_registry() {
- print_step "Fetching component registry..."
-
- mkdir -p "$TEMP_DIR"
-
- # Handle local file:// URLs
- if [[ "$REGISTRY_URL" == file://* ]]; then
- local local_path="${REGISTRY_URL#file://}"
- if [ -f "$local_path" ]; then
- cp "$local_path" "$TEMP_DIR/registry.json"
- print_success "Using local registry: $local_path"
- else
- print_error "Local registry not found: $local_path"
- exit 1
- fi
- else
- # Fetch from remote URL
- if ! curl -fsSL "$REGISTRY_URL" -o "$TEMP_DIR/registry.json"; then
- print_error "Failed to fetch registry from $REGISTRY_URL"
- exit 1
- fi
- print_success "Registry fetched successfully"
- fi
- }
- get_profile_components() {
- local profile=$1
- jq_exec ".profiles.${profile}.components[]?" "$TEMP_DIR/registry.json"
- }
- get_component_info() {
- local component_id=$1
- local component_type=$2
-
- if [ "$component_type" = "context" ] && [[ "$component_id" == */* ]]; then
- jq_exec "first(.components.contexts[]? | select(.path == \".opencode/context/${component_id}.md\"))" "$TEMP_DIR/registry.json"
- return
- fi
- jq_exec ".components.${component_type}[]? | select(.id == \"${component_id}\" or (.aliases // [] | index(\"${component_id}\")))" "$TEMP_DIR/registry.json"
- }
- resolve_component_path() {
- local component_type=$1
- local component_id=$2
- local registry_key
- registry_key=$(get_registry_key "$component_type")
- if [ "$component_type" = "context" ] && [[ "$component_id" == */* ]]; then
- # Try .md extension first (most context files), then fall back to the
- # path as-is for non-markdown files (e.g. paths.json). Fixes #251.
- local result
- result=$(jq_exec "first(.components.contexts[]? | select(.path == \".opencode/context/${component_id}.md\") | .path)" "$TEMP_DIR/registry.json")
- if [ -z "$result" ] || [ "$result" = "null" ]; then
- result=$(jq_exec "first(.components.contexts[]? | select(.path == \".opencode/context/${component_id}\") | .path)" "$TEMP_DIR/registry.json")
- fi
- echo "$result"
- return
- fi
- jq_exec ".components.${registry_key}[]? | select(.id == \"${component_id}\" or (.aliases // [] | index(\"${component_id}\"))) | .path" "$TEMP_DIR/registry.json"
- }
- # Helper function to get the correct registry key for a component type
- get_registry_key() {
- local type=$1
- # Most types are pluralized, but 'config' stays singular
- case "$type" in
- config) echo "config" ;;
- *) echo "${type}s" ;;
- esac
- }
- # Helper function to convert registry path to installation path
- # Registry paths are like ".opencode/agent/foo.md"
- # We need to replace ".opencode" with the actual INSTALL_DIR
- get_install_path() {
- local registry_path=$1
- # Strip leading .opencode/ if present
- local relative_path="${registry_path#.opencode/}"
- # Return INSTALL_DIR + relative path
- echo "${INSTALL_DIR}/${relative_path}"
- }
- expand_context_wildcard() {
- local pattern=$1
- local prefix="${pattern%%\**}"
- prefix="${prefix%/}"
- if [ -n "$prefix" ]; then
- prefix="${prefix}/"
- fi
- jq_exec ".components.contexts[]? | select(.path | startswith(\".opencode/context/${prefix}\")) | .path | sub(\"^\\\\.opencode/context/\"; \"\") | sub(\"\\\\.md$\"; \"\")" "$TEMP_DIR/registry.json"
- }
- expand_selected_components() {
- local expanded=()
- for comp in "${SELECTED_COMPONENTS[@]}"; do
- local type="${comp%%:*}"
- local id="${comp##*:}"
- if [[ "$id" == *"*"* ]]; then
- if [ "$type" != "context" ]; then
- print_warning "Wildcard only supported for context components: ${comp}"
- continue
- fi
- local matches
- matches=$(expand_context_wildcard "$id")
- if [ -z "$matches" ]; then
- print_warning "No contexts matched: ${comp}"
- continue
- fi
- while IFS= read -r match; do
- [ -n "$match" ] && expanded+=("context:${match}")
- done <<< "$matches"
- continue
- fi
- expanded+=("$comp")
- done
- local deduped=()
- for comp in "${expanded[@]}"; do
- local found=0
- for existing in "${deduped[@]}"; do
- if [ "$existing" = "$comp" ]; then
- found=1
- break
- fi
- done
- if [ "$found" -eq 0 ]; then
- deduped+=("$comp")
- fi
- done
- SELECTED_COMPONENTS=("${deduped[@]}")
- }
- resolve_dependencies() {
- local component=$1
- local type="${component%%:*}"
- local id="${component##*:}"
-
- # Get the correct registry key (handles singular/plural)
- local registry_key
- registry_key=$(get_registry_key "$type")
-
- # Get dependencies for this component
- local deps
- deps=$(jq_exec ".components.${registry_key}[] | select(.id == \"${id}\" or (.aliases // [] | index(\"${id}\"))) | .dependencies[]?" "$TEMP_DIR/registry.json" 2>/dev/null || echo "")
-
- if [ -n "$deps" ]; then
- for dep in $deps; do
- if [[ "$dep" == *"*"* ]]; then
- local dep_type="${dep%%:*}"
- local dep_id="${dep##*:}"
- if [ "$dep_type" = "context" ]; then
- local matched
- matched=$(expand_context_wildcard "$dep_id")
- if [ -z "$matched" ]; then
- print_warning "No contexts matched dependency: ${dep}"
- continue
- fi
- while IFS= read -r match; do
- local expanded_dep="context:${match}"
- local found=0
- for existing in "${SELECTED_COMPONENTS[@]}"; do
- if [ "$existing" = "$expanded_dep" ]; then
- found=1
- break
- fi
- done
- if [ "$found" -eq 0 ]; then
- SELECTED_COMPONENTS+=("$expanded_dep")
- resolve_dependencies "$expanded_dep"
- fi
- done <<< "$matched"
- continue
- fi
- fi
- # Add dependency if not already in list
- local found=0
- for existing in "${SELECTED_COMPONENTS[@]}"; do
- if [ "$existing" = "$dep" ]; then
- found=1
- break
- fi
- done
- if [ "$found" -eq 0 ]; then
- SELECTED_COMPONENTS+=("$dep")
- # Recursively resolve dependencies
- resolve_dependencies "$dep"
- fi
- done
- fi
- }
- #############################################################################
- # Installation Mode Selection
- #############################################################################
- check_interactive_mode() {
- # Check if stdin is a terminal (not piped from curl)
- if [ ! -t 0 ]; then
- print_header
- print_error "Interactive mode requires a terminal"
- echo ""
- echo "You're running this script in a pipe (e.g., curl | bash)"
- echo "For interactive mode, download the script first:"
- echo ""
- echo -e "${CYAN}# Download the script${NC}"
- echo "curl -fsSL https://raw.githubusercontent.com/darrenhinde/OpenAgentsControl/main/install.sh -o install.sh"
- echo ""
- echo -e "${CYAN}# Run interactively${NC}"
- echo "bash install.sh"
- echo ""
- echo "Or use a profile directly:"
- echo ""
- echo -e "${CYAN}# Quick install with profile${NC}"
- echo "curl -fsSL https://raw.githubusercontent.com/darrenhinde/OpenAgentsControl/main/install.sh | bash -s essential"
- echo ""
- echo "Available profiles: essential, developer, business, full, advanced"
- echo ""
- cleanup_and_exit 1
- fi
- }
- show_install_location_menu() {
- check_interactive_mode
-
- clear
- print_header
-
- local global_path
- 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 -e " ${CYAN}C:/Users/username/my-agents${NC} or ${CYAN}~/my-agents${NC}"
- ;;
- *)
- echo -e " ${CYAN}/home/username/my-agents${NC} or ${CYAN}~/my-agents${NC}"
- ;;
- esac
- echo ""
- echo " 4) Back / Exit"
- echo ""
- read -r -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 -r -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
- normalized_path=$(normalize_and_validate_path "$custom_path")
-
- if ! normalize_and_validate_path "$custom_path" > /dev/null; then
- print_error "Invalid path"
- sleep 2
- show_install_location_menu
- return
- fi
-
- if ! validate_install_path "$normalized_path"; then
- echo ""
- read -r -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
-
- clear
- print_header
-
- echo -e "${BOLD}Choose installation mode:${NC}\n"
- echo " 1) Quick Install (Choose a profile)"
- echo " 2) Custom Install (Pick individual components)"
- echo " 3) List Available Components"
- echo " 4) Exit"
- echo ""
- read -r -p "Enter your choice [1-4]: " choice
-
- case $choice in
- 1) INSTALL_MODE="profile" ;;
- 2) INSTALL_MODE="custom" ;;
- 3) list_components; read -r -p "Press Enter to continue..."; show_main_menu ;;
- 4) cleanup_and_exit 0 ;;
- *) print_error "Invalid choice"; sleep 2; show_main_menu ;;
- esac
- }
- #############################################################################
- # Profile Installation
- #############################################################################
- show_profile_menu() {
- clear
- print_header
-
- echo -e "${BOLD}Available Installation Profiles:${NC}\n"
-
- # Essential profile
- local essential_name
- essential_name=$(jq_exec '.profiles.essential.name' "$TEMP_DIR/registry.json")
- local essential_desc
- essential_desc=$(jq_exec '.profiles.essential.description' "$TEMP_DIR/registry.json")
- local essential_count
- essential_count=$(jq_exec '.profiles.essential.components | length' "$TEMP_DIR/registry.json")
- echo -e " ${GREEN}1) ${essential_name}${NC}"
- echo -e " ${essential_desc}"
- echo -e " Components: ${essential_count}\n"
-
- # Developer profile
- local dev_desc
- dev_desc=$(jq_exec '.profiles.developer.description' "$TEMP_DIR/registry.json")
- local dev_count
- dev_count=$(jq_exec '.profiles.developer.components | length' "$TEMP_DIR/registry.json")
- local dev_badge
- dev_badge=$(jq_exec '.profiles.developer.badge // ""' "$TEMP_DIR/registry.json")
- if [ -n "$dev_badge" ]; then
- echo -e " ${BLUE}2) Developer ${GREEN}[${dev_badge}]${NC}"
- else
- echo -e " ${BLUE}2) Developer${NC}"
- fi
- echo -e " ${dev_desc}"
- echo -e " Components: ${dev_count}\n"
-
- # Business profile
- local business_name
- business_name=$(jq_exec '.profiles.business.name' "$TEMP_DIR/registry.json")
- local business_desc
- business_desc=$(jq_exec '.profiles.business.description' "$TEMP_DIR/registry.json")
- local business_count
- business_count=$(jq_exec '.profiles.business.components | length' "$TEMP_DIR/registry.json")
- echo -e " ${CYAN}3) ${business_name}${NC}"
- echo -e " ${business_desc}"
- echo -e " Components: ${business_count}\n"
-
- # Full profile
- local full_name
- full_name=$(jq_exec '.profiles.full.name' "$TEMP_DIR/registry.json")
- local full_desc
- full_desc=$(jq_exec '.profiles.full.description' "$TEMP_DIR/registry.json")
- local full_count
- full_count=$(jq_exec '.profiles.full.components | length' "$TEMP_DIR/registry.json")
- echo -e " ${MAGENTA}4) ${full_name}${NC}"
- echo -e " ${full_desc}"
- echo -e " Components: ${full_count}\n"
-
- # Advanced profile
- local adv_name
- adv_name=$(jq_exec '.profiles.advanced.name' "$TEMP_DIR/registry.json")
- local adv_desc
- adv_desc=$(jq_exec '.profiles.advanced.description' "$TEMP_DIR/registry.json")
- local adv_count
- adv_count=$(jq_exec '.profiles.advanced.components | length' "$TEMP_DIR/registry.json")
- echo -e " ${YELLOW}5) ${adv_name}${NC}"
- echo -e " ${adv_desc}"
- echo -e " Components: ${adv_count}\n"
-
- echo " 6) Back to main menu"
- echo ""
- read -r -p "Enter your choice [1-6]: " choice
-
- case $choice in
- 1) PROFILE="essential" ;;
- 2) PROFILE="developer" ;;
- 3) PROFILE="business" ;;
- 4) PROFILE="full" ;;
- 5) PROFILE="advanced" ;;
- 6) show_main_menu; return ;;
- *) print_error "Invalid choice"; sleep 2; show_profile_menu; return ;;
- esac
-
- # Load profile components (compatible with bash 3.2+)
- SELECTED_COMPONENTS=()
- local temp_file="$TEMP_DIR/components.tmp"
- get_profile_components "$PROFILE" > "$temp_file"
- while IFS= read -r component; do
- [ -n "$component" ] && SELECTED_COMPONENTS+=("$component")
- done < "$temp_file"
- expand_selected_components
-
- # Resolve dependencies for profile installs
- print_step "Resolving dependencies..."
- local original_count=${#SELECTED_COMPONENTS[@]}
- for comp in "${SELECTED_COMPONENTS[@]}"; do
- resolve_dependencies "$comp"
- done
-
- local new_count=${#SELECTED_COMPONENTS[@]}
- if [ "$new_count" -gt "$original_count" ]; then
- local added=$((new_count - original_count))
- print_info "Added $added dependencies"
- fi
-
- show_installation_preview
- }
- #############################################################################
- # Custom Component Selection
- #############################################################################
- show_custom_menu() {
- clear
- print_header
-
- echo -e "${BOLD}Select component categories to install:${NC}\n"
- echo "Use space to toggle, Enter to continue"
- echo ""
-
- local categories=("agents" "subagents" "commands" "tools" "plugins" "skills" "contexts" "config")
- local selected_categories=()
-
- # Simple selection (for now, we'll make it interactive later)
- echo "Available categories:"
- for i in "${!categories[@]}"; do
- local cat="${categories[$i]}"
- local count
- count=$(jq_exec ".components.${cat} | length" "$TEMP_DIR/registry.json")
- local cat_display
- cat_display=$(echo "$cat" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
- echo " $((i+1))) ${cat_display} (${count} available)"
- done
- echo " $((${#categories[@]}+1))) Select All"
- echo " $((${#categories[@]}+2))) Continue to component selection"
- echo " $((${#categories[@]}+3))) Back to main menu"
- echo ""
-
- read -r -p "Enter category numbers (space-separated) or option: " -a selections
-
- for sel in "${selections[@]}"; do
- if [ "$sel" -eq $((${#categories[@]}+1)) ]; then
- selected_categories=("${categories[@]}")
- break
- elif [ "$sel" -eq $((${#categories[@]}+2)) ]; then
- break
- elif [ "$sel" -eq $((${#categories[@]}+3)) ]; then
- show_main_menu
- return
- elif [ "$sel" -ge 1 ] && [ "$sel" -le ${#categories[@]} ]; then
- selected_categories+=("${categories[$((sel-1))]}")
- fi
- done
-
- if [ ${#selected_categories[@]} -eq 0 ]; then
- print_warning "No categories selected"
- sleep 2
- show_custom_menu
- return
- fi
-
- show_component_selection "${selected_categories[@]}"
- }
- show_component_selection() {
- local categories=("$@")
- clear
- print_header
-
- echo -e "${BOLD}Select components to install:${NC}\n"
-
- local all_components=()
- local component_details=()
-
- for category in "${categories[@]}"; do
- local cat_display
- cat_display=$(echo "$category" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
- echo -e "${CYAN}${BOLD}${cat_display}:${NC}"
-
- local components
- components=$(jq_exec ".components.${category}[]? | .id" "$TEMP_DIR/registry.json")
-
- local idx=1
- while IFS= read -r comp_id; do
- local comp_name
- comp_name=$(jq_exec ".components.${category}[]? | select(.id == \"${comp_id}\") | .name" "$TEMP_DIR/registry.json")
- local comp_desc
- comp_desc=$(jq_exec ".components.${category}[]? | select(.id == \"${comp_id}\") | .description" "$TEMP_DIR/registry.json")
-
- echo " ${idx}) ${comp_name}"
- echo " ${comp_desc}"
-
- all_components+=("${category}:${comp_id}")
- component_details+=("${comp_name}|${comp_desc}")
-
- idx=$((idx+1))
- done <<< "$components"
-
- echo ""
- done
-
- echo "Enter component numbers (space-separated), 'all' for all, or 'done' to continue:"
- read -r -a selections
-
- for sel in "${selections[@]}"; do
- if [ "$sel" = "all" ]; then
- SELECTED_COMPONENTS=("${all_components[@]}")
- break
- elif [ "$sel" = "done" ]; then
- break
- elif [ "$sel" -ge 1 ] && [ "$sel" -le ${#all_components[@]} ]; then
- SELECTED_COMPONENTS+=("${all_components[$((sel-1))]}")
- fi
- done
-
- if [ ${#SELECTED_COMPONENTS[@]} -eq 0 ]; then
- print_warning "No components selected"
- sleep 2
- show_custom_menu
- return
- fi
-
- # Resolve dependencies
- print_step "Resolving dependencies..."
- local original_count=${#SELECTED_COMPONENTS[@]}
- for comp in "${SELECTED_COMPONENTS[@]}"; do
- resolve_dependencies "$comp"
- done
-
- if [ ${#SELECTED_COMPONENTS[@]} -gt "$original_count" ]; then
- print_info "Added $((${#SELECTED_COMPONENTS[@]} - original_count)) dependencies"
- fi
-
- show_installation_preview
- }
- #############################################################################
- # Installation Preview & Confirmation
- #############################################################################
- show_installation_preview() {
- # Only clear screen in interactive mode
- if [ "$NON_INTERACTIVE" != true ]; then
- clear
- fi
- print_header
-
- echo -e "${BOLD}Installation Preview${NC}\n"
-
- if [ -n "$PROFILE" ]; then
- echo -e "Profile: ${GREEN}${PROFILE}${NC}"
- else
- 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
- local agents=()
- local subagents=()
- local commands=()
- local tools=()
- local plugins=()
- local skills=()
- local contexts=()
- local configs=()
-
- for comp in "${SELECTED_COMPONENTS[@]}"; do
- local type="${comp%%:*}"
- case $type in
- agent) agents+=("$comp") ;;
- subagent) subagents+=("$comp") ;;
- command) commands+=("$comp") ;;
- tool) tools+=("$comp") ;;
- plugin) plugins+=("$comp") ;;
- skill) skills+=("$comp") ;;
- context) contexts+=("$comp") ;;
- config) configs+=("$comp") ;;
- esac
- done
-
- [ ${#agents[@]} -gt 0 ] && echo -e "${CYAN}Agents (${#agents[@]}):${NC} ${agents[*]##*:}"
- [ ${#subagents[@]} -gt 0 ] && echo -e "${CYAN}Subagents (${#subagents[@]}):${NC} ${subagents[*]##*:}"
- [ ${#commands[@]} -gt 0 ] && echo -e "${CYAN}Commands (${#commands[@]}):${NC} ${commands[*]##*:}"
- [ ${#tools[@]} -gt 0 ] && echo -e "${CYAN}Tools (${#tools[@]}):${NC} ${tools[*]##*:}"
- [ ${#plugins[@]} -gt 0 ] && echo -e "${CYAN}Plugins (${#plugins[@]}):${NC} ${plugins[*]##*:}"
- [ ${#skills[@]} -gt 0 ] && echo -e "${CYAN}Skills (${#skills[@]}):${NC} ${skills[*]##*:}"
- [ ${#contexts[@]} -gt 0 ] && echo -e "${CYAN}Contexts (${#contexts[@]}):${NC} ${contexts[*]##*:}"
- [ ${#configs[@]} -gt 0 ] && echo -e "${CYAN}Config (${#configs[@]}):${NC} ${configs[*]##*:}"
-
- echo ""
-
- # Skip confirmation if profile was provided via command line
- if [ "$NON_INTERACTIVE" = true ]; then
- print_info "Installing automatically (profile specified)..."
- perform_installation
- else
- read -r -p "Proceed with installation? [Y/n]: " confirm
-
- if [[ $confirm =~ ^[Nn] ]]; then
- print_info "Installation cancelled"
- cleanup_and_exit 0
- fi
-
- perform_installation
- fi
- }
- #############################################################################
- # Collision Detection
- #############################################################################
- show_collision_report() {
- local collision_count=$1
- shift
- local collisions=("$@")
-
- echo ""
- print_warning "Found ${collision_count} file collision(s):"
- echo ""
-
- # Group by type
- local agents=()
- local subagents=()
- local commands=()
- local tools=()
- local plugins=()
- local skills=()
- local contexts=()
- local configs=()
-
- for file in "${collisions[@]}"; do
- # Skip empty entries
- [ -z "$file" ] && continue
-
- if [[ $file == *"/agent/subagents/"* ]]; then
- subagents+=("$file")
- elif [[ $file == *"/agent/"* ]]; then
- agents+=("$file")
- elif [[ $file == *"/command/"* ]]; then
- commands+=("$file")
- elif [[ $file == *"/tool/"* ]]; then
- tools+=("$file")
- elif [[ $file == *"/plugin/"* ]]; then
- plugins+=("$file")
- elif [[ $file == *"/skills/"* ]]; then
- skills+=("$file")
- elif [[ $file == *"/context/"* ]]; then
- contexts+=("$file")
- else
- configs+=("$file")
- fi
- done
-
- # Display grouped collisions
- [ ${#agents[@]} -gt 0 ] && echo -e "${YELLOW} Agents (${#agents[@]}):${NC}" && printf ' %s\n' "${agents[@]}"
- [ ${#subagents[@]} -gt 0 ] && echo -e "${YELLOW} Subagents (${#subagents[@]}):${NC}" && printf ' %s\n' "${subagents[@]}"
- [ ${#commands[@]} -gt 0 ] && echo -e "${YELLOW} Commands (${#commands[@]}):${NC}" && printf ' %s\n' "${commands[@]}"
- [ ${#tools[@]} -gt 0 ] && echo -e "${YELLOW} Tools (${#tools[@]}):${NC}" && printf ' %s\n' "${tools[@]}"
- [ ${#plugins[@]} -gt 0 ] && echo -e "${YELLOW} Plugins (${#plugins[@]}):${NC}" && printf ' %s\n' "${plugins[@]}"
- [ ${#skills[@]} -gt 0 ] && echo -e "${YELLOW} Skills (${#skills[@]}):${NC}" && printf ' %s\n' "${skills[@]}"
- [ ${#contexts[@]} -gt 0 ] && echo -e "${YELLOW} Context (${#contexts[@]}):${NC}" && printf ' %s\n' "${contexts[@]}"
- [ ${#configs[@]} -gt 0 ] && echo -e "${YELLOW} Config (${#configs[@]}):${NC}" && printf ' %s\n' "${configs[@]}"
-
- echo ""
- }
- get_install_strategy() {
- 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 -r -p "Enter your choice [1-4]: " strategy_choice
-
- case $strategy_choice in
- 1) echo "skip" ;;
- 2)
- echo "" >&2
- print_warning "This will overwrite existing files. Your changes will be lost!"
- read -r -p "Are you sure? Type 'yes' to confirm: " confirm
- if [ "$confirm" = "yes" ]; then
- echo "overwrite"
- else
- echo "cancel"
- fi
- ;;
- 3) echo "backup" ;;
- 4) echo "cancel" ;;
- *) echo "cancel" ;;
- esac
- }
- #############################################################################
- # Installation
- #############################################################################
- perform_installation() {
- print_step "Preparing installation..."
-
- # Create base directory only - subdirectories created on-demand when files are installed
- mkdir -p "$INSTALL_DIR"
-
- # Check for collisions
- local collisions=()
- for comp in "${SELECTED_COMPONENTS[@]}"; do
- local type="${comp%%:*}"
- local id="${comp##*:}"
- local registry_key
- registry_key=$(get_registry_key "$type")
- local path
- path=$(resolve_component_path "$type" "$id")
-
- if [ -n "$path" ] && [ "$path" != "null" ]; then
- local install_path
- install_path=$(get_install_path "$path")
- if [ -f "$install_path" ]; then
- collisions+=("$install_path")
- fi
- fi
- done
-
- # Determine installation strategy
- local install_strategy="fresh"
-
- if [ ${#collisions[@]} -gt 0 ]; then
- # In non-interactive mode, use default strategy (skip existing files)
- if [ "$NON_INTERACTIVE" = true ]; then
- print_info "Found ${#collisions[@]} existing file(s) - using 'skip' strategy (non-interactive mode)"
- print_info "To overwrite, download script and run interactively, or delete existing files first"
- install_strategy="skip"
- else
- show_collision_report ${#collisions[@]} "${collisions[@]}"
- install_strategy=$(get_install_strategy)
-
- if [ "$install_strategy" = "cancel" ]; then
- print_info "Installation cancelled by user"
- cleanup_and_exit 0
- fi
- fi
-
- # Handle backup strategy
- if [ "$install_strategy" = "backup" ]; then
- local backup_dir
- backup_dir="${INSTALL_DIR}.backup.$(date +%Y%m%d-%H%M%S)"
- print_step "Creating backup..."
-
- # Only backup files that will be overwritten
- local backup_count=0
- for file in "${collisions[@]}"; do
- if [ -f "$file" ]; then
- local backup_file="${backup_dir}/${file}"
- mkdir -p "$(dirname "$backup_file")"
- if cp "$file" "$backup_file" 2>/dev/null; then
- backup_count=$((backup_count + 1))
- else
- print_warning "Failed to backup: $file"
- fi
- fi
- done
-
- if [ $backup_count -gt 0 ]; then
- print_success "Backed up ${backup_count} file(s) to $backup_dir"
- install_strategy="overwrite" # Now we can overwrite
- else
- print_error "Backup failed. Installation cancelled."
- cleanup_and_exit 1
- fi
- fi
- fi
-
- # Perform installation
- print_step "Installing components..."
-
- local installed=0
- local skipped=0
- local failed=0
-
- for comp in "${SELECTED_COMPONENTS[@]}"; do
- local type="${comp%%:*}"
- local id="${comp##*:}"
-
- # Get the correct registry key (handles singular/plural)
- local registry_key
- registry_key=$(get_registry_key "$type")
-
- # Get component path
- local path
- path=$(resolve_component_path "$type" "$id")
-
- if [ -z "$path" ] || [ "$path" = "null" ]; then
- print_warning "Could not find path for ${comp}"
- failed=$((failed + 1))
- continue
- fi
-
- # Check if component has additional files (for skills)
- local files_array
- files_array=$(jq_exec ".components.${registry_key}[]? | select(.id == \"${id}\") | .files[]?" "$TEMP_DIR/registry.json")
-
- if [ -n "$files_array" ]; then
- # Component has multiple files - download all of them
- local component_installed=0
- local component_failed=0
-
- while IFS= read -r file_path; do
- [ -z "$file_path" ] && continue
-
- local dest
- dest=$(get_install_path "$file_path")
-
- # Check if file exists and we're in skip mode
- if [ -f "$dest" ] && [ "$install_strategy" = "skip" ]; then
- continue
- fi
-
- # Download file
- local url="${RAW_URL}/${file_path}"
- mkdir -p "$(dirname "$dest")"
-
- if curl -fsSL "$url" -o "$dest"; then
- # Transform paths for global installation
- if [[ "$INSTALL_DIR" != ".opencode" ]] && [[ "$INSTALL_DIR" != *"/.opencode" ]]; then
- local expanded_path="${INSTALL_DIR/#\~/$HOME}"
- sed -i.bak -e "s|@\.opencode/context/|@${expanded_path}/context/|g" \
- -e "s|\.opencode/context|${expanded_path}/context|g" "$dest" 2>/dev/null || true
- rm -f "${dest}.bak" 2>/dev/null || true
- fi
- component_installed=$((component_installed + 1))
- else
- component_failed=$((component_failed + 1))
- fi
- done <<< "$files_array"
-
- if [ $component_failed -eq 0 ]; then
- print_success "Installed ${type}: ${id} (${component_installed} files)"
- installed=$((installed + 1))
- else
- print_error "Failed to install ${type}: ${id} (${component_failed} files failed)"
- failed=$((failed + 1))
- fi
- else
- # Single file component - original logic
- local dest
- dest=$(get_install_path "$path")
-
- # Check if file exists before we install (for proper messaging)
- local file_existed=false
- if [ -f "$dest" ]; then
- file_existed=true
- fi
-
- # 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 + 1))
- continue
- fi
-
- # Download component
- local url="${RAW_URL}/${path}"
-
- # Create parent directory if needed
- mkdir -p "$(dirname "$dest")"
-
- if curl -fsSL "$url" -o "$dest"; then
- # Transform paths for global installation (any non-local path)
- # Local paths: .opencode or */.opencode
- if [[ "$INSTALL_DIR" != ".opencode" ]] && [[ "$INSTALL_DIR" != *"/.opencode" ]]; then
- # Expand tilde and get absolute path for transformation
- local expanded_path="${INSTALL_DIR/#\~/$HOME}"
- # Transform @.opencode/context/ references to actual install path
- sed -i.bak -e "s|@\.opencode/context/|@${expanded_path}/context/|g" \
- -e "s|\.opencode/context|${expanded_path}/context|g" "$dest" 2>/dev/null || true
- rm -f "${dest}.bak" 2>/dev/null || true
- fi
-
- # Show appropriate message based on whether file existed before
- if [ "$file_existed" = true ]; then
- print_success "Updated ${type}: ${id}"
- else
- print_success "Installed ${type}: ${id}"
- fi
- installed=$((installed + 1))
- else
- print_error "Failed to install ${type}: ${id}"
- failed=$((failed + 1))
- fi
- fi
- done
-
- # Handle additional paths for advanced profile
- if [ "$PROFILE" = "advanced" ]; then
- local additional_paths
- additional_paths=$(jq_exec '.profiles.advanced.additionalPaths[]?' "$TEMP_DIR/registry.json")
- if [ -n "$additional_paths" ]; then
- print_step "Installing additional paths..."
- while IFS= read -r path; do
- # For directories, we'd need to recursively download
- # For now, just note them
- print_info "Additional path: $path (manual download required)"
- done <<< "$additional_paths"
- fi
- fi
-
- echo ""
- print_success "Installation complete!"
- echo -e " Installed: ${GREEN}${installed}${NC}"
- [ $skipped -gt 0 ] && echo -e " Skipped: ${CYAN}${skipped}${NC}"
- [ $failed -gt 0 ] && echo -e " Failed: ${RED}${failed}${NC}"
-
- show_post_install
- }
- #############################################################################
- # Post-Installation
- #############################################################################
- show_post_install() {
- echo ""
- print_step "Next Steps"
-
- 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 -e " ${CYAN}cp env.example .env${NC}"
- echo "3. Start using OpenCode agents:"
- else
- echo "2. Start using OpenCode agents:"
- fi
- echo -e " ${CYAN}opencode${NC}"
- echo ""
-
- # Show installation location info
- print_info "Installation directory: ${CYAN}${INSTALL_DIR}${NC}"
-
- # Check for backup directories
- local has_backup=0
- local backup_dir
- local backup_dirs=()
- shopt -s nullglob
- backup_dirs=("${INSTALL_DIR}.backup."*)
- shopt -u nullglob
- for backup_dir in "${backup_dirs[@]}"; do
- if [ -d "$backup_dir" ]; then
- has_backup=1
- break
- fi
- done
- if [ "$has_backup" -eq 1 ]; then
- print_info "Backup created - you can restore files from ${INSTALL_DIR}.backup.* if needed"
- fi
-
- print_info "Documentation: ${REPO_URL}"
- echo ""
-
- cleanup_and_exit 0
- }
- #############################################################################
- # Component Listing
- #############################################################################
- list_components() {
- clear || true
- print_header
-
- echo -e "${BOLD}Available Components${NC}\n"
-
- local categories=("agents" "subagents" "commands" "tools" "plugins" "skills" "contexts")
-
- for category in "${categories[@]}"; do
- local cat_display
- cat_display=$(echo "$category" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
- echo -e "${CYAN}${BOLD}${cat_display}:${NC}"
-
- local components
- components=$(jq_exec ".components.${category}[]? | \"\(.id)|\(.name)|\(.description)\"" "$TEMP_DIR/registry.json")
-
- while IFS='|' read -r id name desc; do
- echo -e " ${GREEN}${name}${NC} (${id})"
- echo -e " ${desc}"
- done <<< "$components"
-
- echo ""
- done
- }
- #############################################################################
- # Cleanup
- #############################################################################
- cleanup_and_exit() {
- rm -rf "$TEMP_DIR"
- exit "$1"
- }
- trap 'cleanup_and_exit 1' INT TERM
- #############################################################################
- # Main
- #############################################################################
- main() {
- # Parse command line arguments
- while [ $# -gt 0 ]; do
- case "$1" in
- --install-dir=*)
- CUSTOM_INSTALL_DIR="${1#*=}"
- # Basic validation - check not empty
- if [ -z "$CUSTOM_INSTALL_DIR" ]; then
- echo "Error: --install-dir requires a non-empty path"
- exit 1
- fi
- 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 -e " ${CYAN}# Interactive mode (choose location and components)${NC}"
- echo " $0"
- echo ""
- echo -e " ${CYAN}# Quick install with default location (.opencode/)${NC}"
- echo " $0 developer"
- echo ""
- echo -e " ${CYAN}# Install to global location (Linux/macOS)${NC}"
- echo " $0 developer --install-dir ~/.config/opencode"
- echo ""
- echo -e " ${CYAN}# Install to global location (Windows Git Bash)${NC}"
- echo " $0 developer --install-dir ~/.config/opencode"
- echo ""
- echo -e " ${CYAN}# Install to custom location${NC}"
- echo " $0 essential --install-dir ~/my-agents"
- echo ""
- echo -e " ${CYAN}# Using environment variable${NC}"
- echo " export OPENCODE_INSTALL_DIR=~/.config/opencode"
- echo " $0 developer"
- echo ""
- echo -e " ${CYAN}# Install from URL (non-interactive)${NC}"
- echo " curl -fsSL https://raw.githubusercontent.com/darrenhinde/OpenAgentsControl/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
- if normalize_and_validate_path "$CUSTOM_INSTALL_DIR" > /dev/null; then
- normalized_path=$(normalize_and_validate_path "$CUSTOM_INSTALL_DIR")
- 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
- fetch_registry
-
- if [ -n "$PROFILE" ]; then
- # Non-interactive mode (compatible with bash 3.2+)
- SELECTED_COMPONENTS=()
- local temp_file="$TEMP_DIR/components.tmp"
- get_profile_components "$PROFILE" > "$temp_file"
- while IFS= read -r component; do
- [ -n "$component" ] && SELECTED_COMPONENTS+=("$component")
- done < "$temp_file"
- expand_selected_components
- # Resolve dependencies for profile installs
- print_step "Resolving dependencies..."
- local original_count=${#SELECTED_COMPONENTS[@]}
- for comp in "${SELECTED_COMPONENTS[@]}"; do
- resolve_dependencies "$comp"
- done
- local new_count=${#SELECTED_COMPONENTS[@]}
- if [ "$new_count" -gt "$original_count" ]; then
- local added=$((new_count - original_count))
- print_info "Added $added dependencies"
- fi
- show_installation_preview
- else
- # Interactive mode - show location menu first
- show_install_location_menu
- show_main_menu
-
- if [ "$INSTALL_MODE" = "profile" ]; then
- show_profile_menu
- elif [ "$INSTALL_MODE" = "custom" ]; then
- show_custom_menu
- fi
- fi
- }
- main "$@"
|