feat(21st): replace the magic MCP with the @21st-dev CLI + skill pack
Upstream supersedes `@21st-dev/magic` with `@21st-dev/cli` (bin `21st`): same endpoint, `21st login` in place of an API key, no MCP process loaded into every session. - install-plugins.sh Step 8.7: `npm i -g @21st-dev/cli` (pinned in plugins.lock.json), staged `21st skills install`, TTY-only login offer, pack disabled by default. update-all.sh 7.4 refreshes both. - The documented `21st install-skill` cannot be used: the installer refuses to follow a symlink on the target path and `~/.claude/skills` is one. The install runs under a throwaway HOME and the result moves into skills-external/21st-* (gitignored), symlinked on demand. - toggle-external.sh manages `21st` as a pack (names globbed from skills-external/21st-*, parked under plain names). `magic` is gone. - The 5 design skills join design/web/web-full/full and MANAGED_EXTERNALS; 21st-registry and 21st-design-sync stay parked. MANAGED_MCPS is now empty and profile.sh's dead magic branches are removed. - Design gate: GATE-BLOCK gains `21st` (required-manual, magic's old slot) and `21st-ui-build`; PATH repair extended to the npm global bin. - settings.json: the 4 mcp__magic__* ask entries go; the outward-facing 21st verbs land in autoMode.soft_deny, the tier that holds under auto mode (LRN-153). - Docs: README, CLAUDE.global.md, design-gate.md, profile SKILL.md, .env.example, .gitleaks.toml, link.sh. BDR-093, LRN-158. Tests: profile-set-managed 17/17, make test green except 2 pre-existing gitflow FAILs (gitleaks binary absent on this host), shellcheck clean.
This commit is contained in:
+13
-12
@@ -41,7 +41,8 @@ Tier does NOT change WHAT gets checked. Every non-trivial design tier draws from
|
||||
the one `design` profile — so the gate checks that profile's **design-core
|
||||
tools** (the `# GATE-BLOCK:` allowlist in `design.profile`: ui-ux-pro-max,
|
||||
frontend-design, emil-design-eng, design-motion-principles, impeccable, design-html,
|
||||
design-review, design-consultation, magic). The profile also bundles
|
||||
design-review, design-consultation, the `21st` CLI and `21st-ui-build` — the
|
||||
canary for the whole 21st skill pack). The profile also bundles
|
||||
browser/plan/shotgun tooling and graphify for convenience; those never trip the
|
||||
gate. Motion (`design-motion-principles`) and static-HTML (`design-html`) are
|
||||
already in the core set — checked regardless; their CLAUDE.md "+motion /
|
||||
@@ -54,7 +55,7 @@ already in the core set — checked regardless; their CLAUDE.md "+motion /
|
||||
It reads the design-core tools (`# GATE-BLOCK:` in `design.profile`) plus their
|
||||
types (`profile.sh show design --plain`) and checks each on its own channel —
|
||||
skill symlink, `claude plugin list`, `claude mcp list`, `command -v`. It never
|
||||
reads `disabledMcpServers` (unreliable for bi-modal servers like magic/context7).
|
||||
reads `disabledMcpServers` (unreliable for bi-modal servers like context7).
|
||||
The core set lives in `design.profile`, not in the script or here — single source.
|
||||
|
||||
Exit codes: `0` = ready · `11` = ready-but-unverified (proceed, but surface it) · `10` = incomplete (gate trips) · `2` = error.
|
||||
@@ -67,21 +68,21 @@ Exit codes: `0` = ready · `11` = ready-but-unverified (proceed, but surface it)
|
||||
|
||||
🎨 DESIGN DETECTED — the design toolchain isn't fully active.
|
||||
activate with /profile design: <skills / ui-ux-pro-max>
|
||||
required + manual step: <e.g. magic — needs MAGIC_API_KEY>
|
||||
required + manual step: <e.g. 21st — needs the CLI>
|
||||
→ run /profile design to activate it, then continue.
|
||||
|
||||
- **activate with /profile design** → skills + the plugin; `/profile design`
|
||||
turns them on directly.
|
||||
- **required + manual step** → required tools the profile can't flip silently.
|
||||
**magic lands here: it TRIPS the gate** (it's required for Build), it is NOT
|
||||
a silent "optional". `/profile design` runs `toggle-external.sh` for magic,
|
||||
which needs a valid `MAGIC_API_KEY` in `~/.claude/.env` — tell the user to verify it.
|
||||
**the `21st` CLI lands here: it TRIPS the gate** (it's required for Build),
|
||||
it is NOT a silent "optional". `/profile design` symlinks the 21st skills,
|
||||
but the CLI they shell out to is a global npm install: tell the user to run
|
||||
`npm i -g @21st-dev/cli` then `21st login` (no API key, no MCP).
|
||||
- Do NOT hand-activate individual tools. The profile is the unit of activation.
|
||||
- **11 / `READY BUT UNVERIFIED`** → `claude` was unreachable, so the design
|
||||
plugin/MCP (magic, ui-ux-pro-max) could NOT be checked. Do NOT report a plain
|
||||
"ready": proceed only after telling the user that N tool(s) went unverified and
|
||||
having them confirm with `claude mcp list` / `claude plugin list`. Fail-visible,
|
||||
not fail-silent — the most important tool (magic) is exactly an unverifiable one.
|
||||
plugin (ui-ux-pro-max) could NOT be checked. Do NOT report a plain "ready":
|
||||
proceed only after telling the user that N tool(s) went unverified and having
|
||||
them confirm with `claude plugin list`. Fail-visible, not fail-silent.
|
||||
|
||||
### 4. Animation library — suggest-only (fires only on a real motion signal)
|
||||
|
||||
@@ -149,8 +150,8 @@ remedy is always `/profile <that>` — a profile, never a lone tool.
|
||||
|
||||
- Remedy is ALWAYS a profile (`/profile design`), never an atomic tool toggle —
|
||||
the profile system is the single source of truth for what's active.
|
||||
- magic is REQUIRED (it trips the gate), but `/profile design` only enables it
|
||||
if `MAGIC_API_KEY` is in `~/.claude/.env` — the gate says so; surface that to the user.
|
||||
- the `21st` CLI is REQUIRED (it trips the gate) and `/profile design` cannot
|
||||
install it — the gate names the two commands; surface them to the user.
|
||||
- The design-core set (what trips the gate) is declared in `design.profile` on
|
||||
the `# GATE-BLOCK:` line(s) — edit there to add/remove a blocking design tool,
|
||||
not in the script.
|
||||
|
||||
+34
-9
@@ -29,12 +29,13 @@
|
||||
# required-manual required but the profile can't flip it silently (API
|
||||
# key / external install) — the gate STILL trips, names
|
||||
# it, and the remedy is `/profile design` + a manual step.
|
||||
# This is where magic lands: required, never silent.
|
||||
# This is where the `21st` CLI lands: required, never
|
||||
# silent (npm i -g @21st-dev/cli, then 21st login).
|
||||
# Both classes trip the gate. Tools NOT on the GATE-BLOCK allowlist are
|
||||
# ignored entirely (browser/plan/shotgun tooling, graphify).
|
||||
#
|
||||
# disabledMcpServers is NEVER read — unreliable for bi-modal servers
|
||||
# (magic/context7 can appear there yet be active via another channel).
|
||||
# (context7 can appear there yet be active via another channel).
|
||||
#
|
||||
# Exit: 0 = ready · 11 = ready-but-unverified (proceed, say so) · 10 = incomplete (trips) · 2 = error.
|
||||
# Usage: design-tool-gate.sh [profile] (default profile: design)
|
||||
@@ -79,6 +80,30 @@ ensure_claude_on_path() {
|
||||
}
|
||||
ensure_claude_on_path
|
||||
|
||||
# Same sanitized-PATH problem for `21st` (an npm global bin), with a twist:
|
||||
# the repair above only fires when claude ITSELF is unresolvable, and claude
|
||||
# often lives in ~/.local/bin while the npm global bin dir is missing from a
|
||||
# hook's PATH. Probe for the binary directly and prepend the dir that has it,
|
||||
# otherwise a perfectly installed CLI reads as "missing" and trips the gate.
|
||||
ensure_21st_on_path() {
|
||||
command -v 21st >/dev/null 2>&1 && return
|
||||
local cand
|
||||
for cand in \
|
||||
"$HOME/.local/bin/21st" \
|
||||
/usr/local/bin/21st; do
|
||||
[ -x "$cand" ] && { PATH="$(dirname "$cand"):$PATH"; return; }
|
||||
done
|
||||
local m newest matches=()
|
||||
for m in "$HOME"/.nvm/versions/node/*/bin/21st; do
|
||||
[ -x "$m" ] && matches+=("$m")
|
||||
done
|
||||
if [ "${#matches[@]}" -gt 0 ]; then
|
||||
newest="$(printf '%s\n' "${matches[@]}" | sort -V | tail -1)"
|
||||
PATH="$(dirname "$newest"):$PATH"
|
||||
fi
|
||||
}
|
||||
ensure_21st_on_path
|
||||
|
||||
# Gate scope: the "# GATE-BLOCK:" allowlist (one or more lines, concatenated).
|
||||
# Empty => fall back to "every gate-relevant entry is in scope" (coarse).
|
||||
core_set="$(grep '^# GATE-BLOCK:' "$PROFILE_FILE" 2>/dev/null \
|
||||
@@ -143,8 +168,8 @@ done <<< "$plain"
|
||||
# Verdict — three outcomes:
|
||||
# blocking/manual non-empty -> INCOMPLETE (exit 10): the gate trips.
|
||||
# only unverified non-empty -> READY BUT UNVERIFIED (exit 11): fail-VISIBLE.
|
||||
# claude was unreachable, so the plugin/MCP (magic, ui-ux-pro-max) could
|
||||
# not be checked. Never pass this as a silent READY — proceed, but say so.
|
||||
# claude was unreachable, so the plugin channel (ui-ux-pro-max) could not
|
||||
# be checked. Never pass this as a silent READY — proceed, but say so.
|
||||
# nothing pending -> READY (exit 0).
|
||||
if [ "${#blocking[@]}" -gt 0 ] || [ "${#manual[@]}" -gt 0 ]; then
|
||||
echo "design toolchain: INCOMPLETE"
|
||||
@@ -152,9 +177,9 @@ if [ "${#blocking[@]}" -gt 0 ] || [ "${#manual[@]}" -gt 0 ]; then
|
||||
echo " activate with /profile $PROFILE: ${blocking[*]}"
|
||||
fi
|
||||
if [ "${#manual[@]}" -gt 0 ]; then
|
||||
echo " required + manual step (API key / external install): ${manual[*]}"
|
||||
echo " required + manual step (external install / sign-in): ${manual[*]}"
|
||||
case " ${manual[*]} " in
|
||||
*" magic "*) echo " magic needs MAGIC_API_KEY in ~/.claude/.env (/profile $PROFILE runs toggle-external.sh)" ;;
|
||||
*" 21st "*) echo " 21st needs the CLI: npm i -g @21st-dev/cli then 21st login" ;;
|
||||
esac
|
||||
fi
|
||||
if [ "${#unverified[@]}" -gt 0 ]; then
|
||||
@@ -167,9 +192,9 @@ fi
|
||||
if [ "${#unverified[@]}" -gt 0 ]; then
|
||||
echo "design toolchain: READY BUT UNVERIFIED — ${#unverified[@]} tool(s) not checked"
|
||||
echo " unverified (claude CLI unreachable): ${unverified[*]}"
|
||||
echo " the gate could NOT confirm the design plugin/MCP (e.g. magic,"
|
||||
echo " ui-ux-pro-max) are active. Proceed only after checking manually:"
|
||||
echo " claude mcp list claude plugin list"
|
||||
echo " the gate could NOT confirm the design plugin (ui-ux-pro-max) is"
|
||||
echo " active. Proceed only after checking manually:"
|
||||
echo " claude plugin list"
|
||||
exit 11
|
||||
fi
|
||||
|
||||
|
||||
+16
-21
@@ -11,7 +11,7 @@
|
||||
# Mechanism:
|
||||
# - Skills (gstack/external/personal): symlink toggle skills/ ↔ skills-disabled/
|
||||
# - Plugins: `claude plugin enable|disable <name>@<marketplace>`
|
||||
# - MCPs: delegated to lib/toggle-external.sh for known servers (magic),
|
||||
# - MCPs: advisory (none managed since BDR-093 — MANAGED_MCPS is empty),
|
||||
# advisory otherwise
|
||||
# - CLIs: advisory only (rtk, gsd, ctx7, graphify — installed externally)
|
||||
# - `set` is SYMMETRIC on managed items (BDR-079): plugins, external packs
|
||||
@@ -51,7 +51,6 @@ SKILLS_DIR="$REPO/skills"
|
||||
DISABLED_DIR="$REPO/skills-disabled"
|
||||
GSTACK_SRC="$REPO/skills-external/gstack" # gstack submodule — source of truth for gstack skills
|
||||
PROFILES_DIR="$REPO/lib/profiles"
|
||||
TOGGLE_EXTERNAL="$REPO/lib/toggle-external.sh"
|
||||
ACTIVE_CACHE="$REPO/.active-profile" # statusline reads this — keep fast (single-line file, profile name only)
|
||||
|
||||
# Plugins that are toggle-managed by `set`. Anything NOT in this list is
|
||||
@@ -73,13 +72,20 @@ MANAGED_EXTERNALS=(
|
||||
frontend-design
|
||||
design-motion-principles
|
||||
impeccable
|
||||
21st-ui-build
|
||||
21st-ui-explore
|
||||
21st-ui-review
|
||||
21st-cli-use
|
||||
21st-ai
|
||||
)
|
||||
|
||||
# MCP servers that are toggle-managed by `set`, both ways (enable AND
|
||||
# disable), delegated to lib/toggle-external.sh. Same allowlist doctrine.
|
||||
MANAGED_MCPS=(
|
||||
magic
|
||||
)
|
||||
# Empty since 2026-09-22: `magic` was the only entry and 21st.dev replaced
|
||||
# its MCP server with a CLI + skill pack (the 5 design skills are managed as
|
||||
# externals above). The `mcp` type itself stays supported — a profile can
|
||||
# still list an MCP, it is then advisory rather than auto-toggled.
|
||||
MANAGED_MCPS=()
|
||||
|
||||
# Plugins that MUST stay enabled — `set` will refuse to disable these even if
|
||||
# they're not in the profile. (Defensive: belt-and-suspenders alongside
|
||||
@@ -324,15 +330,12 @@ enable_skill() {
|
||||
fi
|
||||
;;
|
||||
mcp)
|
||||
# Advisory only. The delegation branch that lived here served `magic`,
|
||||
# the single managed MCP; 21st.dev replaced it with a CLI (BDR-093), so
|
||||
# MANAGED_MCPS is empty and nothing is auto-registered. Re-add a branch
|
||||
# here the day a profile owns an MCP server again.
|
||||
if [ "$(skill_status "$skill" mcp)" = "enabled" ]; then
|
||||
: # already on
|
||||
elif [ "$skill" = "magic" ] && [ -x "$TOGGLE_EXTERNAL" ]; then
|
||||
# Known MCP — delegate to lib/toggle-external.sh which handles env vars.
|
||||
if bash "$TOGGLE_EXTERNAL" enable magic 2>&1 | grep -qE "enabled|already"; then
|
||||
ok "enabled MCP: magic"
|
||||
else
|
||||
info "MCP 'magic' could not be enabled (check .env for MAGIC_API_KEY)"
|
||||
fi
|
||||
else
|
||||
info "MCP '$skill' not registered — run: claude mcp add $skill -- <command>"
|
||||
fi
|
||||
@@ -394,15 +397,7 @@ disable_skill() {
|
||||
info "plugin '$skill' — manual: claude plugin disable $skill@<marketplace>"
|
||||
;;
|
||||
mcp)
|
||||
if [ "$skill" = "magic" ] && [ -x "$TOGGLE_EXTERNAL" ]; then
|
||||
if bash "$TOGGLE_EXTERNAL" disable magic 2>&1 | grep -qE "disabled|already"; then
|
||||
ok "disabled MCP: magic"
|
||||
else
|
||||
info "MCP 'magic' — manual disable failed"
|
||||
fi
|
||||
else
|
||||
info "MCP '$skill' — manual: claude mcp remove $skill"
|
||||
fi
|
||||
info "MCP '$skill' — manual: claude mcp remove $skill"
|
||||
;;
|
||||
cli)
|
||||
: # never auto-uninstall CLIs
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
# tooling, graphify) is bundled for convenience but never blocks. Keep these
|
||||
# lines in sync when adding/removing a core design tool.
|
||||
# GATE-BLOCK: frontend-design ui-ux-pro-max emil-design-eng design-html
|
||||
# GATE-BLOCK: design-motion-principles design-review design-consultation magic
|
||||
# GATE-BLOCK: design-motion-principles design-review design-consultation
|
||||
# GATE-BLOCK: 21st 21st-ui-build
|
||||
|
||||
# Core design skills (gstack)
|
||||
design-shotgun
|
||||
@@ -30,11 +31,19 @@ frontend-design external
|
||||
design-motion-principles external
|
||||
impeccable external
|
||||
|
||||
# External: 21st.dev pack — CLI-driven (no MCP, no API key). 21st-registry
|
||||
# and 21st-design-sync are publishing flows; installed but left parked.
|
||||
21st-ui-build external
|
||||
21st-ui-explore external
|
||||
21st-ui-review external
|
||||
21st-cli-use external
|
||||
21st-ai external
|
||||
|
||||
# Plugin (auto-toggle)
|
||||
ui-ux-pro-max plugin@ui-ux-pro-max-skill
|
||||
|
||||
# MCP — auto-toggle via lib/toggle-external.sh (needs MAGIC_API_KEY in .env)
|
||||
magic mcp
|
||||
|
||||
# CLIs (advisory only — installed/not-installed)
|
||||
# 21st is NOT advisory: it is on the GATE-BLOCK list, so a missing CLI trips
|
||||
# the design gate. Install: npm i -g @21st-dev/cli then 21st login
|
||||
21st cli
|
||||
graphify cli
|
||||
|
||||
@@ -86,9 +86,14 @@ ui-ux-pro-max plugin@ui-ux-pro-max-skill
|
||||
# claude plugin enable pr-review-toolkit@claude-code-plugins
|
||||
# or profile-based: bash lib/profile.sh apply audit (audit.profile keeps it;
|
||||
# a later `set full` re-disables it — MANAGED_PLUGINS lifecycle).
|
||||
magic mcp
|
||||
21st-ui-build external
|
||||
21st-ui-explore external
|
||||
21st-ui-review external
|
||||
21st-cli-use external
|
||||
21st-ai external
|
||||
|
||||
# === CLIs (advisory) =================================================
|
||||
21st cli
|
||||
ctx7 cli
|
||||
graphify cli
|
||||
gsd cli
|
||||
|
||||
@@ -49,9 +49,14 @@ emil-design-eng external
|
||||
frontend-design external
|
||||
design-motion-principles external
|
||||
impeccable external
|
||||
21st-ui-build external
|
||||
21st-ui-explore external
|
||||
21st-ui-review external
|
||||
21st-cli-use external
|
||||
21st-ai external
|
||||
ui-ux-pro-max plugin@ui-ux-pro-max-skill
|
||||
magic mcp
|
||||
|
||||
# === CLIs (advisory) =================================================
|
||||
# === CLIs ============================================================
|
||||
21st cli
|
||||
ctx7 cli
|
||||
graphify cli
|
||||
|
||||
@@ -38,11 +38,19 @@ frontend-design external
|
||||
design-motion-principles external
|
||||
impeccable external
|
||||
|
||||
# External: 21st.dev pack (publishing flows 21st-registry / -design-sync
|
||||
# stay parked)
|
||||
21st-ui-build external
|
||||
21st-ui-explore external
|
||||
21st-ui-review external
|
||||
21st-cli-use external
|
||||
21st-ai external
|
||||
|
||||
# Plugin: UI/UX intelligence (auto-toggle)
|
||||
ui-ux-pro-max plugin@ui-ux-pro-max-skill
|
||||
|
||||
# MCP: 21st-dev Magic component generator
|
||||
magic mcp
|
||||
# CLI: 21st.dev component catalog + UI generation (needs `21st login`)
|
||||
21st cli
|
||||
|
||||
# CLI: ctx7 (doc lookup for fast-evolving libs like Next.js)
|
||||
ctx7 cli
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/tests/profile-set-managed.test.sh — `set` symmetry on managed
|
||||
# externals + MCPs, gstack on-demand, external from-source (BDR-079).
|
||||
# externals, gstack on-demand, external from-source (BDR-079). The MCP
|
||||
# assertions went with `magic` (2026-09-22): MANAGED_MCPS is empty now, the
|
||||
# 21st skills that replaced it are managed as externals, so the pack's
|
||||
# park/restore round-trip is what this covers on that side.
|
||||
# Hermetic: fixture repo via *_REPO_OVERRIDE + fake `claude` on PATH.
|
||||
set -u
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
@@ -10,13 +13,13 @@ check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1));
|
||||
|
||||
FX="$(mktemp -d)"; trap 'rm -rf "$FX"' EXIT
|
||||
mkdir -p "$FX/skills" "$FX/skills-disabled" "$FX/lib/profiles" "$FX/bin" \
|
||||
"$FX/skills-external/emil-design-eng" "$FX/skills-external/other-ext"
|
||||
"$FX/skills-external/emil-design-eng" "$FX/skills-external/other-ext" \
|
||||
"$FX/skills-external/21st-ui-build"
|
||||
for g in gs-a gs-b gs-c; do
|
||||
mkdir -p "$FX/skills-external/gstack/$g"
|
||||
touch "$FX/skills-external/gstack/$g/SKILL.md"
|
||||
done
|
||||
cp "$ROOT/lib/profile.sh" "$ROOT/lib/toggle-external.sh" "$FX/lib/"
|
||||
printf 'MAGIC_API_KEY=test-secret-000\n' > "$FX/.env"
|
||||
|
||||
# Non-managed external, enabled from the start — must never be touched.
|
||||
ln -s "$FX/skills-external/other-ext" "$FX/skills/other-ext"
|
||||
@@ -25,22 +28,18 @@ cat > "$FX/lib/profiles/designish.profile" <<'EOF'
|
||||
gs-a
|
||||
gs-b
|
||||
emil-design-eng external
|
||||
magic mcp
|
||||
21st-ui-build external
|
||||
EOF
|
||||
cat > "$FX/lib/profiles/backendish.profile" <<'EOF'
|
||||
gs-c
|
||||
EOF
|
||||
|
||||
# Fake claude: logs every call; keeps MCP registry state in a flat file.
|
||||
# Fake claude: logs every call. No MCP state to keep — MANAGED_MCPS is empty,
|
||||
# so `set` must never reach for `claude mcp` at all (asserted below).
|
||||
cat > "$FX/bin/claude" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
FX="$FX"
|
||||
echo "\$*" >> "\$FX/claude-calls.log"
|
||||
case "\$1 \${2:-}" in
|
||||
"mcp list") cat "\$FX/mcp-state" 2>/dev/null ;;
|
||||
"mcp add") echo "magic: stub" > "\$FX/mcp-state" ;;
|
||||
"mcp remove") : > "\$FX/mcp-state" ;;
|
||||
esac
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$FX/bin/claude"
|
||||
@@ -48,14 +47,14 @@ chmod +x "$FX/bin/claude"
|
||||
run() { PATH="$FX/bin:$PATH" PROFILE_REPO_OVERRIDE="$FX" \
|
||||
TOGGLE_EXTERNAL_REPO_OVERRIDE="$FX" bash "$FX/lib/profile.sh" "$@"; }
|
||||
|
||||
# --- set designish: gstack on-demand + external from-source + magic on ---
|
||||
# --- set designish: gstack on-demand + externals from-source (21st + emil) ---
|
||||
run set designish >/dev/null 2>&1
|
||||
check T1-gsa-on "$([ -e "$FX/skills/gs-a" ] && echo on || echo off)" on
|
||||
check T2-gsb-on "$([ -e "$FX/skills/gs-b" ] && echo on || echo off)" on
|
||||
check T3-gsc-off "$([ -e "$FX/skills/gs-c" ] && echo on || echo off)" off
|
||||
check T4-emil-src "$([ -L "$FX/skills/emil-design-eng" ] && echo on || echo off)" on
|
||||
check T5-magic-on "$(grep -c '^magic:' "$FX/mcp-state" 2>/dev/null)" 1
|
||||
check T6-add-call "$(grep -c '^mcp add magic' "$FX/claude-calls.log")" 1
|
||||
check T5-21st-src "$([ -L "$FX/skills/21st-ui-build" ] && echo on || echo off)" on
|
||||
check T6-no-mcp "$(grep -c '^mcp ' "$FX/claude-calls.log" || true)" 0
|
||||
|
||||
# --- set backendish: managed leftovers parked/unregistered ---
|
||||
run set backendish >/dev/null 2>&1
|
||||
@@ -63,14 +62,15 @@ check T7-gsc-on "$([ -e "$FX/skills/gs-c" ] && echo on || echo off)" on
|
||||
check T8-gsa-park "$([ -e "$FX/skills-disabled/gstack__gs-a" ] && echo p || echo n)" p
|
||||
check T9-emil-off "$([ -e "$FX/skills/emil-design-eng" ] && echo on || echo off)" off
|
||||
check T10-emil-park "$([ -e "$FX/skills-disabled/emil-design-eng" ] && echo p || echo n)" p
|
||||
check T11-magic-off "$(grep -c '^magic:' "$FX/mcp-state" 2>/dev/null || true)" 0
|
||||
check T12-rm-call "$(grep -c '^mcp remove magic' "$FX/claude-calls.log")" 1
|
||||
check T11-21st-off "$([ -e "$FX/skills/21st-ui-build" ] && echo on || echo off)" off
|
||||
check T12-21st-park "$([ -e "$FX/skills-disabled/21st-ui-build" ] && echo p || echo n)" p
|
||||
check T13-other-untouched "$([ -e "$FX/skills/other-ext" ] && echo on || echo off)" on
|
||||
|
||||
# --- back to designish: parked external restored (not re-sourced) ---
|
||||
run set designish >/dev/null 2>&1
|
||||
check T14-emil-back "$([ -e "$FX/skills/emil-design-eng" ] && echo on || echo off)" on
|
||||
check T15-park-gone "$([ -e "$FX/skills-disabled/emil-design-eng" ] && echo p || echo n)" n
|
||||
check T16-magic-back "$(grep -c '^magic:' "$FX/mcp-state" 2>/dev/null)" 1
|
||||
check T16-21st-back "$([ -e "$FX/skills/21st-ui-build" ] && echo on || echo off)" on
|
||||
check T17-no-mcp-ever "$(grep -c '^mcp ' "$FX/claude-calls.log" || true)" 0
|
||||
|
||||
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
|
||||
|
||||
+61
-41
@@ -8,7 +8,7 @@
|
||||
# as symlinks inside skills/. This script moves those symlinks
|
||||
# to/from skills-disabled/ so Claude Code stops/starts scanning them.
|
||||
#
|
||||
# MCP servers are toggled via `claude mcp add|remove` (not symlinks).
|
||||
# A multi-skill pack (gstack, 21st) toggles all of its skills at once.
|
||||
#
|
||||
# Usage:
|
||||
# toggle-external.sh list
|
||||
@@ -20,7 +20,7 @@
|
||||
# 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
|
||||
# magic — 21st-dev Magic MCP server (API key in .env)
|
||||
# 21st — 21st.dev skill pack (needs the `21st` CLI + login)
|
||||
#
|
||||
# For fine-grained activation (only design skills, only qa skills, only
|
||||
# audit skills, etc.) instead of all-or-nothing gstack toggling, use:
|
||||
@@ -40,17 +40,17 @@ 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 magic)
|
||||
MANAGED_TOOLS=(gstack emil-design-eng darwin-skill 21st)
|
||||
|
||||
# Load MAGIC_API_KEY (and any other secrets) from $REPO/.env if present.
|
||||
# Called only by the magic branch — other tools don't need env vars.
|
||||
load_env() {
|
||||
if [ -z "${MAGIC_API_KEY:-}" ] && [ -f "$REPO/.env" ]; then
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
source "$REPO/.env"
|
||||
set +a
|
||||
fi
|
||||
# Prints the skill names that belong to the "21st" pack. Source of truth:
|
||||
# skills-external/21st-* — the `21st skills install` run in install-plugins.sh
|
||||
# owns that list, so adding a skill upstream needs no edit here.
|
||||
twentyfirst_skills() {
|
||||
local d
|
||||
for d in "$REPO"/skills-external/21st-*/; do
|
||||
[ -f "${d}SKILL.md" ] || continue
|
||||
basename "$d"
|
||||
done
|
||||
}
|
||||
|
||||
# Prints the names (directory basenames) that belong to "gstack".
|
||||
@@ -84,13 +84,13 @@ status_tool() {
|
||||
[ -d "$HOME/.agents/skills/$tool" ] || { echo "missing"; return; }
|
||||
[ -e "$SKILLS_DIR/$tool" ] && echo "enabled" || echo "disabled"
|
||||
;;
|
||||
magic)
|
||||
command -v claude >/dev/null || { echo "missing"; return; }
|
||||
if claude mcp list 2>/dev/null | grep -q '^magic:'; then
|
||||
echo "enabled"
|
||||
else
|
||||
echo "disabled"
|
||||
fi
|
||||
21st)
|
||||
local installed=0
|
||||
while read -r name; do
|
||||
installed=1
|
||||
[ -e "$SKILLS_DIR/$name" ] && { echo "enabled"; return; }
|
||||
done < <(twentyfirst_skills)
|
||||
[ "$installed" -eq 1 ] && echo "disabled" || echo "missing"
|
||||
;;
|
||||
*)
|
||||
echo "unknown"; return 1 ;;
|
||||
@@ -124,12 +124,20 @@ disable_tool() {
|
||||
warn "$tool already disabled"
|
||||
fi
|
||||
;;
|
||||
magic)
|
||||
if [ "$(status_tool magic)" = "enabled" ]; then
|
||||
claude mcp remove magic -s user >/dev/null
|
||||
ok "magic disabled"
|
||||
21st)
|
||||
# Parked under the plain skill name — same convention as the other
|
||||
# externals, so profile.sh's park/restore path stays interoperable.
|
||||
local parked=0
|
||||
while read -r name; do
|
||||
[ -e "$SKILLS_DIR/$name" ] || continue
|
||||
rm -rf "${DISABLED_DIR:?}/${name:?}"
|
||||
mv "$SKILLS_DIR/$name" "$DISABLED_DIR/$name"
|
||||
parked=$((parked + 1))
|
||||
done < <(twentyfirst_skills)
|
||||
if [ "$parked" -gt 0 ]; then
|
||||
ok "21st disabled ($parked skills parked)"
|
||||
else
|
||||
warn "magic already disabled"
|
||||
warn "21st already disabled"
|
||||
fi
|
||||
;;
|
||||
*) err "Unknown tool: $tool"; return 1 ;;
|
||||
@@ -177,25 +185,37 @@ enable_tool() {
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
magic)
|
||||
load_env
|
||||
if [ -z "${MAGIC_API_KEY:-}" ]; then
|
||||
err "MAGIC_API_KEY not set — add it to ~/.claude/.env (template: .env.example)"
|
||||
return 1
|
||||
fi
|
||||
if [ "$(status_tool magic)" = "enabled" ]; then
|
||||
warn "magic already enabled"
|
||||
21st)
|
||||
local restored=0 linked=0
|
||||
while read -r name; do
|
||||
if [ -e "$DISABLED_DIR/$name" ]; then
|
||||
rm -rf "${SKILLS_DIR:?}/${name:?}"
|
||||
mv "$DISABLED_DIR/$name" "$SKILLS_DIR/$name"
|
||||
restored=$((restored + 1))
|
||||
elif [ -e "$SKILLS_DIR/$name" ]; then
|
||||
: # already enabled
|
||||
else
|
||||
ln -sf "$REPO/skills-external/$name" "$SKILLS_DIR/$name"
|
||||
linked=$((linked + 1))
|
||||
fi
|
||||
done < <(twentyfirst_skills)
|
||||
if [ "$((restored + linked))" -eq 0 ]; then
|
||||
if [ "$(status_tool 21st)" = "missing" ]; then
|
||||
err "21st pack not installed in $REPO/skills-external — run: make plugin"
|
||||
return 1
|
||||
fi
|
||||
warn "21st already enabled"
|
||||
return 0
|
||||
fi
|
||||
# Reference, not value: Claude Code expands ${VAR} in mcpServers.env at
|
||||
# launch (job7/BDR-026) — MAGIC_API_KEY itself never lands in
|
||||
# ~/.claude.json. The check above still confirms the var IS set in
|
||||
# ~/.claude/.env before wiring the reference, so a missing key fails
|
||||
# here instead of silently at Claude Code startup.
|
||||
claude mcp add magic --scope user \
|
||||
--env 'API_KEY=${MAGIC_API_KEY}' \
|
||||
-- npx -y @21st-dev/magic@latest
|
||||
ok "magic enabled (user scope)"
|
||||
ok "21st enabled ($((restored + linked)) skills: $restored restored, $linked linked)"
|
||||
# The skills shell out to the CLI; without it (or without a session)
|
||||
# they can only report failure. Warn, never block — the pack is still
|
||||
# correctly wired and `make plugin` installs the CLI.
|
||||
if ! command -v 21st >/dev/null 2>&1; then
|
||||
warn "the \`21st\` CLI is not on PATH — install it: npm i -g @21st-dev/cli"
|
||||
elif ! 21st whoami 2>/dev/null | grep -q '^Logged in as '; then
|
||||
warn "not signed in to 21st — component retrieval and 21st AI need: 21st login"
|
||||
fi
|
||||
;;
|
||||
*) err "Unknown tool: $tool"; return 1 ;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user