install.sh 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832
  1. #!/usr/bin/env bash
  2. #############################################################################
  3. # OpenCode Agents 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/opencode-agents"
  43. BRANCH="${OPENCODE_BRANCH:-main}" # Allow override via environment variable
  44. RAW_URL="https://raw.githubusercontent.com/darrenhinde/opencode-agents/${BRANCH}"
  45. REGISTRY_URL="${RAW_URL}/registry.json"
  46. INSTALL_DIR=".opencode"
  47. TEMP_DIR="/tmp/opencode-installer-$$"
  48. # Global variables
  49. SELECTED_COMPONENTS=()
  50. INSTALL_MODE=""
  51. PROFILE=""
  52. #############################################################################
  53. # Utility Functions
  54. #############################################################################
  55. print_header() {
  56. echo -e "${CYAN}${BOLD}"
  57. echo "╔════════════════════════════════════════════════════════════════╗"
  58. echo "║ ║"
  59. echo "║ OpenCode Agents Installer v1.0.0 ║"
  60. echo "║ ║"
  61. echo "╚════════════════════════════════════════════════════════════════╝"
  62. echo -e "${NC}"
  63. }
  64. print_success() {
  65. echo -e "${GREEN}✓${NC} $1"
  66. }
  67. print_error() {
  68. echo -e "${RED}✗${NC} $1"
  69. }
  70. print_info() {
  71. echo -e "${BLUE}ℹ${NC} $1"
  72. }
  73. print_warning() {
  74. echo -e "${YELLOW}⚠${NC} $1"
  75. }
  76. print_step() {
  77. echo -e "\n${MAGENTA}${BOLD}▶${NC} $1\n"
  78. }
  79. #############################################################################
  80. # Dependency Checks
  81. #############################################################################
  82. check_bash_version() {
  83. # Check bash version (need 3.2+)
  84. local bash_version="${BASH_VERSION%%.*}"
  85. if [ "$bash_version" -lt 3 ]; then
  86. echo "Error: This script requires Bash 3.2 or higher"
  87. echo "Current version: $BASH_VERSION"
  88. echo ""
  89. echo "Please upgrade bash or use a different shell:"
  90. echo " macOS: brew install bash"
  91. echo " Linux: Use your package manager to update bash"
  92. echo " Windows: Use Git Bash or WSL"
  93. exit 1
  94. fi
  95. }
  96. check_dependencies() {
  97. print_step "Checking dependencies..."
  98. local missing_deps=()
  99. if ! command -v curl &> /dev/null; then
  100. missing_deps+=("curl")
  101. fi
  102. if ! command -v jq &> /dev/null; then
  103. missing_deps+=("jq")
  104. fi
  105. if [ ${#missing_deps[@]} -ne 0 ]; then
  106. print_error "Missing required dependencies: ${missing_deps[*]}"
  107. echo ""
  108. echo "Please install them:"
  109. case "$PLATFORM" in
  110. macOS)
  111. echo " brew install ${missing_deps[*]}"
  112. ;;
  113. Linux)
  114. echo " Ubuntu/Debian: sudo apt-get install ${missing_deps[*]}"
  115. echo " Fedora/RHEL: sudo dnf install ${missing_deps[*]}"
  116. echo " Arch: sudo pacman -S ${missing_deps[*]}"
  117. ;;
  118. Windows)
  119. echo " Git Bash: Install via https://git-scm.com/"
  120. echo " WSL: sudo apt-get install ${missing_deps[*]}"
  121. echo " Scoop: scoop install ${missing_deps[*]}"
  122. ;;
  123. *)
  124. echo " Use your package manager to install: ${missing_deps[*]}"
  125. ;;
  126. esac
  127. exit 1
  128. fi
  129. print_success "All dependencies found"
  130. }
  131. #############################################################################
  132. # Registry Functions
  133. #############################################################################
  134. fetch_registry() {
  135. print_step "Fetching component registry..."
  136. mkdir -p "$TEMP_DIR"
  137. if ! curl -fsSL "$REGISTRY_URL" -o "$TEMP_DIR/registry.json"; then
  138. print_error "Failed to fetch registry from $REGISTRY_URL"
  139. exit 1
  140. fi
  141. print_success "Registry fetched successfully"
  142. }
  143. get_profile_components() {
  144. local profile=$1
  145. jq -r ".profiles.${profile}.components[]" "$TEMP_DIR/registry.json"
  146. }
  147. get_component_info() {
  148. local component_id=$1
  149. local component_type=$2
  150. jq -r ".components.${component_type}[] | select(.id == \"${component_id}\")" "$TEMP_DIR/registry.json"
  151. }
  152. resolve_dependencies() {
  153. local component=$1
  154. local type="${component%%:*}"
  155. local id="${component##*:}"
  156. # Get dependencies for this component
  157. local deps=$(jq -r ".components.${type}s[] | select(.id == \"${id}\") | .dependencies[]?" "$TEMP_DIR/registry.json" 2>/dev/null || echo "")
  158. if [ -n "$deps" ]; then
  159. for dep in $deps; do
  160. # Add dependency if not already in list
  161. if [[ ! " ${SELECTED_COMPONENTS[@]} " =~ " ${dep} " ]]; then
  162. SELECTED_COMPONENTS+=("$dep")
  163. # Recursively resolve dependencies
  164. resolve_dependencies "$dep"
  165. fi
  166. done
  167. fi
  168. }
  169. #############################################################################
  170. # Installation Mode Selection
  171. #############################################################################
  172. show_main_menu() {
  173. clear
  174. print_header
  175. echo -e "${BOLD}Choose installation mode:${NC}\n"
  176. echo " 1) Quick Install (Choose a profile)"
  177. echo " 2) Custom Install (Pick individual components)"
  178. echo " 3) List Available Components"
  179. echo " 4) Exit"
  180. echo ""
  181. read -p "Enter your choice [1-4]: " choice
  182. case $choice in
  183. 1) INSTALL_MODE="profile" ;;
  184. 2) INSTALL_MODE="custom" ;;
  185. 3) list_components; show_main_menu ;;
  186. 4) cleanup_and_exit 0 ;;
  187. *) print_error "Invalid choice"; sleep 2; show_main_menu ;;
  188. esac
  189. }
  190. #############################################################################
  191. # Profile Installation
  192. #############################################################################
  193. show_profile_menu() {
  194. clear
  195. print_header
  196. echo -e "${BOLD}Available Installation Profiles:${NC}\n"
  197. # Core profile
  198. local core_desc=$(jq -r '.profiles.core.description' "$TEMP_DIR/registry.json")
  199. local core_count=$(jq -r '.profiles.core.components | length' "$TEMP_DIR/registry.json")
  200. echo -e " ${GREEN}1) Core${NC}"
  201. echo -e " ${core_desc}"
  202. echo -e " Components: ${core_count}\n"
  203. # Developer profile
  204. local dev_desc=$(jq -r '.profiles.developer.description' "$TEMP_DIR/registry.json")
  205. local dev_count=$(jq -r '.profiles.developer.components | length' "$TEMP_DIR/registry.json")
  206. echo -e " ${BLUE}2) Developer${NC}"
  207. echo -e " ${dev_desc}"
  208. echo -e " Components: ${dev_count}\n"
  209. # Full profile
  210. local full_desc=$(jq -r '.profiles.full.description' "$TEMP_DIR/registry.json")
  211. local full_count=$(jq -r '.profiles.full.components | length' "$TEMP_DIR/registry.json")
  212. echo -e " ${MAGENTA}3) Full${NC}"
  213. echo -e " ${full_desc}"
  214. echo -e " Components: ${full_count}\n"
  215. # Advanced profile
  216. local adv_desc=$(jq -r '.profiles.advanced.description' "$TEMP_DIR/registry.json")
  217. local adv_count=$(jq -r '.profiles.advanced.components | length' "$TEMP_DIR/registry.json")
  218. echo -e " ${YELLOW}4) Advanced${NC}"
  219. echo -e " ${adv_desc}"
  220. echo -e " Components: ${adv_count}\n"
  221. echo " 5) Back to main menu"
  222. echo ""
  223. read -p "Enter your choice [1-5]: " choice
  224. case $choice in
  225. 1) PROFILE="core" ;;
  226. 2) PROFILE="developer" ;;
  227. 3) PROFILE="full" ;;
  228. 4) PROFILE="advanced" ;;
  229. 5) show_main_menu; return ;;
  230. *) print_error "Invalid choice"; sleep 2; show_profile_menu; return ;;
  231. esac
  232. # Load profile components (compatible with bash 3.2+)
  233. SELECTED_COMPONENTS=()
  234. local temp_file="$TEMP_DIR/components.tmp"
  235. get_profile_components "$PROFILE" > "$temp_file"
  236. while IFS= read -r component; do
  237. [ -n "$component" ] && SELECTED_COMPONENTS+=("$component")
  238. done < "$temp_file"
  239. show_installation_preview
  240. }
  241. #############################################################################
  242. # Custom Component Selection
  243. #############################################################################
  244. show_custom_menu() {
  245. clear
  246. print_header
  247. echo -e "${BOLD}Select component categories to install:${NC}\n"
  248. echo "Use space to toggle, Enter to continue"
  249. echo ""
  250. local categories=("agents" "subagents" "commands" "tools" "plugins" "contexts" "config")
  251. local selected_categories=()
  252. # Simple selection (for now, we'll make it interactive later)
  253. echo "Available categories:"
  254. for i in "${!categories[@]}"; do
  255. local cat="${categories[$i]}"
  256. local count=$(jq -r ".components.${cat} | length" "$TEMP_DIR/registry.json")
  257. local cat_display=$(echo "$cat" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
  258. echo " $((i+1))) ${cat_display} (${count} available)"
  259. done
  260. echo " $((${#categories[@]}+1))) Select All"
  261. echo " $((${#categories[@]}+2))) Continue to component selection"
  262. echo " $((${#categories[@]}+3))) Back to main menu"
  263. echo ""
  264. read -p "Enter category numbers (space-separated) or option: " -a selections
  265. for sel in "${selections[@]}"; do
  266. if [ "$sel" -eq $((${#categories[@]}+1)) ]; then
  267. selected_categories=("${categories[@]}")
  268. break
  269. elif [ "$sel" -eq $((${#categories[@]}+2)) ]; then
  270. break
  271. elif [ "$sel" -eq $((${#categories[@]}+3)) ]; then
  272. show_main_menu
  273. return
  274. elif [ "$sel" -ge 1 ] && [ "$sel" -le ${#categories[@]} ]; then
  275. selected_categories+=("${categories[$((sel-1))]}")
  276. fi
  277. done
  278. if [ ${#selected_categories[@]} -eq 0 ]; then
  279. print_warning "No categories selected"
  280. sleep 2
  281. show_custom_menu
  282. return
  283. fi
  284. show_component_selection "${selected_categories[@]}"
  285. }
  286. show_component_selection() {
  287. local categories=("$@")
  288. clear
  289. print_header
  290. echo -e "${BOLD}Select components to install:${NC}\n"
  291. local all_components=()
  292. local component_details=()
  293. for category in "${categories[@]}"; do
  294. local cat_display=$(echo "$category" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
  295. echo -e "${CYAN}${BOLD}${cat_display}:${NC}"
  296. local components=$(jq -r ".components.${category}[] | .id" "$TEMP_DIR/registry.json")
  297. local idx=1
  298. while IFS= read -r comp_id; do
  299. local comp_name=$(jq -r ".components.${category}[] | select(.id == \"${comp_id}\") | .name" "$TEMP_DIR/registry.json")
  300. local comp_desc=$(jq -r ".components.${category}[] | select(.id == \"${comp_id}\") | .description" "$TEMP_DIR/registry.json")
  301. echo " ${idx}) ${comp_name}"
  302. echo " ${comp_desc}"
  303. all_components+=("${category}:${comp_id}")
  304. component_details+=("${comp_name}|${comp_desc}")
  305. idx=$((idx+1))
  306. done <<< "$components"
  307. echo ""
  308. done
  309. echo "Enter component numbers (space-separated), 'all' for all, or 'done' to continue:"
  310. read -a selections
  311. for sel in "${selections[@]}"; do
  312. if [ "$sel" = "all" ]; then
  313. SELECTED_COMPONENTS=("${all_components[@]}")
  314. break
  315. elif [ "$sel" = "done" ]; then
  316. break
  317. elif [ "$sel" -ge 1 ] && [ "$sel" -le ${#all_components[@]} ]; then
  318. SELECTED_COMPONENTS+=("${all_components[$((sel-1))]}")
  319. fi
  320. done
  321. if [ ${#SELECTED_COMPONENTS[@]} -eq 0 ]; then
  322. print_warning "No components selected"
  323. sleep 2
  324. show_custom_menu
  325. return
  326. fi
  327. # Resolve dependencies
  328. print_step "Resolving dependencies..."
  329. local original_count=${#SELECTED_COMPONENTS[@]}
  330. for comp in "${SELECTED_COMPONENTS[@]}"; do
  331. resolve_dependencies "$comp"
  332. done
  333. if [ ${#SELECTED_COMPONENTS[@]} -gt $original_count ]; then
  334. print_info "Added $((${#SELECTED_COMPONENTS[@]} - original_count)) dependencies"
  335. fi
  336. show_installation_preview
  337. }
  338. #############################################################################
  339. # Installation Preview & Confirmation
  340. #############################################################################
  341. show_installation_preview() {
  342. clear
  343. print_header
  344. echo -e "${BOLD}Installation Preview${NC}\n"
  345. if [ -n "$PROFILE" ]; then
  346. echo -e "Profile: ${GREEN}${PROFILE}${NC}"
  347. else
  348. echo -e "Mode: ${GREEN}Custom${NC}"
  349. fi
  350. echo -e "\nComponents to install (${#SELECTED_COMPONENTS[@]} total):\n"
  351. # Group by type
  352. local agents=()
  353. local subagents=()
  354. local commands=()
  355. local tools=()
  356. local plugins=()
  357. local contexts=()
  358. local configs=()
  359. for comp in "${SELECTED_COMPONENTS[@]}"; do
  360. local type="${comp%%:*}"
  361. case $type in
  362. agent) agents+=("$comp") ;;
  363. subagent) subagents+=("$comp") ;;
  364. command) commands+=("$comp") ;;
  365. tool) tools+=("$comp") ;;
  366. plugin) plugins+=("$comp") ;;
  367. context) contexts+=("$comp") ;;
  368. config) configs+=("$comp") ;;
  369. esac
  370. done
  371. [ ${#agents[@]} -gt 0 ] && echo -e "${CYAN}Agents (${#agents[@]}):${NC} ${agents[*]##*:}"
  372. [ ${#subagents[@]} -gt 0 ] && echo -e "${CYAN}Subagents (${#subagents[@]}):${NC} ${subagents[*]##*:}"
  373. [ ${#commands[@]} -gt 0 ] && echo -e "${CYAN}Commands (${#commands[@]}):${NC} ${commands[*]##*:}"
  374. [ ${#tools[@]} -gt 0 ] && echo -e "${CYAN}Tools (${#tools[@]}):${NC} ${tools[*]##*:}"
  375. [ ${#plugins[@]} -gt 0 ] && echo -e "${CYAN}Plugins (${#plugins[@]}):${NC} ${plugins[*]##*:}"
  376. [ ${#contexts[@]} -gt 0 ] && echo -e "${CYAN}Contexts (${#contexts[@]}):${NC} ${contexts[*]##*:}"
  377. [ ${#configs[@]} -gt 0 ] && echo -e "${CYAN}Config (${#configs[@]}):${NC} ${configs[*]##*:}"
  378. echo ""
  379. read -p "Proceed with installation? [Y/n]: " confirm
  380. if [[ $confirm =~ ^[Nn] ]]; then
  381. print_info "Installation cancelled"
  382. cleanup_and_exit 0
  383. fi
  384. perform_installation
  385. }
  386. #############################################################################
  387. # Collision Detection
  388. #############################################################################
  389. show_collision_report() {
  390. local collision_count=$1
  391. shift
  392. local collisions=("$@")
  393. echo ""
  394. print_warning "Found ${collision_count} file collision(s):"
  395. echo ""
  396. # Group by type
  397. local agents=()
  398. local subagents=()
  399. local commands=()
  400. local tools=()
  401. local plugins=()
  402. local contexts=()
  403. local configs=()
  404. for file in "${collisions[@]}"; do
  405. # Skip empty entries
  406. [ -z "$file" ] && continue
  407. if [[ $file == *"/agent/subagents/"* ]]; then
  408. subagents+=("$file")
  409. elif [[ $file == *"/agent/"* ]]; then
  410. agents+=("$file")
  411. elif [[ $file == *"/command/"* ]]; then
  412. commands+=("$file")
  413. elif [[ $file == *"/tool/"* ]]; then
  414. tools+=("$file")
  415. elif [[ $file == *"/plugin/"* ]]; then
  416. plugins+=("$file")
  417. elif [[ $file == *"/context/"* ]]; then
  418. contexts+=("$file")
  419. else
  420. configs+=("$file")
  421. fi
  422. done
  423. # Display grouped collisions
  424. [ ${#agents[@]} -gt 0 ] && echo -e "${YELLOW} Agents (${#agents[@]}):${NC}" && printf ' %s\n' "${agents[@]}"
  425. [ ${#subagents[@]} -gt 0 ] && echo -e "${YELLOW} Subagents (${#subagents[@]}):${NC}" && printf ' %s\n' "${subagents[@]}"
  426. [ ${#commands[@]} -gt 0 ] && echo -e "${YELLOW} Commands (${#commands[@]}):${NC}" && printf ' %s\n' "${commands[@]}"
  427. [ ${#tools[@]} -gt 0 ] && echo -e "${YELLOW} Tools (${#tools[@]}):${NC}" && printf ' %s\n' "${tools[@]}"
  428. [ ${#plugins[@]} -gt 0 ] && echo -e "${YELLOW} Plugins (${#plugins[@]}):${NC}" && printf ' %s\n' "${plugins[@]}"
  429. [ ${#contexts[@]} -gt 0 ] && echo -e "${YELLOW} Context (${#contexts[@]}):${NC}" && printf ' %s\n' "${contexts[@]}"
  430. [ ${#configs[@]} -gt 0 ] && echo -e "${YELLOW} Config (${#configs[@]}):${NC}" && printf ' %s\n' "${configs[@]}"
  431. echo ""
  432. }
  433. get_install_strategy() {
  434. echo -e "${BOLD}How would you like to proceed?${NC}\n"
  435. echo " 1) ${GREEN}Skip existing${NC} - Only install new files, keep all existing files unchanged"
  436. echo " 2) ${YELLOW}Overwrite all${NC} - Replace existing files with new versions (your changes will be lost)"
  437. echo " 3) ${CYAN}Backup & overwrite${NC} - Backup existing files, then install new versions"
  438. echo " 4) ${RED}Cancel${NC} - Exit without making changes"
  439. echo ""
  440. read -p "Enter your choice [1-4]: " strategy_choice
  441. case $strategy_choice in
  442. 1) echo "skip" ;;
  443. 2)
  444. echo ""
  445. print_warning "This will overwrite existing files. Your changes will be lost!"
  446. read -p "Are you sure? Type 'yes' to confirm: " confirm
  447. if [ "$confirm" = "yes" ]; then
  448. echo "overwrite"
  449. else
  450. echo "cancel"
  451. fi
  452. ;;
  453. 3) echo "backup" ;;
  454. 4) echo "cancel" ;;
  455. *) echo "cancel" ;;
  456. esac
  457. }
  458. #############################################################################
  459. # Installation
  460. #############################################################################
  461. perform_installation() {
  462. print_step "Preparing installation..."
  463. # Create directory structure if it doesn't exist
  464. mkdir -p "$INSTALL_DIR"/{agent/subagents,command,tool,plugin,context/{core,project}}
  465. # Check for collisions
  466. local collisions=()
  467. for comp in "${SELECTED_COMPONENTS[@]}"; do
  468. local type="${comp%%:*}"
  469. local id="${comp##*:}"
  470. local path=$(jq -r ".components.${type}s[] | select(.id == \"${id}\") | .path" "$TEMP_DIR/registry.json")
  471. if [ -n "$path" ] && [ "$path" != "null" ] && [ -f "$path" ]; then
  472. collisions+=("$path")
  473. fi
  474. done
  475. # Determine installation strategy
  476. local install_strategy="fresh"
  477. if [ ${#collisions[@]} -gt 0 ]; then
  478. show_collision_report ${#collisions[@]} "${collisions[@]}"
  479. install_strategy=$(get_install_strategy)
  480. if [ "$install_strategy" = "cancel" ]; then
  481. print_info "Installation cancelled by user"
  482. cleanup_and_exit 0
  483. fi
  484. # Handle backup strategy
  485. if [ "$install_strategy" = "backup" ]; then
  486. local backup_dir="${INSTALL_DIR}.backup.$(date +%Y%m%d-%H%M%S)"
  487. print_step "Creating backup..."
  488. # Only backup files that will be overwritten
  489. local backup_count=0
  490. for file in "${collisions[@]}"; do
  491. if [ -f "$file" ]; then
  492. local backup_file="${backup_dir}/${file}"
  493. mkdir -p "$(dirname "$backup_file")"
  494. if cp "$file" "$backup_file" 2>/dev/null; then
  495. ((backup_count++))
  496. else
  497. print_warning "Failed to backup: $file"
  498. fi
  499. fi
  500. done
  501. if [ $backup_count -gt 0 ]; then
  502. print_success "Backed up ${backup_count} file(s) to $backup_dir"
  503. install_strategy="overwrite" # Now we can overwrite
  504. else
  505. print_error "Backup failed. Installation cancelled."
  506. cleanup_and_exit 1
  507. fi
  508. fi
  509. fi
  510. # Perform installation
  511. print_step "Installing components..."
  512. local installed=0
  513. local skipped=0
  514. local failed=0
  515. for comp in "${SELECTED_COMPONENTS[@]}"; do
  516. local type="${comp%%:*}"
  517. local id="${comp##*:}"
  518. # Get component path
  519. local path=$(jq -r ".components.${type}s[] | select(.id == \"${id}\") | .path" "$TEMP_DIR/registry.json")
  520. if [ -z "$path" ] || [ "$path" = "null" ]; then
  521. print_warning "Could not find path for ${comp}"
  522. ((failed++))
  523. continue
  524. fi
  525. # Check if file exists before we install (for proper messaging)
  526. local file_existed=false
  527. if [ -f "$path" ]; then
  528. file_existed=true
  529. fi
  530. # Check if file exists and we're in skip mode
  531. if [ "$file_existed" = true ] && [ "$install_strategy" = "skip" ]; then
  532. print_info "Skipped existing: ${type}:${id}"
  533. ((skipped++))
  534. continue
  535. fi
  536. # Download component
  537. local url="${RAW_URL}/${path}"
  538. local dest="${path}"
  539. # Create parent directory if needed
  540. mkdir -p "$(dirname "$dest")"
  541. if curl -fsSL "$url" -o "$dest"; then
  542. # Show appropriate message based on whether file existed before
  543. if [ "$file_existed" = true ]; then
  544. print_success "Updated ${type}: ${id}"
  545. else
  546. print_success "Installed ${type}: ${id}"
  547. fi
  548. ((installed++))
  549. else
  550. print_error "Failed to install ${type}: ${id}"
  551. ((failed++))
  552. fi
  553. done
  554. # Handle additional paths for advanced profile
  555. if [ "$PROFILE" = "advanced" ]; then
  556. local additional_paths=$(jq -r '.profiles.advanced.additionalPaths[]?' "$TEMP_DIR/registry.json")
  557. if [ -n "$additional_paths" ]; then
  558. print_step "Installing additional paths..."
  559. while IFS= read -r path; do
  560. # For directories, we'd need to recursively download
  561. # For now, just note them
  562. print_info "Additional path: $path (manual download required)"
  563. done <<< "$additional_paths"
  564. fi
  565. fi
  566. echo ""
  567. print_success "Installation complete!"
  568. echo -e " Installed: ${GREEN}${installed}${NC}"
  569. [ $skipped -gt 0 ] && echo -e " Skipped: ${CYAN}${skipped}${NC}"
  570. [ $failed -gt 0 ] && echo -e " Failed: ${RED}${failed}${NC}"
  571. show_post_install
  572. }
  573. #############################################################################
  574. # Post-Installation
  575. #############################################################################
  576. show_post_install() {
  577. echo ""
  578. print_step "Next Steps"
  579. echo "1. Review the installed components in .opencode/"
  580. echo "2. Copy env.example to .env and configure:"
  581. echo " ${CYAN}cp env.example .env${NC}"
  582. echo "3. Start using OpenCode agents:"
  583. echo " ${CYAN}opencode${NC}"
  584. echo ""
  585. if [ -d "${INSTALL_DIR}.backup."* ] 2>/dev/null; then
  586. print_info "Backup created - you can restore files from .opencode.backup.* if needed"
  587. fi
  588. print_info "Documentation: ${REPO_URL}"
  589. echo ""
  590. cleanup_and_exit 0
  591. }
  592. #############################################################################
  593. # Component Listing
  594. #############################################################################
  595. list_components() {
  596. clear
  597. print_header
  598. echo -e "${BOLD}Available Components${NC}\n"
  599. local categories=("agents" "subagents" "commands" "tools" "plugins" "contexts")
  600. for category in "${categories[@]}"; do
  601. local cat_display=$(echo "$category" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
  602. echo -e "${CYAN}${BOLD}${cat_display}:${NC}"
  603. local components=$(jq -r ".components.${category}[] | \"\(.id)|\(.name)|\(.description)\"" "$TEMP_DIR/registry.json")
  604. while IFS='|' read -r id name desc; do
  605. echo -e " ${GREEN}${name}${NC} (${id})"
  606. echo -e " ${desc}"
  607. done <<< "$components"
  608. echo ""
  609. done
  610. read -p "Press Enter to continue..."
  611. }
  612. #############################################################################
  613. # Cleanup
  614. #############################################################################
  615. cleanup_and_exit() {
  616. rm -rf "$TEMP_DIR"
  617. exit "$1"
  618. }
  619. trap 'cleanup_and_exit 1' INT TERM
  620. #############################################################################
  621. # Main
  622. #############################################################################
  623. main() {
  624. # Parse command line arguments
  625. case "${1:-}" in
  626. core|--core)
  627. INSTALL_MODE="profile"
  628. PROFILE="core"
  629. ;;
  630. developer|--developer)
  631. INSTALL_MODE="profile"
  632. PROFILE="developer"
  633. ;;
  634. full|--full)
  635. INSTALL_MODE="profile"
  636. PROFILE="full"
  637. ;;
  638. advanced|--advanced)
  639. INSTALL_MODE="profile"
  640. PROFILE="advanced"
  641. ;;
  642. list|--list)
  643. check_dependencies
  644. fetch_registry
  645. list_components
  646. cleanup_and_exit 0
  647. ;;
  648. --help|-h|help)
  649. print_header
  650. echo "Usage: $0 [OPTIONS]"
  651. echo ""
  652. echo "Options:"
  653. echo " core, --core Install core profile"
  654. echo " developer, --developer Install developer profile"
  655. echo " full, --full Install full profile"
  656. echo " advanced, --advanced Install advanced profile"
  657. echo " list, --list List all available components"
  658. echo " help, --help, -h Show this help message"
  659. echo ""
  660. echo "Examples:"
  661. echo " $0 core"
  662. echo " $0 --developer"
  663. echo " curl -fsSL https://raw.githubusercontent.com/darrenhinde/opencode-agents/main/install.sh | bash -s core"
  664. echo ""
  665. echo "Without options, runs in interactive mode"
  666. exit 0
  667. ;;
  668. esac
  669. check_bash_version
  670. check_dependencies
  671. fetch_registry
  672. if [ -n "$PROFILE" ]; then
  673. # Non-interactive mode (compatible with bash 3.2+)
  674. SELECTED_COMPONENTS=()
  675. local temp_file="$TEMP_DIR/components.tmp"
  676. get_profile_components "$PROFILE" > "$temp_file"
  677. while IFS= read -r component; do
  678. [ -n "$component" ] && SELECTED_COMPONENTS+=("$component")
  679. done < "$temp_file"
  680. show_installation_preview
  681. else
  682. # Interactive mode
  683. show_main_menu
  684. if [ "$INSTALL_MODE" = "profile" ]; then
  685. show_profile_menu
  686. elif [ "$INSTALL_MODE" = "custom" ]; then
  687. show_custom_menu
  688. fi
  689. fi
  690. }
  691. main "$@"