install.sh 41 KB

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