install.sh 51 KB

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