* fix(install): resolve non-.md context paths like paths.json (closes #251) resolve_component_path() was appending .md to all slash-containing context IDs, so context:core/config/paths.json was looked up as paths.json.md and returned empty, triggering the 'Could not find path' warning on install. Now tries the .md path first (preserving existing behaviour for all markdown context files), then falls back to the bare path for non-markdown files. * fix(shellcheck): use read -ra for word-split array assignment (SC2206) Replace local arr=($var) with read -ra arr <<< "$var" in install-context.sh to satisfy ShellCheck SC2206 and unblock CI.
1500 lines
51 KiB
Bash
Executable file
1500 lines
51 KiB
Bash
Executable file
#!/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 "$@"
|