install.sh 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168
  1. #!/usr/bin/env bash
  2. #############################################################################
  3. # OpenAgents Installer
  4. # Interactive installer for OpenCode agents, commands, tools, and plugins
  5. #
  6. # Compatible with:
  7. # - macOS (bash 3.2+)
  8. # - Linux (bash 3.2+)
  9. # - Windows (Git Bash, WSL)
  10. #############################################################################
  11. set -e
  12. # Detect platform
  13. PLATFORM="$(uname -s)"
  14. case "$PLATFORM" in
  15. Linux*) PLATFORM="Linux";;
  16. Darwin*) PLATFORM="macOS";;
  17. CYGWIN*|MINGW*|MSYS*) PLATFORM="Windows";;
  18. *) PLATFORM="Unknown";;
  19. esac
  20. # Colors for output (disable on Windows if not supported)
  21. if [ "$PLATFORM" = "Windows" ] && [ -z "$WT_SESSION" ] && [ -z "$ConEmuPID" ]; then
  22. # Basic Windows terminal without color support
  23. RED=''
  24. GREEN=''
  25. YELLOW=''
  26. BLUE=''
  27. MAGENTA=''
  28. CYAN=''
  29. BOLD=''
  30. NC=''
  31. else
  32. RED='\033[0;31m'
  33. GREEN='\033[0;32m'
  34. YELLOW='\033[1;33m'
  35. BLUE='\033[0;34m'
  36. MAGENTA='\033[0;35m'
  37. CYAN='\033[0;36m'
  38. BOLD='\033[1m'
  39. NC='\033[0m' # No Color
  40. fi
  41. # Configuration
  42. REPO_URL="https://github.com/darrenhinde/OpenAgents"
  43. BRANCH="${OPENCODE_BRANCH:-main}" # Allow override via environment variable
  44. RAW_URL="https://raw.githubusercontent.com/darrenhinde/OpenAgents/${BRANCH}"
  45. REGISTRY_URL="${RAW_URL}/registry.json"
  46. INSTALL_DIR="${OPENCODE_INSTALL_DIR:-.opencode}" # Allow override via environment variable
  47. TEMP_DIR="/tmp/opencode-installer-$$"
  48. # Global variables
  49. SELECTED_COMPONENTS=()
  50. INSTALL_MODE=""
  51. PROFILE=""
  52. NON_INTERACTIVE=false
  53. CUSTOM_INSTALL_DIR="" # Set via --install-dir argument
  54. #############################################################################
  55. # Utility Functions
  56. #############################################################################
  57. print_header() {
  58. echo -e "${CYAN}${BOLD}"
  59. echo "╔════════════════════════════════════════════════════════════════╗"
  60. echo "║ ║"
  61. echo "║ OpenAgents Installer v1.0.0 ║"
  62. echo "║ ║"
  63. echo "╚════════════════════════════════════════════════════════════════╝"
  64. echo -e "${NC}"
  65. }
  66. print_success() {
  67. echo -e "${GREEN}✓${NC} $1"
  68. }
  69. print_error() {
  70. echo -e "${RED}✗${NC} $1"
  71. }
  72. print_info() {
  73. echo -e "${BLUE}ℹ${NC} $1"
  74. }
  75. print_warning() {
  76. echo -e "${YELLOW}⚠${NC} $1"
  77. }
  78. print_step() {
  79. echo -e "\n${MAGENTA}${BOLD}▶${NC} $1\n"
  80. }
  81. #############################################################################
  82. # Path Handling (Cross-Platform)
  83. #############################################################################
  84. normalize_and_validate_path() {
  85. local input_path="$1"
  86. local normalized_path
  87. # Handle empty path
  88. if [ -z "$input_path" ]; then
  89. echo ""
  90. return 1
  91. fi
  92. # Expand tilde to $HOME (works on Linux, macOS, Windows Git Bash)
  93. if [[ $input_path == ~* ]]; then
  94. normalized_path="${HOME}${input_path:1}"
  95. else
  96. normalized_path="$input_path"
  97. fi
  98. # Convert backslashes to forward slashes (Windows compatibility)
  99. normalized_path="${normalized_path//\\//}"
  100. # Remove trailing slashes
  101. normalized_path="${normalized_path%/}"
  102. # If path is relative, make it absolute based on current directory
  103. if [[ ! "$normalized_path" = /* ]] && [[ ! "$normalized_path" =~ ^[A-Za-z]: ]]; then
  104. normalized_path="$(pwd)/${normalized_path}"
  105. fi
  106. echo "$normalized_path"
  107. return 0
  108. }
  109. validate_install_path() {
  110. local path="$1"
  111. local parent_dir
  112. # Get parent directory
  113. parent_dir="$(dirname "$path")"
  114. # Check if parent directory exists
  115. if [ ! -d "$parent_dir" ]; then
  116. print_error "Parent directory does not exist: $parent_dir"
  117. return 1
  118. fi
  119. # Check if parent directory is writable
  120. if [ ! -w "$parent_dir" ]; then
  121. print_error "No write permission for directory: $parent_dir"
  122. return 1
  123. fi
  124. # If target directory exists, check if it's writable
  125. if [ -d "$path" ] && [ ! -w "$path" ]; then
  126. print_error "No write permission for directory: $path"
  127. return 1
  128. fi
  129. return 0
  130. }
  131. get_global_install_path() {
  132. # Return platform-appropriate global installation path
  133. case "$PLATFORM" in
  134. macOS)
  135. # macOS: Use XDG standard (consistent with Linux)
  136. echo "${HOME}/.config/opencode"
  137. ;;
  138. Linux)
  139. echo "${HOME}/.config/opencode"
  140. ;;
  141. Windows)
  142. # Windows Git Bash/WSL: Use same as Linux
  143. echo "${HOME}/.config/opencode"
  144. ;;
  145. *)
  146. echo "${HOME}/.config/opencode"
  147. ;;
  148. esac
  149. }
  150. #############################################################################
  151. # Dependency Checks
  152. #############################################################################
  153. check_bash_version() {
  154. # Check bash version (need 3.2+)
  155. local bash_version="${BASH_VERSION%%.*}"
  156. if [ "$bash_version" -lt 3 ]; then
  157. echo "Error: This script requires Bash 3.2 or higher"
  158. echo "Current version: $BASH_VERSION"
  159. echo ""
  160. echo "Please upgrade bash or use a different shell:"
  161. echo " macOS: brew install bash"
  162. echo " Linux: Use your package manager to update bash"
  163. echo " Windows: Use Git Bash or WSL"
  164. exit 1
  165. fi
  166. }
  167. check_dependencies() {
  168. print_step "Checking dependencies..."
  169. local missing_deps=()
  170. if ! command -v curl &> /dev/null; then
  171. missing_deps+=("curl")
  172. fi
  173. if ! command -v jq &> /dev/null; then
  174. missing_deps+=("jq")
  175. fi
  176. if [ ${#missing_deps[@]} -ne 0 ]; then
  177. print_error "Missing required dependencies: ${missing_deps[*]}"
  178. echo ""
  179. echo "Please install them:"
  180. case "$PLATFORM" in
  181. macOS)
  182. echo " brew install ${missing_deps[*]}"
  183. ;;
  184. Linux)
  185. echo " Ubuntu/Debian: sudo apt-get install ${missing_deps[*]}"
  186. echo " Fedora/RHEL: sudo dnf install ${missing_deps[*]}"
  187. echo " Arch: sudo pacman -S ${missing_deps[*]}"
  188. ;;
  189. Windows)
  190. echo " Git Bash: Install via https://git-scm.com/"
  191. echo " WSL: sudo apt-get install ${missing_deps[*]}"
  192. echo " Scoop: scoop install ${missing_deps[*]}"
  193. ;;
  194. *)
  195. echo " Use your package manager to install: ${missing_deps[*]}"
  196. ;;
  197. esac
  198. exit 1
  199. fi
  200. print_success "All dependencies found"
  201. }
  202. #############################################################################
  203. # Registry Functions
  204. #############################################################################
  205. fetch_registry() {
  206. print_step "Fetching component registry..."
  207. mkdir -p "$TEMP_DIR"
  208. if ! curl -fsSL "$REGISTRY_URL" -o "$TEMP_DIR/registry.json"; then
  209. print_error "Failed to fetch registry from $REGISTRY_URL"
  210. exit 1
  211. fi
  212. print_success "Registry fetched successfully"
  213. }
  214. get_profile_components() {
  215. local profile=$1
  216. jq -r ".profiles.${profile}.components[]" "$TEMP_DIR/registry.json"
  217. }
  218. get_component_info() {
  219. local component_id=$1
  220. local component_type=$2
  221. jq -r ".components.${component_type}[] | select(.id == \"${component_id}\")" "$TEMP_DIR/registry.json"
  222. }
  223. # Helper function to get the correct registry key for a component type
  224. get_registry_key() {
  225. local type=$1
  226. # Most types are pluralized, but 'config' stays singular
  227. case "$type" in
  228. config) echo "config" ;;
  229. *) echo "${type}s" ;;
  230. esac
  231. }
  232. resolve_dependencies() {
  233. local component=$1
  234. local type="${component%%:*}"
  235. local id="${component##*:}"
  236. # Get the correct registry key (handles singular/plural)
  237. local registry_key=$(get_registry_key "$type")
  238. # Get dependencies for this component
  239. local deps=$(jq -r ".components.${registry_key}[] | select(.id == \"${id}\") | .dependencies[]?" "$TEMP_DIR/registry.json" 2>/dev/null || echo "")
  240. if [ -n "$deps" ]; then
  241. for dep in $deps; do
  242. # Add dependency if not already in list
  243. if [[ ! " ${SELECTED_COMPONENTS[@]} " =~ " ${dep} " ]]; then
  244. SELECTED_COMPONENTS+=("$dep")
  245. # Recursively resolve dependencies
  246. resolve_dependencies "$dep"
  247. fi
  248. done
  249. fi
  250. }
  251. #############################################################################
  252. # Installation Mode Selection
  253. #############################################################################
  254. check_interactive_mode() {
  255. # Check if stdin is a terminal (not piped from curl)
  256. if [ ! -t 0 ]; then
  257. print_header
  258. print_error "Interactive mode requires a terminal"
  259. echo ""
  260. echo "You're running this script in a pipe (e.g., curl | bash)"
  261. echo "For interactive mode, download the script first:"
  262. echo ""
  263. echo -e "${CYAN}# Download the script${NC}"
  264. echo "curl -fsSL https://raw.githubusercontent.com/darrenhinde/OpenAgents/main/install.sh -o install.sh"
  265. echo ""
  266. echo -e "${CYAN}# Run interactively${NC}"
  267. echo "bash install.sh"
  268. echo ""
  269. echo "Or use a profile directly:"
  270. echo ""
  271. echo -e "${CYAN}# Quick install with profile${NC}"
  272. echo "curl -fsSL https://raw.githubusercontent.com/darrenhinde/OpenAgents/main/install.sh | bash -s essential"
  273. echo ""
  274. echo "Available profiles: essential, developer, business, full, advanced"
  275. echo ""
  276. cleanup_and_exit 1
  277. fi
  278. }
  279. show_install_location_menu() {
  280. check_interactive_mode
  281. clear
  282. print_header
  283. local global_path=$(get_global_install_path)
  284. echo -e "${BOLD}Choose installation location:${NC}\n"
  285. echo -e " ${GREEN}1) Local${NC} - Install to ${CYAN}.opencode/${NC} in current directory"
  286. echo " (Best for project-specific agents)"
  287. echo ""
  288. echo -e " ${BLUE}2) Global${NC} - Install to ${CYAN}${global_path}${NC}"
  289. echo " (Best for user-wide agents available everywhere)"
  290. echo ""
  291. echo -e " ${MAGENTA}3) Custom${NC} - Enter exact path"
  292. echo " Examples:"
  293. case "$PLATFORM" in
  294. Windows)
  295. echo " ${CYAN}C:/Users/username/my-agents${NC} or ${CYAN}~/my-agents${NC}"
  296. ;;
  297. *)
  298. echo " ${CYAN}/home/username/my-agents${NC} or ${CYAN}~/my-agents${NC}"
  299. ;;
  300. esac
  301. echo ""
  302. echo " 4) Back / Exit"
  303. echo ""
  304. read -p "Enter your choice [1-4]: " location_choice
  305. case $location_choice in
  306. 1)
  307. INSTALL_DIR=".opencode"
  308. print_success "Installing to local directory: .opencode/"
  309. sleep 1
  310. ;;
  311. 2)
  312. INSTALL_DIR="$global_path"
  313. print_success "Installing to global directory: $global_path"
  314. sleep 1
  315. ;;
  316. 3)
  317. echo ""
  318. read -p "Enter installation path: " custom_path
  319. if [ -z "$custom_path" ]; then
  320. print_error "No path entered"
  321. sleep 2
  322. show_install_location_menu
  323. return
  324. fi
  325. local normalized_path=$(normalize_and_validate_path "$custom_path")
  326. if [ $? -ne 0 ]; then
  327. print_error "Invalid path"
  328. sleep 2
  329. show_install_location_menu
  330. return
  331. fi
  332. if ! validate_install_path "$normalized_path"; then
  333. echo ""
  334. read -p "Continue anyway? [y/N]: " continue_choice
  335. if [[ ! $continue_choice =~ ^[Yy] ]]; then
  336. show_install_location_menu
  337. return
  338. fi
  339. fi
  340. INSTALL_DIR="$normalized_path"
  341. print_success "Installing to custom directory: $INSTALL_DIR"
  342. sleep 1
  343. ;;
  344. 4)
  345. cleanup_and_exit 0
  346. ;;
  347. *)
  348. print_error "Invalid choice"
  349. sleep 2
  350. show_install_location_menu
  351. return
  352. ;;
  353. esac
  354. }
  355. show_main_menu() {
  356. check_interactive_mode
  357. clear
  358. print_header
  359. echo -e "${BOLD}Choose installation mode:${NC}\n"
  360. echo " 1) Quick Install (Choose a profile)"
  361. echo " 2) Custom Install (Pick individual components)"
  362. echo " 3) List Available Components"
  363. echo " 4) Exit"
  364. echo ""
  365. read -p "Enter your choice [1-4]: " choice
  366. case $choice in
  367. 1) INSTALL_MODE="profile" ;;
  368. 2) INSTALL_MODE="custom" ;;
  369. 3) list_components; show_main_menu ;;
  370. 4) cleanup_and_exit 0 ;;
  371. *) print_error "Invalid choice"; sleep 2; show_main_menu ;;
  372. esac
  373. }
  374. #############################################################################
  375. # Profile Installation
  376. #############################################################################
  377. show_profile_menu() {
  378. clear
  379. print_header
  380. echo -e "${BOLD}Available Installation Profiles:${NC}\n"
  381. # Essential profile
  382. local essential_name=$(jq -r '.profiles.essential.name' "$TEMP_DIR/registry.json")
  383. local essential_desc=$(jq -r '.profiles.essential.description' "$TEMP_DIR/registry.json")
  384. local essential_count=$(jq -r '.profiles.essential.components | length' "$TEMP_DIR/registry.json")
  385. echo -e " ${GREEN}1) ${essential_name}${NC}"
  386. echo -e " ${essential_desc}"
  387. echo -e " Components: ${essential_count}\n"
  388. # Developer profile
  389. local dev_desc=$(jq -r '.profiles.developer.description' "$TEMP_DIR/registry.json")
  390. local dev_count=$(jq -r '.profiles.developer.components | length' "$TEMP_DIR/registry.json")
  391. local dev_badge=$(jq -r '.profiles.developer.badge // ""' "$TEMP_DIR/registry.json")
  392. if [ -n "$dev_badge" ]; then
  393. echo -e " ${BLUE}2) Developer ${GREEN}[${dev_badge}]${NC}"
  394. else
  395. echo -e " ${BLUE}2) Developer${NC}"
  396. fi
  397. echo -e " ${dev_desc}"
  398. echo -e " Components: ${dev_count}\n"
  399. # Business profile
  400. local business_name=$(jq -r '.profiles.business.name' "$TEMP_DIR/registry.json")
  401. local business_desc=$(jq -r '.profiles.business.description' "$TEMP_DIR/registry.json")
  402. local business_count=$(jq -r '.profiles.business.components | length' "$TEMP_DIR/registry.json")
  403. echo -e " ${CYAN}3) ${business_name}${NC}"
  404. echo -e " ${business_desc}"
  405. echo -e " Components: ${business_count}\n"
  406. # Full profile
  407. local full_name=$(jq -r '.profiles.full.name' "$TEMP_DIR/registry.json")
  408. local full_desc=$(jq -r '.profiles.full.description' "$TEMP_DIR/registry.json")
  409. local full_count=$(jq -r '.profiles.full.components | length' "$TEMP_DIR/registry.json")
  410. echo -e " ${MAGENTA}4) ${full_name}${NC}"
  411. echo -e " ${full_desc}"
  412. echo -e " Components: ${full_count}\n"
  413. # Advanced profile
  414. local adv_name=$(jq -r '.profiles.advanced.name' "$TEMP_DIR/registry.json")
  415. local adv_desc=$(jq -r '.profiles.advanced.description' "$TEMP_DIR/registry.json")
  416. local adv_count=$(jq -r '.profiles.advanced.components | length' "$TEMP_DIR/registry.json")
  417. echo -e " ${YELLOW}5) ${adv_name}${NC}"
  418. echo -e " ${adv_desc}"
  419. echo -e " Components: ${adv_count}\n"
  420. echo " 6) Back to main menu"
  421. echo ""
  422. read -p "Enter your choice [1-6]: " choice
  423. case $choice in
  424. 1) PROFILE="essential" ;;
  425. 2) PROFILE="developer" ;;
  426. 3) PROFILE="business" ;;
  427. 4) PROFILE="full" ;;
  428. 5) PROFILE="advanced" ;;
  429. 6) show_main_menu; return ;;
  430. *) print_error "Invalid choice"; sleep 2; show_profile_menu; return ;;
  431. esac
  432. # Load profile components (compatible with bash 3.2+)
  433. SELECTED_COMPONENTS=()
  434. local temp_file="$TEMP_DIR/components.tmp"
  435. get_profile_components "$PROFILE" > "$temp_file"
  436. while IFS= read -r component; do
  437. [ -n "$component" ] && SELECTED_COMPONENTS+=("$component")
  438. done < "$temp_file"
  439. show_installation_preview
  440. }
  441. #############################################################################
  442. # Custom Component Selection
  443. #############################################################################
  444. show_custom_menu() {
  445. clear
  446. print_header
  447. echo -e "${BOLD}Select component categories to install:${NC}\n"
  448. echo "Use space to toggle, Enter to continue"
  449. echo ""
  450. local categories=("agents" "subagents" "commands" "tools" "plugins" "contexts" "config")
  451. local selected_categories=()
  452. # Simple selection (for now, we'll make it interactive later)
  453. echo "Available categories:"
  454. for i in "${!categories[@]}"; do
  455. local cat="${categories[$i]}"
  456. local count=$(jq -r ".components.${cat} | length" "$TEMP_DIR/registry.json")
  457. local cat_display=$(echo "$cat" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
  458. echo " $((i+1))) ${cat_display} (${count} available)"
  459. done
  460. echo " $((${#categories[@]}+1))) Select All"
  461. echo " $((${#categories[@]}+2))) Continue to component selection"
  462. echo " $((${#categories[@]}+3))) Back to main menu"
  463. echo ""
  464. read -p "Enter category numbers (space-separated) or option: " -a selections
  465. for sel in "${selections[@]}"; do
  466. if [ "$sel" -eq $((${#categories[@]}+1)) ]; then
  467. selected_categories=("${categories[@]}")
  468. break
  469. elif [ "$sel" -eq $((${#categories[@]}+2)) ]; then
  470. break
  471. elif [ "$sel" -eq $((${#categories[@]}+3)) ]; then
  472. show_main_menu
  473. return
  474. elif [ "$sel" -ge 1 ] && [ "$sel" -le ${#categories[@]} ]; then
  475. selected_categories+=("${categories[$((sel-1))]}")
  476. fi
  477. done
  478. if [ ${#selected_categories[@]} -eq 0 ]; then
  479. print_warning "No categories selected"
  480. sleep 2
  481. show_custom_menu
  482. return
  483. fi
  484. show_component_selection "${selected_categories[@]}"
  485. }
  486. show_component_selection() {
  487. local categories=("$@")
  488. clear
  489. print_header
  490. echo -e "${BOLD}Select components to install:${NC}\n"
  491. local all_components=()
  492. local component_details=()
  493. for category in "${categories[@]}"; do
  494. local cat_display=$(echo "$category" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
  495. echo -e "${CYAN}${BOLD}${cat_display}:${NC}"
  496. local components=$(jq -r ".components.${category}[] | .id" "$TEMP_DIR/registry.json")
  497. local idx=1
  498. while IFS= read -r comp_id; do
  499. local comp_name=$(jq -r ".components.${category}[] | select(.id == \"${comp_id}\") | .name" "$TEMP_DIR/registry.json")
  500. local comp_desc=$(jq -r ".components.${category}[] | select(.id == \"${comp_id}\") | .description" "$TEMP_DIR/registry.json")
  501. echo " ${idx}) ${comp_name}"
  502. echo " ${comp_desc}"
  503. all_components+=("${category}:${comp_id}")
  504. component_details+=("${comp_name}|${comp_desc}")
  505. idx=$((idx+1))
  506. done <<< "$components"
  507. echo ""
  508. done
  509. echo "Enter component numbers (space-separated), 'all' for all, or 'done' to continue:"
  510. read -a selections
  511. for sel in "${selections[@]}"; do
  512. if [ "$sel" = "all" ]; then
  513. SELECTED_COMPONENTS=("${all_components[@]}")
  514. break
  515. elif [ "$sel" = "done" ]; then
  516. break
  517. elif [ "$sel" -ge 1 ] && [ "$sel" -le ${#all_components[@]} ]; then
  518. SELECTED_COMPONENTS+=("${all_components[$((sel-1))]}")
  519. fi
  520. done
  521. if [ ${#SELECTED_COMPONENTS[@]} -eq 0 ]; then
  522. print_warning "No components selected"
  523. sleep 2
  524. show_custom_menu
  525. return
  526. fi
  527. # Resolve dependencies
  528. print_step "Resolving dependencies..."
  529. local original_count=${#SELECTED_COMPONENTS[@]}
  530. for comp in "${SELECTED_COMPONENTS[@]}"; do
  531. resolve_dependencies "$comp"
  532. done
  533. if [ ${#SELECTED_COMPONENTS[@]} -gt $original_count ]; then
  534. print_info "Added $((${#SELECTED_COMPONENTS[@]} - original_count)) dependencies"
  535. fi
  536. show_installation_preview
  537. }
  538. #############################################################################
  539. # Installation Preview & Confirmation
  540. #############################################################################
  541. show_installation_preview() {
  542. # Only clear screen in interactive mode
  543. if [ "$NON_INTERACTIVE" != true ]; then
  544. clear
  545. fi
  546. print_header
  547. echo -e "${BOLD}Installation Preview${NC}\n"
  548. if [ -n "$PROFILE" ]; then
  549. echo -e "Profile: ${GREEN}${PROFILE}${NC}"
  550. else
  551. echo -e "Mode: ${GREEN}Custom${NC}"
  552. fi
  553. echo -e "Installation directory: ${CYAN}${INSTALL_DIR}${NC}"
  554. echo -e "\nComponents to install (${#SELECTED_COMPONENTS[@]} total):\n"
  555. # Group by type
  556. local agents=()
  557. local subagents=()
  558. local commands=()
  559. local tools=()
  560. local plugins=()
  561. local contexts=()
  562. local configs=()
  563. for comp in "${SELECTED_COMPONENTS[@]}"; do
  564. local type="${comp%%:*}"
  565. case $type in
  566. agent) agents+=("$comp") ;;
  567. subagent) subagents+=("$comp") ;;
  568. command) commands+=("$comp") ;;
  569. tool) tools+=("$comp") ;;
  570. plugin) plugins+=("$comp") ;;
  571. context) contexts+=("$comp") ;;
  572. config) configs+=("$comp") ;;
  573. esac
  574. done
  575. [ ${#agents[@]} -gt 0 ] && echo -e "${CYAN}Agents (${#agents[@]}):${NC} ${agents[*]##*:}"
  576. [ ${#subagents[@]} -gt 0 ] && echo -e "${CYAN}Subagents (${#subagents[@]}):${NC} ${subagents[*]##*:}"
  577. [ ${#commands[@]} -gt 0 ] && echo -e "${CYAN}Commands (${#commands[@]}):${NC} ${commands[*]##*:}"
  578. [ ${#tools[@]} -gt 0 ] && echo -e "${CYAN}Tools (${#tools[@]}):${NC} ${tools[*]##*:}"
  579. [ ${#plugins[@]} -gt 0 ] && echo -e "${CYAN}Plugins (${#plugins[@]}):${NC} ${plugins[*]##*:}"
  580. [ ${#contexts[@]} -gt 0 ] && echo -e "${CYAN}Contexts (${#contexts[@]}):${NC} ${contexts[*]##*:}"
  581. [ ${#configs[@]} -gt 0 ] && echo -e "${CYAN}Config (${#configs[@]}):${NC} ${configs[*]##*:}"
  582. echo ""
  583. # Skip confirmation if profile was provided via command line
  584. if [ "$NON_INTERACTIVE" = true ]; then
  585. print_info "Installing automatically (profile specified)..."
  586. perform_installation
  587. else
  588. read -p "Proceed with installation? [Y/n]: " confirm
  589. if [[ $confirm =~ ^[Nn] ]]; then
  590. print_info "Installation cancelled"
  591. cleanup_and_exit 0
  592. fi
  593. perform_installation
  594. fi
  595. }
  596. #############################################################################
  597. # Collision Detection
  598. #############################################################################
  599. show_collision_report() {
  600. local collision_count=$1
  601. shift
  602. local collisions=("$@")
  603. echo ""
  604. print_warning "Found ${collision_count} file collision(s):"
  605. echo ""
  606. # Group by type
  607. local agents=()
  608. local subagents=()
  609. local commands=()
  610. local tools=()
  611. local plugins=()
  612. local contexts=()
  613. local configs=()
  614. for file in "${collisions[@]}"; do
  615. # Skip empty entries
  616. [ -z "$file" ] && continue
  617. if [[ $file == *"/agent/subagents/"* ]]; then
  618. subagents+=("$file")
  619. elif [[ $file == *"/agent/"* ]]; then
  620. agents+=("$file")
  621. elif [[ $file == *"/command/"* ]]; then
  622. commands+=("$file")
  623. elif [[ $file == *"/tool/"* ]]; then
  624. tools+=("$file")
  625. elif [[ $file == *"/plugin/"* ]]; then
  626. plugins+=("$file")
  627. elif [[ $file == *"/context/"* ]]; then
  628. contexts+=("$file")
  629. else
  630. configs+=("$file")
  631. fi
  632. done
  633. # Display grouped collisions
  634. [ ${#agents[@]} -gt 0 ] && echo -e "${YELLOW} Agents (${#agents[@]}):${NC}" && printf ' %s\n' "${agents[@]}"
  635. [ ${#subagents[@]} -gt 0 ] && echo -e "${YELLOW} Subagents (${#subagents[@]}):${NC}" && printf ' %s\n' "${subagents[@]}"
  636. [ ${#commands[@]} -gt 0 ] && echo -e "${YELLOW} Commands (${#commands[@]}):${NC}" && printf ' %s\n' "${commands[@]}"
  637. [ ${#tools[@]} -gt 0 ] && echo -e "${YELLOW} Tools (${#tools[@]}):${NC}" && printf ' %s\n' "${tools[@]}"
  638. [ ${#plugins[@]} -gt 0 ] && echo -e "${YELLOW} Plugins (${#plugins[@]}):${NC}" && printf ' %s\n' "${plugins[@]}"
  639. [ ${#contexts[@]} -gt 0 ] && echo -e "${YELLOW} Context (${#contexts[@]}):${NC}" && printf ' %s\n' "${contexts[@]}"
  640. [ ${#configs[@]} -gt 0 ] && echo -e "${YELLOW} Config (${#configs[@]}):${NC}" && printf ' %s\n' "${configs[@]}"
  641. echo ""
  642. }
  643. get_install_strategy() {
  644. echo -e "${BOLD}How would you like to proceed?${NC}\n" >&2
  645. echo " 1) ${GREEN}Skip existing${NC} - Only install new files, keep all existing files unchanged" >&2
  646. echo " 2) ${YELLOW}Overwrite all${NC} - Replace existing files with new versions (your changes will be lost)" >&2
  647. echo " 3) ${CYAN}Backup & overwrite${NC} - Backup existing files, then install new versions" >&2
  648. echo " 4) ${RED}Cancel${NC} - Exit without making changes" >&2
  649. echo "" >&2
  650. read -p "Enter your choice [1-4]: " strategy_choice
  651. case $strategy_choice in
  652. 1) echo "skip" ;;
  653. 2)
  654. echo "" >&2
  655. print_warning "This will overwrite existing files. Your changes will be lost!"
  656. read -p "Are you sure? Type 'yes' to confirm: " confirm
  657. if [ "$confirm" = "yes" ]; then
  658. echo "overwrite"
  659. else
  660. echo "cancel"
  661. fi
  662. ;;
  663. 3) echo "backup" ;;
  664. 4) echo "cancel" ;;
  665. *) echo "cancel" ;;
  666. esac
  667. }
  668. #############################################################################
  669. # Installation
  670. #############################################################################
  671. perform_installation() {
  672. print_step "Preparing installation..."
  673. # Create base directory only - subdirectories created on-demand when files are installed
  674. mkdir -p "$INSTALL_DIR"
  675. # Check for collisions
  676. local collisions=()
  677. for comp in "${SELECTED_COMPONENTS[@]}"; do
  678. local type="${comp%%:*}"
  679. local id="${comp##*:}"
  680. local registry_key=$(get_registry_key "$type")
  681. local path=$(jq -r ".components.${registry_key}[] | select(.id == \"${id}\") | .path" "$TEMP_DIR/registry.json")
  682. if [ -n "$path" ] && [ "$path" != "null" ] && [ -f "$path" ]; then
  683. collisions+=("$path")
  684. fi
  685. done
  686. # Determine installation strategy
  687. local install_strategy="fresh"
  688. if [ ${#collisions[@]} -gt 0 ]; then
  689. show_collision_report ${#collisions[@]} "${collisions[@]}"
  690. install_strategy=$(get_install_strategy)
  691. if [ "$install_strategy" = "cancel" ]; then
  692. print_info "Installation cancelled by user"
  693. cleanup_and_exit 0
  694. fi
  695. # Handle backup strategy
  696. if [ "$install_strategy" = "backup" ]; then
  697. local backup_dir="${INSTALL_DIR}.backup.$(date +%Y%m%d-%H%M%S)"
  698. print_step "Creating backup..."
  699. # Only backup files that will be overwritten
  700. local backup_count=0
  701. for file in "${collisions[@]}"; do
  702. if [ -f "$file" ]; then
  703. local backup_file="${backup_dir}/${file}"
  704. mkdir -p "$(dirname "$backup_file")"
  705. if cp "$file" "$backup_file" 2>/dev/null; then
  706. backup_count=$((backup_count + 1))
  707. else
  708. print_warning "Failed to backup: $file"
  709. fi
  710. fi
  711. done
  712. if [ $backup_count -gt 0 ]; then
  713. print_success "Backed up ${backup_count} file(s) to $backup_dir"
  714. install_strategy="overwrite" # Now we can overwrite
  715. else
  716. print_error "Backup failed. Installation cancelled."
  717. cleanup_and_exit 1
  718. fi
  719. fi
  720. fi
  721. # Perform installation
  722. print_step "Installing components..."
  723. local installed=0
  724. local skipped=0
  725. local failed=0
  726. for comp in "${SELECTED_COMPONENTS[@]}"; do
  727. local type="${comp%%:*}"
  728. local id="${comp##*:}"
  729. # Get the correct registry key (handles singular/plural)
  730. local registry_key=$(get_registry_key "$type")
  731. # Get component path
  732. local path=$(jq -r ".components.${registry_key}[] | select(.id == \"${id}\") | .path" "$TEMP_DIR/registry.json")
  733. if [ -z "$path" ] || [ "$path" = "null" ]; then
  734. print_warning "Could not find path for ${comp}"
  735. failed=$((failed + 1))
  736. continue
  737. fi
  738. # Check if file exists before we install (for proper messaging)
  739. local file_existed=false
  740. if [ -f "$path" ]; then
  741. file_existed=true
  742. fi
  743. # Check if file exists and we're in skip mode
  744. if [ "$file_existed" = true ] && [ "$install_strategy" = "skip" ]; then
  745. print_info "Skipped existing: ${type}:${id}"
  746. skipped=$((skipped + 1))
  747. continue
  748. fi
  749. # Download component
  750. local url="${RAW_URL}/${path}"
  751. local dest="${path}"
  752. # Create parent directory if needed
  753. mkdir -p "$(dirname "$dest")"
  754. if curl -fsSL "$url" -o "$dest"; then
  755. # Show appropriate message based on whether file existed before
  756. if [ "$file_existed" = true ]; then
  757. print_success "Updated ${type}: ${id}"
  758. else
  759. print_success "Installed ${type}: ${id}"
  760. fi
  761. installed=$((installed + 1))
  762. else
  763. print_error "Failed to install ${type}: ${id}"
  764. failed=$((failed + 1))
  765. fi
  766. done
  767. # Handle additional paths for advanced profile
  768. if [ "$PROFILE" = "advanced" ]; then
  769. local additional_paths=$(jq -r '.profiles.advanced.additionalPaths[]?' "$TEMP_DIR/registry.json")
  770. if [ -n "$additional_paths" ]; then
  771. print_step "Installing additional paths..."
  772. while IFS= read -r path; do
  773. # For directories, we'd need to recursively download
  774. # For now, just note them
  775. print_info "Additional path: $path (manual download required)"
  776. done <<< "$additional_paths"
  777. fi
  778. fi
  779. echo ""
  780. print_success "Installation complete!"
  781. echo -e " Installed: ${GREEN}${installed}${NC}"
  782. [ $skipped -gt 0 ] && echo -e " Skipped: ${CYAN}${skipped}${NC}"
  783. [ $failed -gt 0 ] && echo -e " Failed: ${RED}${failed}${NC}"
  784. show_post_install
  785. }
  786. #############################################################################
  787. # Post-Installation
  788. #############################################################################
  789. show_post_install() {
  790. echo ""
  791. print_step "Next Steps"
  792. echo "1. Review the installed components in ${CYAN}${INSTALL_DIR}/${NC}"
  793. # Check if env.example was installed
  794. if [ -f "${INSTALL_DIR}/../env.example" ] || [ -f "env.example" ]; then
  795. echo "2. Copy env.example to .env and configure:"
  796. echo " ${CYAN}cp env.example .env${NC}"
  797. echo "3. Start using OpenCode agents:"
  798. else
  799. echo "2. Start using OpenCode agents:"
  800. fi
  801. echo " ${CYAN}opencode${NC}"
  802. echo ""
  803. # Show installation location info
  804. print_info "Installation directory: ${CYAN}${INSTALL_DIR}${NC}"
  805. if [ -d "${INSTALL_DIR}.backup."* ] 2>/dev/null; then
  806. print_info "Backup created - you can restore files from ${INSTALL_DIR}.backup.* if needed"
  807. fi
  808. print_info "Documentation: ${REPO_URL}"
  809. echo ""
  810. cleanup_and_exit 0
  811. }
  812. #############################################################################
  813. # Component Listing
  814. #############################################################################
  815. list_components() {
  816. clear
  817. print_header
  818. echo -e "${BOLD}Available Components${NC}\n"
  819. local categories=("agents" "subagents" "commands" "tools" "plugins" "contexts")
  820. for category in "${categories[@]}"; do
  821. local cat_display=$(echo "$category" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
  822. echo -e "${CYAN}${BOLD}${cat_display}:${NC}"
  823. local components=$(jq -r ".components.${category}[] | \"\(.id)|\(.name)|\(.description)\"" "$TEMP_DIR/registry.json")
  824. while IFS='|' read -r id name desc; do
  825. echo -e " ${GREEN}${name}${NC} (${id})"
  826. echo -e " ${desc}"
  827. done <<< "$components"
  828. echo ""
  829. done
  830. read -p "Press Enter to continue..."
  831. }
  832. #############################################################################
  833. # Cleanup
  834. #############################################################################
  835. cleanup_and_exit() {
  836. rm -rf "$TEMP_DIR"
  837. exit "$1"
  838. }
  839. trap 'cleanup_and_exit 1' INT TERM
  840. #############################################################################
  841. # Main
  842. #############################################################################
  843. main() {
  844. # Parse command line arguments
  845. while [ $# -gt 0 ]; do
  846. case "$1" in
  847. --install-dir=*)
  848. CUSTOM_INSTALL_DIR="${1#*=}"
  849. shift
  850. ;;
  851. --install-dir)
  852. if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then
  853. CUSTOM_INSTALL_DIR="$2"
  854. shift 2
  855. else
  856. echo "Error: --install-dir requires a path argument"
  857. exit 1
  858. fi
  859. ;;
  860. essential|--essential)
  861. INSTALL_MODE="profile"
  862. PROFILE="essential"
  863. NON_INTERACTIVE=true
  864. shift
  865. ;;
  866. developer|--developer)
  867. INSTALL_MODE="profile"
  868. PROFILE="developer"
  869. NON_INTERACTIVE=true
  870. shift
  871. ;;
  872. business|--business)
  873. INSTALL_MODE="profile"
  874. PROFILE="business"
  875. NON_INTERACTIVE=true
  876. shift
  877. ;;
  878. full|--full)
  879. INSTALL_MODE="profile"
  880. PROFILE="full"
  881. NON_INTERACTIVE=true
  882. shift
  883. ;;
  884. advanced|--advanced)
  885. INSTALL_MODE="profile"
  886. PROFILE="advanced"
  887. NON_INTERACTIVE=true
  888. shift
  889. ;;
  890. list|--list)
  891. check_dependencies
  892. fetch_registry
  893. list_components
  894. cleanup_and_exit 0
  895. ;;
  896. --help|-h|help)
  897. print_header
  898. echo "Usage: $0 [PROFILE] [OPTIONS]"
  899. echo ""
  900. echo -e "${BOLD}Profiles:${NC}"
  901. echo " essential, --essential Minimal setup with core agents"
  902. echo " developer, --developer Code-focused development tools"
  903. echo " business, --business Content and business-focused tools"
  904. echo " full, --full Everything except system-builder"
  905. echo " advanced, --advanced Complete system with all components"
  906. echo ""
  907. echo -e "${BOLD}Options:${NC}"
  908. echo " --install-dir PATH Custom installation directory"
  909. echo " (default: .opencode)"
  910. echo " list, --list List all available components"
  911. echo " help, --help, -h Show this help message"
  912. echo ""
  913. echo -e "${BOLD}Environment Variables:${NC}"
  914. echo " OPENCODE_INSTALL_DIR Installation directory"
  915. echo " OPENCODE_BRANCH Git branch to install from (default: main)"
  916. echo ""
  917. echo -e "${BOLD}Examples:${NC}"
  918. echo ""
  919. echo " ${CYAN}# Interactive mode (choose location and components)${NC}"
  920. echo " $0"
  921. echo ""
  922. echo " ${CYAN}# Quick install with default location (.opencode/)${NC}"
  923. echo " $0 developer"
  924. echo ""
  925. echo " ${CYAN}# Install to global location (Linux/macOS)${NC}"
  926. echo " $0 developer --install-dir ~/.config/opencode"
  927. echo ""
  928. echo " ${CYAN}# Install to global location (Windows Git Bash)${NC}"
  929. echo " $0 developer --install-dir ~/.config/opencode"
  930. echo ""
  931. echo " ${CYAN}# Install to custom location${NC}"
  932. echo " $0 essential --install-dir ~/my-agents"
  933. echo ""
  934. echo " ${CYAN}# Using environment variable${NC}"
  935. echo " export OPENCODE_INSTALL_DIR=~/.config/opencode"
  936. echo " $0 developer"
  937. echo ""
  938. echo " ${CYAN}# Install from URL (non-interactive)${NC}"
  939. echo " curl -fsSL https://raw.githubusercontent.com/darrenhinde/OpenAgents/main/install.sh | bash -s developer"
  940. echo ""
  941. echo -e "${BOLD}Platform Support:${NC}"
  942. echo " ✓ Linux (bash 3.2+)"
  943. echo " ✓ macOS (bash 3.2+)"
  944. echo " ✓ Windows (Git Bash, WSL)"
  945. echo ""
  946. exit 0
  947. ;;
  948. *)
  949. echo "Unknown option: $1"
  950. echo "Run '$0 --help' for usage information"
  951. exit 1
  952. ;;
  953. esac
  954. done
  955. # Apply custom install directory if specified (CLI arg overrides env var)
  956. if [ -n "$CUSTOM_INSTALL_DIR" ]; then
  957. local normalized_path=$(normalize_and_validate_path "$CUSTOM_INSTALL_DIR")
  958. if [ $? -eq 0 ]; then
  959. INSTALL_DIR="$normalized_path"
  960. if ! validate_install_path "$INSTALL_DIR"; then
  961. print_warning "Installation path may have issues, but continuing..."
  962. fi
  963. else
  964. print_error "Invalid installation directory: $CUSTOM_INSTALL_DIR"
  965. exit 1
  966. fi
  967. fi
  968. check_bash_version
  969. check_dependencies
  970. fetch_registry
  971. if [ -n "$PROFILE" ]; then
  972. # Non-interactive mode (compatible with bash 3.2+)
  973. SELECTED_COMPONENTS=()
  974. local temp_file="$TEMP_DIR/components.tmp"
  975. get_profile_components "$PROFILE" > "$temp_file"
  976. while IFS= read -r component; do
  977. [ -n "$component" ] && SELECTED_COMPONENTS+=("$component")
  978. done < "$temp_file"
  979. show_installation_preview
  980. else
  981. # Interactive mode - show location menu first
  982. show_install_location_menu
  983. show_main_menu
  984. if [ "$INSTALL_MODE" = "profile" ]; then
  985. show_profile_menu
  986. elif [ "$INSTALL_MODE" = "custom" ]; then
  987. show_custom_menu
  988. fi
  989. fi
  990. }
  991. main "$@"