Files
2026-07-13 12:40:33 +08:00

584 lines
18 KiB
Bash
Executable File

#!/bin/bash
# Claude Code Agents Installer
# Interactive script to install/uninstall agents from this repository
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
BOLD='\033[1m'
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CATEGORIES_DIR="$SCRIPT_DIR/categories"
GLOBAL_AGENTS_DIR="$HOME/.claude/agents"
LOCAL_AGENTS_DIR=".claude/agents"
CLAUDE_AGENTS_DIR="" # Will be set by select_install_mode
INSTALL_MODE="" # "global" or "local"
SOURCE_MODE="" # "local" or "remote"
# GitHub API configuration
GITHUB_API_BASE="https://api.github.com/repos/VoltAgent/awesome-claude-code-subagents/contents"
GITHUB_RAW_BASE="https://raw.githubusercontent.com/VoltAgent/awesome-claude-code-subagents/main"
# Cache for remote data
REMOTE_CATEGORIES=()
REMOTE_AGENTS=()
# Function to check if local .claude directory exists
has_local_claude_dir() {
[[ -d ".claude" ]]
}
# Function to check if local categories directory exists
has_local_categories() {
[[ -d "$CATEGORIES_DIR" ]]
}
# Function to check if curl is available
check_curl() {
if ! command -v curl &> /dev/null; then
echo -e "${RED}Error: curl is required for remote mode but not installed.${NC}"
exit 1
fi
}
# Function to fetch categories from GitHub API
fetch_categories_remote() {
local response
response=$(curl -s "$GITHUB_API_BASE/categories")
# Check for rate limiting or errors
if echo "$response" | grep -q "API rate limit exceeded"; then
echo -e "${RED}GitHub API rate limit exceeded. Please try again later or use local mode.${NC}"
sleep 3
return 1
fi
if echo "$response" | grep -q '"message"'; then
echo -e "${RED}Error fetching from GitHub API.${NC}"
sleep 3
return 1
fi
# Parse JSON response - extract directory names starting with numbers
REMOTE_CATEGORIES=()
while IFS= read -r line; do
if [[ -n "$line" ]]; then
REMOTE_CATEGORIES+=("$line")
fi
done < <(echo "$response" | grep -o '"name": "[0-9][^"]*"' | sed 's/"name": "//;s/"$//' | sort)
return 0
}
# Function to fetch agents from a category via GitHub API
fetch_agents_remote() {
local category="$1"
local response
response=$(curl -s "$GITHUB_API_BASE/categories/$category")
# Check for errors
if echo "$response" | grep -q '"message"'; then
return 1
fi
# Parse JSON response - extract .md files excluding README.md
REMOTE_AGENTS=()
while IFS= read -r line; do
if [[ -n "$line" && "$line" != "README.md" ]]; then
REMOTE_AGENTS+=("$line")
fi
done < <(echo "$response" | grep -o '"name": "[^"]*\.md"' | sed 's/"name": "//;s/"$//' | sort)
return 0
}
# Function to download an agent file from GitHub
download_agent() {
local category="$1"
local agent_file="$2"
local dest_path="$3"
local url="$GITHUB_RAW_BASE/categories/$category/$agent_file"
if curl -sS "$url" -o "$dest_path" 2>/dev/null; then
return 0
else
return 1
fi
}
# Function to select source mode (local or remote)
select_source_mode() {
# If no local categories, automatically use remote
if ! has_local_categories; then
SOURCE_MODE="remote"
check_curl
echo -e "${YELLOW}No local repository found. Using remote mode (GitHub).${NC}"
sleep 1
return
fi
show_header
echo -e "${BOLD}Select source:${NC}\n"
echo -e " ${YELLOW}1)${NC} Local files ${CYAN}(from cloned repository)${NC}"
echo -e " Faster, works offline"
echo ""
echo -e " ${YELLOW}2)${NC} Remote ${CYAN}(download from GitHub)${NC}"
echo -e " Always up-to-date"
echo ""
echo -e " ${YELLOW}q)${NC} Quit"
echo ""
read -p "Enter your choice: " choice
case "$choice" in
1)
SOURCE_MODE="local"
;;
2)
SOURCE_MODE="remote"
check_curl
;;
q|Q)
echo -e "\n${GREEN}Goodbye!${NC}"
exit 0
;;
*)
echo -e "${RED}Invalid choice. Please try again.${NC}"
sleep 1
select_source_mode
;;
esac
}
# Function to select installation mode
select_install_mode() {
show_header
echo -e "${BOLD}Select installation mode:${NC}\n"
echo -e " ${YELLOW}1)${NC} Global installation ${CYAN}(~/.claude/agents/)${NC}"
echo -e " Available for all projects"
echo ""
if has_local_claude_dir; then
echo -e " ${YELLOW}2)${NC} Local installation ${CYAN}(.claude/agents/)${NC}"
echo -e " Only for current project"
else
echo -e " ${BLUE}2)${NC} Local installation ${CYAN}(not available)${NC}"
echo -e " ${YELLOW}No .claude/ directory found in current directory${NC}"
fi
echo ""
echo -e " ${YELLOW}q)${NC} Quit"
echo ""
read -p "Enter your choice: " choice
case "$choice" in
1)
CLAUDE_AGENTS_DIR="$GLOBAL_AGENTS_DIR"
INSTALL_MODE="global"
mkdir -p "$CLAUDE_AGENTS_DIR"
;;
2)
if has_local_claude_dir; then
CLAUDE_AGENTS_DIR="$LOCAL_AGENTS_DIR"
INSTALL_MODE="local"
mkdir -p "$CLAUDE_AGENTS_DIR"
else
echo -e "\n${RED}Local installation not available. No .claude/ directory found.${NC}"
sleep 2
select_install_mode
return
fi
;;
q|Q)
echo -e "\n${GREEN}Goodbye!${NC}"
exit 0
;;
*)
echo -e "${RED}Invalid choice. Please try again.${NC}"
sleep 1
select_install_mode
;;
esac
}
# Function to display a header
show_header() {
clear
echo -e "${BOLD}${CYAN}"
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ Claude Code Agents Installer ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo -e "${NC}"
if [[ -n "$INSTALL_MODE" ]]; then
local mode_str=""
if [[ "$INSTALL_MODE" == "global" ]]; then
mode_str="Global (~/.claude/agents/)"
else
mode_str="Local (.claude/agents/)"
fi
local source_str=""
if [[ "$SOURCE_MODE" == "remote" ]]; then
source_str=" | Source: GitHub"
else
source_str=" | Source: Local"
fi
echo -e "${BLUE}Mode: ${mode_str}${source_str}${NC}\n"
fi
}
# Function to get category display name (remove number prefix)
# Uses awk for Title Case conversion (compatible with macOS and Linux)
get_category_name() {
local dir="$1"
echo "$dir" | sed 's/^[0-9]*-//' | tr '-' ' ' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1'
}
# Function to check if an agent is installed
is_agent_installed() {
local agent_file="$1"
local agent_name=$(basename "$agent_file")
[[ -f "$CLAUDE_AGENTS_DIR/$agent_name" ]]
}
# Function to get agent description from frontmatter
get_agent_description() {
local agent_file="$1"
grep -A1 "^description:" "$agent_file" 2>/dev/null | head -1 | sed 's/^description: *//' | cut -c1-60
}
# Function to display category selection menu
select_category() {
show_header
echo -e "${BOLD}Select a category:${NC}\n"
local categories=()
local i=1
if [[ "$SOURCE_MODE" == "remote" ]]; then
# Remote mode: fetch from GitHub API
echo -e "${CYAN}Fetching categories from GitHub...${NC}\n"
if ! fetch_categories_remote; then
echo -e "${RED}Failed to fetch categories. Press Enter to retry.${NC}"
read
select_category
return
fi
for dirname in "${REMOTE_CATEGORIES[@]}"; do
categories+=("$dirname")
local display_name=$(get_category_name "$dirname")
echo -e " ${YELLOW}$i)${NC} $display_name"
((i++))
done
else
# Local mode: read from filesystem
for dir in "$CATEGORIES_DIR"/*/; do
if [[ -d "$dir" && $(basename "$dir") != "." ]]; then
local dirname=$(basename "$dir")
# Skip if it's not a category directory (doesn't start with number)
if [[ "$dirname" =~ ^[0-9]+ ]]; then
categories+=("$dirname")
local display_name=$(get_category_name "$dirname")
local agent_count=$(ls "$dir"/*.md 2>/dev/null | grep -v README.md | wc -l | tr -d ' ')
echo -e " ${YELLOW}$i)${NC} $display_name ${CYAN}($agent_count agents)${NC}"
((i++))
fi
fi
done
fi
echo ""
echo -e " ${YELLOW}q)${NC} Quit"
echo ""
read -p "Enter your choice: " choice
if [[ "$choice" == "q" || "$choice" == "Q" ]]; then
echo -e "\n${GREEN}Goodbye!${NC}"
exit 0
fi
if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#categories[@]} )); then
SELECTED_CATEGORY="${categories[$((choice-1))]}"
return 0
else
echo -e "${RED}Invalid choice. Please try again.${NC}"
sleep 1
select_category
fi
}
# Function to display agent selection menu with multi-select
select_agents() {
local category="$1"
local category_name=$(get_category_name "$category")
# Build list of agents (excluding README.md)
local agents=()
local agent_states=()
if [[ "$SOURCE_MODE" == "remote" ]]; then
# Remote mode: fetch from GitHub API
show_header
echo -e "${BOLD}Category: ${CYAN}$category_name${NC}\n"
echo -e "${CYAN}Fetching agents from GitHub...${NC}\n"
if ! fetch_agents_remote "$category"; then
echo -e "${RED}Failed to fetch agents. Press Enter to go back.${NC}"
read
return 1
fi
for agent_file in "${REMOTE_AGENTS[@]}"; do
agents+=("$agent_file")
# Check if installed (by filename)
if [[ -f "$CLAUDE_AGENTS_DIR/$agent_file" ]]; then
agent_states+=(1)
else
agent_states+=(0)
fi
done
else
# Local mode: read from filesystem
local category_path="$CATEGORIES_DIR/$category"
for agent_file in "$category_path"/*.md; do
local basename=$(basename "$agent_file")
if [[ "$basename" != "README.md" ]]; then
agents+=("$basename")
if [[ -f "$CLAUDE_AGENTS_DIR/$basename" ]]; then
agent_states+=(1)
else
agent_states+=(0)
fi
fi
done
fi
# Store original states to calculate changes
local original_states=("${agent_states[@]}")
while true; do
show_header
echo -e "${BOLD}Category: ${CYAN}$category_name${NC}\n"
echo -e "Use number keys to toggle selection. ${GREEN}[✓]${NC} = will be installed, ${RED}[ ]${NC} = will be removed\n"
local i=1
for agent_file in "${agents[@]}"; do
local agent_name="${agent_file%.md}"
local is_installed=""
local status_icon=""
local status_color=""
if [[ -f "$CLAUDE_AGENTS_DIR/$agent_file" ]]; then
is_installed=" ${BLUE}(installed)${NC}"
fi
if [[ ${agent_states[$((i-1))]} -eq 1 ]]; then
status_icon="[✓]"
status_color="${GREEN}"
else
status_icon="[ ]"
status_color="${RED}"
fi
echo -e " ${YELLOW}$i)${NC} ${status_color}${status_icon}${NC} $agent_name$is_installed"
((i++))
done
echo ""
echo -e " ${YELLOW}a)${NC} Select all"
echo -e " ${YELLOW}n)${NC} Deselect all"
echo -e " ${YELLOW}c)${NC} Confirm selection"
echo -e " ${YELLOW}b)${NC} Back to categories"
echo -e " ${YELLOW}q)${NC} Quit"
echo ""
read -p "Enter your choice: " choice
case "$choice" in
[0-9]*)
if (( choice >= 1 && choice <= ${#agents[@]} )); then
# Toggle selection
local idx=$((choice-1))
if [[ ${agent_states[$idx]} -eq 1 ]]; then
agent_states[$idx]=0
else
agent_states[$idx]=1
fi
fi
;;
a|A)
for i in "${!agent_states[@]}"; do
agent_states[$i]=1
done
;;
n|N)
for i in "${!agent_states[@]}"; do
agent_states[$i]=0
done
;;
c|C)
# Calculate changes
local to_install=()
local to_uninstall=()
for i in "${!agents[@]}"; do
local agent_file="${agents[$i]}"
local is_selected=${agent_states[$i]}
# Check if currently installed
local was_installed=0
if [[ -f "$CLAUDE_AGENTS_DIR/$agent_file" ]]; then
was_installed=1
fi
if [[ $was_installed -eq 0 && $is_selected -eq 1 ]]; then
to_install+=("$agent_file")
elif [[ $was_installed -eq 1 && $is_selected -eq 0 ]]; then
to_uninstall+=("$agent_file")
fi
done
confirm_and_apply "$category" "${to_install[*]}" "${to_uninstall[*]}"
return
;;
b|B)
return 1
;;
q|Q)
echo -e "\n${GREEN}Goodbye!${NC}"
exit 0
;;
esac
done
}
# Function to confirm and apply changes
confirm_and_apply() {
local category="$1"
local install_list="$2"
local uninstall_list="$3"
# Convert space-separated strings back to arrays
IFS=' ' read -ra to_install <<< "$install_list"
IFS=' ' read -ra to_uninstall <<< "$uninstall_list"
# Filter out empty entries
local install_count=0
local uninstall_count=0
for item in "${to_install[@]}"; do
[[ -n "$item" ]] && ((install_count++))
done
for item in "${to_uninstall[@]}"; do
[[ -n "$item" ]] && ((uninstall_count++))
done
show_header
echo -e "${BOLD}Confirmation${NC}\n"
if [[ $install_count -eq 0 && $uninstall_count -eq 0 ]]; then
echo -e "${YELLOW}No changes to apply.${NC}"
echo ""
read -p "Press Enter to continue..."
return
fi
if [[ $install_count -gt 0 ]]; then
echo -e "${GREEN}Agents to install ($install_count):${NC}"
for agent_file in "${to_install[@]}"; do
if [[ -n "$agent_file" ]]; then
echo -e " ${GREEN}+${NC} ${agent_file%.md}"
fi
done
echo ""
fi
if [[ $uninstall_count -gt 0 ]]; then
echo -e "${RED}Agents to uninstall ($uninstall_count):${NC}"
for agent_file in "${to_uninstall[@]}"; do
if [[ -n "$agent_file" ]]; then
echo -e " ${RED}-${NC} ${agent_file%.md}"
fi
done
echo ""
fi
echo -e "${BOLD}Summary:${NC} ${GREEN}$install_count to install${NC}, ${RED}$uninstall_count to uninstall${NC}"
echo ""
read -p "Apply these changes? (y/N): " confirm
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
echo ""
# Perform installations
for agent_file in "${to_install[@]}"; do
if [[ -n "$agent_file" ]]; then
if [[ "$SOURCE_MODE" == "remote" ]]; then
# Download from GitHub
echo -e "${CYAN}Downloading $agent_file...${NC}"
if download_agent "$category" "$agent_file" "$CLAUDE_AGENTS_DIR/$agent_file"; then
echo -e "${GREEN}${NC} Installed: $agent_file"
else
echo -e "${RED}${NC} Failed to download: $agent_file"
fi
else
# Copy from local
local source_path="$CATEGORIES_DIR/$category/$agent_file"
if [[ -f "$source_path" ]]; then
cp "$source_path" "$CLAUDE_AGENTS_DIR/$agent_file"
echo -e "${GREEN}${NC} Installed: $agent_file"
fi
fi
fi
done
# Perform uninstallations
for agent_file in "${to_uninstall[@]}"; do
if [[ -n "$agent_file" ]]; then
if [[ -f "$CLAUDE_AGENTS_DIR/$agent_file" ]]; then
rm "$CLAUDE_AGENTS_DIR/$agent_file"
echo -e "${RED}${NC} Uninstalled: $agent_file"
fi
fi
done
echo ""
echo -e "${GREEN}${BOLD}Changes applied successfully!${NC}"
else
echo -e "${YELLOW}Changes cancelled.${NC}"
fi
echo ""
read -p "Press Enter to continue..."
}
# Main loop
main() {
select_install_mode
select_source_mode
while true; do
select_category
while select_agents "$SELECTED_CATEGORY"; do
:
done
done
}
# Run main function
main