install.sh 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. #!/usr/bin/env bash
  2. #############################################################################
  3. # OpenCode Agents Installer
  4. # Interactive installer for OpenCode agents, commands, tools, and plugins
  5. #############################################################################
  6. set -e
  7. # Colors for output
  8. RED='\033[0;31m'
  9. GREEN='\033[0;32m'
  10. YELLOW='\033[1;33m'
  11. BLUE='\033[0;34m'
  12. MAGENTA='\033[0;35m'
  13. CYAN='\033[0;36m'
  14. BOLD='\033[1m'
  15. NC='\033[0m' # No Color
  16. # Configuration
  17. REPO_URL="https://github.com/darrenhinde/opencode-agents"
  18. BRANCH="${OPENCODE_BRANCH:-main}" # Allow override via environment variable
  19. RAW_URL="https://raw.githubusercontent.com/darrenhinde/opencode-agents/${BRANCH}"
  20. REGISTRY_URL="${RAW_URL}/registry.json"
  21. INSTALL_DIR=".opencode"
  22. TEMP_DIR="/tmp/opencode-installer-$$"
  23. # Global variables
  24. SELECTED_COMPONENTS=()
  25. INSTALL_MODE=""
  26. PROFILE=""
  27. #############################################################################
  28. # Utility Functions
  29. #############################################################################
  30. print_header() {
  31. echo -e "${CYAN}${BOLD}"
  32. echo "╔════════════════════════════════════════════════════════════════╗"
  33. echo "║ ║"
  34. echo "║ OpenCode Agents Installer v1.0.0 ║"
  35. echo "║ ║"
  36. echo "╚════════════════════════════════════════════════════════════════╝"
  37. echo -e "${NC}"
  38. }
  39. print_success() {
  40. echo -e "${GREEN}✓${NC} $1"
  41. }
  42. print_error() {
  43. echo -e "${RED}✗${NC} $1"
  44. }
  45. print_info() {
  46. echo -e "${BLUE}ℹ${NC} $1"
  47. }
  48. print_warning() {
  49. echo -e "${YELLOW}⚠${NC} $1"
  50. }
  51. print_step() {
  52. echo -e "\n${MAGENTA}${BOLD}▶${NC} $1\n"
  53. }
  54. #############################################################################
  55. # Dependency Checks
  56. #############################################################################
  57. check_dependencies() {
  58. print_step "Checking dependencies..."
  59. local missing_deps=()
  60. if ! command -v curl &> /dev/null; then
  61. missing_deps+=("curl")
  62. fi
  63. if ! command -v jq &> /dev/null; then
  64. missing_deps+=("jq")
  65. fi
  66. if [ ${#missing_deps[@]} -ne 0 ]; then
  67. print_error "Missing required dependencies: ${missing_deps[*]}"
  68. echo ""
  69. echo "Please install them:"
  70. echo " macOS: brew install ${missing_deps[*]}"
  71. echo " Ubuntu: sudo apt-get install ${missing_deps[*]}"
  72. echo " Fedora: sudo dnf install ${missing_deps[*]}"
  73. exit 1
  74. fi
  75. print_success "All dependencies found"
  76. }
  77. #############################################################################
  78. # Registry Functions
  79. #############################################################################
  80. fetch_registry() {
  81. print_step "Fetching component registry..."
  82. mkdir -p "$TEMP_DIR"
  83. if ! curl -fsSL "$REGISTRY_URL" -o "$TEMP_DIR/registry.json"; then
  84. print_error "Failed to fetch registry from $REGISTRY_URL"
  85. exit 1
  86. fi
  87. print_success "Registry fetched successfully"
  88. }
  89. get_profile_components() {
  90. local profile=$1
  91. jq -r ".profiles.${profile}.components[]" "$TEMP_DIR/registry.json"
  92. }
  93. get_component_info() {
  94. local component_id=$1
  95. local component_type=$2
  96. jq -r ".components.${component_type}[] | select(.id == \"${component_id}\")" "$TEMP_DIR/registry.json"
  97. }
  98. resolve_dependencies() {
  99. local component=$1
  100. local type="${component%%:*}"
  101. local id="${component##*:}"
  102. # Get dependencies for this component
  103. local deps=$(jq -r ".components.${type}s[] | select(.id == \"${id}\") | .dependencies[]?" "$TEMP_DIR/registry.json" 2>/dev/null || echo "")
  104. if [ -n "$deps" ]; then
  105. for dep in $deps; do
  106. # Add dependency if not already in list
  107. if [[ ! " ${SELECTED_COMPONENTS[@]} " =~ " ${dep} " ]]; then
  108. SELECTED_COMPONENTS+=("$dep")
  109. # Recursively resolve dependencies
  110. resolve_dependencies "$dep"
  111. fi
  112. done
  113. fi
  114. }
  115. #############################################################################
  116. # Installation Mode Selection
  117. #############################################################################
  118. show_main_menu() {
  119. clear
  120. print_header
  121. echo -e "${BOLD}Choose installation mode:${NC}\n"
  122. echo " 1) Quick Install (Choose a profile)"
  123. echo " 2) Custom Install (Pick individual components)"
  124. echo " 3) List Available Components"
  125. echo " 4) Exit"
  126. echo ""
  127. read -p "Enter your choice [1-4]: " choice
  128. case $choice in
  129. 1) INSTALL_MODE="profile" ;;
  130. 2) INSTALL_MODE="custom" ;;
  131. 3) list_components; show_main_menu ;;
  132. 4) cleanup_and_exit 0 ;;
  133. *) print_error "Invalid choice"; sleep 2; show_main_menu ;;
  134. esac
  135. }
  136. #############################################################################
  137. # Profile Installation
  138. #############################################################################
  139. show_profile_menu() {
  140. clear
  141. print_header
  142. echo -e "${BOLD}Available Installation Profiles:${NC}\n"
  143. # Core profile
  144. local core_desc=$(jq -r '.profiles.core.description' "$TEMP_DIR/registry.json")
  145. local core_count=$(jq -r '.profiles.core.components | length' "$TEMP_DIR/registry.json")
  146. echo -e " ${GREEN}1) Core${NC}"
  147. echo -e " ${core_desc}"
  148. echo -e " Components: ${core_count}\n"
  149. # Developer profile
  150. local dev_desc=$(jq -r '.profiles.developer.description' "$TEMP_DIR/registry.json")
  151. local dev_count=$(jq -r '.profiles.developer.components | length' "$TEMP_DIR/registry.json")
  152. echo -e " ${BLUE}2) Developer${NC}"
  153. echo -e " ${dev_desc}"
  154. echo -e " Components: ${dev_count}\n"
  155. # Full profile
  156. local full_desc=$(jq -r '.profiles.full.description' "$TEMP_DIR/registry.json")
  157. local full_count=$(jq -r '.profiles.full.components | length' "$TEMP_DIR/registry.json")
  158. echo -e " ${MAGENTA}3) Full${NC}"
  159. echo -e " ${full_desc}"
  160. echo -e " Components: ${full_count}\n"
  161. # Advanced profile
  162. local adv_desc=$(jq -r '.profiles.advanced.description' "$TEMP_DIR/registry.json")
  163. local adv_count=$(jq -r '.profiles.advanced.components | length' "$TEMP_DIR/registry.json")
  164. echo -e " ${YELLOW}4) Advanced${NC}"
  165. echo -e " ${adv_desc}"
  166. echo -e " Components: ${adv_count}\n"
  167. echo " 5) Back to main menu"
  168. echo ""
  169. read -p "Enter your choice [1-5]: " choice
  170. case $choice in
  171. 1) PROFILE="core" ;;
  172. 2) PROFILE="developer" ;;
  173. 3) PROFILE="full" ;;
  174. 4) PROFILE="advanced" ;;
  175. 5) show_main_menu; return ;;
  176. *) print_error "Invalid choice"; sleep 2; show_profile_menu; return ;;
  177. esac
  178. # Load profile components
  179. mapfile -t SELECTED_COMPONENTS < <(get_profile_components "$PROFILE")
  180. show_installation_preview
  181. }
  182. #############################################################################
  183. # Custom Component Selection
  184. #############################################################################
  185. show_custom_menu() {
  186. clear
  187. print_header
  188. echo -e "${BOLD}Select component categories to install:${NC}\n"
  189. echo "Use space to toggle, Enter to continue"
  190. echo ""
  191. local categories=("agents" "subagents" "commands" "tools" "plugins" "contexts" "config")
  192. local selected_categories=()
  193. # Simple selection (for now, we'll make it interactive later)
  194. echo "Available categories:"
  195. for i in "${!categories[@]}"; do
  196. local cat="${categories[$i]}"
  197. local count=$(jq -r ".components.${cat} | length" "$TEMP_DIR/registry.json")
  198. local cat_display=$(echo "$cat" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
  199. echo " $((i+1))) ${cat_display} (${count} available)"
  200. done
  201. echo " $((${#categories[@]}+1))) Select All"
  202. echo " $((${#categories[@]}+2))) Continue to component selection"
  203. echo " $((${#categories[@]}+3))) Back to main menu"
  204. echo ""
  205. read -p "Enter category numbers (space-separated) or option: " -a selections
  206. for sel in "${selections[@]}"; do
  207. if [ "$sel" -eq $((${#categories[@]}+1)) ]; then
  208. selected_categories=("${categories[@]}")
  209. break
  210. elif [ "$sel" -eq $((${#categories[@]}+2)) ]; then
  211. break
  212. elif [ "$sel" -eq $((${#categories[@]}+3)) ]; then
  213. show_main_menu
  214. return
  215. elif [ "$sel" -ge 1 ] && [ "$sel" -le ${#categories[@]} ]; then
  216. selected_categories+=("${categories[$((sel-1))]}")
  217. fi
  218. done
  219. if [ ${#selected_categories[@]} -eq 0 ]; then
  220. print_warning "No categories selected"
  221. sleep 2
  222. show_custom_menu
  223. return
  224. fi
  225. show_component_selection "${selected_categories[@]}"
  226. }
  227. show_component_selection() {
  228. local categories=("$@")
  229. clear
  230. print_header
  231. echo -e "${BOLD}Select components to install:${NC}\n"
  232. local all_components=()
  233. local component_details=()
  234. for category in "${categories[@]}"; do
  235. local cat_display=$(echo "$category" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
  236. echo -e "${CYAN}${BOLD}${cat_display}:${NC}"
  237. local components=$(jq -r ".components.${category}[] | .id" "$TEMP_DIR/registry.json")
  238. local idx=1
  239. while IFS= read -r comp_id; do
  240. local comp_name=$(jq -r ".components.${category}[] | select(.id == \"${comp_id}\") | .name" "$TEMP_DIR/registry.json")
  241. local comp_desc=$(jq -r ".components.${category}[] | select(.id == \"${comp_id}\") | .description" "$TEMP_DIR/registry.json")
  242. echo " ${idx}) ${comp_name}"
  243. echo " ${comp_desc}"
  244. all_components+=("${category}:${comp_id}")
  245. component_details+=("${comp_name}|${comp_desc}")
  246. idx=$((idx+1))
  247. done <<< "$components"
  248. echo ""
  249. done
  250. echo "Enter component numbers (space-separated), 'all' for all, or 'done' to continue:"
  251. read -a selections
  252. for sel in "${selections[@]}"; do
  253. if [ "$sel" = "all" ]; then
  254. SELECTED_COMPONENTS=("${all_components[@]}")
  255. break
  256. elif [ "$sel" = "done" ]; then
  257. break
  258. elif [ "$sel" -ge 1 ] && [ "$sel" -le ${#all_components[@]} ]; then
  259. SELECTED_COMPONENTS+=("${all_components[$((sel-1))]}")
  260. fi
  261. done
  262. if [ ${#SELECTED_COMPONENTS[@]} -eq 0 ]; then
  263. print_warning "No components selected"
  264. sleep 2
  265. show_custom_menu
  266. return
  267. fi
  268. # Resolve dependencies
  269. print_step "Resolving dependencies..."
  270. local original_count=${#SELECTED_COMPONENTS[@]}
  271. for comp in "${SELECTED_COMPONENTS[@]}"; do
  272. resolve_dependencies "$comp"
  273. done
  274. if [ ${#SELECTED_COMPONENTS[@]} -gt $original_count ]; then
  275. print_info "Added $((${#SELECTED_COMPONENTS[@]} - original_count)) dependencies"
  276. fi
  277. show_installation_preview
  278. }
  279. #############################################################################
  280. # Installation Preview & Confirmation
  281. #############################################################################
  282. show_installation_preview() {
  283. clear
  284. print_header
  285. echo -e "${BOLD}Installation Preview${NC}\n"
  286. if [ -n "$PROFILE" ]; then
  287. echo -e "Profile: ${GREEN}${PROFILE}${NC}"
  288. else
  289. echo -e "Mode: ${GREEN}Custom${NC}"
  290. fi
  291. echo -e "\nComponents to install (${#SELECTED_COMPONENTS[@]} total):\n"
  292. # Group by type
  293. local agents=()
  294. local subagents=()
  295. local commands=()
  296. local tools=()
  297. local plugins=()
  298. local contexts=()
  299. local configs=()
  300. for comp in "${SELECTED_COMPONENTS[@]}"; do
  301. local type="${comp%%:*}"
  302. case $type in
  303. agent) agents+=("$comp") ;;
  304. subagent) subagents+=("$comp") ;;
  305. command) commands+=("$comp") ;;
  306. tool) tools+=("$comp") ;;
  307. plugin) plugins+=("$comp") ;;
  308. context) contexts+=("$comp") ;;
  309. config) configs+=("$comp") ;;
  310. esac
  311. done
  312. [ ${#agents[@]} -gt 0 ] && echo -e "${CYAN}Agents (${#agents[@]}):${NC} ${agents[*]##*:}"
  313. [ ${#subagents[@]} -gt 0 ] && echo -e "${CYAN}Subagents (${#subagents[@]}):${NC} ${subagents[*]##*:}"
  314. [ ${#commands[@]} -gt 0 ] && echo -e "${CYAN}Commands (${#commands[@]}):${NC} ${commands[*]##*:}"
  315. [ ${#tools[@]} -gt 0 ] && echo -e "${CYAN}Tools (${#tools[@]}):${NC} ${tools[*]##*:}"
  316. [ ${#plugins[@]} -gt 0 ] && echo -e "${CYAN}Plugins (${#plugins[@]}):${NC} ${plugins[*]##*:}"
  317. [ ${#contexts[@]} -gt 0 ] && echo -e "${CYAN}Contexts (${#contexts[@]}):${NC} ${contexts[*]##*:}"
  318. [ ${#configs[@]} -gt 0 ] && echo -e "${CYAN}Config (${#configs[@]}):${NC} ${configs[*]##*:}"
  319. echo ""
  320. read -p "Proceed with installation? [Y/n]: " confirm
  321. if [[ $confirm =~ ^[Nn] ]]; then
  322. print_info "Installation cancelled"
  323. cleanup_and_exit 0
  324. fi
  325. perform_installation
  326. }
  327. #############################################################################
  328. # Installation
  329. #############################################################################
  330. perform_installation() {
  331. print_step "Installing components..."
  332. # Check if .opencode already exists
  333. if [ -d "$INSTALL_DIR" ]; then
  334. print_warning "$INSTALL_DIR directory already exists"
  335. read -p "Backup and continue? [Y/n]: " backup_confirm
  336. if [[ ! $backup_confirm =~ ^[Nn] ]]; then
  337. local backup_dir="${INSTALL_DIR}.backup.$(date +%Y%m%d-%H%M%S)"
  338. mv "$INSTALL_DIR" "$backup_dir"
  339. print_success "Backed up to $backup_dir"
  340. else
  341. print_error "Installation cancelled"
  342. cleanup_and_exit 1
  343. fi
  344. fi
  345. # Create directory structure
  346. mkdir -p "$INSTALL_DIR"/{agent/subagents,command,tool,plugin,context/{core,project}}
  347. local installed=0
  348. local failed=0
  349. for comp in "${SELECTED_COMPONENTS[@]}"; do
  350. local type="${comp%%:*}"
  351. local id="${comp##*:}"
  352. # Get component path
  353. local path=$(jq -r ".components.${type}s[] | select(.id == \"${id}\") | .path" "$TEMP_DIR/registry.json")
  354. if [ -z "$path" ] || [ "$path" = "null" ]; then
  355. print_warning "Could not find path for ${comp}"
  356. ((failed++))
  357. continue
  358. fi
  359. # Download component
  360. local url="${RAW_URL}/${path}"
  361. local dest="${path}"
  362. # Create parent directory if needed
  363. mkdir -p "$(dirname "$dest")"
  364. if curl -fsSL "$url" -o "$dest"; then
  365. print_success "Installed ${type}: ${id}"
  366. ((installed++))
  367. else
  368. print_error "Failed to install ${type}: ${id}"
  369. ((failed++))
  370. fi
  371. done
  372. # Handle additional paths for advanced profile
  373. if [ "$PROFILE" = "advanced" ]; then
  374. local additional_paths=$(jq -r '.profiles.advanced.additionalPaths[]?' "$TEMP_DIR/registry.json")
  375. if [ -n "$additional_paths" ]; then
  376. print_step "Installing additional paths..."
  377. while IFS= read -r path; do
  378. # For directories, we'd need to recursively download
  379. # For now, just note them
  380. print_info "Additional path: $path (manual download required)"
  381. done <<< "$additional_paths"
  382. fi
  383. fi
  384. echo ""
  385. print_success "Installation complete!"
  386. echo -e " Installed: ${GREEN}${installed}${NC}"
  387. [ $failed -gt 0 ] && echo -e " Failed: ${RED}${failed}${NC}"
  388. show_post_install
  389. }
  390. #############################################################################
  391. # Post-Installation
  392. #############################################################################
  393. show_post_install() {
  394. echo ""
  395. print_step "Next Steps"
  396. echo "1. Review the installed components in .opencode/"
  397. echo "2. Copy env.example to .env and configure:"
  398. echo " ${CYAN}cp env.example .env${NC}"
  399. echo "3. Start using OpenCode agents:"
  400. echo " ${CYAN}opencode${NC}"
  401. echo ""
  402. print_info "Documentation: ${REPO_URL}"
  403. echo ""
  404. cleanup_and_exit 0
  405. }
  406. #############################################################################
  407. # Component Listing
  408. #############################################################################
  409. list_components() {
  410. clear
  411. print_header
  412. echo -e "${BOLD}Available Components${NC}\n"
  413. local categories=("agents" "subagents" "commands" "tools" "plugins" "contexts")
  414. for category in "${categories[@]}"; do
  415. local cat_display=$(echo "$category" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
  416. echo -e "${CYAN}${BOLD}${cat_display}:${NC}"
  417. local components=$(jq -r ".components.${category}[] | \"\(.id)|\(.name)|\(.description)\"" "$TEMP_DIR/registry.json")
  418. while IFS='|' read -r id name desc; do
  419. echo -e " ${GREEN}${name}${NC} (${id})"
  420. echo -e " ${desc}"
  421. done <<< "$components"
  422. echo ""
  423. done
  424. read -p "Press Enter to continue..."
  425. }
  426. #############################################################################
  427. # Cleanup
  428. #############################################################################
  429. cleanup_and_exit() {
  430. rm -rf "$TEMP_DIR"
  431. exit "$1"
  432. }
  433. trap 'cleanup_and_exit 1' INT TERM
  434. #############################################################################
  435. # Main
  436. #############################################################################
  437. main() {
  438. # Parse command line arguments
  439. case "${1:-}" in
  440. --core)
  441. INSTALL_MODE="profile"
  442. PROFILE="core"
  443. ;;
  444. --developer)
  445. INSTALL_MODE="profile"
  446. PROFILE="developer"
  447. ;;
  448. --full)
  449. INSTALL_MODE="profile"
  450. PROFILE="full"
  451. ;;
  452. --advanced)
  453. INSTALL_MODE="profile"
  454. PROFILE="advanced"
  455. ;;
  456. --list)
  457. check_dependencies
  458. fetch_registry
  459. list_components
  460. cleanup_and_exit 0
  461. ;;
  462. --help|-h)
  463. print_header
  464. echo "Usage: $0 [OPTIONS]"
  465. echo ""
  466. echo "Options:"
  467. echo " --core Install core profile"
  468. echo " --developer Install developer profile"
  469. echo " --full Install full profile"
  470. echo " --advanced Install advanced profile"
  471. echo " --list List all available components"
  472. echo " --help Show this help message"
  473. echo ""
  474. echo "Without options, runs in interactive mode"
  475. exit 0
  476. ;;
  477. esac
  478. check_dependencies
  479. fetch_registry
  480. if [ -n "$PROFILE" ]; then
  481. # Non-interactive mode
  482. mapfile -t SELECTED_COMPONENTS < <(get_profile_components "$PROFILE")
  483. show_installation_preview
  484. else
  485. # Interactive mode
  486. show_main_menu
  487. if [ "$INSTALL_MODE" = "profile" ]; then
  488. show_profile_menu
  489. elif [ "$INSTALL_MODE" = "custom" ]; then
  490. show_custom_menu
  491. fi
  492. fi
  493. }
  494. main "$@"