ring/install-symlinks.sh
Fred Amaral 9cb5a72737
fix(codereview): align reviewer references and harden lib-commons/multi-tenant agents
Propagates the 10-reviewer peer list across agent frontmatter, Position/Critical prose, shared-patterns, skill dispatchers, gate validators, and docs — resolving drift left behind when multi-tenant-reviewer and lib-commons-reviewer were added to the pool. Also fixes broken shared-pattern paths in lib-commons-reviewer and adds substantive blocker criteria to multi-tenant-reviewer plus codebase-context severity heuristic (Lerian third-rail vs external recommendation) to lib-commons-reviewer.

X-Lerian-Ref: 0x1
2026-04-18 20:18:16 -03:00

371 lines
11 KiB
Bash
Executable file

#!/usr/bin/env bash
# ==============================================================================
# Ring - Claude Code Symlinks Installer
# ==============================================================================
# Creates symlinks from ~/.claude/{agents,commands,skills} to the Ring repo,
# enabling all Ring agents, commands, and skills in your Claude Code environment.
#
# Usage:
# bash install-symlinks.sh # Auto-detects Ring repo from script location
# bash install-symlinks.sh /path/to/ring # Explicit Ring repo path
# bash install-symlinks.sh --remove # Remove all Ring symlinks
#
# Requirements:
# - Claude Code CLI installed
# - Ring repository cloned locally
# ==============================================================================
set -euo pipefail
# --- Colors ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color
# --- Globals ---
CLAUDE_DIR="$HOME/.claude"
FACTORY_DIR="$HOME/.factory"
RING_DIR=""
INSTALL_CLAUDE=true
INSTALL_FACTORY=false
CREATED=0
SKIPPED=0
ERRORS=0
REMOVED=0
# --- Functions ---
print_banner() {
echo -e "${CYAN}"
echo " ╔══════════════════════════════════════════════════╗"
echo " ║ Ring - Claude Code Symlinks Installer ║"
echo " ╚══════════════════════════════════════════════════╝"
echo -e "${NC}"
}
log_info() { echo -e " ${BLUE}INFO${NC} $1"; }
log_success() { echo -e " ${GREEN}OK${NC} $1"; }
log_skip() { echo -e " ${YELLOW}SKIP${NC} $1"; }
log_error() { echo -e " ${RED}ERROR${NC} $1"; }
log_section() { echo -e "\n ${BOLD}${CYAN}── $1 ──${NC}\n"; }
resolve_ring_dir() {
if [[ -n "${1:-}" && "$1" != "--remove" && "$1" != "--factory" && "$1" != "--all" && "$1" != "--claude" ]]; then
RING_DIR="$(cd "$1" && pwd)"
else
# Auto-detect from script location
RING_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
fi
# Validate Ring repo
if [[ ! -f "$RING_DIR/CLAUDE.md" ]]; then
log_error "Not a Ring repository: $RING_DIR"
log_error "CLAUDE.md not found. Please provide the correct path."
exit 1
fi
if [[ ! -d "$RING_DIR/default/agents" ]]; then
log_error "Missing default/agents directory in: $RING_DIR"
exit 1
fi
}
create_directories() {
local base_dirs=("agents" "commands" "skills" "hooks")
if [[ "$INSTALL_CLAUDE" == true ]]; then
for subdir in "${base_dirs[@]}"; do
local dir="$CLAUDE_DIR/$subdir"
if [[ ! -d "$dir" ]]; then
mkdir -p "$dir"
log_info "Created directory: $dir"
fi
done
fi
if [[ "$INSTALL_FACTORY" == true ]]; then
for subdir in "${base_dirs[@]}"; do
local dir="$FACTORY_DIR/$subdir"
if [[ ! -d "$dir" ]]; then
mkdir -p "$dir"
log_info "Created directory: $dir"
fi
done
fi
}
create_symlink() {
local source="$1"
local target="$2"
local name
name="$(basename "$target")"
if [[ -L "$target" ]]; then
local existing
existing="$(readlink "$target")"
if [[ "$existing" == "$source" ]]; then
SKIPPED=$((SKIPPED + 1))
return
else
# Symlink exists but points elsewhere - update it
rm "$target"
ln -s "$source" "$target"
log_success "$name (updated)"
CREATED=$((CREATED + 1))
return
fi
elif [[ -e "$target" ]]; then
log_error "$name already exists as a regular file (not a symlink). Skipping."
ERRORS=$((ERRORS + 1))
return
fi
ln -s "$source" "$target"
CREATED=$((CREATED + 1))
}
link_agents() {
local plugin="$1"
local target_dir="$2"
local agents_dir="$RING_DIR/$plugin/agents"
[[ ! -d "$agents_dir" ]] && return
for agent in "$agents_dir"/*.md; do
[[ ! -f "$agent" ]] && continue
local name
name="$(basename "$agent")"
create_symlink "$agent" "$target_dir/agents/$name"
done
}
link_commands() {
local plugin="$1"
local target_dir="$2"
local commands_dir="$RING_DIR/$plugin/commands"
[[ ! -d "$commands_dir" ]] && return
for cmd in "$commands_dir"/*.md; do
[[ ! -f "$cmd" ]] && continue
local name
name="$(basename "$cmd")"
create_symlink "$cmd" "$target_dir/commands/$name"
done
}
link_skills() {
local plugin="$1"
local target_dir="$2"
local skills_dir="$RING_DIR/$plugin/skills"
[[ ! -d "$skills_dir" ]] && return
for skill in "$skills_dir"/*/; do
[[ ! -d "$skill" ]] && continue
local name
name="$(basename "$skill")"
# Skip shared-patterns directories (internal to each plugin, not standalone skills)
[[ "$name" == "shared-patterns" ]] && continue
create_symlink "$skill" "$target_dir/skills/$name"
done
}
link_hooks() {
local plugin="$1"
local target_dir="$2"
local hooks_dir="$RING_DIR/$plugin/hooks"
[[ ! -d "$hooks_dir" ]] && return
# 1. Symlink executable hook scripts (.sh) to target/hooks/
for hook_script in "$hooks_dir"/*.sh; do
[[ ! -f "$hook_script" ]] && continue
local name
name="$(basename "$hook_script")"
create_symlink "$hook_script" "$target_dir/hooks/$name"
done
# 2. Merge hooks.json into settings.json (rewriting paths)
local hooks_json="$hooks_dir/hooks.json"
[[ ! -f "$hooks_json" ]] && return
local settings_file="$target_dir/settings.json"
local hooks_target="$target_dir/hooks"
# Rewrite ${CLAUDE_PLUGIN_ROOT}/hooks/ → absolute path to target/hooks/
local rewritten
rewritten=$(sed "s|\${CLAUDE_PLUGIN_ROOT}/hooks/|$hooks_target/|g" "$hooks_json")
if [[ ! -f "$settings_file" ]]; then
local formatted
formatted=$(echo "$rewritten" | jq '.' 2>/dev/null)
if [[ -n "$formatted" ]]; then
echo "$formatted" > "$settings_file"
log_success "Created settings.json with hooks from $plugin"
else
log_error "Invalid hooks.json in $plugin — skipping"
ERRORS=$((ERRORS + 1))
fi
return
fi
# Deep merge hook arrays per event type, deduplicating by matcher+command
local merged
merged=$(
echo "$rewritten" | jq -s '
.[0] as $base | .[1] as $new |
($base.hooks // {}) as $bh | ($new.hooks // {}) as $nh |
($bh | keys) + ($nh | keys) | unique | reduce .[] as $evt ({};
. + {($evt): (($bh[$evt] // []) + ($nh[$evt] // []) | unique_by({matcher: (.matcher // ""), hooks: .hooks}))}
) | $base * {hooks: .}
' "$settings_file" -
)
if [[ -n "$merged" ]]; then
echo "$merged" | jq '.' > "$settings_file"
log_success "Merged hooks from $plugin into settings.json"
else
log_error "Failed to merge hooks from $plugin"
ERRORS=$((ERRORS + 1))
fi
}
install_symlinks() {
local plugins=("default" "dev-team" "pm-team" "pmo-team" "finops-team" "tw-team")
for plugin in "${plugins[@]}"; do
log_section "$plugin"
if [[ "$INSTALL_CLAUDE" == true ]]; then
link_agents "$plugin" "$CLAUDE_DIR"
link_commands "$plugin" "$CLAUDE_DIR"
link_skills "$plugin" "$CLAUDE_DIR"
link_hooks "$plugin" "$CLAUDE_DIR"
fi
if [[ "$INSTALL_FACTORY" == true ]]; then
link_agents "$plugin" "$FACTORY_DIR"
link_commands "$plugin" "$FACTORY_DIR"
link_skills "$plugin" "$FACTORY_DIR"
link_hooks "$plugin" "$FACTORY_DIR"
fi
done
}
remove_symlinks() {
log_section "Removing Ring symlinks"
for dir in agents commands skills hooks; do
local target_dir="$CLAUDE_DIR/$dir"
[[ ! -d "$target_dir" ]] && continue
for item in "$target_dir"/*; do
[[ ! -L "$item" ]] && continue
local link_target
link_target="$(readlink "$item")"
# Only remove symlinks that point to the Ring repo
if [[ "$link_target" == *"/ring/"* ]]; then
rm "$item"
log_success "Removed: $dir/$(basename "$item")"
REMOVED=$((REMOVED + 1))
fi
done
done
# Remove Ring hook entries from settings.json
local settings_file="$CLAUDE_DIR/settings.json"
if [[ -f "$settings_file" ]]; then
local cleaned
cleaned=$(jq '
if .hooks then
.hooks |= with_entries(
.value |= map(select(
(.hooks // []) | all(.command | contains("/.claude/hooks/") | not)
))
)
else . end
' "$settings_file")
if [[ -n "$cleaned" ]]; then
echo "$cleaned" | jq '.' > "$settings_file"
log_success "Cleaned Ring hooks from settings.json"
fi
fi
echo ""
echo -e " ${GREEN}${BOLD}Done!${NC} Removed ${REMOVED} symlinks."
}
print_summary() {
echo ""
echo -e " ${BOLD}════════════════════════════════════════${NC}"
echo -e " ${GREEN}Created:${NC} $CREATED symlinks"
echo -e " ${YELLOW}Skipped:${NC} $SKIPPED (already correct)"
if [[ $ERRORS -gt 0 ]]; then
echo -e " ${RED}Errors:${NC} $ERRORS"
fi
echo -e " ${BOLD}════════════════════════════════════════${NC}"
echo ""
echo -e " ${CYAN}Ring repo:${NC} $RING_DIR"
[[ "$INSTALL_CLAUDE" == true ]] && echo -e " ${CYAN}Claude dir:${NC} $CLAUDE_DIR"
[[ "$INSTALL_FACTORY" == true ]] && echo -e " ${CYAN}Factory dir:${NC} $FACTORY_DIR"
echo ""
local total=$((CREATED + SKIPPED))
if [[ $total -gt 0 ]]; then
local target_names=""
[[ "$INSTALL_CLAUDE" == true ]] && target_names="Claude Code"
[[ "$INSTALL_FACTORY" == true ]] && { [[ -n "$target_names" ]] && target_names="$target_names and Factory AI" || target_names="Factory AI"; }
echo -e " ${GREEN}${BOLD}Ring is ready!${NC} Open $target_names to use all skills, agents, and commands."
echo ""
echo -e " Try these commands:"
echo -e " ${BOLD}/ring:brainstorm${NC} - Socratic design refinement"
echo -e " ${BOLD}/ring:dev-cycle${NC} - 10-gate development cycle"
echo -e " ${BOLD}/ring:pre-dev-feature${NC} - Lightweight pre-dev workflow"
echo -e " ${BOLD}/ring:codereview${NC} - Parallel code review (10 reviewers)"
echo ""
fi
}
# --- Main ---
print_banner
if [[ "${1:-}" == "--remove" ]]; then
resolve_ring_dir "${2:-}"
remove_symlinks
exit 0
fi
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
echo " Usage:"
echo " bash install-symlinks.sh # Install for Claude Code (default)"
echo " bash install-symlinks.sh --factory # Install for Factory AI"
echo " bash install-symlinks.sh --all # Install for both Claude Code and Factory AI"
echo " bash install-symlinks.sh /path/to/ring # Explicit path"
echo " bash install-symlinks.sh --remove # Remove Ring symlinks"
echo ""
exit 0
fi
if [[ "${1:-}" == "--factory" ]]; then
INSTALL_CLAUDE=false
INSTALL_FACTORY=true
shift
elif [[ "${1:-}" == "--all" ]]; then
INSTALL_CLAUDE=true
INSTALL_FACTORY=true
shift
fi
resolve_ring_dir "${1:-}"
log_info "Ring repo: $RING_DIR"
[[ "$INSTALL_CLAUDE" == true ]] && log_info "Claude dir: $CLAUDE_DIR"
[[ "$INSTALL_FACTORY" == true ]] && log_info "Factory dir: $FACTORY_DIR"
create_directories
install_symlinks
print_summary