|
| 1 | +#!/usr/bin/env bash |
| 2 | +# .agents/setup-agent — Set up AI coding tool integration for this repository. |
| 3 | +# |
| 4 | +# Creates the tool-specific symlinks and config files needed to use the shared |
| 5 | +# resources in AGENTS.md and .agents/ with your preferred AI coding assistant. |
| 6 | +# Run "setup-agent <tool>" once after cloning; re-running is always safe. |
| 7 | +# |
| 8 | +# The canonical project instructions live in AGENTS.md (root) and skills live |
| 9 | +# in .agents/skills/<name>/SKILL.md. Everything tool-specific is either a |
| 10 | +# symlink into those locations, or a thin wrapper that references them. |
| 11 | +# Tool-specific directories (.claude/, .cursor/, etc.) are git-ignored so that |
| 12 | +# each developer runs this script for the tool they actually use. |
| 13 | +# |
| 14 | +# ───────────────────────────────────────────────────────────────────────────── |
| 15 | +# TOOL STRATEGIES (update this section when tool conventions change) |
| 16 | +# ───────────────────────────────────────────────────────────────────────────── |
| 17 | +# |
| 18 | +# CLAUDE CODE |
| 19 | +# Reads: .claude/CLAUDE.md (project) or CLAUDE.md (root) — NOT AGENTS.md natively. |
| 20 | +# If Anthropic eventually adds native AGENTS.md support, the |
| 21 | +# .claude/CLAUDE.md wrapper can be retired. |
| 22 | +# Skills: .claude/skills/ (each subdirectory is a skill) |
| 23 | +# Setup: Write .claude/CLAUDE.md containing "@AGENTS.md" (Claude's include |
| 24 | +# syntax), symlink .claude/skills -> ../.agents/skills. |
| 25 | +# Docs: https://docs.anthropic.com/en/docs/claude-code/memory |
| 26 | +# https://docs.anthropic.com/en/docs/claude-code/skills |
| 27 | +# |
| 28 | +# OPENAI CODEX |
| 29 | +# Reads: AGENTS.md natively — no wrapper needed. |
| 30 | +# Skills: .agents/skills/ natively, and also .codex/skills/ as a fallback. |
| 31 | +# Setup: Symlink .codex/skills -> ../.agents/skills so either path works. |
| 32 | +# Docs: https://developers.openai.com/codex/guides/agents-md |
| 33 | +# https://developers.openai.com/codex/skills |
| 34 | +# |
| 35 | +# CURSOR |
| 36 | +# Reads: .cursor/rules/*.mdc files — does NOT read AGENTS.md natively. |
| 37 | +# If Cursor adds native AGENTS.md support, the rules symlink can be |
| 38 | +# retired. |
| 39 | +# Skills: Cursor uses a different slash-command mechanism; .agents/skills/ |
| 40 | +# are not directly usable today, but the convention may change. |
| 41 | +# Setup: Symlink .cursor/rules/project.mdc -> ../../AGENTS.md so Cursor |
| 42 | +# picks up our instructions as a project rule. |
| 43 | +# Docs: https://docs.cursor.com/context/rules-for-ai |
| 44 | +# https://docs.cursor.com/cmdk/overview |
| 45 | +# |
| 46 | +# OPENCODE |
| 47 | +# Reads: AGENTS.md natively — no wrapper needed. |
| 48 | +# Skills: Documented to read .agents/skills/ natively, but in practice also |
| 49 | +# requires .opencode/commands/<name>.md symlinks (as of 2025-05). |
| 50 | +# Remove the per-command symlinks once opencode reliably finds |
| 51 | +# .agents/skills/ on its own. |
| 52 | +# Setup: Symlink .opencode/skills -> ../.agents/skills (future-proof), |
| 53 | +# plus individual .opencode/commands/<name>.md -> skill SKILL.md |
| 54 | +# files (current workaround). |
| 55 | +# Docs: https://opencode.ai/docs/configuration |
| 56 | +# https://opencode.ai/docs/skills |
| 57 | +# |
| 58 | +# GITHUB COPILOT |
| 59 | +# Reads: AGENTS.md natively (VS Code Copilot ≥ 1.99 / GitHub Copilot Chat). |
| 60 | +# Also reads .github/copilot-instructions.md as a fallback for older |
| 61 | +# clients or non-VS Code surfaces. |
| 62 | +# Skills: .agents/skills/ natively supported. |
| 63 | +# Setup: Symlink .github/copilot-instructions.md -> ../AGENTS.md so older |
| 64 | +# clients also receive our instructions. Remove once all Copilot |
| 65 | +# surfaces reliably pick up AGENTS.md directly. |
| 66 | +# Docs: https://code.visualstudio.com/docs/copilot/customization/custom-instructions |
| 67 | +# https://docs.github.com/en/copilot/concepts/about-prompting-copilot |
| 68 | +# https://docs.github.com/en/copilot/concepts/agents/about-agent-skills |
| 69 | +# ───────────────────────────────────────────────────────────────────────────── |
| 70 | + |
| 71 | +set -euo pipefail |
| 72 | + |
| 73 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 74 | +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" |
| 75 | +SKILLS_DIR=".agents/skills" |
| 76 | +SUPPORTED_TOOLS="claude codex cursor opencode copilot" |
| 77 | + |
| 78 | +usage() { |
| 79 | + cat <<EOF |
| 80 | +Usage: .agents/setup-agent <tool|all|clear|list> [tool...] |
| 81 | +
|
| 82 | +Commands: |
| 83 | + <tool> [...] Set up one or more tools |
| 84 | + all Set up all supported tools |
| 85 | + clear Remove all tool-specific setup created by this script |
| 86 | + clear <tool> Remove setup for one specific tool |
| 87 | + list List supported tools |
| 88 | +
|
| 89 | +Supported tools: $SUPPORTED_TOOLS |
| 90 | +
|
| 91 | +Examples: |
| 92 | + .agents/setup-agent claude |
| 93 | + .agents/setup-agent claude cursor |
| 94 | + .agents/setup-agent all |
| 95 | + .agents/setup-agent clear |
| 96 | + .agents/setup-agent clear opencode |
| 97 | +EOF |
| 98 | +} |
| 99 | + |
| 100 | +rel() { echo "${1#"$REPO_ROOT"/}"; } |
| 101 | + |
| 102 | +# Create a symlink only if the path doesn't already exist. |
| 103 | +# Silent if the link already points to the correct target. |
| 104 | +safe_symlink() { |
| 105 | + local link="$1" target="$2" |
| 106 | + if [ -L "$link" ] && [ "$(readlink "$link")" = "$target" ]; then |
| 107 | + return 0 |
| 108 | + elif [ -e "$link" ] || [ -L "$link" ]; then |
| 109 | + echo " SKIP (already exists): $(rel "$link")" |
| 110 | + return 0 |
| 111 | + fi |
| 112 | + mkdir -p "$(dirname "$link")" |
| 113 | + ln -s "$target" "$link" |
| 114 | + echo " LINK: $(rel "$link") -> $target" |
| 115 | +} |
| 116 | + |
| 117 | +# Write a file only if the path doesn't already exist. |
| 118 | +safe_write() { |
| 119 | + local file="$1" |
| 120 | + shift |
| 121 | + local content="$*" |
| 122 | + if [ -e "$file" ]; then |
| 123 | + echo " SKIP (already exists): $(rel "$file")" |
| 124 | + return 0 |
| 125 | + fi |
| 126 | + mkdir -p "$(dirname "$file")" |
| 127 | + printf '%s\n' "$content" > "$file" |
| 128 | + echo " FILE: $(rel "$file")" |
| 129 | +} |
| 130 | + |
| 131 | +# Remove a symlink only if it still points to the expected target. |
| 132 | +safe_remove_symlink() { |
| 133 | + local link="$1" expected_target="$2" |
| 134 | + [ ! -L "$link" ] && return 0 |
| 135 | + if [ "$(readlink "$link")" = "$expected_target" ]; then |
| 136 | + rm "$link" |
| 137 | + echo " REMOVED: $(rel "$link")" |
| 138 | + else |
| 139 | + echo " SKIP (modified, not removing): $(rel "$link")" |
| 140 | + fi |
| 141 | +} |
| 142 | + |
| 143 | +# Remove a file only if its content still matches what we wrote. |
| 144 | +safe_remove_file() { |
| 145 | + local file="$1" |
| 146 | + shift |
| 147 | + local expected="$*" |
| 148 | + [ ! -f "$file" ] || [ -L "$file" ] && return 0 |
| 149 | + if [ "$(cat "$file")" = "$expected" ]; then |
| 150 | + rm "$file" |
| 151 | + echo " REMOVED: $(rel "$file")" |
| 152 | + else |
| 153 | + echo " SKIP (modified, not removing): $(rel "$file")" |
| 154 | + fi |
| 155 | +} |
| 156 | + |
| 157 | +try_rmdir() { rmdir "$1" 2>/dev/null && echo " RMDIR: $(rel "$1")" || true; } |
| 158 | + |
| 159 | +#------------------------------------------------------------------------------ |
| 160 | +# Claude Code (.claude/) |
| 161 | +#------------------------------------------------------------------------------ |
| 162 | +setup_claude() { |
| 163 | + echo "Setting up Claude Code..." |
| 164 | + safe_write "$REPO_ROOT/.claude/CLAUDE.md" "@AGENTS.md" |
| 165 | + safe_symlink "$REPO_ROOT/.claude/skills" "../.agents/skills" |
| 166 | + safe_write "$REPO_ROOT/.claude/.gitignore" "settings.local.json" |
| 167 | +} |
| 168 | +clear_claude() { |
| 169 | + echo "Removing Claude Code setup..." |
| 170 | + safe_remove_file "$REPO_ROOT/.claude/CLAUDE.md" "@AGENTS.md" |
| 171 | + safe_remove_symlink "$REPO_ROOT/.claude/skills" "../.agents/skills" |
| 172 | + safe_remove_file "$REPO_ROOT/.claude/.gitignore" "settings.local.json" |
| 173 | + try_rmdir "$REPO_ROOT/.claude" |
| 174 | +} |
| 175 | + |
| 176 | +#------------------------------------------------------------------------------ |
| 177 | +# OpenAI Codex (.codex/) |
| 178 | +#------------------------------------------------------------------------------ |
| 179 | +setup_codex() { |
| 180 | + echo "Setting up OpenAI Codex..." |
| 181 | + safe_symlink "$REPO_ROOT/.codex/skills" "../.agents/skills" |
| 182 | + safe_write "$REPO_ROOT/.codex/.gitignore" "*.log" |
| 183 | +} |
| 184 | +clear_codex() { |
| 185 | + echo "Removing OpenAI Codex setup..." |
| 186 | + safe_remove_symlink "$REPO_ROOT/.codex/skills" "../.agents/skills" |
| 187 | + safe_remove_file "$REPO_ROOT/.codex/.gitignore" "*.log" |
| 188 | + try_rmdir "$REPO_ROOT/.codex" |
| 189 | +} |
| 190 | + |
| 191 | +#------------------------------------------------------------------------------ |
| 192 | +# Cursor (.cursor/) |
| 193 | +#------------------------------------------------------------------------------ |
| 194 | +setup_cursor() { |
| 195 | + echo "Setting up Cursor..." |
| 196 | + safe_symlink "$REPO_ROOT/.cursor/rules/project.mdc" "../../AGENTS.md" |
| 197 | + safe_write "$REPO_ROOT/.cursor/.gitignore" "$(printf 'composer/\nchat/')" |
| 198 | +} |
| 199 | +clear_cursor() { |
| 200 | + echo "Removing Cursor setup..." |
| 201 | + safe_remove_symlink "$REPO_ROOT/.cursor/rules/project.mdc" "../../AGENTS.md" |
| 202 | + safe_remove_file "$REPO_ROOT/.cursor/.gitignore" "$(printf 'composer/\nchat/')" |
| 203 | + try_rmdir "$REPO_ROOT/.cursor/rules" |
| 204 | + try_rmdir "$REPO_ROOT/.cursor" |
| 205 | +} |
| 206 | + |
| 207 | +#------------------------------------------------------------------------------ |
| 208 | +# Opencode (.opencode/) |
| 209 | +#------------------------------------------------------------------------------ |
| 210 | +OPENCODE_GITIGNORE="$(printf 'node_modules\npackage.json\npackage-lock.json\nbun.lock')" |
| 211 | + |
| 212 | +setup_opencode() { |
| 213 | + echo "Setting up Opencode..." |
| 214 | + safe_symlink "$REPO_ROOT/.opencode/skills" "../.agents/skills" |
| 215 | + safe_write "$REPO_ROOT/.opencode/.gitignore" "$OPENCODE_GITIGNORE" |
| 216 | + # Individual command links (needed in practice despite docs implying otherwise) |
| 217 | + for skill_dir in "$REPO_ROOT/$SKILLS_DIR"/*/; do |
| 218 | + [ -d "$skill_dir" ] || continue |
| 219 | + local skill_name |
| 220 | + skill_name="$(basename "$skill_dir")" |
| 221 | + safe_symlink "$REPO_ROOT/.opencode/commands/${skill_name}.md" \ |
| 222 | + "../../$SKILLS_DIR/${skill_name}/SKILL.md" |
| 223 | + done |
| 224 | +} |
| 225 | +clear_opencode() { |
| 226 | + echo "Removing Opencode setup..." |
| 227 | + safe_remove_symlink "$REPO_ROOT/.opencode/skills" "../.agents/skills" |
| 228 | + safe_remove_file "$REPO_ROOT/.opencode/.gitignore" "$OPENCODE_GITIGNORE" |
| 229 | + for skill_dir in "$REPO_ROOT/$SKILLS_DIR"/*/; do |
| 230 | + [ -d "$skill_dir" ] || continue |
| 231 | + local skill_name |
| 232 | + skill_name="$(basename "$skill_dir")" |
| 233 | + safe_remove_symlink "$REPO_ROOT/.opencode/commands/${skill_name}.md" \ |
| 234 | + "../../$SKILLS_DIR/${skill_name}/SKILL.md" |
| 235 | + done |
| 236 | + try_rmdir "$REPO_ROOT/.opencode/commands" |
| 237 | + try_rmdir "$REPO_ROOT/.opencode" |
| 238 | +} |
| 239 | + |
| 240 | +#------------------------------------------------------------------------------ |
| 241 | +# GitHub Copilot (.github/copilot-instructions.md) |
| 242 | +# Copilot natively reads AGENTS.md, but also checks copilot-instructions.md. |
| 243 | +#------------------------------------------------------------------------------ |
| 244 | +setup_copilot() { |
| 245 | + echo "Setting up GitHub Copilot..." |
| 246 | + safe_symlink "$REPO_ROOT/.github/copilot-instructions.md" "../AGENTS.md" |
| 247 | +} |
| 248 | +clear_copilot() { |
| 249 | + echo "Removing GitHub Copilot setup..." |
| 250 | + safe_remove_symlink "$REPO_ROOT/.github/copilot-instructions.md" "../AGENTS.md" |
| 251 | +} |
| 252 | + |
| 253 | +#------------------------------------------------------------------------------ |
| 254 | +# Dispatch |
| 255 | +#------------------------------------------------------------------------------ |
| 256 | +run_tool() { |
| 257 | + local action="$1" tool="$2" |
| 258 | + case "$tool" in |
| 259 | + claude) "${action}_claude" ;; |
| 260 | + codex) "${action}_codex" ;; |
| 261 | + cursor) "${action}_cursor" ;; |
| 262 | + opencode) "${action}_opencode" ;; |
| 263 | + copilot) "${action}_copilot" ;; |
| 264 | + *) |
| 265 | + echo "Unknown tool: $tool (supported: $SUPPORTED_TOOLS)" >&2 |
| 266 | + exit 1 |
| 267 | + ;; |
| 268 | + esac |
| 269 | +} |
| 270 | + |
| 271 | +[ $# -eq 0 ] && { usage; exit 0; } |
| 272 | + |
| 273 | +case "$1" in |
| 274 | + all) |
| 275 | + for t in $SUPPORTED_TOOLS; do run_tool setup "$t"; done |
| 276 | + ;; |
| 277 | + clear) |
| 278 | + shift |
| 279 | + if [ $# -eq 0 ]; then |
| 280 | + for t in $SUPPORTED_TOOLS; do run_tool clear "$t"; done |
| 281 | + else |
| 282 | + for t in "$@"; do run_tool clear "$t"; done |
| 283 | + fi |
| 284 | + ;; |
| 285 | + list) |
| 286 | + echo "Supported tools: $SUPPORTED_TOOLS" |
| 287 | + ;; |
| 288 | + -h|--help|help) |
| 289 | + usage |
| 290 | + ;; |
| 291 | + *) |
| 292 | + for t in "$@"; do run_tool setup "$t"; done |
| 293 | + ;; |
| 294 | +esac |
0 commit comments