toggle-external.sh 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. #!/usr/bin/env bash
  2. # ============================================================
  3. # lib/toggle-external.sh — enable/disable non-plugin tools
  4. #
  5. # Marketplace plugins are toggled by `claude plugin enable|disable`.
  6. # Tools distributed outside the marketplace (gstack submodule, emil
  7. # curl install, npx-installed skills) have no such lever — they live
  8. # as symlinks inside skills/. This script moves those symlinks
  9. # to/from skills-disabled/ so Claude Code stops/starts scanning them.
  10. #
  11. # MCP servers are toggled via `claude mcp add|remove` (not symlinks).
  12. #
  13. # Usage:
  14. # toggle-external.sh list
  15. # toggle-external.sh status <tool>
  16. # toggle-external.sh enable <tool>
  17. # toggle-external.sh disable <tool>
  18. #
  19. # Managed tools:
  20. # gstack — per-skill symlinks populated by gstack's own setup
  21. # emil-design-eng — single symlink → skills-external/emil-design-eng
  22. # darwin-skill — single symlink → ~/.agents/skills/darwin-skill
  23. # find-skills — single symlink → ~/.agents/skills/find-skills
  24. # magic — 21st-dev Magic MCP server (API key in .env)
  25. #
  26. # For fine-grained activation (only design skills, only qa skills, only
  27. # audit skills, etc.) instead of all-or-nothing gstack toggling, use:
  28. # bash lib/profile.sh list
  29. # bash lib/profile.sh set <design|dev|qa|audit|minimal>
  30. # bash lib/profile.sh reset
  31. # ============================================================
  32. set -euo pipefail
  33. REPO="$(cd "$(dirname "$0")/.." && pwd)"
  34. SKILLS_DIR="$REPO/skills"
  35. DISABLED_DIR="$REPO/skills-disabled"
  36. GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
  37. ok() { echo -e "${GREEN}✓${NC} $1"; }
  38. warn() { echo -e "${YELLOW}⚠${NC} $1"; }
  39. err() { echo -e "${RED}✗${NC} $1"; }
  40. # All non-plugin tools this script can toggle.
  41. MANAGED_TOOLS=(gstack emil-design-eng darwin-skill find-skills magic)
  42. # Load MAGIC_API_KEY (and any other secrets) from $REPO/.env if present.
  43. # Called only by the magic branch — other tools don't need env vars.
  44. load_env() {
  45. if [ -z "${MAGIC_API_KEY:-}" ] && [ -f "$REPO/.env" ]; then
  46. set -a
  47. # shellcheck source=/dev/null
  48. source "$REPO/.env"
  49. set +a
  50. fi
  51. }
  52. # Prints the names (directory basenames) that belong to "gstack".
  53. # Source of truth: skills-external/gstack/*/SKILL.md. The repo's
  54. # skills/<name> symlinks are generated from these by gstack ./setup.
  55. gstack_skills() {
  56. local gstack_src="$REPO/skills-external/gstack"
  57. [ -d "$gstack_src" ] || return 0
  58. for d in "$gstack_src"/*/; do
  59. [ -f "${d}SKILL.md" ] || continue
  60. basename "$d"
  61. done
  62. }
  63. # Prints "enabled" / "disabled" / "missing" for a tool.
  64. status_tool() {
  65. local tool="$1"
  66. case "$tool" in
  67. gstack)
  68. [ -d "$REPO/skills-external/gstack" ] || { echo "missing"; return; }
  69. while read -r name; do
  70. [ -e "$SKILLS_DIR/$name" ] && { echo "enabled"; return; }
  71. done < <(gstack_skills)
  72. echo "disabled"
  73. ;;
  74. emil-design-eng)
  75. [ -d "$REPO/skills-external/emil-design-eng" ] || { echo "missing"; return; }
  76. [ -e "$SKILLS_DIR/emil-design-eng" ] && echo "enabled" || echo "disabled"
  77. ;;
  78. darwin-skill|find-skills)
  79. [ -d "$HOME/.agents/skills/$tool" ] || { echo "missing"; return; }
  80. [ -e "$SKILLS_DIR/$tool" ] && echo "enabled" || echo "disabled"
  81. ;;
  82. magic)
  83. command -v claude >/dev/null || { echo "missing"; return; }
  84. if claude mcp list 2>/dev/null | grep -q '^magic:'; then
  85. echo "enabled"
  86. else
  87. echo "disabled"
  88. fi
  89. ;;
  90. *)
  91. echo "unknown"; return 1 ;;
  92. esac
  93. }
  94. disable_tool() {
  95. local tool="$1"
  96. mkdir -p "$DISABLED_DIR"
  97. case "$tool" in
  98. gstack)
  99. local moved=0
  100. while read -r name; do
  101. [ -e "$SKILLS_DIR/$name" ] || continue
  102. # Clobber any stale destination. gstack ./setup now creates
  103. # skills/<name>/ as directories, so mv onto an existing dir
  104. # would nest it (gstack__<name>/<name>/) instead of renaming.
  105. # Content is symlinks to the submodule — `gstack ./setup` regenerates.
  106. rm -rf "$DISABLED_DIR/gstack__$name"
  107. mv "$SKILLS_DIR/$name" "$DISABLED_DIR/gstack__$name"
  108. moved=$((moved + 1))
  109. done < <(gstack_skills)
  110. ok "gstack disabled ($moved symlinks moved)"
  111. ;;
  112. emil-design-eng|darwin-skill|find-skills)
  113. if [ -e "$SKILLS_DIR/$tool" ]; then
  114. rm -rf "${DISABLED_DIR:?}/${tool:?}"
  115. mv "$SKILLS_DIR/$tool" "$DISABLED_DIR/$tool"
  116. ok "$tool disabled"
  117. else
  118. warn "$tool already disabled"
  119. fi
  120. ;;
  121. magic)
  122. if [ "$(status_tool magic)" = "enabled" ]; then
  123. claude mcp remove magic -s user >/dev/null
  124. ok "magic disabled"
  125. else
  126. warn "magic already disabled"
  127. fi
  128. ;;
  129. *) err "Unknown tool: $tool"; return 1 ;;
  130. esac
  131. }
  132. enable_tool() {
  133. local tool="$1"
  134. case "$tool" in
  135. gstack)
  136. local moved=0
  137. if [ -d "$DISABLED_DIR" ]; then
  138. for entry in "$DISABLED_DIR"/gstack__*; do
  139. [ -e "$entry" ] || continue
  140. local name
  141. name="$(basename "$entry" | sed 's/^gstack__//')"
  142. rm -rf "${SKILLS_DIR:?}/${name:?}"
  143. mv "$entry" "$SKILLS_DIR/$name"
  144. moved=$((moved + 1))
  145. done
  146. fi
  147. if [ "$moved" -eq 0 ]; then
  148. warn "gstack was not disabled — re-run gstack setup to (re)create symlinks"
  149. else
  150. ok "gstack enabled ($moved symlinks restored)"
  151. fi
  152. ;;
  153. emil-design-eng|darwin-skill|find-skills)
  154. if [ -e "$DISABLED_DIR/$tool" ]; then
  155. rm -rf "${SKILLS_DIR:?}/${tool:?}"
  156. mv "$DISABLED_DIR/$tool" "$SKILLS_DIR/$tool"
  157. ok "$tool enabled"
  158. elif [ -e "$SKILLS_DIR/$tool" ]; then
  159. warn "$tool already enabled"
  160. else
  161. err "$tool not installed — run: make plugin"
  162. return 1
  163. fi
  164. ;;
  165. magic)
  166. load_env
  167. if [ -z "${MAGIC_API_KEY:-}" ]; then
  168. err "MAGIC_API_KEY not set — add it to $REPO/.env (template: .env.example)"
  169. return 1
  170. fi
  171. if [ "$(status_tool magic)" = "enabled" ]; then
  172. warn "magic already enabled"
  173. return 0
  174. fi
  175. claude mcp add magic --scope user \
  176. --env API_KEY="$MAGIC_API_KEY" \
  177. -- npx -y @21st-dev/magic@latest
  178. ok "magic enabled (user scope)"
  179. ;;
  180. *) err "Unknown tool: $tool"; return 1 ;;
  181. esac
  182. }
  183. list_all() {
  184. printf "%-20s %s\n" "TOOL" "STATUS"
  185. printf "%-20s %s\n" "----" "------"
  186. for t in "${MANAGED_TOOLS[@]}"; do
  187. printf "%-20s %s\n" "$t" "$(status_tool "$t")"
  188. done
  189. }
  190. usage() {
  191. sed -n '3,23p' "$0" | sed 's/^# \?//'
  192. exit "${1:-0}"
  193. }
  194. main() {
  195. local cmd="${1:-}"
  196. case "$cmd" in
  197. list) list_all ;;
  198. status) [ $# -ge 2 ] || usage 1; status_tool "$2" ;;
  199. enable) [ $# -ge 2 ] || usage 1; enable_tool "$2" ;;
  200. disable) [ $# -ge 2 ] || usage 1; disable_tool "$2" ;;
  201. ""|-h|--help|help) usage 0 ;;
  202. *) err "Unknown command: $cmd"; usage 1 ;;
  203. esac
  204. }
  205. main "$@"