install.sh 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. #!/usr/bin/env bash
  2. #
  3. # claude-mods Installer (Linux / macOS / Windows Git Bash)
  4. # Copies commands, skills, agents, and rules to ~/.claude/
  5. # Handles cleanup of deprecated items and command-to-skill migrations.
  6. #
  7. # Usage:
  8. # Linux/macOS: ./scripts/install.sh
  9. # Windows Git Bash: bash scripts/install.sh
  10. # Opt into the statusline: add --statusline
  11. set -e
  12. # --statusline is opt-in: off by default so a shared install never changes the
  13. # user's prompt UI without being asked.
  14. WANT_STATUSLINE=false
  15. for arg in "$@"; do
  16. case "$arg" in
  17. --statusline) WANT_STATUSLINE=true ;;
  18. esac
  19. done
  20. BLUE='\033[0;34m'
  21. GREEN='\033[0;32m'
  22. YELLOW='\033[1;33m'
  23. RED='\033[0;31m'
  24. NC='\033[0m'
  25. echo -e "${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}"
  26. echo -e "${BLUE}║ claude-mods Installer (Linux / macOS / Git Bash) ║${NC}"
  27. echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}"
  28. echo ""
  29. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  30. PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
  31. CLAUDE_DIR="${CLAUDE_DIR:-$HOME/.claude}"
  32. # Detect Windows (Git Bash / MINGW / MSYS) — chmod is a no-op on NTFS
  33. IS_WINDOWS=false
  34. case "$(uname -s)" in
  35. MINGW*|MSYS*|CYGWIN*) IS_WINDOWS=true ;;
  36. esac
  37. # Wrapper: chmod is meaningful only on Unix
  38. make_executable() {
  39. $IS_WINDOWS || chmod +x "$1"
  40. }
  41. # Ensure ~/.claude directories exist
  42. for dir in commands skills agents rules output-styles hooks; do
  43. mkdir -p "$CLAUDE_DIR/$dir"
  44. done
  45. # =============================================================================
  46. # DEPRECATED ITEMS - Remove these from user's config
  47. # =============================================================================
  48. echo -e "${YELLOW}Cleaning up deprecated items...${NC}"
  49. deprecated_items=(
  50. # Removed commands (migrated to skills or deleted)
  51. "$CLAUDE_DIR/commands/review.md" # Migrated to skill
  52. "$CLAUDE_DIR/commands/testgen.md" # Migrated to skill
  53. "$CLAUDE_DIR/commands/conclave.md" # Deprecated
  54. "$CLAUDE_DIR/commands/pulse.md" # Now a skill only
  55. # Removed skills
  56. "$CLAUDE_DIR/skills/conclave" # Deprecated
  57. "$CLAUDE_DIR/skills/claude-code-templates" # Replaced by skill-creator
  58. "$CLAUDE_DIR/skills/agentmail" # Renamed to pigeon (v2.3.0)
  59. "$CLAUDE_DIR/skills/claude-code-debug" # Merged into claude-code-ops (v3.0)
  60. "$CLAUDE_DIR/skills/claude-code-headless" # Merged into claude-code-ops (v3.0)
  61. "$CLAUDE_DIR/skills/claude-code-hooks" # Merged into claude-code-ops (v3.0)
  62. "$CLAUDE_DIR/skills/dsp-launch" # Superseded by fleet-worker + native background agents (2026-07)
  63. # Deprecated agents (v3.0): folded into their -ops skill twins
  64. "$CLAUDE_DIR/agents/python-expert.md"
  65. "$CLAUDE_DIR/agents/typescript-expert.md"
  66. "$CLAUDE_DIR/agents/javascript-expert.md"
  67. "$CLAUDE_DIR/agents/go-expert.md"
  68. "$CLAUDE_DIR/agents/rust-expert.md"
  69. "$CLAUDE_DIR/agents/react-expert.md"
  70. "$CLAUDE_DIR/agents/vue-expert.md"
  71. "$CLAUDE_DIR/agents/astro-expert.md"
  72. "$CLAUDE_DIR/agents/laravel-expert.md"
  73. "$CLAUDE_DIR/agents/sql-expert.md"
  74. "$CLAUDE_DIR/agents/postgres-expert.md"
  75. "$CLAUDE_DIR/agents/cypress-expert.md" # -> skills/cypress-ops
  76. "$CLAUDE_DIR/agents/cloudflare-expert.md" # -> skills/cloudflare-ops
  77. "$CLAUDE_DIR/agents/wrangler-expert.md" # -> skills/cloudflare-ops
  78. "$CLAUDE_DIR/agents/bash-expert.md" # -> skills/bash-ops
  79. "$CLAUDE_DIR/agents/claude-architect.md" # -> skills/claude-code-ops
  80. "$CLAUDE_DIR/agents/aws-fargate-ecs-expert.md" # -> skills/container-orchestration
  81. "$CLAUDE_DIR/agents/craftcms-expert.md" # -> skills/craftcms-ops
  82. "$CLAUDE_DIR/agents/payloadcms-expert.md" # -> skills/payloadcms-ops
  83. "$CLAUDE_DIR/agents/asus-router-expert.md" # -> skills/asus-router-ops
  84. )
  85. # Renamed skills: -patterns -> -ops (March 2026)
  86. renamed_skills=(
  87. cli-patterns
  88. mcp-patterns
  89. python-async-patterns
  90. python-cli-patterns
  91. python-database-patterns
  92. python-fastapi-patterns
  93. python-observability-patterns
  94. python-pytest-patterns
  95. python-typing-patterns
  96. rest-patterns
  97. security-patterns
  98. sql-patterns
  99. tailwind-patterns
  100. testing-patterns
  101. )
  102. for old_skill in "${renamed_skills[@]}"; do
  103. old_path="$CLAUDE_DIR/skills/$old_skill"
  104. if [ -d "$old_path" ]; then
  105. rm -rf "$old_path"
  106. echo -e " ${RED}Removed renamed: $old_skill (now ${old_skill%-patterns}-ops)${NC}"
  107. fi
  108. done
  109. for item in "${deprecated_items[@]}"; do
  110. if [ -e "$item" ]; then
  111. rm -rf "$item"
  112. echo -e " ${RED}Removed: $item${NC}"
  113. fi
  114. done
  115. echo ""
  116. # =============================================================================
  117. # COMMANDS - Only copy commands that haven't been migrated to skills
  118. # =============================================================================
  119. echo -e "${BLUE}Installing commands...${NC}"
  120. # Commands that should NOT be copied (migrated to skills)
  121. skip_commands=("review.md" "testgen.md")
  122. for file in "$PROJECT_ROOT/commands"/*.md; do
  123. [ -f "$file" ] || continue
  124. filename=$(basename "$file")
  125. # Skip migrated commands
  126. skip=false
  127. for skip_cmd in "${skip_commands[@]}"; do
  128. if [ "$filename" = "$skip_cmd" ]; then
  129. skip=true
  130. break
  131. fi
  132. done
  133. # Skip archive directory contents
  134. [[ "$file" == *"/archive/"* ]] && continue
  135. if [ "$skip" = false ]; then
  136. cp "$file" "$CLAUDE_DIR/commands/"
  137. echo -e " ${GREEN}$filename${NC}"
  138. fi
  139. done
  140. echo ""
  141. # =============================================================================
  142. # SKILLS - Copy all skill directories
  143. # =============================================================================
  144. echo -e "${BLUE}Installing skills...${NC}"
  145. for skill_dir in "$PROJECT_ROOT/skills"/*/; do
  146. [ -d "$skill_dir" ] || continue
  147. skill_name=$(basename "$skill_dir")
  148. # _lib is the shared terminal library (skills/_lib/term.sh) that many skill
  149. # scripts source. It is NOT a skill, but it MUST be refreshed — scripts that
  150. # use newer term.sh features (TERM_DOT, brand glyphs, term_pip_bar) break with
  151. # an "unbound variable" under `set -u` against a stale copy.
  152. if [ "$skill_name" = "_lib" ]; then
  153. rm -rf "$CLAUDE_DIR/skills/_lib"
  154. cp -r "${skill_dir%/}" "$CLAUDE_DIR/skills/"
  155. echo -e " ${GREEN}_lib/${NC} (shared term library)"
  156. continue
  157. fi
  158. # Remove existing and copy fresh. Strip trailing slash from $skill_dir
  159. # so cp creates a subdirectory rather than merging contents (the *.*/* glob
  160. # always returns paths with trailing slashes, which makes cp behave as if
  161. # asked to copy contents — that's a long-standing bug we just fixed).
  162. rm -rf "$CLAUDE_DIR/skills/$skill_name"
  163. cp -r "${skill_dir%/}" "$CLAUDE_DIR/skills/"
  164. echo -e " ${GREEN}$skill_name/${NC}"
  165. done
  166. echo ""
  167. # =============================================================================
  168. # AGENTS - Copy all agent files
  169. # =============================================================================
  170. echo -e "${BLUE}Installing agents...${NC}"
  171. for file in "$PROJECT_ROOT/agents"/*.md; do
  172. [ -f "$file" ] || continue
  173. cp "$file" "$CLAUDE_DIR/agents/"
  174. echo -e " ${GREEN}$(basename "$file")${NC}"
  175. done
  176. echo ""
  177. # =============================================================================
  178. # RULES - Copy all rule files
  179. # =============================================================================
  180. echo -e "${BLUE}Installing rules...${NC}"
  181. for file in "$PROJECT_ROOT/rules"/*.md; do
  182. [ -f "$file" ] || continue
  183. cp "$file" "$CLAUDE_DIR/rules/"
  184. echo -e " ${GREEN}$(basename "$file")${NC}"
  185. done
  186. echo ""
  187. # =============================================================================
  188. # OUTPUT STYLES - Copy all output style files
  189. # =============================================================================
  190. echo -e "${BLUE}Installing output styles...${NC}"
  191. if [ -d "$PROJECT_ROOT/output-styles" ]; then
  192. for file in "$PROJECT_ROOT/output-styles"/*.md; do
  193. [ -f "$file" ] || continue
  194. cp "$file" "$CLAUDE_DIR/output-styles/"
  195. echo -e " ${GREEN}$(basename "$file")${NC}"
  196. done
  197. fi
  198. echo ""
  199. # =============================================================================
  200. # HOOKS - Copy scripts and merge plugin-equivalent wiring into settings.json
  201. # =============================================================================
  202. echo -e "${BLUE}Installing hooks...${NC}"
  203. for file in "$PROJECT_ROOT/hooks"/*.sh; do
  204. [ -f "$file" ] || continue
  205. cp "$file" "$CLAUDE_DIR/hooks/"
  206. make_executable "$CLAUDE_DIR/hooks/$(basename "$file")"
  207. done
  208. # Capability probe, not existence probe: walk/1 needs jq >= 1.6, and a 1.5 jq
  209. # passes `command -v` then crashes mid-install under set -e, leaving hooks
  210. # copied but settings.json unwired (adversarial-review finding, 2026-07).
  211. if echo '{}' | jq -e 'walk(.) | true' >/dev/null 2>&1; then
  212. settings_path="$CLAUDE_DIR/settings.json"
  213. [ -f "$settings_path" ] || printf '{}\n' > "$settings_path"
  214. tmp_settings="$(mktemp)"
  215. jq --arg hook_dir "$CLAUDE_DIR/hooks" --slurpfile desired "$PROJECT_ROOT/hooks/hooks.json" '
  216. # Dedup on the script NAME under hooks/, not the resolved path: a hook
  217. # wired by a plugin install carries the ${CLAUDE_PLUGIN_ROOT}/hooks/
  218. # form and must still count as already-wired (mixed-method double-fire).
  219. def script_name:
  220. .command | sub("^.*hooks[/\\\\]"; "") | sub("\\\"$"; "");
  221. ($desired[0]
  222. | walk(if type == "string" then gsub("\\$\\{CLAUDE_PLUGIN_ROOT\\}/hooks"; $hook_dir) else . end)
  223. ) as $wanted
  224. | .hooks = (.hooks // {})
  225. | reduce ($wanted.hooks | to_entries[]) as $event (.;
  226. reduce $event.value[] as $group (.;
  227. ([.hooks[$event.key][]?.hooks[]?.command // ""]) as $existing
  228. |
  229. ($group.hooks | map(
  230. . as $hook
  231. | ($hook | script_name) as $name
  232. | select(any($existing[]; contains("hooks/" + $name) or contains("hooks\\" + $name)) | not)
  233. )) as $missing
  234. | if ($missing | length) > 0 then
  235. .hooks[$event.key] = ((.hooks[$event.key] // []) + [($group | .hooks = $missing)])
  236. else . end
  237. )
  238. )
  239. ' "$settings_path" > "$tmp_settings"
  240. mv "$tmp_settings" "$settings_path"
  241. echo -e " ${GREEN}Security and peer-guard hooks wired in settings.json${NC}"
  242. else
  243. echo -e " ${YELLOW}jq with walk/1 (>=1.6) not available, skipping hook wiring (plugin installs unaffected)${NC}"
  244. fi
  245. # STATUSLINE - opt-in only (--statusline). Even when opted in we add it ONLY if
  246. # the user has none: a statusline is a whole-config key, so we never clobber one
  247. # the user already set, and we touch nothing else.
  248. statusline_tpl="$PROJECT_ROOT/templates/settings.json"
  249. if ! $WANT_STATUSLINE; then
  250. echo -e " ${YELLOW}Statusline skipped (re-run with --statusline to install it)${NC}"
  251. elif command -v jq >/dev/null 2>&1 && [ -f "$statusline_tpl" ]; then
  252. settings_path="$CLAUDE_DIR/settings.json"
  253. [ -f "$settings_path" ] || printf '{}\n' > "$settings_path"
  254. if [ "$(jq 'has("statusLine")' "$settings_path" 2>/dev/null)" = "true" ]; then
  255. echo -e " ${GREEN}Existing statusline preserved (remove it first to use the claude-mods one)${NC}"
  256. else
  257. tmp_settings="$(mktemp)"
  258. if jq --slurpfile tpl "$statusline_tpl" '.statusLine = $tpl[0].statusLine' "$settings_path" > "$tmp_settings"; then
  259. mv "$tmp_settings" "$settings_path"
  260. echo -e " ${GREEN}Context-usage statusline added to settings.json${NC}"
  261. else
  262. rm -f "$tmp_settings"
  263. fi
  264. fi
  265. fi
  266. echo ""
  267. # =============================================================================
  268. # PIGEON - Global install (scripts + hook config hint)
  269. # =============================================================================
  270. echo -e "${BLUE}Installing pigeon (pmail)...${NC}"
  271. # Clean up old agentmail install if present
  272. if [ -d "$CLAUDE_DIR/agentmail" ]; then
  273. rm -rf "$CLAUDE_DIR/agentmail"
  274. echo -e " ${RED}Removed old agentmail/ (renamed to pigeon/)${NC}"
  275. fi
  276. mkdir -p "$CLAUDE_DIR/pigeon"
  277. if [ -f "$PROJECT_ROOT/skills/pigeon/scripts/mail-db.sh" ]; then
  278. cp "$PROJECT_ROOT/skills/pigeon/scripts/mail-db.sh" "$CLAUDE_DIR/pigeon/"
  279. make_executable "$CLAUDE_DIR/pigeon/mail-db.sh"
  280. echo -e " ${GREEN}mail-db.sh${NC}"
  281. fi
  282. if [ -f "$PROJECT_ROOT/hooks/check-mail.sh" ]; then
  283. cp "$PROJECT_ROOT/hooks/check-mail.sh" "$CLAUDE_DIR/pigeon/"
  284. make_executable "$CLAUDE_DIR/pigeon/check-mail.sh"
  285. echo -e " ${GREEN}check-mail.sh${NC}"
  286. fi
  287. # Migrate stale agentmail hook path → pigeon
  288. if grep -q "agentmail/check-mail.sh" "$CLAUDE_DIR/settings.json" 2>/dev/null; then
  289. sed -i 's|agentmail/check-mail\.sh|pigeon/check-mail.sh|g' "$CLAUDE_DIR/settings.json"
  290. echo -e " ${GREEN}Migrated agentmail hook → pigeon in settings.json${NC}"
  291. fi
  292. # Check if hook is already configured (pigeon path)
  293. if grep -q "pigeon/check-mail.sh" "$CLAUDE_DIR/settings.json" 2>/dev/null; then
  294. echo -e " ${GREEN}Hook already configured in settings.json${NC}"
  295. else
  296. echo ""
  297. echo -e " ${YELLOW}To enable automatic pmail notifications, add this to ~/.claude/settings.json:${NC}"
  298. echo ""
  299. echo ' "hooks": {'
  300. echo ' "PreToolUse": [{'
  301. echo ' "matcher": "*",'
  302. echo ' "hooks": [{'
  303. echo ' "type": "command",'
  304. echo ' "command": "bash \"$HOME/.claude/pigeon/check-mail.sh\"",'
  305. echo ' "timeout": 5'
  306. echo ' }]'
  307. echo ' }]'
  308. echo ' }'
  309. echo ""
  310. echo -e " ${YELLOW}Without this, pigeon works but you must check manually (pigeon read).${NC}"
  311. fi
  312. echo ""
  313. # =============================================================================
  314. # AUTO-SKILL - Global install (tracking + evaluation hooks)
  315. # =============================================================================
  316. echo -e "${BLUE}Installing auto-skill...${NC}"
  317. mkdir -p "$CLAUDE_DIR/auto-skill"
  318. for script in track-tools.sh evaluate.sh; do
  319. if [ -f "$PROJECT_ROOT/skills/auto-skill/scripts/$script" ]; then
  320. cp "$PROJECT_ROOT/skills/auto-skill/scripts/$script" "$CLAUDE_DIR/auto-skill/"
  321. make_executable "$CLAUDE_DIR/auto-skill/$script"
  322. echo -e " ${GREEN}$script${NC}"
  323. fi
  324. done
  325. # Check if hooks are already configured
  326. if grep -q "auto-skill" "$CLAUDE_DIR/settings.json" 2>/dev/null; then
  327. echo -e " ${GREEN}Hooks already configured in settings.json${NC}"
  328. else
  329. echo ""
  330. echo -e " ${YELLOW}To enable automatic skill suggestions, add these hooks to ~/.claude/settings.json:${NC}"
  331. echo ""
  332. echo ' "PostToolUse": [{ "matcher": "*", "hooks": [{'
  333. echo ' "type": "command",'
  334. echo ' "command": "bash \"$HOME/.claude/auto-skill/track-tools.sh\"", "timeout": 2'
  335. echo ' }] }],'
  336. echo ' "Stop": [{ "hooks": [{'
  337. echo ' "type": "command",'
  338. echo ' "command": "bash \"$HOME/.claude/auto-skill/evaluate.sh\"", "timeout": 5'
  339. echo ' }] }]'
  340. echo ""
  341. echo -e " ${YELLOW}Without this, /auto-skill still works but won't suggest automatically.${NC}"
  342. fi
  343. echo ""
  344. # =============================================================================
  345. # SUMMARY
  346. # =============================================================================
  347. echo -e "${BLUE}════════════════════════════════════════════════════════════════${NC}"
  348. echo -e " ${GREEN}Installation complete!${NC}"
  349. echo -e "${BLUE}════════════════════════════════════════════════════════════════${NC}"
  350. echo ""
  351. echo -e "${YELLOW}Restart Claude Code to load the new extensions.${NC}"
  352. echo ""