mirror of
https://github.com/giuseppe-trisciuoglio/developer-kit.git
synced 2026-09-14 18:22:07 +08:00
507 lines
18 KiB
Bash
Executable File
507 lines
18 KiB
Bash
Executable File
#!/bin/bash
|
||
# Claude Code Interactive Developer Kit Installer Script
|
||
# Usage: install-claude.sh "<plugin_json_files>"
|
||
|
||
set -e
|
||
|
||
# Colors
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[0;33m'
|
||
RED='\033[0;31m'
|
||
BLUE='\033[0;34m'
|
||
CYAN='\033[0;36m'
|
||
NC='\033[0m' # No Color
|
||
|
||
# Get plugin files from argument
|
||
PLUGIN_JSON_FILES="$1"
|
||
DEVKIT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
|
||
# Confirm installation
|
||
read -p "Are you installing for Claude Code? (y/N): " confirm
|
||
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
||
echo -e "${RED}✗ Installation cancelled.${NC}"
|
||
exit 1
|
||
fi
|
||
|
||
echo ""
|
||
echo -e "${GREEN}Step 1: Target Project${NC}"
|
||
read -p "Enter the project path (absolute or relative, or press Enter for current directory): " project_path
|
||
|
||
if [[ -z "$project_path" ]]; then
|
||
project_path="."
|
||
fi
|
||
|
||
if [[ ! -d "$project_path" ]]; then
|
||
echo -n " ⚠ $project_path does not exist. Create it? (y/N): "
|
||
read create_dir
|
||
if [[ "$create_dir" != "y" && "$create_dir" != "Y" ]]; then
|
||
echo -e "${RED}✗ Installation cancelled.${NC}"
|
||
exit 1
|
||
fi
|
||
mkdir -p "$project_path"
|
||
fi
|
||
|
||
TARGET_PROJECT="$(cd "$project_path" && pwd)"
|
||
TARGET_DIR="$TARGET_PROJECT/.claude"
|
||
|
||
mkdir -p "$TARGET_DIR/agents"
|
||
mkdir -p "$TARGET_DIR/commands"
|
||
mkdir -p "$TARGET_DIR/skills"
|
||
mkdir -p "$TARGET_DIR/rules"
|
||
mkdir -p "$TARGET_DIR/hooks"
|
||
|
||
echo " → Target: $TARGET_PROJECT"
|
||
echo ""
|
||
|
||
# List available plugins
|
||
echo -e "${GREEN}Step 2: Available Plugins${NC}"
|
||
echo ""
|
||
|
||
declare -a PLUGIN_NAMES
|
||
declare -a PLUGIN_PATHS
|
||
declare -a PLUGIN_DIRS
|
||
declare -a PLUGIN_BASE_DIRS
|
||
|
||
plugin_num=0
|
||
for plugin_json in $PLUGIN_JSON_FILES; do
|
||
if [[ -f "$plugin_json" ]]; then
|
||
plugin_num=$((plugin_num + 1))
|
||
plugin_name=$(jq -r '.name' "$plugin_json" 2>/dev/null || echo "unknown")
|
||
plugin_desc=$(jq -r '.description' "$plugin_json" 2>/dev/null || echo "")
|
||
num_agents=$(jq -r '.agents | length' "$plugin_json" 2>/dev/null || echo "0")
|
||
num_commands=$(jq -r '.commands | length' "$plugin_json" 2>/dev/null || echo "0")
|
||
num_skills=$(jq -r '.skills | length' "$plugin_json" 2>/dev/null || echo "0")
|
||
num_rules=$(jq -r '.rules | length' "$plugin_json" 2>/dev/null || echo "0")
|
||
|
||
PLUGIN_NAMES[$plugin_num]="$plugin_name"
|
||
PLUGIN_PATHS[$plugin_num]="$plugin_json"
|
||
PLUGIN_DIRS[$plugin_num]=$(dirname "$plugin_json")
|
||
PLUGIN_BASE_DIRS[$plugin_num]=$(dirname "$(dirname "$plugin_json")")
|
||
|
||
printf " %2d) ${GREEN}%s${NC}\n" "$plugin_num" "$plugin_name"
|
||
echo " $plugin_desc"
|
||
echo " Components: $num_agents agents, $num_commands commands, $num_skills skills, $num_rules rules"
|
||
echo ""
|
||
fi
|
||
done
|
||
|
||
if [[ $plugin_num -eq 0 ]]; then
|
||
echo -e "${RED}✗ No plugins found.${NC}"
|
||
exit 1
|
||
fi
|
||
|
||
read -p "Select plugins to install (comma-separated numbers, or 'all'): " selected_plugins
|
||
|
||
echo ""
|
||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo -e "${BLUE}ℹ Starting Installation...${NC}"
|
||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo ""
|
||
|
||
installed_count=0
|
||
skipped_count=0
|
||
|
||
# Register legacy single-file PreToolUse Bash hook in .claude/settings.json
|
||
_register_legacy_hook_in_settings() {
|
||
local target_dir="$1"
|
||
local hook_name="$2"
|
||
local settings_file="$target_dir/settings.json"
|
||
local hook_command="python3 \"\$CLAUDE_PROJECT_DIR/.claude/hooks/$hook_name\""
|
||
|
||
if [[ ! -f "$settings_file" ]]; then
|
||
cat > "$settings_file" << EOF
|
||
{
|
||
"hooks": {
|
||
"PreToolUse": [
|
||
{
|
||
"matcher": "Bash",
|
||
"hooks": [
|
||
{
|
||
"type": "command",
|
||
"command": "$hook_command"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
}
|
||
EOF
|
||
echo " ✓ Created .claude/settings.json with legacy hook: $hook_name"
|
||
return
|
||
fi
|
||
|
||
python3 - << PYEOF
|
||
import json, sys
|
||
|
||
settings_file = "$settings_file"
|
||
hook_command = "$hook_command"
|
||
|
||
with open(settings_file) as f:
|
||
settings = json.load(f)
|
||
|
||
settings.setdefault("hooks", {})
|
||
settings["hooks"].setdefault("PreToolUse", [])
|
||
|
||
bash_entry = None
|
||
for entry in settings["hooks"]["PreToolUse"]:
|
||
if entry.get("matcher") == "Bash":
|
||
bash_entry = entry
|
||
break
|
||
|
||
if bash_entry is None:
|
||
bash_entry = {"matcher": "Bash", "hooks": []}
|
||
settings["hooks"]["PreToolUse"].append(bash_entry)
|
||
|
||
bash_entry.setdefault("hooks", [])
|
||
|
||
for h in bash_entry["hooks"]:
|
||
if h.get("command") == hook_command:
|
||
print(" ○ Legacy hook already registered in settings.json")
|
||
sys.exit(0)
|
||
|
||
bash_entry["hooks"].append({"type": "command", "command": hook_command})
|
||
|
||
with open(settings_file, "w") as f:
|
||
json.dump(settings, f, indent=2)
|
||
f.write("\n")
|
||
|
||
print(" ✓ Registered legacy hook in .claude/settings.json: $hook_name")
|
||
PYEOF
|
||
}
|
||
|
||
_copy_hook_file() {
|
||
local source_file="$1"
|
||
local target_dir="$2"
|
||
local hook_name
|
||
local target_file
|
||
|
||
hook_name=$(basename "$source_file")
|
||
target_file="$target_dir/hooks/$hook_name"
|
||
|
||
if [[ -f "$target_file" ]]; then
|
||
echo -n " ⚠ $hook_name already exists. [O]verwrite, [S]kip? "
|
||
read -n 1 conflict_action
|
||
echo ""
|
||
case $conflict_action in
|
||
O|o)
|
||
cp "$source_file" "$target_file"
|
||
chmod +x "$target_file"
|
||
echo " ✓ Overwritten: $hook_name"
|
||
installed_count=$((installed_count + 1))
|
||
return 0
|
||
;;
|
||
*)
|
||
echo " ○ Skipped: $hook_name"
|
||
skipped_count=$((skipped_count + 1))
|
||
return 1
|
||
;;
|
||
esac
|
||
fi
|
||
|
||
cp "$source_file" "$target_file"
|
||
chmod +x "$target_file"
|
||
echo " ✓ Hook file: $hook_name"
|
||
installed_count=$((installed_count + 1))
|
||
return 0
|
||
}
|
||
|
||
_register_hooks_config_in_settings() {
|
||
local target_dir="$1"
|
||
local hooks_config_file="$2"
|
||
local settings_file="$target_dir/settings.json"
|
||
|
||
python3 - << PYEOF
|
||
import json
|
||
from pathlib import Path
|
||
|
||
settings_file = Path("$settings_file")
|
||
hooks_file = Path("$hooks_config_file")
|
||
|
||
if settings_file.exists():
|
||
with settings_file.open() as f:
|
||
settings = json.load(f)
|
||
else:
|
||
settings = {}
|
||
|
||
with hooks_file.open() as f:
|
||
hook_config = json.load(f)
|
||
|
||
settings.setdefault("hooks", {})
|
||
|
||
for event_name, matcher_entries in hook_config.get("hooks", {}).items():
|
||
target_entries = settings["hooks"].setdefault(event_name, [])
|
||
|
||
for matcher_entry in matcher_entries:
|
||
normalized_entry = json.loads(json.dumps(matcher_entry))
|
||
|
||
for hook in normalized_entry.get("hooks", []):
|
||
command = hook.get("command")
|
||
if isinstance(command, str):
|
||
hook["command"] = command.replace(
|
||
"${CLAUDE_PLUGIN_ROOT}",
|
||
"$CLAUDE_PROJECT_DIR/.claude"
|
||
)
|
||
|
||
if normalized_entry not in target_entries:
|
||
target_entries.append(normalized_entry)
|
||
|
||
with settings_file.open("w") as f:
|
||
json.dump(settings, f, indent=2)
|
||
f.write("\n")
|
||
PYEOF
|
||
|
||
echo " ✓ Registered hooks config in .claude/settings.json: $(basename "$hooks_config_file")"
|
||
}
|
||
|
||
# Install selected plugins
|
||
for ((i=1; i<=plugin_num; i++)); do
|
||
should_install=0
|
||
|
||
if [[ "$selected_plugins" == "all" ]]; then
|
||
should_install=1
|
||
else
|
||
# Check if current plugin number is in selected list
|
||
for num in $(echo "$selected_plugins" | tr ',' ' '); do
|
||
if [[ "$num" == "$i" ]]; then
|
||
should_install=1
|
||
break
|
||
fi
|
||
done
|
||
fi
|
||
|
||
if [[ "$should_install" == "1" ]]; then
|
||
plugin_json="${PLUGIN_PATHS[$i]}"
|
||
base_dir="${PLUGIN_BASE_DIRS[$i]}"
|
||
plugin_name="${PLUGIN_NAMES[$i]}"
|
||
|
||
echo ""
|
||
echo -e "${CYAN}Installing from: $plugin_name${NC}"
|
||
|
||
# Install agents
|
||
agents=$(jq -r '.agents[]? // empty' "$plugin_json" 2>/dev/null)
|
||
if [[ -n "$agents" ]]; then
|
||
for agent in $agents; do
|
||
agent_path="$base_dir/$agent"
|
||
if [[ -f "$agent_path" ]]; then
|
||
agent_name=$(basename "$agent")
|
||
target_file="$TARGET_DIR/agents/$agent_name"
|
||
|
||
if [[ -f "$target_file" ]]; then
|
||
echo -n " ⚠ $agent_name already exists. [O]verwrite, [S]kip, [R]ename? "
|
||
read -n 1 conflict_action
|
||
echo ""
|
||
case $conflict_action in
|
||
O|o)
|
||
cp "$agent_path" "$target_file"
|
||
echo " ✓ Overwritten: $agent_name"
|
||
installed_count=$((installed_count + 1))
|
||
;;
|
||
R|r)
|
||
read -p " Enter new name: " new_name
|
||
cp "$agent_path" "$TARGET_DIR/agents/$new_name.md"
|
||
echo " ✓ Installed as: $new_name.md"
|
||
installed_count=$((installed_count + 1))
|
||
;;
|
||
*)
|
||
echo " ○ Skipped: $agent_name"
|
||
skipped_count=$((skipped_count + 1))
|
||
;;
|
||
esac
|
||
else
|
||
cp "$agent_path" "$target_file"
|
||
echo " ✓ Agent: $agent_name"
|
||
installed_count=$((installed_count + 1))
|
||
fi
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# Install commands
|
||
commands=$(jq -r '.commands[]? // empty' "$plugin_json" 2>/dev/null)
|
||
if [[ -n "$commands" ]]; then
|
||
for cmd in $commands; do
|
||
cmd_path="$base_dir/$cmd"
|
||
if [[ -f "$cmd_path" ]]; then
|
||
cmd_name=$(basename "$cmd")
|
||
cmd_subdir=$(dirname "$cmd")
|
||
cmd_subdir_rel=""
|
||
|
||
if [[ "$cmd_subdir" != "." ]]; then
|
||
cmd_subdir_rel=$(echo "$cmd_subdir" | sed 's|^\./commands||' | sed 's|^\./commands/||')
|
||
if [[ -n "$cmd_subdir_rel" ]]; then
|
||
mkdir -p "$TARGET_DIR/commands/$cmd_subdir_rel"
|
||
target_file="$TARGET_DIR/commands/$cmd_subdir_rel/$cmd_name"
|
||
else
|
||
target_file="$TARGET_DIR/commands/$cmd_name"
|
||
fi
|
||
else
|
||
target_file="$TARGET_DIR/commands/$cmd_name"
|
||
fi
|
||
|
||
if [[ -f "$target_file" ]]; then
|
||
echo -n " ⚠ $cmd_name already exists. [O]verwrite, [S]kip, [R]ename? "
|
||
read -n 1 conflict_action
|
||
echo ""
|
||
case $conflict_action in
|
||
O|o)
|
||
cp "$cmd_path" "$target_file"
|
||
echo " ✓ Overwritten: $cmd_name"
|
||
installed_count=$((installed_count + 1))
|
||
;;
|
||
R|r)
|
||
read -p " Enter new name: " new_name
|
||
if [[ -n "$cmd_subdir_rel" ]]; then
|
||
cp "$cmd_path" "$TARGET_DIR/commands/$cmd_subdir_rel/$new_name.md"
|
||
else
|
||
cp "$cmd_path" "$TARGET_DIR/commands/$new_name.md"
|
||
fi
|
||
echo " ✓ Installed as: $new_name.md"
|
||
installed_count=$((installed_count + 1))
|
||
;;
|
||
*)
|
||
echo " ○ Skipped: $cmd_name"
|
||
skipped_count=$((skipped_count + 1))
|
||
;;
|
||
esac
|
||
else
|
||
cp "$cmd_path" "$target_file"
|
||
echo " ✓ Command: $cmd_name"
|
||
installed_count=$((installed_count + 1))
|
||
fi
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# Install skills
|
||
skills=$(jq -r '.skills[]? // empty' "$plugin_json" 2>/dev/null)
|
||
if [[ -n "$skills" ]]; then
|
||
for skill_pattern in $skills; do
|
||
for skill_dir in $base_dir/$skill_pattern; do
|
||
if [[ -d "$skill_dir" ]]; then
|
||
skill_name=$(basename "$skill_dir")
|
||
target_skill_dir="$TARGET_DIR/skills/$skill_name"
|
||
|
||
if [[ -d "$target_skill_dir" ]]; then
|
||
echo -n " ⚠ $skill_name already exists. [O]verwrite, [S]kip, [R]ename? "
|
||
read -n 1 conflict_action
|
||
echo ""
|
||
case $conflict_action in
|
||
O|o)
|
||
rm -rf "$target_skill_dir"
|
||
cp -r "$skill_dir" "$target_skill_dir"
|
||
echo " ✓ Overwritten: $skill_name"
|
||
installed_count=$((installed_count + 1))
|
||
;;
|
||
R|r)
|
||
read -p " Enter new name: " new_name
|
||
cp -r "$skill_dir" "$TARGET_DIR/skills/$new_name"
|
||
echo " ✓ Installed as: $new_name"
|
||
installed_count=$((installed_count + 1))
|
||
;;
|
||
*)
|
||
echo " ○ Skipped: $skill_name"
|
||
skipped_count=$((skipped_count + 1))
|
||
;;
|
||
esac
|
||
else
|
||
cp -r "$skill_dir" "$target_skill_dir"
|
||
echo " ✓ Skill: $skill_name"
|
||
installed_count=$((installed_count + 1))
|
||
fi
|
||
fi
|
||
done
|
||
done
|
||
fi
|
||
|
||
# Install rules
|
||
rules=$(jq -r '.rules[]? // empty' "$plugin_json" 2>/dev/null)
|
||
if [[ -n "$rules" ]]; then
|
||
rules_target_dir="$TARGET_DIR/rules/$plugin_name"
|
||
mkdir -p "$rules_target_dir"
|
||
for rule in $rules; do
|
||
rule_path="$base_dir/$rule"
|
||
if [[ -f "$rule_path" ]]; then
|
||
rule_name=$(basename "$rule")
|
||
target_file="$rules_target_dir/$rule_name"
|
||
|
||
if [[ -f "$target_file" ]]; then
|
||
echo -n " ⚠ Rule $rule_name already exists. [O]verwrite, [S]kip? "
|
||
read -n 1 conflict_action
|
||
echo ""
|
||
case $conflict_action in
|
||
O|o)
|
||
cp "$rule_path" "$target_file"
|
||
echo " ✓ Overwritten: $rule_name"
|
||
installed_count=$((installed_count + 1))
|
||
;;
|
||
*)
|
||
echo " ○ Skipped: $rule_name"
|
||
skipped_count=$((skipped_count + 1))
|
||
;;
|
||
esac
|
||
else
|
||
cp "$rule_path" "$target_file"
|
||
echo " ✓ Rule: $plugin_name/$rule_name"
|
||
installed_count=$((installed_count + 1))
|
||
fi
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# Install hooks
|
||
hooks=$(jq -r '
|
||
if .hooks == null then empty
|
||
elif (.hooks | type) == "array" then .hooks[]
|
||
elif (.hooks | type) == "string" then .hooks
|
||
else empty
|
||
end
|
||
' "$plugin_json" 2>/dev/null)
|
||
if [[ -n "$hooks" ]]; then
|
||
for hook in $hooks; do
|
||
hook_path="$base_dir/$hook"
|
||
if [[ -f "$hook_path" ]]; then
|
||
hook_name=$(basename "$hook_path")
|
||
|
||
if [[ "$hook_name" == "hooks.json" ]]; then
|
||
hook_dir=$(dirname "$hook_path")
|
||
|
||
for hook_impl in "$hook_dir"/*; do
|
||
if [[ -f "$hook_impl" && "$(basename "$hook_impl")" != "hooks.json" ]]; then
|
||
_copy_hook_file "$hook_impl" "$TARGET_DIR"
|
||
fi
|
||
done
|
||
|
||
_register_hooks_config_in_settings "$TARGET_DIR" "$hook_path"
|
||
else
|
||
if _copy_hook_file "$hook_path" "$TARGET_DIR"; then
|
||
_register_legacy_hook_in_settings "$TARGET_DIR" "$hook_name"
|
||
fi
|
||
fi
|
||
fi
|
||
done
|
||
fi
|
||
fi
|
||
done
|
||
|
||
echo ""
|
||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo -e "${GREEN}✓ Installation Complete!${NC}"
|
||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo ""
|
||
echo " Target directory: $TARGET_DIR/"
|
||
echo " Files installed: $installed_count"
|
||
echo " Files skipped: $skipped_count"
|
||
echo ""
|
||
echo -e "${YELLOW}Next Steps:${NC}"
|
||
echo " 1. Navigate to your project: cd $TARGET_PROJECT"
|
||
echo " 2. Start Claude Code in the project directory"
|
||
echo " 3. Your skills, agents, and commands are now available!"
|
||
echo ""
|
||
echo -e "${YELLOW}Usage:${NC}"
|
||
echo " - Skills are automatically discovered by Claude"
|
||
echo " - Rules are loaded from .claude/rules/ to enforce coding standards"
|
||
echo " - Hooks run automatically on tool events (configured in .claude/settings.json)"
|
||
echo " - Use @agent-name to invoke agents"
|
||
echo " - Use /command-name to run commands"
|
||
echo ""
|