584 lines
18 KiB
Bash
Executable File
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
|