install.sh 52 KB

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