From e389ac2f3cd36cc5503334f9517b3671713ecf43 Mon Sep 17 00:00:00 2001 From: bastien Date: Tue, 21 Apr 2026 13:50:40 +0200 Subject: [PATCH] feat(toggle): enable/disable non-marketplace tools via lib/toggle-external.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `claude plugin enable|disable` only toggles marketplace plugins. Tools living as symlinks (gstack per-skill entries, emil-design-eng, darwin-skill, find-skills) had no lever — users had to edit symlinks by hand. The new script moves symlinks in/out of skills-disabled/ so Claude Code stops or starts scanning them. Also removes the legacy global `skills/gstack` symlink that shadowed per-skill entries with a duplicate top-level "gstack" skill (same description as "browse"). gstack detection in detect-plugins.sh now probes an individual skill instead. plugin-advisor reads the new script's `list` command when gathering state and emits its `enable|disable` commands in recommendations. Co-Authored-By: Claude --- .gitignore | 3 + agents/plugin-advisor.md | 23 +++++- lib/detect-plugins.sh | 5 +- lib/toggle-external.sh | 158 +++++++++++++++++++++++++++++++++++++++ link.sh | 20 +++-- 5 files changed, 198 insertions(+), 11 deletions(-) create mode 100755 lib/toggle-external.sh diff --git a/.gitignore b/.gitignore index 204086d..fe9ff2b 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,9 @@ skills/emil-design-eng skills/darwin-skill skills/find-skills +# Staging area used by lib/toggle-external.sh when disabling a tool +skills-disabled/ + # Local project config (per-machine, not shared) .claude/ diff --git a/agents/plugin-advisor.md b/agents/plugin-advisor.md index 98041e9..1aab844 100644 --- a/agents/plugin-advisor.md +++ b/agents/plugin-advisor.md @@ -18,8 +18,10 @@ Detect active plugins and project signals. Recommend enable/disable. Apply compa # Claude Code plugins claude plugin list 2>/dev/null || echo "plugin-list-unavailable" -# GStack skills count (toggle CC plugin) -ls $HOME/.claude/skills/gstack/skills/ 2>/dev/null | wc -l || echo "0" +# External (non-marketplace) tools status — gstack, emil-design-eng, +# darwin-skill, find-skills. Managed by lib/toggle-external.sh since +# `claude plugin enable|disable` does not apply to them. +bash "$HOME/.claude/lib/toggle-external.sh" list 2>/dev/null || echo "toggle-external-unavailable" # Context7 CLI command -v ctx7 &>/dev/null && ctx7 --version 2>/dev/null | head -1 || echo "ctx7-not-installed" @@ -271,6 +273,23 @@ RULE: IF `complex-arch` signal (multiple services, event bus, distributed system --- +## TOGGLING EXTERNAL TOOLS + +Marketplace plugins toggle via `claude plugin enable|disable @`. +Non-marketplace tools (gstack per-skill symlinks, emil-design-eng, darwin-skill, +find-skills) toggle via `bash $HOME/.claude/lib/toggle-external.sh enable|disable `. + +When a recommendation flips the state of one of those tools, emit the exact +command — never write files directly. + +``` +# Enable gstack for a browser-QA signal: +bash $HOME/.claude/lib/toggle-external.sh enable gstack + +# Disable darwin-skill when passive cost is too high for a hotfix: +bash $HOME/.claude/lib/toggle-external.sh disable darwin-skill +``` + ## BLOCK if - Superpowers not active → install: `claude plugin marketplace add obra/superpowers-marketplace && claude plugin install --scope user superpowers@superpowers-marketplace` diff --git a/lib/detect-plugins.sh b/lib/detect-plugins.sh index 48c6467..052bfba 100644 --- a/lib/detect-plugins.sh +++ b/lib/detect-plugins.sh @@ -33,7 +33,10 @@ detect_security_guidance() { # --- Toggle plugins --- detect_gstack() { - [ -d "$HOME/.claude/skills/gstack" ] + # gstack is exposed via per-skill symlinks (browse, canary, qa, …); + # the legacy top-level symlink was removed to avoid duplicate entries. + # Detect by checking any of its individual skills. + [ -L "$HOME/.claude/skills/browse" ] || [ -L "$HOME/.claude/skills/qa" ] } detect_gsd() { diff --git a/lib/toggle-external.sh b/lib/toggle-external.sh new file mode 100755 index 0000000..780ea55 --- /dev/null +++ b/lib/toggle-external.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +# ============================================================ +# lib/toggle-external.sh — enable/disable non-plugin tools +# +# Marketplace plugins are toggled by `claude plugin enable|disable`. +# Tools distributed outside the marketplace (gstack submodule, emil +# curl install, npx-installed skills) have no such lever — they live +# as symlinks inside skills/. This script moves those symlinks +# to/from skills-disabled/ so Claude Code stops/starts scanning them. +# +# Usage: +# toggle-external.sh list +# toggle-external.sh status +# toggle-external.sh enable +# toggle-external.sh disable +# +# Managed tools: +# gstack — per-skill symlinks populated by gstack's own setup +# emil-design-eng — single symlink → skills-external/emil-design-eng +# darwin-skill — single symlink → ~/.agents/skills/darwin-skill +# find-skills — single symlink → ~/.agents/skills/find-skills +# ============================================================ +set -euo pipefail + +REPO="$(cd "$(dirname "$0")/.." && pwd)" +SKILLS_DIR="$REPO/skills" +DISABLED_DIR="$REPO/skills-disabled" + +GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m' +ok() { echo -e "${GREEN}✓${NC} $1"; } +warn() { echo -e "${YELLOW}⚠${NC} $1"; } +err() { echo -e "${RED}✗${NC} $1"; } + +# All non-plugin tools this script can toggle. +MANAGED_TOOLS=(gstack emil-design-eng darwin-skill find-skills) + +# Prints the names (directory basenames) that belong to "gstack". +# Source of truth: skills-external/gstack/*/SKILL.md. The repo's +# skills/ symlinks are generated from these by gstack ./setup. +gstack_skills() { + local gstack_src="$REPO/skills-external/gstack" + [ -d "$gstack_src" ] || return 0 + for d in "$gstack_src"/*/; do + [ -f "${d}SKILL.md" ] || continue + basename "$d" + done +} + +# Prints "enabled" / "disabled" / "missing" for a tool. +status_tool() { + local tool="$1" + case "$tool" in + gstack) + [ -d "$REPO/skills-external/gstack" ] || { echo "missing"; return; } + while read -r name; do + [ -e "$SKILLS_DIR/$name" ] && { echo "enabled"; return; } + done < <(gstack_skills) + echo "disabled" + ;; + emil-design-eng) + [ -d "$REPO/skills-external/emil-design-eng" ] || { echo "missing"; return; } + [ -e "$SKILLS_DIR/emil-design-eng" ] && echo "enabled" || echo "disabled" + ;; + darwin-skill|find-skills) + [ -d "$HOME/.agents/skills/$tool" ] || { echo "missing"; return; } + [ -e "$SKILLS_DIR/$tool" ] && echo "enabled" || echo "disabled" + ;; + *) + echo "unknown"; return 1 ;; + esac +} + +disable_tool() { + local tool="$1" + mkdir -p "$DISABLED_DIR" + case "$tool" in + gstack) + local moved=0 + while read -r name; do + [ -e "$SKILLS_DIR/$name" ] || continue + mv "$SKILLS_DIR/$name" "$DISABLED_DIR/gstack__$name" + moved=$((moved + 1)) + done < <(gstack_skills) + ok "gstack disabled ($moved symlinks moved)" + ;; + emil-design-eng|darwin-skill|find-skills) + if [ -e "$SKILLS_DIR/$tool" ]; then + mv "$SKILLS_DIR/$tool" "$DISABLED_DIR/$tool" + ok "$tool disabled" + else + warn "$tool already disabled" + fi + ;; + *) err "Unknown tool: $tool"; return 1 ;; + esac +} + +enable_tool() { + local tool="$1" + case "$tool" in + gstack) + local moved=0 + if [ -d "$DISABLED_DIR" ]; then + for entry in "$DISABLED_DIR"/gstack__*; do + [ -e "$entry" ] || continue + local name + name="$(basename "$entry" | sed 's/^gstack__//')" + mv "$entry" "$SKILLS_DIR/$name" + moved=$((moved + 1)) + done + fi + if [ "$moved" -eq 0 ]; then + warn "gstack was not disabled — re-run gstack setup to (re)create symlinks" + else + ok "gstack enabled ($moved symlinks restored)" + fi + ;; + emil-design-eng|darwin-skill|find-skills) + if [ -e "$DISABLED_DIR/$tool" ]; then + mv "$DISABLED_DIR/$tool" "$SKILLS_DIR/$tool" + ok "$tool enabled" + elif [ -e "$SKILLS_DIR/$tool" ]; then + warn "$tool already enabled" + else + err "$tool not installed — run: make plugin" + return 1 + fi + ;; + *) err "Unknown tool: $tool"; return 1 ;; + esac +} + +list_all() { + printf "%-20s %s\n" "TOOL" "STATUS" + printf "%-20s %s\n" "----" "------" + for t in "${MANAGED_TOOLS[@]}"; do + printf "%-20s %s\n" "$t" "$(status_tool "$t")" + done +} + +usage() { + sed -n '3,23p' "$0" | sed 's/^# \?//' + exit "${1:-0}" +} + +main() { + local cmd="${1:-}" + case "$cmd" in + list) list_all ;; + status) [ $# -ge 2 ] || usage 1; status_tool "$2" ;; + enable) [ $# -ge 2 ] || usage 1; enable_tool "$2" ;; + disable) [ $# -ge 2 ] || usage 1; disable_tool "$2" ;; + ""|-h|--help|help) usage 0 ;; + *) err "Unknown command: $cmd"; usage 1 ;; + esac +} + +main "$@" diff --git a/link.sh b/link.sh index 0171397..96a3759 100644 --- a/link.sh +++ b/link.sh @@ -35,14 +35,18 @@ for item in hooks agents skills lib templates; do CHANGED=$((CHANGED + 1)) done -if [ -d "$REPO/skills-external/gstack" ]; then - if [ -L "$CLAUDE/skills/gstack" ] && [ "$(readlink "$CLAUDE/skills/gstack")" = "$REPO/skills-external/gstack" ]; then - : # already correct - else - ln -sf "$REPO/skills-external/gstack" "$CLAUDE/skills/gstack" - CHANGED=$((CHANGED + 1)) - fi -else +# GStack is exposed via per-skill symlinks under skills/ (browse, +# canary, autoplan, design-review, …) created by gstack's own +# `./setup`. A global `skills/gstack -> skills-external/gstack/` +# symlink duplicated the top-level gstack SKILL.md alongside those +# individual skills, producing two entries with the same description +# ("Fast headless browser for QA testing…"). Remove any stale global +# link — only per-skill entries remain. +if [ -L "$REPO/skills/gstack" ] || [ -L "$CLAUDE/skills/gstack" ]; then + rm -f "$REPO/skills/gstack" "$CLAUDE/skills/gstack" + CHANGED=$((CHANGED + 1)) +fi +if [ ! -d "$REPO/skills-external/gstack" ]; then echo "⚠️ GStack submodule not found — run: git submodule update --init" fi