install.sh 41 KB

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