install.sh 31 KB

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