From 7c05f75eab1d6950784da92f0eed138f0981a095 Mon Sep 17 00:00:00 2001 From: bastien Date: Tue, 22 Sep 2026 02:53:31 +0000 Subject: [PATCH] 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. --- .claude/memory/decisions.md | 10 +++ .claude/memory/journal.md | 8 ++ .claude/memory/learnings.md | 9 ++ .claude/tasks/TODO.md | 42 +++++++++ .env.example | 6 +- .gitignore | 13 +++ .gitleaks.toml | 5 +- CHANGELOG.md | 36 ++++++++ CLAUDE.global.md | 10 ++- README.md | 66 +++++++++----- agents/plugin-advisor.md | 4 +- install-plugins.sh | 122 +++++++++++++++++++------- lib/design-gate.md | 25 +++--- lib/design-tool-gate.sh | 43 +++++++-- lib/profile.sh | 37 ++++---- lib/profiles/design.profile | 17 +++- lib/profiles/full.profile | 7 +- lib/profiles/web-full.profile | 9 +- lib/profiles/web.profile | 12 ++- lib/tests/profile-set-managed.test.sh | 32 +++---- lib/toggle-external.sh | 102 ++++++++++++--------- link.sh | 2 - plugins.lock.json | 5 ++ settings.json | 9 +- skills/profile/SKILL.md | 29 +++--- update-all.sh | 50 ++++++++++- 26 files changed, 515 insertions(+), 195 deletions(-) diff --git a/.claude/memory/decisions.md b/.claude/memory/decisions.md index 2cfd963..cf542b8 100644 --- a/.claude/memory/decisions.md +++ b/.claude/memory/decisions.md @@ -102,6 +102,7 @@ rules: | BDR-090 | 2026-09-15 | Destructive shell work → autoMode soft_deny/hard_deny; `ask` tier abandoned (inert under auto) | accepted | | BDR-091 | 2026-09-16 | Ask, don't guess: open-choice sweep (3 classes) at plan step + mid-run CLASS channel supersede "one question upfront" | accepted | | BDR-092 | 2026-09-16 | docker + node framed by the classifier via autoMode.allow + soft_deny; ask entries retired | accepted | +| BDR-093 | 2026-09-22 | 21st.dev: magic MCP → CLI + skill pack; staged install past the ~/.claude/skills symlink; CLI = gate's required-manual | accepted | --- @@ -1178,3 +1179,12 @@ Branch feature/user-writing-web-rules, UNMERGED (human gate). - **Guardrail**: S6 (loosening = user's own edit) overridden explicitly by the user for this change; diff reviewed on the branch before merge. - **Status**: accepted. Verified: `jq` valid; `claude auto-mode config` shows the 4 entries with `$defaults` expanded; `doctor.sh` autoMode PASS; live `docker exec -i supabase_db_game psql … -f - < verify/0043 … | tail` → `ROLLBACK`, exit 0, no prompt. `claude auto-mode critique` printed nothing (2.1.273). - **Reference**: `settings.json`, `templates/settings/SETTINGS.md` (`autoMode.allow` row + interpreter note), commit `5eccc3f`, merge `ddadca6`. Links [[BDR-090]], [[LRN-153]], [[LRN-155]], [[LRN-156]]. + +## BDR-093 — 21st.dev: magic MCP retired for the `21st` CLI + skill pack +- **Date**: 2026-09-22 +- **Decision**: `@21st-dev/magic` MCP out, `@21st-dev/cli` (bin `21st`) in — upstream supersedes it (README 1.17.1: "one unified CLI", old magic config now a thin proxy to the same endpoint). Auth = `21st login`, browser token in `~/.config/21st`; no API key, no MCP process. `install-plugins.sh` Step 8.7: `npm i -g` (pin `21st` in plugins.lock.json) + staged `21st skills install --global --agent claude` + TTY-only login offer + pack disabled by default. `toggle-external.sh` manages `21st` as a pack (names globbed from `skills-external/21st-*`, parked under plain names = interoperable with profile.sh's external path). 5 design skills (`21st-ui-build`, `-ui-explore`, `-ui-review`, `-cli-use`, `-ai`) in design/web/web-full/full + `MANAGED_EXTERNALS`; `-registry`/`-design-sync` installed, parked. `MANAGED_MCPS` now empty (mcp type kept, advisory). Gate: `GATE-BLOCK: 21st 21st-ui-build` — CLI = required-manual class, `magic`'s old slot. Outward-facing verbs (`publish*`, `submit`, `edit`, `delete`, `remove-from-catalog`, `profile set|upload`) → one `autoMode.soft_deny` entry, NOT `ask` ([[LRN-153]]). +- **Why**: user ask ("plus besoin de mcp / api, juste en cli"), confirmed at the source, not the marketing page — the 21st.dev web docs still show the MCP `init --client` flow and an API key; the npm package README is what states the supersession. Net wins: one less MCP loaded per session, no API key to protect by reference ([[BDR-026]]/[[BDR-057]] vector gone), no unauthenticated local callback server ([[LRN-110]] gone with the tool). +- **Blocker + shape it forced**: `21st skills install --global` writes `/.claude/skills//SKILL.md` and calls `assertNoSymlinkComponents` on every path segment — `~/.claude/skills` IS a symlink to `repo/skills`, so the documented `21st install-skill` fails hard ("Refusing to access symbolic link …/.claude/skills", reproduced live). → install under `mktemp -d` as HOME, move each skill into `skills-external/21st-*` (gitignored), symlink on demand. Same impeccable/ctx7 machine-owned pattern. +- **Alternatives rejected**: project-scope install (`/.claude/skills`) — Claude Code would ALSO scan it as project skills in this repo = every 21st skill listed twice; add the pack to `link.sh`'s `EXTERNAL_SKILLS` — that loop force-creates symlinks, resurrecting a default-disabled pack on every `make link`; all 7 skills in the design profiles — publishing flows cost 2 descriptions/session for a workflow the user does not run; `ask` entries for the publish verbs — inert under auto ([[LRN-153]], [[BDR-090]]/[[BDR-092]] already retired that tier). +- **Status**: accepted. Verified: toggle enable/disable/restore/idempotent round-trip (7 skills), `profile.sh show design`, gate INCOMPLETE→names `21st` with the two commands, gate PATH repair proven under `env -i PATH=/usr/bin:/bin` with an nvm-stub, `profile-set-managed.test.sh` 17/17, `make test` (2 pre-existing FAILs, gitleaks binary absent on this host), shellcheck clean. OPEN for the user: `npm i -g @21st-dev/cli && 21st login` — the deny rule `Bash(npm install -g *)` means the agent cannot run it. +- **Reference**: `install-plugins.sh` Step 8.7, `update-all.sh` 7.4, `lib/toggle-external.sh`, `lib/profile.sh`, `lib/profiles/*.profile`, `lib/design-tool-gate.sh`, `lib/design-gate.md`, `CLAUDE.global.md`, `README.md`, `settings.json`, `.gitignore`, `.gitleaks.toml`, `.env.example`, `link.sh`. Supersedes the operative parts of [[BDR-059]] (the 4 `mcp__magic__*` ask entries) and the magic instance of [[BDR-026]]/[[BDR-057]]; [[BDR-025]]'s required-manual class stands, its magic example does not. Links [[LRN-158]], [[LRN-110]]. diff --git a/.claude/memory/journal.md b/.claude/memory/journal.md index 481c517..50a265e 100644 --- a/.claude/memory/journal.md +++ b/.claude/memory/journal.md @@ -476,3 +476,11 @@ rules: - Ask, don't guess ([[BDR-091]]): spec + plan, 9 lock-first tasks (contract-interview CLARIFY two passes, MID-RUN CLARIFICATION with `CLASS:` tag, HOW TO ASK; global rule; feat / bugfix / hotfix / ship-feature / init-project wired; interviewer; 3 executors), suite green. Behavioral fixture check still open ([[LRN-157]]). - docker + node under auto mode ([[BDR-092]]): `ask` entries retired (inert on 2.1.273, probe — [[LRN-155]]), `autoMode.allow` + 2 soft_deny, live `docker exec … psql` OK. Static interpreter allow is suspended under auto → prose only ([[LRN-156]]). - Both merged into develop 2026-09-17 via gitflow (`ddadca6`, `56bd035`), two stack conflicts (TODO, CHANGELOG) resolved keeping both blocks. Symlinked `settings.json` follows the checkout: live config = whatever branch is out. + +## 2026-09-22 + +- 21st.dev magic MCP → `@21st-dev/cli` + 7-skill pack, user ask. Install/update/toggle/profiles/gate/docs/permissions migrated on `feature/21st-cli-migration`. +- Blocker: documented `21st install-skill` refuses the `~/.claude/skills` symlink → staged install under a throwaway HOME (LRN-158). +- Gate: `magic`+MAGIC_API_KEY required-manual slot → the `21st` CLI; publish verbs moved to `autoMode.soft_deny` (ask inert under auto). +- BDR-093, LRN-158. `make test` green except 2 pre-existing gitflow FAILs (gitleaks binary absent on this host). Branch UNMERGED — human gate. + diff --git a/.claude/memory/learnings.md b/.claude/memory/learnings.md index 67e1cd9..321f024 100644 --- a/.claude/memory/learnings.md +++ b/.claude/memory/learnings.md @@ -147,6 +147,7 @@ rules: | LRN-155 | 2026-09-16 | ask under auto: doc says prompt, probe on 2.1.273 says no; re-probe after upgrades | any permission-tier reasoning | | LRN-156 | 2026-09-16 | autoMode.allow = exception tier; static interpreter allow suspended under auto → conditions live in prose | conditional permissions | | LRN-157 | 2026-09-16 | gap-only trigger blind to taste → add a trigger class, not budget; ask at plan, mid-run for leftovers | any "ask more" request | +| LRN-158 | 2026-09-22 | Installer refusing symlinked paths vs a symlinked config dir → stage under a throwaway HOME, move the result | any vendor installer writing into ~/.claude or ~/.config | --- @@ -1490,3 +1491,11 @@ Rule: when editing a doctrine file under structure locks, grep the test's lock s - **Pattern**: a trigger that fires only on missing outcome / scope / constraints lets every taste choice through — "add a share icon" is complete by those criteria and the icon's side is decided downstream. More budget changes nothing; the fix is a new trigger class (VISIBLE / PUBLIC NAME / SCOPE). Cost geometry: a fresh re-dispatch keeps the working tree and loses the executor's reasoning → the same question costs about one executor run more mid-run than at PLAN. So: sweep once at the plan step, keep the mid-run channel for leftovers. Executor tags the class; orchestrator re-reads it (tag = hint, a mis-tag would offload class 4 onto the human). Relayed questions obey [[LRN-102]]: context inside `AskUserQuestion`, nothing the user needs printed before it. - **Future application**: any "ask more" request → check WHICH trigger is blind before touching a quota. Any orchestrator with a "decide it yourself" fallback on an executor halt → route by class first. - **Reference**: [[BDR-091]], `lib/contract-interview.md` STEP 2 + MID-RUN CLARIFICATION. + +## LRN-158 — A hardened installer + a symlinked config dir = documented command fails; stage under a throwaway HOME +- **Date**: 2026-09-22 +- **Context**: `21st install-skill` (= `21st skills install --global`) is upstream's documented one-liner. Here it dies: `Refusing to access symbolic link /home/…/.claude/skills`. The installer walks every segment of `/.claude/skills//SKILL.md` with an `assertNoSymlinkComponents` guard (anti symlink-escape); this repo's whole model is `~/.claude/skills -> repo/skills`. Two correct designs, mutually exclusive on the same path. +- **Pattern**: don't fight the guard and don't unlink the config dir. Run the installer with `HOME=$(mktemp -d)` so it writes into a pristine real tree, then move the output to the vendored dir the repo controls and symlink from there. Same shape as the impeccable/ctx7 staging (`mktemp -d`, install, `mv` into `skills-external/`), with HOME as the extra lever. Two conditions make it safe: the command must need nothing else from HOME (checked: manifest + content fetch are unauthenticated, hash-verified), and the moved payload must be self-contained. +- **Also**: read the npm tarball, not the vendor's web page. 21st.dev's `/mcp` and `/llms.txt` still document the MCP `init --client` flow with an API key; the package README states the CLI supersedes it. `curl registry.npmjs.org/` + untar + read `README.md`/`dist` answered every question (commands, exit codes, where files land) that the site got wrong. +- **Future application**: any vendor installer that writes into `~/.claude`, `~/.config` or `~/.agents` on this machine. Probe first with a fake HOME containing the symlink, before wiring it into `install-plugins.sh` — the failure is instant and unambiguous. +- **Reference**: [[BDR-093]], `install-plugins.sh` Step 8.7, `update-all.sh` 7.4. Links [[LRN-034]] (run the real thing), [[BLK-014]]-class symlink/self-heal issues. diff --git a/.claude/tasks/TODO.md b/.claude/tasks/TODO.md index 58c527a..c0329ca 100644 --- a/.claude/tasks/TODO.md +++ b/.claude/tasks/TODO.md @@ -1,5 +1,47 @@ # TODO +## 2026-09-22 — 21st: magic MCP → CLI + skills (feature/21st-cli-migration) +User: "remplacer pour 21st, il n'y a plus besoin de mcp / api, mais juste en +cli". Upstream confirmed (`@21st-dev/cli` 1.17.1 README): the CLI supersedes +`@21st-dev/magic`; auth is `21st login` (browser token in `~/.config/21st`), +no API key; `21st install-skill` = alias of `21st skills install --global`. +Gates answered by user: 5 design skills in profiles (registry + design-sync +parked), `make plugin` auto-installs the CLI + offers login on TTY only, +missing `21st` CLI trips the design gate (magic's old required-manual slot). + +Blocker found + solved: `21st skills install --global` REFUSES to write +through a symlinked path (`assertNoSymlinkComponents`), and `~/.claude/skills` +IS a symlink → repo/skills. Verified live: "Refusing to access symbolic link +…/.claude/skills". → install into a staged HOME (mktemp), move each skill to +`skills-external/21st-*/` (impeccable pattern), symlink from there. + +- [x] T1 install-plugins.sh STEP 8.7: magic block → 21st CLI (`npm i -g`, + pinned via plugins.lock.json) + staged `skills install` → + skills-external/21st-*, TTY-gated `21st login`, pack disabled by default. +- [x] T2 lib/toggle-external.sh: managed tool `magic` (mcp) → `21st` (skill + pack, glob-derived from skills-external/21st-*), drop load_env. +- [x] T3 profiles + profile.sh: `magic mcp` → 5 externals + `21st cli` in + design/web/web-full/full; GATE-BLOCK `21st 21st-ui-build`; + MANAGED_EXTERNALS += the 5; MANAGED_MCPS emptied (kept as a live + allowlist, mcp type machinery stays generic). +- [x] T4 lib/design-tool-gate.sh + lib/design-gate.md: manual-step hint + magic/MAGIC_API_KEY → 21st/`npm i -g` + `21st login`; PATH repair + extended to the npm-global bin dir (21st lives in nvm's bin, the + existing repair only fires when `claude` itself is unresolvable). +- [x] T5 doctrine + docs: CLAUDE.global.md design toolchain, README (drop the + magic callback-injection section + the MCP env-var worked example), + .env.example, link.sh MAGIC_API_KEY warning, .gitleaks.toml allowlist, + update-all.sh, .gitignore, settings.json (drop 4 mcp__magic__*; the + outward-facing verbs landed in autoMode.soft_deny, NOT ask — LRN-153 + says ask is inert under auto mode). +- [x] T6 lib/tests/profile-set-managed.test.sh retargeted (mcp fixture → 21st + external pack), `make test` + shellcheck green. +- [x] T7 CHANGELOG + BDR-093 + LRN-158 + journal. Also cleaned along the way: + dead `magic` branches in profile.sh enable/disable_skill, + skills/profile/SKILL.md. OPEN for the user: `npm i -g @21st-dev/cli` + then `21st login` (`Bash(npm install -g *)` is denied to the agent). + Branch UNMERGED — human gate. + ## 2026-09-17 — /deploy hand-back: one-line commands + post-deploy test list (feature/deploy-oneline-tests) User: commands in the /deploy checklist arrive broken across lines (cannot copy-paste), and the hand-back stops at the deploy steps — wants, after the diff --git a/.env.example b/.env.example index 91fbe99..d3eea7c 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,9 @@ # Local secrets for Claude Code plugin install scripts. # Copy to ~/.claude/.env and fill in real values. link.sh symlinks repo/.env to it; the secret never enters git. # -# Used by: lib/toggle-external.sh enable|disable magic -# Get a key at: https://21st.dev/magic (dashboard → API keys) -MAGIC_API_KEY=your_21st_dev_magic_api_key_here +# 21st.dev needs nothing here since 2026-09-22: the Magic MCP server was +# replaced by the `21st` CLI, whose auth is `21st login` (browser token in +# ~/.config/21st). Any leftover MAGIC_API_KEY line is dead — delete it. # ── Google SEO data layer (lib/seo-data) — used by /seo FULL ── # OAuth Desktop client: GCP console → APIs & Services → Credentials → OAuth client (Desktop). diff --git a/.gitignore b/.gitignore index 2ea2b36..a5e13a8 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,12 @@ skills/impeccable # External skills installed via `npx skills add` — auto-created by link.sh skills/darwin-skill +# 21st.dev skill pack symlinks — created on demand by toggle-external.sh / +# profile.sh (the pack is DISABLED by default, so these usually don't exist). +# A glob, not one line per skill: the `21st skills install` manifest owns the +# membership, so the pack can gain a skill with no edit here. +skills/21st-* + # Context7 docs-lookup skill — installed by `ctx7 setup --claude --cli` # (install-plugins.sh Step 6, when absent) into ~/.claude/skills (a symlink to # this repo's skills/). ctx7-managed and re-created on demand — not vendored here. @@ -156,6 +162,13 @@ skills-external/frontend-design/ # an edit. The source is always re-fetched, so no offline copy is needed. skills-external/emil-design-eng/ +# 21st.dev skill pack — machine-owned: `21st skills install` output, staged by +# install-plugins.sh Step 8.7 (the installer refuses to write through the +# ~/.claude/skills symlink, so it runs under a throwaway HOME and the skills +# are moved here). Refreshed by update-all.sh. Not vendored: the CLI owns the +# layout and the content is sha256-verified against 21st.dev's manifest. +skills-external/21st-*/ + # Impeccable — machine-owned dist produced by `npx impeccable skills install` # (install-plugins.sh Step 8d, update-all.sh), pinned in plugins.lock.json. # Not vendored: the installer owns the layout and rewrites it on update diff --git a/.gitleaks.toml b/.gitleaks.toml index 7794295..27a5616 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -67,8 +67,9 @@ regexTarget = "line" regexes = [ '''X-Amz-Credential=AKIA[0-9A-Z]{16}''', '''private-user-images\.githubusercontent\.com/[^"]*\?jwt=''', - '''MAGIC_API_KEY=abc123''', - # magic MCP docs example — base64 of "the ..." ASCII sample text. + # Docs/test example — base64 of the "the ..." ASCII sample text, never a key. + # (The MAGIC_API_KEY=abc123 placeholder that sat here went with the magic + # MCP, removed 2026-09-22 when 21st.dev moved to a CLI with no API key.) '''clientKey = 'dGhlIH[A-Za-z0-9+/=]*'''', ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 3829c49..b839431 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,8 +26,35 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). classifier lists, `$defaults` splice semantics, `classifyAllShell`, the user-scope vs project-scope rule, and why `ask` is the wrong tier for a destructive command under auto mode. +- **21st.dev moved from an MCP server to a CLI.** `install-plugins.sh` Step 8.7 + installs `@21st-dev/cli` globally (pinned in `plugins.lock.json`), offers + `21st login` in an interactive terminal only, and stages the 7-skill pack + into `skills-external/21st-*`. `update-all.sh` refreshes both. The pack + ships disabled, same policy the MCP had. +- `lib/toggle-external.sh` manages `21st` as a skill pack (glob-derived from + `skills-external/21st-*`, parked under plain names so `profile.sh`'s + external park/restore stays interoperable). `magic` is gone from the + managed tools. +- The five design skills (`21st-ui-build`, `-ui-explore`, `-ui-review`, + `-cli-use`, `-ai`) are in the `design`, `web`, `web-full` and `full` + profiles and in `profile.sh`'s `MANAGED_EXTERNALS`; `21st-registry` and + `21st-design-sync` are installed but left parked. +- `autoMode.soft_deny` gains one entry for the outward-facing 21st verbs + (`publish*`, `submit`, `edit`, `delete`, `remove-from-catalog`, + `profile set|upload`) — publishing puts a component on a public listing. + That tier rather than `ask`, per LRN-153. ### Changed +- **Design gate: `magic` → the `21st` CLI in the required-manual slot.** + `design.profile`'s `GATE-BLOCK` now lists `21st` (CLI channel) and + `21st-ui-build` (the pack's canary on the skill channel); a missing CLI + trips the gate with `npm i -g @21st-dev/cli` + `21st login` instead of the + old `MAGIC_API_KEY` hint. `design-tool-gate.sh` also repairs `PATH` for the + npm global bin, whose absence in a hook's sanitized `PATH` would otherwise + read as "21st missing" (the existing repair only fired when `claude` itself + was unresolvable). +- `profile.sh`'s `MANAGED_MCPS` is empty: no MCP server is auto-toggled any + more. The `mcp` type stays supported for an advisory profile entry. - **`/deploy` hand-back: one physical line per command, then a post-deploy tests block.** Every command in the checklist is emitted on exactly one line, however long; a legacy `\` continuation in the runbook is joined at @@ -100,6 +127,15 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). `Bash(grep * .env*)` family: a `cat .env | sed` pipe still slips past, which is what the `hard_deny` exfiltration rule is there to catch. +### Removed +- **`magic` MCP (`@21st-dev/magic`) and `MAGIC_API_KEY`**, with the two risks + attached to them: the unauthenticated `127.0.0.1` callback server + `21st_magic_component_builder` opened (LRN-110) and the plaintext key copy + that `claude mcp add --env` wrote into `~/.claude.json` (BDR-026/057). Gone + with it: the 4 `mcp__magic__*` `permissions.ask` entries (BDR-059), the + `MAGIC_API_KEY` block in `.env.example`, `link.sh`'s missing-key warning, + and the dead `MAGIC_API_KEY=abc123` gitleaks allowlist regex. + ### Fixed - **`make update` no longer drops the Playwright OS-support bump** — a gstack submodule update used to leave the bump unapplied until the next diff --git a/CLAUDE.global.md b/CLAUDE.global.md index a51c3b6..d07aee7 100644 --- a/CLAUDE.global.md +++ b/CLAUDE.global.md @@ -290,16 +290,18 @@ OR a design/UI request — not the keyword "design" alone in a prompt. Single source for design routing; the design-toolchain hook reinforces it. - Trivial (≤2 files, one cosmetic value) → /hotfix, no toolchain. - Build UI (component, page, redesign) → ui-ux-pro-max + frontend-design - (anti-slop) + Magic MCP /ui + emil-design-eng (polish) + - design-motion-principles (if motion) + design-html (if static). + (anti-slop) + 21st-ui-build (catalog + generation) + emil-design-eng + (polish) + design-motion-principles (if motion) + design-html (if static). Post-build floor: `npx impeccable detect ` (45 deterministic anti-slop rules, exit 2 = findings) when impeccable installed. - Design system / brand → design-consultation first, then the build tools. - Review / audit → design-review + emil-design-eng + design-motion-principles - + /impeccable audit|critique (skill) + `impeccable detect` floor. + + 21st-ui-review + /impeccable audit|critique + `impeccable detect` floor. Scope doubt → don't silently skip: ask, or default to Build tier. Gate: lightweight skills run `~/.claude/lib/design-gate.md`; orchestrators via -plugin-check. Magic MCP costs API calls — generation, not micro-tweaks. +plugin-check. 21st = CLI (`npm i -g @21st-dev/cli`, `21st login`), no MCP, +no API key. Search is free; `21st get` and `21st generate` are metered — +generation, not micro-tweaks. ## graphify diff --git a/README.md b/README.md index bcb684b..1912e01 100644 --- a/README.md +++ b/README.md @@ -253,10 +253,9 @@ in `env`, `command`, `args`, `url`, and `headers` — for both project (`.mcp.js and user (`~/.claude.json`) scope. Use that instead of a literal value: ```bash -MAGIC_API_KEY= # single-quoted so bash doesn't expand it; Claude Code expands it at # launch, reading the var from its own process environment: -claude mcp add magic --scope user --env 'API_KEY=${MAGIC_API_KEY}' -- npx -y @21st-dev/magic@latest +claude mcp add --scope user --env 'API_KEY=${SOME_API_KEY}' -- ``` The var still has to exist in the **environment of the process that starts @@ -265,8 +264,12 @@ would defeat the point (every subprocess, every stray `env`/`printenv`, would then see it). This repo's `~/.bashrc` instead wraps the `claude` command itself: a `claude()` shell function sources `~/.claude/.env` into a subshell and `exec`s the real binary, so the var reaches `claude` and its children only -— never the ambient shell. See `lib/toggle-external.sh`'s `magic` case for -the pattern to copy for a new MCP server. +— never the ambient shell. + +This config currently registers no MCP server at all. The one it used to +carry, `@21st-dev/magic`, is gone: 21st.dev replaced it with a plain CLI (see +below), so there is no key left to protect by reference. The pattern stays +documented for the next MCP server that needs a secret. There is no `claude mcp add` flag that writes the reference form for you — the `${VAR}` syntax has to be typed by hand (or via a wrapper script), same as @@ -292,25 +295,44 @@ Then run the one-time consent flow: `make seo-connect` (per-label token store, multi-site safe). Missing credentials never break an audit — `/seo` degrades gracefully to anonymous PageSpeed lab data. -### magic MCP (`@21st-dev/magic`) — known callback-injection risk +### 21st.dev CLI (replaces the magic MCP) -`21st_magic_component_builder` opens an **unauthenticated** local callback -server (`127.0.0.1:9221+`, `Access-Control-Allow-Origin: *`, no token/origin -check) for up to 10 minutes per call; any local process or open browser tab -can `POST` to it and that body is injected **verbatim** into the tool result -the model consumes (job8 audit, `dist/utils/callback-server.js:36`). This is -in the third-party package's code, not this repo's config — **we don't patch -it**. The mitigation lives on our side: `settings.json` -`permissions.ask` explicitly lists all 4 `mcp__magic__*` tools. -Read that as a declared intent, not a proven hard gate: under -`defaultMode: auto` (this config's default) Bash `ask` rules were observed -auto-approving with no prompt raised (LRN-146). Whether MCP `ask` rules -behave the same has not been verified here, so re-check before relying on -it. `deny` is the only tier the auto-mode classifier cannot lift; for a -gate that holds under auto mode without banning the tool outright, the -right home is `autoMode.soft_deny`. Don't allowlist -`21st_magic_component_builder` or `21st_magic_component_refiner` (arbitrary -absolute-path read → vendor exfil, same audit) under any circumstance. +`@21st-dev/cli` (bin `21st`) supersedes the `@21st-dev/magic` MCP server that +this config used to register. Same endpoint, one browser login, no API key, +and nothing loaded into a session that isn't using it: + +```bash +npm i -g @21st-dev/cli +21st login # browser flow, token saved in ~/.config/21st +``` + +`make plugin` does both (Step 8.7 installs the CLI, then offers the login in +an interactive terminal) and installs the skill pack that drives it: +`21st-ui-build`, `-ui-explore`, `-ui-review`, `-cli-use`, `-ai`, plus the two +publishing skills `-registry` and `-design-sync`. The pack is disabled by +default, the same policy the MCP had. `/profile design` turns on the five +design skills; `bash lib/toggle-external.sh enable 21st` turns on all seven. + +The pack is machine-owned and gitignored. It cannot be installed the way +upstream documents it (`21st install-skill`, i.e. `21st skills install +--global`): that writes into `~/.claude/skills/`, and the installer refuses to +follow a symlink anywhere on that path, while `~/.claude/skills` is itself a +symlink to this repo's `skills/`. So the install runs under a throwaway `HOME` +and the result is moved into `skills-external/21st-*`, where +`toggle-external.sh` and `profile.sh` symlink it in on demand. + +Two risks from the MCP era go away with it. The unauthenticated local callback +server `21st_magic_component_builder` opened (`127.0.0.1:9221+`, CORS `*`, a +10-minute local prompt-injection window, job8 audit / LRN-110). And the API +key that `claude mcp add --env` materialized into `~/.claude.json`. + +The permission gate is now one `autoMode.soft_deny` entry covering the +outward-facing verbs (`21st publish*`, `submit`, `edit`, `delete`, +`remove-from-catalog`, `profile set|upload`), because publishing a component +puts it on a public listing under your account. That tier rather than `ask`: +under `defaultMode: auto` (this config's default) `ask` rules were observed +auto-approving with no prompt raised (LRN-153), so an `ask` entry would have +declared an intent without gating anything. --- diff --git a/agents/plugin-advisor.md b/agents/plugin-advisor.md index a7cd0df..5388a5d 100644 --- a/agents/plugin-advisor.md +++ b/agents/plugin-advisor.md @@ -292,8 +292,8 @@ activate a curated subset of skills + plugins + MCPs and disable the rest of gstack + managed plugins — sessions stay focused and passive token cost drops. `profile set ` actually toggles plugins (`claude plugin enable|disable`) -and MCPs (delegates to `lib/toggle-external.sh` for `magic`) — not just -advisory. Always-on plugins (`security-guidance`, `superpowers`) +and external skill packs (delegates to `lib/toggle-external.sh`) — not just +advisory. No MCP server is auto-toggled today. Always-on plugins (`security-guidance`, `superpowers`) are protected. Managed plugins that `set` may toggle: `ui-ux-pro-max@ui-ux-pro-max-skill`, `plugin-dev@claude-code-plugins`, `pr-review-toolkit@claude-code-plugins`. Other plugins are never auto-toggled. diff --git a/install-plugins.sh b/install-plugins.sh index e599fef..fa6e51c 100644 --- a/install-plugins.sh +++ b/install-plugins.sh @@ -892,42 +892,104 @@ done echo "" # ============================================================ -# STEP 8.7 — MAGIC MCP (21st-dev) — installed but DISABLED by default +# STEP 8.7 — 21ST.DEV CLI + SKILL PACK — installed but DISABLED by default # ============================================================ -# Magic MCP is a stdio MCP server providing UI component generation -# from 21st.dev. Toggled via lib/toggle-external.sh (same interface as -# gstack, emil-design-eng, etc.). Registered in Claude Code user scope. +# `@21st-dev/cli` (bin `21st`) supersedes the `@21st-dev/magic` MCP server: +# same endpoint, one browser login (`21st login`, token in ~/.config/21st), +# no API key, no MCP process loaded into every session. It ships a pack of +# verified skills (21st-ui-build / -explore / -review / -cli-use / -ai / +# -registry / -design-sync) that drive the CLI from Claude Code. # -# Default policy: DISABLED at install time. Rationale: MCP tools load -# into every Claude Code session and consume context tokens. Enable -# only when you're actively using Magic. +# Machine-owned dist (impeccable pattern): `21st skills install` writes to +# /.claude/skills// and REFUSES to follow a symlink anywhere on +# that path — and ~/.claude/skills IS a symlink to this repo's skills/. So +# install under a staged HOME, then move each skill into skills-external/ +# (gitignored), where toggle-external.sh / profile.sh symlink it in. # -# API key: read from $REPO/.env (MAGIC_API_KEY=...) — NEVER committed. -# Template: $REPO/.env.example. Get a key at https://21st.dev/magic -echo "── Step 8.7: Magic MCP (21st-dev) ──────────────────────────" +# Default policy: pack DISABLED at install time — every skill description +# loads into every session. Enable on demand: +# bash lib/toggle-external.sh enable 21st (whole pack) +# /profile design (the 5 design skills) +echo "── Step 8.7: 21st.dev CLI + skill pack ─────────────────────" echo "" -if [ -x "$REPO/lib/toggle-external.sh" ]; then - MAGIC_STATUS="$(bash "$REPO/lib/toggle-external.sh" status magic 2>/dev/null || echo missing)" - if [ "$MAGIC_STATUS" = "enabled" ]; then - info "Disabling magic MCP by default (enable on demand)..." - bash "$REPO/lib/toggle-external.sh" disable magic >/dev/null - ok "magic MCP disabled — enable with: bash lib/toggle-external.sh enable magic" +if command -v 21st &>/dev/null; then + ok "21st CLI already installed" +else + TFD_VER=$(pinned_version "21st") + if [ "$TFD_VER" != "latest" ]; then + info "Installing @21st-dev/cli@${TFD_VER} (pinned in plugins.lock.json)..." + npm install -g "@21st-dev/cli@${TFD_VER}" else - ok "magic MCP disabled (default)" + info "Installing @21st-dev/cli@latest (consider pinning in plugins.lock.json)..." + npm install -g @21st-dev/cli fi - # The key lives in ~/.claude/.env (canonical, BDR-026), reached via the - # repo/.env symlink that toggle-external.sh sources. Self-heal the common - # fresh-machine case: ~/.claude/.env was created AFTER link.sh ran, so the - # symlink is missing and the key looks absent though it's set. - HOME_ENV="$HOME/.claude/.env" - if [ ! -e "$REPO/.env" ] && [ -f "$HOME_ENV" ]; then - ln -sf "$HOME_ENV" "$REPO/.env" 2>/dev/null \ - && info "Linked repo/.env → ~/.claude/.env (was missing)" + if command -v 21st &>/dev/null; then + ok "21st CLI installed" + else + err "21st CLI install failed — run manually: npm install -g @21st-dev/cli" fi - # Tolerate optional `export ` and leading whitespace; require a value. - MAGIC_KEY_RE='^[[:space:]]*(export[[:space:]]+)?MAGIC_API_KEY=.' - if [ ! -f "$REPO/.env" ] || ! grep -qE "$MAGIC_KEY_RE" "$REPO/.env" 2>/dev/null; then - warn "MAGIC_API_KEY not set in ~/.claude/.env — add it (and run 'make link') before enabling magic" +fi + +# Skill pack — staged install, then moved under skills-external/. +if command -v 21st &>/dev/null; then + TFD_STAGE=$(mktemp -d) + if HOME="$TFD_STAGE" 21st skills install --global --agent claude >/dev/null 2>&1; then + TFD_N=0 + for _tfd in "$TFD_STAGE"/.claude/skills/*/; do + [ -f "${_tfd}SKILL.md" ] || continue + _tfd_name=$(basename "$_tfd") + rm -rf "${REPO:?}/skills-external/${_tfd_name:?}" + mv "$_tfd" "$REPO/skills-external/$_tfd_name" + TFD_N=$((TFD_N + 1)) + done + if [ "$TFD_N" -gt 0 ]; then + ok "21st skill pack synced to skills-external/ ($TFD_N skills)" + else + warn "21st skills install ran but produced no SKILL.md — layout changed? Inspect: 21st skills install --global --agent claude" + fi + elif [ -f "$REPO/skills-external/21st-ui-build/SKILL.md" ]; then + ok "21st skill pack already present (refresh failed — existing copy kept)" + else + warn "21st skill pack install failed — run manually: 21st skills install --global --agent claude" + fi + rm -rf "$TFD_STAGE" +fi + +# Auth — detect, then offer login ONLY in an interactive TTY. A non-interactive +# run (CI / headless / re-run) must never open a browser or block on OAuth. +# Search and logo lookup are free; retrieving component code and 21st AI need +# the session. Mirrors the ctx7 auth block (Step 6). +if command -v 21st &>/dev/null; then + # `whoami` is a local token read (no network): "Logged in as (saved …)." + TFD_WHO="$(21st whoami 2>/dev/null | head -1)" + if [[ "$TFD_WHO" == "Logged in as "* ]]; then + ok "21st: ${TFD_WHO%.}" + elif [ -t 0 ] && [ -t 1 ]; then + printf '%b' "${BLUE}→${NC} Sign in to 21st now? (opens a browser) [y/N] " + read -r tfd_ans || tfd_ans="" + if [[ "$tfd_ans" =~ ^[Yy]([Ee][Ss])?$ ]]; then + if 21st login; then + ok "21st authenticated" + else + warn "21st login did not finish — re-run '21st login' anytime" + fi + else + info "Skipped — sign in later with: 21st login" + fi + else + info "Not signed in. Component retrieval and 21st AI need: 21st login" + fi +fi + +# Default-disabled, same policy as before the MCP→CLI move. +if [ -x "$REPO/lib/toggle-external.sh" ]; then + TFD_STATUS="$(bash "$REPO/lib/toggle-external.sh" status 21st 2>/dev/null || echo missing)" + if [ "$TFD_STATUS" = "enabled" ]; then + info "Disabling the 21st skill pack by default (enable on demand)..." + bash "$REPO/lib/toggle-external.sh" disable 21st >/dev/null + ok "21st skill pack disabled — enable with: bash lib/toggle-external.sh enable 21st" + else + ok "21st skill pack disabled (default)" fi else warn "lib/toggle-external.sh not found or not executable — skipping" @@ -1040,7 +1102,7 @@ echo " 🔄 frontend-design — distinctive frontend interfaces, anti-AI- echo " 🔄 impeccable — /impeccable design verbs + 45-rule deterministic detector (npx impeccable detect)" echo " 🔄 design-motion-principles — motion/animation design, 3-designer lens (kylezantos)" echo " 🔄 darwin-skill — autonomous skill optimizer (npx skills, ~/.agents/skills/)" -echo " 🔄 magic MCP — 21st-dev UI generation MCP (toggle: lib/toggle-external.sh enable magic)" +echo " 🔄 21st skill pack — 21st.dev CLI skills, 7 (toggle: lib/toggle-external.sh enable 21st)" echo "" echo " All plugins installed at: user scope (~/.claude/plugins/)" echo " GStack skills symlinked individually into ~/.claude/skills/ (→ submodule)" diff --git a/lib/design-gate.md b/lib/design-gate.md index 2b2bfed..34bec19 100644 --- a/lib/design-gate.md +++ b/lib/design-gate.md @@ -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: - required + manual step: + required + manual step: → 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 ` — 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. diff --git a/lib/design-tool-gate.sh b/lib/design-tool-gate.sh index 6ecebbc..1a25456 100755 --- a/lib/design-tool-gate.sh +++ b/lib/design-tool-gate.sh @@ -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 diff --git a/lib/profile.sh b/lib/profile.sh index 57d2e33..a7ba29c 100755 --- a/lib/profile.sh +++ b/lib/profile.sh @@ -11,7 +11,7 @@ # Mechanism: # - Skills (gstack/external/personal): symlink toggle skills/ ↔ skills-disabled/ # - Plugins: `claude plugin enable|disable @` -# - 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 -- " fi @@ -394,15 +397,7 @@ disable_skill() { info "plugin '$skill' — manual: claude plugin disable $skill@" ;; 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 diff --git a/lib/profiles/design.profile b/lib/profiles/design.profile index 8610b7c..06c5e61 100644 --- a/lib/profiles/design.profile +++ b/lib/profiles/design.profile @@ -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 diff --git a/lib/profiles/full.profile b/lib/profiles/full.profile index 0907157..da8a89c 100644 --- a/lib/profiles/full.profile +++ b/lib/profiles/full.profile @@ -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 diff --git a/lib/profiles/web-full.profile b/lib/profiles/web-full.profile index a5c5be5..8ee1dbb 100644 --- a/lib/profiles/web-full.profile +++ b/lib/profiles/web-full.profile @@ -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 diff --git a/lib/profiles/web.profile b/lib/profiles/web.profile index d7a340b..718c721 100644 --- a/lib/profiles/web.profile +++ b/lib/profiles/web.profile @@ -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 diff --git a/lib/tests/profile-set-managed.test.sh b/lib/tests/profile-set-managed.test.sh index e0a3eea..934949e 100644 --- a/lib/tests/profile-set-managed.test.sh +++ b/lib/tests/profile-set-managed.test.sh @@ -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" <> "\$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 ] diff --git a/lib/toggle-external.sh b/lib/toggle-external.sh index c291439..a18774a 100755 --- a/lib/toggle-external.sh +++ b/lib/toggle-external.sh @@ -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 diff --git a/link.sh b/link.sh index 4fa02d3..023f1e2 100644 --- a/link.sh +++ b/link.sh @@ -117,8 +117,6 @@ link_env() { echo " cp \"$REPO/.env.example\" \"$home_env\" && \"\${EDITOR:-nano}\" \"$home_env\"" return fi - grep -qE '^[[:space:]]*(export[[:space:]]+)?MAGIC_API_KEY=.' "$home_env" 2>/dev/null \ - || echo "⚠️ $home_env has no MAGIC_API_KEY line — magic won't enable until added." if [ -L "$repo_env" ]; then [ "$(readlink "$repo_env")" = "$home_env" ] && return ln -sf "$home_env" "$repo_env"; CHANGED=$((CHANGED + 1)) diff --git a/plugins.lock.json b/plugins.lock.json index 833223c..c7a5a8f 100644 --- a/plugins.lock.json +++ b/plugins.lock.json @@ -20,6 +20,11 @@ "version": "latest", "note": "Context7 CLI — doc lookup for fast-evolving libs. Standalone CLI, not an MCP server. Install: npm install -g ctx7. Standalone: ctx7 docs /vercel/next.js \"middleware\"." }, + "21st": { + "source": "npm:@21st-dev/cli", + "version": "latest", + "note": "21st.dev CLI (bin `21st`) — supersedes the @21st-dev/magic MCP server (2026-09-22). Standalone CLI + a pack of 7 skills, no MCP, no API key: auth is `21st login` (browser token in ~/.config/21st). Install: npm install -g @21st-dev/cli. The skill pack is staged-installed into skills-external/21st-* by install-plugins.sh Step 8.7 — `21st skills install` refuses to write through the ~/.claude/skills symlink." + }, "graphifyy": { "source": "pypi:graphifyy", "version": "latest", diff --git a/settings.json b/settings.json index 4e55d4f..9cbefca 100644 --- a/settings.json +++ b/settings.json @@ -235,11 +235,7 @@ "WebFetch", "Bash(git stash pop*)", "Bash(git stash drop*)", - "Bash(git stash clear)", - "mcp__magic__21st_magic_component_builder", - "mcp__magic__21st_magic_component_refiner", - "mcp__magic__21st_magic_component_inspiration", - "mcp__magic__logo_search" + "Bash(git stash clear)" ], "defaultMode": "auto", "disableBypassPermissionsMode": "disable", @@ -370,7 +366,8 @@ "Moving or renaming a directory inside the repo (`mv src/api src/api_old`, or any `mv` of a tree). It breaks imports and paths silently, and the breakage surfaces far from the command. Clear only when the user asked for that move.", "An inline interpreter or `xargs` that deletes, or that writes outside the current working directory: `python3 -c`, `python -c` or `node -e` calling `rmtree`, `remove`, `unlink` or `truncate`; `xargs` feeding `rm`, `mv` or `dd`. `find ... | xargs rm` is the case that matters, since it routes around the `find * -exec rm` deny rule. Reading, computing, and editing a file inside the working directory pass untouched.", "Docker data destruction on this workstation: `docker rm -f`, `docker volume rm` or `prune`, `docker system prune`, `docker compose down -v` (drops named volumes, which hold local database data with no undo), and `docker run` with `--privileged` or a bind mount outside the current working directory. Clear only when the user named the container or volume in this turn.", - "Undeclared node packages: `npx `, `pnpm dlx` or `yarn dlx` of a package absent from the manifest and lockfile runs code fetched at call time; `npm install ` or `pnpm add ` adds a dependency the house rule requires naming first. Clear only when the user named the package in this turn." + "Undeclared node packages: `npx `, `pnpm dlx` or `yarn dlx` of a package absent from the manifest and lockfile runs code fetched at call time; `npm install ` or `pnpm add ` adds a dependency the house rule requires naming first. Clear only when the user named the package in this turn.", + "Publishing to the 21st.dev public catalog: `21st publish`, `publish-theme`, `publish-template`, `publish-gradient`, `publish-ascii`, `submit` and `resubmit` push a component, theme or template from this machine onto a public listing under the user's account; `21st edit`, `delete`, `withdraw`, `remove-from-catalog` and `21st profile set|upload` change or remove what is already published there. Retrieval and generation (`search`, `logo`, `get`, `add`, `generate`, `iterate`) are ordinary design work and pass. Clear only when the user asked to publish or change that specific item in this turn." ], "hard_deny": [ "$defaults", diff --git a/skills/profile/SKILL.md b/skills/profile/SKILL.md index 40922a1..fbc0c16 100644 --- a/skills/profile/SKILL.md +++ b/skills/profile/SKILL.md @@ -52,8 +52,7 @@ lists items + types: | `personal` | symlink move skills/ ↔ skills-disabled/\ (no prefix) | | `external` | symlink move skills/ ↔ skills-disabled/\ | | `plugin@` | `claude plugin enable\|disable @` (auto) | -| `mcp` (known: magic) | delegate to `lib/toggle-external.sh` (uses `.env`) | -| `mcp` (other) | advisory — prints manual `claude mcp add …` command | +| `mcp` | advisory — prints manual `claude mcp add …` command (no server is managed today: `MANAGED_MCPS` is empty since 21st.dev moved to a CLI) | | `cli` | advisory only — reports installed/not-installed | **Always-on plugins** (`security-guidance`, `superpowers`) are @@ -62,12 +61,14 @@ protected — `set` will refuse to disable them even if the profile omits them. `ui-ux-pro-max@ui-ux-pro-max-skill`, `plugin-dev@claude-code-plugins`, `pr-review-toolkit@claude-code-plugins`. Other plugins are never auto-toggled. **Managed externals** (`emil-design-eng`, `frontend-design`, -`design-motion-principles`, `impeccable`) and **managed MCPs** (`magic`) -follow the same symmetry (BDR-079): `set` enables them when the profile -lists them (from parked state, or from `skills-external/` if the symlink -never existed) and parks/unregisters them when it does not — e.g. `set -backend` after design work turns emil and magic off. `darwin-skill` and any -other unlisted external are never auto-touched. gstack works the same +`design-motion-principles`, `impeccable`, and the five 21st design skills +`21st-ui-build`, `21st-ui-explore`, `21st-ui-review`, `21st-cli-use`, +`21st-ai`) follow the same symmetry (BDR-079): `set` enables them when the +profile lists them (from parked state, or from `skills-external/` if the +symlink never existed) and parks them when it does not — e.g. `set backend` +after design work turns emil and the 21st pack off. `darwin-skill`, +`21st-registry`, `21st-design-sync` and any other unlisted external are never +auto-touched. gstack works the same all the way down: a profile listing gstack skills while the whole pack is off (via `toggle-external.sh`) re-enables JUST those skills on demand. @@ -137,11 +138,11 @@ bash "$HOME/.claude/lib/profile.sh" $ARGUMENTS update-check, learnings — script doesn't touch that infra. Disabled skills are just hidden from Claude Code's scanner; the gstack repo stays installed. - Profile changes DO toggle the managed Claude Code plugins (ui-ux-pro-max, - plugin-dev, pr-review-toolkit), the managed external packs (emil-design-eng, - frontend-design, design-motion-principles, impeccable) and the `magic` MCP — - in BOTH directions: `set` enables what the profile lists and disables the - managed leftovers it doesn't (BDR-008, BDR-079). Anything outside those - allowlists stays manual: `claude plugin enable|disable`, `claude mcp - add|remove`. + plugin-dev, pr-review-toolkit) and the managed external packs + (emil-design-eng, frontend-design, design-motion-principles, impeccable, + the 21st design skills) — in BOTH directions: `set` enables what the profile + lists and disables the managed leftovers it doesn't (BDR-008, BDR-079). + Anything outside those allowlists stays manual: `claude plugin + enable|disable`, `bash lib/toggle-external.sh enable|disable `. - `set` is destructive in the sense that it disables non-listed gstack skills. Use `apply` if the user wants additive behavior. diff --git a/update-all.sh b/update-all.sh index 2c4119d..99d3a9a 100644 --- a/update-all.sh +++ b/update-all.sh @@ -321,8 +321,6 @@ else info "bun not installed — skipping" fi # NOT updated here, deliberately (audit 2026-07-02): -# - magic MCP: registered as `npx -y @21st-dev/magic@latest` — npx resolves -# the latest release at every invocation, nothing to upgrade. # - graphify Claude integration (`graphify claude install`): rewrites curated # CLAUDE.md / .claude/settings.json (BDR-028 guard territory) — re-run # MANUALLY only if a graphify upgrade changes its hook format. @@ -422,6 +420,54 @@ print(d.get('impeccable',{}).get('version','latest')) fi fi +# ── 7.4. Update the 21st.dev CLI + skill pack ── +# The CLI is a global npm bin; the skills are its hash-verified output, staged +# under a throwaway HOME because `21st skills install` refuses to write +# through the ~/.claude/skills symlink (see install-plugins.sh Step 8.7). +echo "" +echo "── Updating 21st.dev CLI + skill pack..." +if ! command -v 21st &>/dev/null; then + info "21st CLI not installed — skipping (run: make plugin)" +else + TFD_VER="" + if [ -f "$REPO/plugins.lock.json" ] && command -v python3 &>/dev/null; then + TFD_VER=$(python3 -c " +import json +with open('$REPO/plugins.lock.json') as f: + d = json.load(f) +print(d.get('21st',{}).get('version','latest')) +" 2>/dev/null || true) + fi + TFD_PKG="@21st-dev/cli@latest" + [ -n "$TFD_VER" ] && [ "$TFD_VER" != "latest" ] && TFD_PKG="@21st-dev/cli@${TFD_VER}" + if npm install -g "$TFD_PKG" 2>/dev/null; then + ok "21st CLI updated (${TFD_VER:-latest})" + else + warn "21st CLI update failed — existing binary kept" + fi + TFD_STAGE=$(mktemp -d) + if HOME="$TFD_STAGE" 21st skills install --global --agent claude >/dev/null 2>&1; then + TFD_N=0 + for _tfd in "$TFD_STAGE"/.claude/skills/*/; do + [ -f "${_tfd}SKILL.md" ] || continue + _tfd_name=$(basename "$_tfd") + # Refresh the SOURCE only. A parked copy in skills-disabled/ is left + # alone: re-enabling restores it, and the next update refreshes it. + rm -rf "${REPO:?}/skills-external/${_tfd_name:?}" + mv "$_tfd" "$REPO/skills-external/$_tfd_name" + TFD_N=$((TFD_N + 1)) + done + if [ "$TFD_N" -gt 0 ]; then + ok "21st skill pack refreshed ($TFD_N skills)" + else + warn "21st skills install produced no SKILL.md — existing pack kept" + fi + else + warn "21st skill pack refresh failed — existing pack kept" + fi + rm -rf "$TFD_STAGE" +fi + # ── 7.5. Update external skills (npx skills) ── echo "" echo "── Updating external skills (npx skills)..."