toggle-external.sh 6.6 KB

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