install.sh 52 KB

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