Merge feature/21st-cli-migration into develop
This commit is contained in:
@@ -242,3 +242,9 @@ rules:
|
|||||||
- **Status**: resolved (A: ext hooks only terminals born after activation → install ext THEN start/re-attach session; B: Code app volume 0 in Windows mixer).
|
- **Status**: resolved (A: ext hooks only terminals born after activation → install ext THEN start/re-attach session; B: Code app volume 0 in Windows mixer).
|
||||||
- **Lesson**: two independent client faults presented as one symptom ("nothing works"). Splitting probe = run signal in FRESH terminal + play VS Code's own sound preview. Preview bypasses terminal/BEL/hook/dtach/ext → isolates renderer audio in one step. Do that FIRST next time, before any server-side archaeology.
|
- **Lesson**: two independent client faults presented as one symptom ("nothing works"). Splitting probe = run signal in FRESH terminal + play VS Code's own sound preview. Preview bypasses terminal/BEL/hook/dtach/ext → isolates renderer audio in one step. Do that FIRST next time, before any server-side archaeology.
|
||||||
- **Reference**: [[BLK-019]] bell-only variant (resolved differently — setting alone insufficient here), [[LRN-145]] terminalSequence-not-/dev/tty pattern. Silent-degradation class [[LRN-047]].
|
- **Reference**: [[BLK-019]] bell-only variant (resolved differently — setting alone insufficient here), [[LRN-145]] terminalSequence-not-/dev/tty pattern. Silent-degradation class [[LRN-047]].
|
||||||
|
|
||||||
|
## BLK-021 — Bash tool dead mid-session ("every command exits 1"): /tmp usrquota blown by a dead session's probe HOMEs — 2026-09-22
|
||||||
|
- **Friction**: previous session on `feature/21st-cli-migration` lost its shell before tests + commit: every Bash call, `echo` included, returned 1. Its harness file `imptest2/step8d-test.sh` landed as 0 bytes.
|
||||||
|
- **Real cause** (strong evidence, not reproduced on purpose): `/tmp` = tmpfs 7.4 GB mounted `usrquota`; `/tmp/claude-1000/-home-bchanot-Documents-claude/fefd277c-…/scratchpad` holds 5.9 GB of sandbox HOMEs (`pinprobe/` 2.1 GB, `pinrc/` 1.6 GB, `imp1 impg imptest sbx1 sbx2 v3.2.0 v3.6.1 v4.0.5 …`) from the impeccable pin probes. `dd` 40 MB to `/tmp/claude-1000` → "Disk quota exceeded" (EDQUOT) while `df` still shows 1.6 GB avail. Same write to `~/.cache` OK. Bash tool + `mktemp` + heredocs live in /tmp → all die together. This session: first impeccable probe failed with `Quota exceeded (os error 122)` on the installer's `/tmp/impeccable-update-*` staging, same cause.
|
||||||
|
- **Solution**: this round ran everything with `TMPDIR=~/.cache/imp-probe/tmp` (probe, harness, `make test`). Durable fix = delete the dead session's scratchpad: `rm -rf /tmp/claude-1000/-home-bchanot-Documents-claude/fefd277c-e143-4d51-b589-a566641079b5` (agent's `rm -rf` on /tmp denied by the classifier → user action). Rule for probes: sandbox HOMEs that pull npm/node payloads go under `~/.cache/<probe>/`, never the /tmp scratchpad, and get removed at the end of the session.
|
||||||
|
- **Status**: open until the user frees /tmp. Links [[BDR-094]], [[LRN-159]].
|
||||||
|
|||||||
@@ -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-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-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-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,20 @@ 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.
|
- **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).
|
- **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]].
|
- **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 `<HOME>/.claude/skills/<n>/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 (`<cwd>/.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]].
|
||||||
|
|
||||||
|
## BDR-094 — impeccable: global-scope install through the repo symlinks, pin + @latest fallback, output-read failure check
|
||||||
|
- **Date**: 2026-09-22
|
||||||
|
- **Decision**: `install-plugins.sh` Step 8d + `update-all.sh` run `npx -y impeccable@<pin> skills install -y --providers=claude --scope=global --no-hooks` straight through the `~/.claude/{skills,agents}` symlinks → lands in `skills/impeccable` + `agents/impeccable-*.md` (both gitignored, machine-owned). No staging, no `mv`. Precondition guard: both symlinks must already point into the repo, else "run make link first". Pin failure → `@latest` + loud "bump plugins.lock.json" warn. Profile-parked copy stays parked (install to live slot, `mv` back to `skills-disabled/`). Success = rc 0 AND installer output free of `Download failed|Could not check for skill updates` (`imp_install`, mirrored in both scripts, sets `IMP_FAIL`). Pin 3.2.0 → 4.1.0 (CLI only; skill dist 4.3.1 + engine 0.1.5 own tracks). `link.sh` `EXTERNAL_SKILLS` drops impeccable; `skills-external/impeccable/` gone. `lib/design-gate.md` §5: suggest `/impeccable init` once when frontend project lacks `PRODUCT.md`.
|
||||||
|
- **Why**: (1) 3.2.0 skill dist gone upstream → rc 1 → `make plugin` printed "run manually" forever. (2) `--scope=project` + staged `mv` moved skill dir only, dropped the 4 subagents the same run wrote. (3) Global scope IS the repo install under the symlink model; staging bought nothing. (4) rc lies once a copy exists. Probe 2026-09-22, sandbox HOME, real installer: 4.1.0 then 3.2.0 → rc 0, "Could not check for skill updates: invalid zip data … Existing skills were left unchanged"; same-pin rerun → rc 0, "Skills are up to date (v4.3.1)"; both leave SKILL.md byte-identical (same mtime, same sha).
|
||||||
|
- **Alternatives rejected**: before/after skill-version compare (first idea) → cannot separate rotted-pin no-op from up-to-date no-op, identical files + rc 0 both times → false warn on every rerun. `--force` → re-downloads ~15 MB engine + dist on every `make plugin`, and the CLI's own update check already refreshes without it. Shared `lib/impeccable.sh` for `imp_install` → deferred: two mirrored 12-line helpers vs new lib + test; revisit at a third caller. Project-scope install inside this repo → Claude Code scans `.claude/skills` too = skill listed twice, shadows the global copy (seen live, TODO T6).
|
||||||
|
- **Status**: accepted. Verified: harness on extracted Step 8d, sandbox HOME, real installer, 4/4: fresh install (skill 4.3.1, 4 agents); rotted pin over a copy → fallback fires; same pin rerun → no false warn; parked + rotted → fallback, returned to `skills-disabled/`. `make test` green minus 2 pre-existing T16a (gitleaks absent), shellcheck clean. `update-all.sh` block: `bash -n` + shellcheck only, same helper, not run end to end.
|
||||||
|
- **Reference**: `install-plugins.sh` Step 8d, `update-all.sh`, `plugins.lock.json`, `.gitignore`, `link.sh`, `lib/design-gate.md` §5. Links [[LRN-159]], [[LRN-158]] (21st: opposite case, installer refuses symlinks → stage), [[LRN-077]] (pin doctrine), [[BLK-021]].
|
||||||
|
|||||||
@@ -476,3 +476,12 @@ 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]]).
|
- 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]]).
|
- 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.
|
- 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.
|
||||||
|
- impeccable install repaired ([[BDR-094]]): global scope through the symlinks, 4 agents kept, pin 3.2.0 → 4.1.0 with @latest fallback, design-gate §5 `/impeccable init` hint. Residue probed: rotted pin over an existing copy exits 0 → `imp_install` reads the installer output ([[LRN-159]]); before/after version compare rejected (identical no-op). Harness 4/4, sandbox HOME, real installer.
|
||||||
|
- Previous shell death traced: /tmp tmpfs usrquota blown by 5.9 GB of dead-session probe HOMEs ([[BLK-021]], open, user frees). Tests + harness ran with TMPDIR under ~/.cache. `make test` green minus 2 pre-existing T16a, shellcheck clean. Committed on feature/21st-cli-migration, UNMERGED. `skills/synced/` (claude.ai synced skills, 4.4 MB) untracked + unignored, left for the user.
|
||||||
|
|||||||
@@ -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-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-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-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,18 @@ 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.
|
- **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.
|
- **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.
|
- **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 `<HOME>/.claude/skills/<n>/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/<pkg>` + 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.
|
||||||
|
|
||||||
|
## LRN-159 — A pin whose payload is fetched at install time rots: pin + fallback, and read the installer's output, not its exit code
|
||||||
|
- **Date**: 2026-09-22
|
||||||
|
- **Context**: `impeccable@3.2.0` still on npm, but `skills install` downloads the skill dist at run time and that release's zip is gone → "Download failed: invalid zip data". Strict pin = `make plugin` fails forever, prints "run it yourself". Second layer: once a copy exists, same CLI exits 0 on the same failure ("Could not check for skill updates … Existing skills were left unchanged"), indistinguishable by rc, by SKILL.md version or by mtime/sha from "Skills are up to date".
|
||||||
|
- **Pattern**: two classes of npm pin. (a) self-contained package → pin freezes behaviour, rc is truth. (b) package that fetches its payload at install time (impeccable, ctx7, `skills add` style) → pin freezes only the fetcher; payload can vanish or drift. For (b): pin + `@latest` fallback + loud "bump the lock" warn, never pin-or-die. And when the tool has an "already installed" branch, capture stdout+stderr and match the failure text; rc and before/after compare both read "unchanged" for a no-op AND for a swallowed failure.
|
||||||
|
- **Future application**: any `install-plugins.sh` step whose pinned tool downloads something at install time. Probe both HOME states (clean, copy present) before trusting rc. Cheap recipe: sandbox HOME with the repo-shaped symlinks, pinned install twice, then the rotted pin; diff rc + output + `stat`/`sha256sum` of the landed file.
|
||||||
|
- **Reference**: [[BDR-094]], `install-plugins.sh` Step 8d `imp_install`, `update-all.sh`. Links [[LRN-077]] (why pin), [[LRN-034]] (run the real thing), [[LRN-158]].
|
||||||
|
|||||||
@@ -1,5 +1,113 @@
|
|||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
|
## 2026-09-22 — impeccable install repaired: global scope + agents + rotted pin (feature/21st-cli-migration)
|
||||||
|
User: `make plugin` never installs impeccable, it just prints "run it
|
||||||
|
yourself"; running it by hand needs `--scope=global` to land right, and then
|
||||||
|
`/impeccable init` is still required. Three separate defects, all confirmed:
|
||||||
|
1. **Pin rotted.** `npx -y impeccable@3.2.0 skills install` → `Download
|
||||||
|
failed: invalid zip data`, rc 1. The CLI fetches its skill dist at install
|
||||||
|
time and that release's artifact is gone. 3.6.1 / 4.0.5 / 4.1.0 all work.
|
||||||
|
That rc 1 is the "run manually" warn the user sees.
|
||||||
|
2. **Wrong scope + half the payload dropped.** The step staged
|
||||||
|
`--scope=project` in a tmpdir and `mv`'d only the skill dir, silently
|
||||||
|
discarding the 4 `impeccable-*` subagents the installer also writes.
|
||||||
|
`--scope=global` writes `~/.claude/skills/impeccable` +
|
||||||
|
`~/.claude/agents/impeccable-*.md`, and both are symlinks INTO this repo,
|
||||||
|
so a global install is the repo install. Verified in a sandbox HOME.
|
||||||
|
3. **`/impeccable init` never surfaced.** It writes per-project PRODUCT.md
|
||||||
|
(design context the skill reads); it runs in the agent chat, so install
|
||||||
|
can only announce it and the design gate has to check it.
|
||||||
|
|
||||||
|
- [x] T1 install-plugins.sh Step 8d rewritten: global scope, no staging,
|
||||||
|
pin→latest fallback with a loud bump-the-lock warn, park-aware
|
||||||
|
(profile may hold impeccable in skills-disabled), symlink precondition
|
||||||
|
guard, agent count + skill version reported, init hint printed.
|
||||||
|
Harness-tested against a fake HOME with repo-shaped symlinks: happy
|
||||||
|
path OK, park/restore OK. Caught + fixed there: `find` stops at the
|
||||||
|
`~/.claude/agents` symlink without `-L`, so the agent count read 0
|
||||||
|
while 4 agents were installed.
|
||||||
|
- [x] T2 update-all.sh impeccable block: same shape. `bash -n` only, NOT
|
||||||
|
run end to end.
|
||||||
|
- [x] T3 plugins.lock.json: 3.2.0 → 4.1.0 + honest note (pin covers the CLI
|
||||||
|
only; skill dist 4.3.1 and engine 0.1.5 have their own tracks).
|
||||||
|
- [x] T4 .gitignore: `agents/impeccable-*.md` (machine-owned, tracked dir);
|
||||||
|
drop `skills-external/impeccable/`. link.sh: impeccable out of
|
||||||
|
EXTERNAL_SKILLS (nothing to symlink any more). `git check-ignore`
|
||||||
|
confirms both paths.
|
||||||
|
- [x] T5 lib/design-gate.md §5: suggest-only PRODUCT.md / `/impeccable init`
|
||||||
|
check, same shape as the §4 animation-library check.
|
||||||
|
- [x] T6 duplicate project-scope install: already gone at resume (user ran
|
||||||
|
`rm -rf .claude/skills .claude/agents` before restarting).
|
||||||
|
- [x] T7 CHANGELOG (Added/Changed/Fixed) + BDR-094 + LRN-159 + BLK-021 +
|
||||||
|
journal. `make test` green except the 2 pre-existing gitflow T16a FAILs
|
||||||
|
(gitleaks binary absent on this host), shellcheck clean. Committed on
|
||||||
|
the branch, UNMERGED — human gate.
|
||||||
|
|
||||||
|
**Residual, probed and fixed (round 3)**: with a copy already installed a
|
||||||
|
rotted pin DOES exit 0 ("Could not check for skill updates: invalid zip data
|
||||||
|
… Existing skills were left unchanged"), and so does a genuine rerun of a
|
||||||
|
good pin ("Skills are up to date (v4.3.1)"). Both leave SKILL.md
|
||||||
|
byte-identical, so a before/after version compare cannot separate them.
|
||||||
|
`imp_install` (Step 8d and update-all.sh) now captures the installer output
|
||||||
|
and fails on `Download failed|Could not check for skill updates`, whatever
|
||||||
|
the exit code. Harness on the extracted step, sandbox HOME, real installer:
|
||||||
|
fresh install; rotted pin over a copy → fallback fires; same pin rerun → no
|
||||||
|
false warn; parked copy + rotted pin → fallback, then returned to
|
||||||
|
skills-disabled/. update-all.sh: `bash -n` + shellcheck only.
|
||||||
|
|
||||||
|
OPEN for the user:
|
||||||
|
- /tmp is a tmpfs with a per-user quota and the dead session's scratchpad
|
||||||
|
holds 5.9 GB of probe HOMEs. Writes to /tmp fail with EDQUOT: the likely
|
||||||
|
cause of the "every command exits 1" shell death (BLK-021). Free it:
|
||||||
|
`rm -rf /tmp/claude-1000/-home-bchanot-Documents-claude/fefd277c-e143-4d51-b589-a566641079b5`
|
||||||
|
(the agent's `rm -rf` under /tmp is denied). This round ran tests and the
|
||||||
|
harness with TMPDIR under ~/.cache.
|
||||||
|
- `skills/synced/` (4.4 MB, untracked, not ignored): claude.ai's synced
|
||||||
|
skills, written through the ~/.claude/skills symlink. Decide whether to
|
||||||
|
gitignore it; not touched here.
|
||||||
|
|
||||||
|
## 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)
|
## 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
|
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
|
copy-paste), and the hand-back stops at the deploy steps — wants, after the
|
||||||
|
|||||||
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
# Local secrets for Claude Code plugin install scripts.
|
# 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.
|
# 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
|
# 21st.dev needs nothing here since 2026-09-22: the Magic MCP server was
|
||||||
# Get a key at: https://21st.dev/magic (dashboard → API keys)
|
# replaced by the `21st` CLI, whose auth is `21st login` (browser token in
|
||||||
MAGIC_API_KEY=your_21st_dev_magic_api_key_here
|
# ~/.config/21st). Any leftover MAGIC_API_KEY line is dead — delete it.
|
||||||
|
|
||||||
# ── Google SEO data layer (lib/seo-data) — used by /seo FULL ──
|
# ── Google SEO data layer (lib/seo-data) — used by /seo FULL ──
|
||||||
# OAuth Desktop client: GCP console → APIs & Services → Credentials → OAuth client (Desktop).
|
# OAuth Desktop client: GCP console → APIs & Services → Credentials → OAuth client (Desktop).
|
||||||
|
|||||||
+20
-5
@@ -64,11 +64,25 @@ skills/ios-sync
|
|||||||
skills/design-motion-principles
|
skills/design-motion-principles
|
||||||
skills/emil-design-eng
|
skills/emil-design-eng
|
||||||
skills/frontend-design
|
skills/frontend-design
|
||||||
|
|
||||||
|
# Impeccable — NOT a symlink: `impeccable skills install --scope=global`
|
||||||
|
# writes the skill dir (and its ~15 MB engine binary) straight in through the
|
||||||
|
# ~/.claude/skills symlink. Machine-owned, regenerated by make plugin/update.
|
||||||
skills/impeccable
|
skills/impeccable
|
||||||
|
|
||||||
|
# …and the 4 subagents the same installer drops through ~/.claude/agents.
|
||||||
|
# agents/ is a tracked directory, so these need naming explicitly.
|
||||||
|
agents/impeccable-*.md
|
||||||
|
|
||||||
# External skills installed via `npx skills add` — auto-created by link.sh
|
# External skills installed via `npx skills add` — auto-created by link.sh
|
||||||
skills/darwin-skill
|
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`
|
# Context7 docs-lookup skill — installed by `ctx7 setup --claude --cli`
|
||||||
# (install-plugins.sh Step 6, when absent) into ~/.claude/skills (a symlink to
|
# (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.
|
# this repo's skills/). ctx7-managed and re-created on demand — not vendored here.
|
||||||
@@ -156,11 +170,12 @@ skills-external/frontend-design/
|
|||||||
# an edit. The source is always re-fetched, so no offline copy is needed.
|
# an edit. The source is always re-fetched, so no offline copy is needed.
|
||||||
skills-external/emil-design-eng/
|
skills-external/emil-design-eng/
|
||||||
|
|
||||||
# Impeccable — machine-owned dist produced by `npx impeccable skills install`
|
# 21st.dev skill pack — machine-owned: `21st skills install` output, staged by
|
||||||
# (install-plugins.sh Step 8d, update-all.sh), pinned in plugins.lock.json.
|
# install-plugins.sh Step 8.7 (the installer refuses to write through the
|
||||||
# Not vendored: the installer owns the layout and rewrites it on update
|
# ~/.claude/skills symlink, so it runs under a throwaway HOME and the skills
|
||||||
# (ctx7 pattern). Symlinked into skills/ by link.sh.
|
# are moved here). Refreshed by update-all.sh. Not vendored: the CLI owns the
|
||||||
skills-external/impeccable/
|
# layout and the content is sha256-verified against 21st.dev's manifest.
|
||||||
|
skills-external/21st-*/
|
||||||
|
|
||||||
# npx `skills add` project-scope artifacts — darwin-skill copies itself into
|
# npx `skills add` project-scope artifacts — darwin-skill copies itself into
|
||||||
# the repo's .agents/ and writes skills-lock.json at root. Our own agents live
|
# the repo's .agents/ and writes skills-lock.json at root. Our own agents live
|
||||||
|
|||||||
+3
-2
@@ -67,8 +67,9 @@ regexTarget = "line"
|
|||||||
regexes = [
|
regexes = [
|
||||||
'''X-Amz-Credential=AKIA[0-9A-Z]{16}''',
|
'''X-Amz-Credential=AKIA[0-9A-Z]{16}''',
|
||||||
'''private-user-images\.githubusercontent\.com/[^"]*\?jwt=''',
|
'''private-user-images\.githubusercontent\.com/[^"]*\?jwt=''',
|
||||||
'''MAGIC_API_KEY=abc123''',
|
# Docs/test example — base64 of the "the ..." ASCII sample text, never a key.
|
||||||
# magic MCP docs example — base64 of "the ..." ASCII sample text.
|
# (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+/=]*'''',
|
'''clientKey = 'dGhlIH[A-Za-z0-9+/=]*'''',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,41 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
|
|||||||
classifier lists, `$defaults` splice semantics, `classifyAllShell`, the
|
classifier lists, `$defaults` splice semantics, `classifyAllShell`, the
|
||||||
user-scope vs project-scope rule, and why `ask` is the wrong tier for a
|
user-scope vs project-scope rule, and why `ask` is the wrong tier for a
|
||||||
destructive command under auto mode.
|
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.
|
||||||
|
|
||||||
|
- `lib/design-gate.md` §5: a suggest-only check, same shape as the §4
|
||||||
|
animation-library one. When impeccable is active and the frontend project
|
||||||
|
has no `PRODUCT.md` at its root, the gate proposes `/impeccable init` once
|
||||||
|
and never runs it itself (it interviews the user). Skipped for single
|
||||||
|
component reviews and non-UI work.
|
||||||
|
|
||||||
### Changed
|
### 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
|
- **`/deploy` hand-back: one physical line per command, then a post-deploy
|
||||||
tests block.** Every command in the checklist is emitted on exactly one
|
tests block.** Every command in the checklist is emitted on exactly one
|
||||||
line, however long; a legacy `\` continuation in the runbook is joined at
|
line, however long; a legacy `\` continuation in the runbook is joined at
|
||||||
@@ -92,6 +125,23 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
|
|||||||
`bypassPermissions`). Adding a restriction stays allowed; removing one
|
`bypassPermissions`). Adding a restriction stays allowed; removing one
|
||||||
does not. No instruction clears these.
|
does not. No instruction clears these.
|
||||||
|
|
||||||
|
- **impeccable installs at `--scope=global`, subagents included, and the
|
||||||
|
pin fails safe.** `install-plugins.sh` Step 8d no longer stages a
|
||||||
|
`--scope=project` install in a tmpdir and moves the skill directory alone.
|
||||||
|
The installer writes through the `~/.claude/skills` and `~/.claude/agents`
|
||||||
|
symlinks straight into the repo: `skills/impeccable` plus the four
|
||||||
|
`agents/impeccable-*.md`, both gitignored and machine-owned, which is what
|
||||||
|
the manual `--scope=global` command already did. The step refuses to run
|
||||||
|
before `make link` has created those symlinks (an install before them
|
||||||
|
materializes real directories that `link.sh` then refuses to replace),
|
||||||
|
keeps a profile-parked copy parked, reports the skill version and agent
|
||||||
|
count, and prints the per-project `/impeccable init` hint. A pinned
|
||||||
|
install that fails falls back to `impeccable@latest` with a warning to
|
||||||
|
bump `plugins.lock.json`. `update-all.sh` follows the same shape.
|
||||||
|
`plugins.lock.json` pin 3.2.0 → 4.1.0 (the CLI only: the skill dist and
|
||||||
|
the engine binary have their own release tracks). `link.sh` drops
|
||||||
|
impeccable from `EXTERNAL_SKILLS`; `skills-external/impeccable/` is gone.
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
- **Ten secret-reader deny rules added**: `sed`, `awk`, `cut`, `tr`,
|
- **Ten secret-reader deny rules added**: `sed`, `awk`, `cut`, `tr`,
|
||||||
`sort`, `uniq`, `diff`, `od`, `xxd`, `strings` against `.env*`. Six of
|
`sort`, `uniq`, `diff`, `od`, `xxd`, `strings` against `.env*`. Six of
|
||||||
@@ -100,6 +150,15 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
|
|||||||
`Bash(grep * .env*)` family: a `cat .env | sed` pipe still slips past,
|
`Bash(grep * .env*)` family: a `cat .env | sed` pipe still slips past,
|
||||||
which is what the `hard_deny` exfiltration rule is there to catch.
|
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
|
### Fixed
|
||||||
- **`make update` no longer drops the Playwright OS-support bump** — a
|
- **`make update` no longer drops the Playwright OS-support bump** — a
|
||||||
gstack submodule update used to leave the bump unapplied until the next
|
gstack submodule update used to leave the bump unapplied until the next
|
||||||
@@ -124,6 +183,20 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
|
|||||||
holds under `defaultMode: default`, not under this config's `auto`. The
|
holds under `defaultMode: default`, not under this config's `auto`. The
|
||||||
paragraph now separates what is verified from what is not, and names
|
paragraph now separates what is verified from what is not, and names
|
||||||
`deny` as the only tier the classifier cannot lift.
|
`deny` as the only tier the classifier cannot lift.
|
||||||
|
- **`make plugin` never installed impeccable.** Three defects. The 3.2.0
|
||||||
|
pin had rotted upstream: the CLI fetches its skill dist at install time and
|
||||||
|
that release's artifact is gone (`Download failed: invalid zip data`),
|
||||||
|
which the step reported as "run it yourself" on every run. The
|
||||||
|
project-scope staging dropped the four subagents the same install writes.
|
||||||
|
And `/impeccable init` was never announced. A fourth, found while probing
|
||||||
|
the fix: once a copy is already installed, a rotted pin exits 0
|
||||||
|
(`Could not check for skill updates … Existing skills were left
|
||||||
|
unchanged`), byte-identical on disk to a genuine "Skills are up to date"
|
||||||
|
rerun, so `imp_install` now reads the installer output instead of trusting
|
||||||
|
the exit code or a version compare. Verified with the real installer in a
|
||||||
|
sandbox HOME: fresh install, rotted pin over a copy (fallback fires), same
|
||||||
|
pin rerun (no false warning), parked copy plus rotted pin (fallback, then
|
||||||
|
returned to `skills-disabled/`).
|
||||||
|
|
||||||
## [1.5.0] — 2026-09-13
|
## [1.5.0] — 2026-09-13
|
||||||
|
|
||||||
|
|||||||
+6
-4
@@ -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.
|
source for design routing; the design-toolchain hook reinforces it.
|
||||||
- Trivial (≤2 files, one cosmetic value) → /hotfix, no toolchain.
|
- Trivial (≤2 files, one cosmetic value) → /hotfix, no toolchain.
|
||||||
- Build UI (component, page, redesign) → ui-ux-pro-max + frontend-design
|
- Build UI (component, page, redesign) → ui-ux-pro-max + frontend-design
|
||||||
(anti-slop) + Magic MCP /ui + emil-design-eng (polish) +
|
(anti-slop) + 21st-ui-build (catalog + generation) + emil-design-eng
|
||||||
design-motion-principles (if motion) + design-html (if static).
|
(polish) + design-motion-principles (if motion) + design-html (if static).
|
||||||
Post-build floor: `npx impeccable detect <files>` (45 deterministic
|
Post-build floor: `npx impeccable detect <files>` (45 deterministic
|
||||||
anti-slop rules, exit 2 = findings) when impeccable installed.
|
anti-slop rules, exit 2 = findings) when impeccable installed.
|
||||||
- Design system / brand → design-consultation first, then the build tools.
|
- Design system / brand → design-consultation first, then the build tools.
|
||||||
- Review / audit → design-review + emil-design-eng + design-motion-principles
|
- 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.
|
Scope doubt → don't silently skip: ask, or default to Build tier.
|
||||||
Gate: lightweight skills run `~/.claude/lib/design-gate.md`; orchestrators via
|
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
|
## graphify
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
and user (`~/.claude.json`) scope. Use that instead of a literal value:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
MAGIC_API_KEY=<Enter your magic api key here from https://21st.dev/settings/api-keys >
|
|
||||||
# single-quoted so bash doesn't expand it; Claude Code expands it at
|
# single-quoted so bash doesn't expand it; Claude Code expands it at
|
||||||
# launch, reading the var from its own process environment:
|
# 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 <name> --scope user --env 'API_KEY=${SOME_API_KEY}' -- <command>
|
||||||
```
|
```
|
||||||
|
|
||||||
The var still has to exist in the **environment of the process that starts
|
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
|
then see it). This repo's `~/.bashrc` instead wraps the `claude` command
|
||||||
itself: a `claude()` shell function sources `~/.claude/.env` into a subshell
|
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
|
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
|
— never the ambient shell.
|
||||||
the pattern to copy for a new MCP server.
|
|
||||||
|
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 —
|
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
|
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`
|
store, multi-site safe). Missing credentials never break an audit — `/seo`
|
||||||
degrades gracefully to anonymous PageSpeed lab data.
|
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
|
`@21st-dev/cli` (bin `21st`) supersedes the `@21st-dev/magic` MCP server that
|
||||||
server (`127.0.0.1:9221+`, `Access-Control-Allow-Origin: *`, no token/origin
|
this config used to register. Same endpoint, one browser login, no API key,
|
||||||
check) for up to 10 minutes per call; any local process or open browser tab
|
and nothing loaded into a session that isn't using it:
|
||||||
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
|
```bash
|
||||||
in the third-party package's code, not this repo's config — **we don't patch
|
npm i -g @21st-dev/cli
|
||||||
it**. The mitigation lives on our side: `settings.json`
|
21st login # browser flow, token saved in ~/.config/21st
|
||||||
`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
|
`make plugin` does both (Step 8.7 installs the CLI, then offers the login in
|
||||||
auto-approving with no prompt raised (LRN-146). Whether MCP `ask` rules
|
an interactive terminal) and installs the skill pack that drives it:
|
||||||
behave the same has not been verified here, so re-check before relying on
|
`21st-ui-build`, `-ui-explore`, `-ui-review`, `-cli-use`, `-ai`, plus the two
|
||||||
it. `deny` is the only tier the auto-mode classifier cannot lift; for a
|
publishing skills `-registry` and `-design-sync`. The pack is disabled by
|
||||||
gate that holds under auto mode without banning the tool outright, the
|
default, the same policy the MCP had. `/profile design` turns on the five
|
||||||
right home is `autoMode.soft_deny`. Don't allowlist
|
design skills; `bash lib/toggle-external.sh enable 21st` turns on all seven.
|
||||||
`21st_magic_component_builder` or `21st_magic_component_refiner` (arbitrary
|
|
||||||
absolute-path read → vendor exfil, same audit) under any circumstance.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
gstack + managed plugins — sessions stay focused and passive token cost drops.
|
||||||
|
|
||||||
`profile set <name>` actually toggles plugins (`claude plugin enable|disable`)
|
`profile set <name>` actually toggles plugins (`claude plugin enable|disable`)
|
||||||
and MCPs (delegates to `lib/toggle-external.sh` for `magic`) — not just
|
and external skill packs (delegates to `lib/toggle-external.sh`) — not just
|
||||||
advisory. Always-on plugins (`security-guidance`, `superpowers`)
|
advisory. No MCP server is auto-toggled today. Always-on plugins (`security-guidance`, `superpowers`)
|
||||||
are protected. Managed plugins that `set` may toggle:
|
are protected. Managed plugins that `set` may toggle:
|
||||||
`ui-ux-pro-max@ui-ux-pro-max-skill`, `plugin-dev@claude-code-plugins`,
|
`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.
|
`pr-review-toolkit@claude-code-plugins`. Other plugins are never auto-toggled.
|
||||||
|
|||||||
+186
-64
@@ -788,54 +788,114 @@ else
|
|||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ── Step 8d: Impeccable (design anti-pattern detector + skill) ──
|
# ── Step 8d: Impeccable (design detector + skill + subagents) ──
|
||||||
# 45 deterministic detector rules (CLI `impeccable detect`, exit 0/2) +
|
# 45 deterministic detector rules (`impeccable detect`, exit 0/2), the
|
||||||
# /impeccable skill (23 verbs). Machine-owned dist: the installer produces
|
# /impeccable skill (23 verbs) and 4 `impeccable-*` subagents.
|
||||||
# it, we stage it in a tmpdir then move it under skills-external/
|
#
|
||||||
# (gitignored, ctx7 pattern) — never let the installer write through the
|
# GLOBAL scope, no staging: the installer writes ~/.claude/skills/impeccable/
|
||||||
# ~/.claude/skills symlink into the tracked repo dir.
|
# (skill + its self-contained engine binary) and ~/.claude/agents/
|
||||||
echo "── Step 8d: Impeccable — design anti-pattern detector ────"
|
# impeccable-*.md, and both of those are symlinks into this repo — so the
|
||||||
|
# global install IS the repo install. Machine-owned and gitignored on both
|
||||||
|
# sides. `--scope=project` was wrong twice over: it writes <cwd>/.claude/,
|
||||||
|
# which serves only the directory it ran in, and the staged `mv` that
|
||||||
|
# followed it moved the skill alone, silently dropping the subagents.
|
||||||
|
#
|
||||||
|
# The pin rots. The CLI downloads its skill dist at install time and an older
|
||||||
|
# release's artifact eventually disappears (`impeccable@3.2.0` → "Download
|
||||||
|
# failed: invalid zip data", 2026-09-22) — which is what left `make plugin`
|
||||||
|
# telling the user to run the command by hand. So a pin failure falls back to
|
||||||
|
# @latest and says, loudly, that the lock needs bumping.
|
||||||
|
echo "── Step 8d: Impeccable — design detector, skill + agents ──"
|
||||||
echo ""
|
echo ""
|
||||||
IMP_DIR="$REPO/skills-external/impeccable"
|
IMP_SKILL_DIR="$HOME/.claude/skills/impeccable"
|
||||||
|
IMP_PARKED="$REPO/skills-disabled/impeccable"
|
||||||
IMP_VER=$(pinned_version "impeccable")
|
IMP_VER=$(pinned_version "impeccable")
|
||||||
NODE_MAJOR=$(node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1)
|
NODE_MAJOR=$(node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1)
|
||||||
if [ -z "${NODE_MAJOR:-}" ] || [ "$NODE_MAJOR" -lt 24 ]; then
|
|
||||||
if [ -f "$IMP_DIR/SKILL.md" ]; then
|
# One install attempt. $1 = "latest" or an exact version. On failure, IMP_FAIL
|
||||||
|
# holds the reason. The exit code alone is not enough: with a copy already in
|
||||||
|
# place, a rotted pin exits 0 ("Could not check for skill updates: invalid
|
||||||
|
# zip data … Existing skills were left unchanged"), exactly like a genuine
|
||||||
|
# up-to-date no-op ("Skills are up to date") — only the output tells them
|
||||||
|
# apart. Probed 2026-09-22 on 4.1.0 vs 3.2.0 in a sandbox HOME.
|
||||||
|
imp_install() {
|
||||||
|
local pkg="impeccable" out rc=0
|
||||||
|
[ "$1" != "latest" ] && pkg="impeccable@$1"
|
||||||
|
out=$(npx -y "$pkg" skills install -y --providers=claude --scope=global \
|
||||||
|
--no-hooks 2>&1) || rc=$?
|
||||||
|
IMP_FAIL=$(printf '%s\n' "$out" \
|
||||||
|
| grep -E 'Download failed|Could not check for skill updates' \
|
||||||
|
| head -1 || true)
|
||||||
|
if [ "$rc" -ne 0 ] && [ -z "$IMP_FAIL" ]; then
|
||||||
|
IMP_FAIL="installer exited $rc"
|
||||||
|
fi
|
||||||
|
[ -z "$IMP_FAIL" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Precondition: ~/.claude/{skills,agents} must already be link.sh's symlinks.
|
||||||
|
# Installing before they exist materializes real directories there, and
|
||||||
|
# link.sh then refuses to replace them ("is a real directory") — a worse
|
||||||
|
# failure than skipping, because it needs manual repair.
|
||||||
|
IMP_READY=true
|
||||||
|
for _imp_d in skills agents; do
|
||||||
|
if [ "$(readlink "$HOME/.claude/$_imp_d" 2>/dev/null || true)" != "$REPO/$_imp_d" ]; then
|
||||||
|
IMP_READY=false
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$IMP_READY" != true ]; then
|
||||||
|
warn "impeccable: ~/.claude/skills and ~/.claude/agents are not this repo's symlinks yet"
|
||||||
|
warn " → run 'make link' first, then re-run 'make plugin'"
|
||||||
|
elif [ -z "${NODE_MAJOR:-}" ] || [ "$NODE_MAJOR" -lt 24 ]; then
|
||||||
|
if [ -f "$IMP_SKILL_DIR/SKILL.md" ] || [ -f "$IMP_PARKED/SKILL.md" ]; then
|
||||||
ok "impeccable already present (update skipped — needs Node >= 24, found ${NODE_MAJOR:-none})"
|
ok "impeccable already present (update skipped — needs Node >= 24, found ${NODE_MAJOR:-none})"
|
||||||
else
|
else
|
||||||
warn "impeccable: needs Node >= 24 (found ${NODE_MAJOR:-none}) — skipped. Bump Node, then: make plugin"
|
warn "impeccable: needs Node >= 24 (found ${NODE_MAJOR:-none}) — skipped. Bump Node, then: make plugin"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
IMP_PKG="impeccable"
|
# A profile may hold impeccable parked in skills-disabled/. Install writes
|
||||||
|
# to the live slot, so remember the state and put the fresh copy back where
|
||||||
|
# it was — otherwise `make plugin` silently re-enables a disabled skill.
|
||||||
|
IMP_WAS_PARKED=false
|
||||||
|
[ -d "$IMP_PARKED" ] && IMP_WAS_PARKED=true
|
||||||
|
IMP_USED=""
|
||||||
if [ "$IMP_VER" != "latest" ]; then
|
if [ "$IMP_VER" != "latest" ]; then
|
||||||
IMP_PKG="impeccable@${IMP_VER}"
|
info "Installing impeccable ${IMP_VER} (pinned in plugins.lock.json, global scope)..."
|
||||||
info "Installing impeccable ${IMP_VER} (pinned in plugins.lock.json, staged)..."
|
if imp_install "$IMP_VER"; then
|
||||||
|
IMP_USED="$IMP_VER"
|
||||||
|
else
|
||||||
|
warn "impeccable@${IMP_VER} did not install (${IMP_FAIL}) — that release's skill dist is gone upstream"
|
||||||
|
info "Falling back to impeccable@latest..."
|
||||||
|
if imp_install latest; then
|
||||||
|
IMP_USED="latest"
|
||||||
|
warn "installed @latest instead of the pin. Bump \"impeccable\".version in plugins.lock.json to the version this produced, so the next run is reproducible again."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
info "Installing impeccable latest (consider pinning in plugins.lock.json)..."
|
info "Installing impeccable latest (consider pinning in plugins.lock.json)..."
|
||||||
|
imp_install latest && IMP_USED="latest"
|
||||||
fi
|
fi
|
||||||
IMP_STAGE=$(mktemp -d)
|
|
||||||
if (cd "$IMP_STAGE" && npx -y "$IMP_PKG" skills install -y --providers=claude --scope=project --no-hooks >/dev/null 2>&1); then
|
if [ -n "$IMP_USED" ] && [ -f "$IMP_SKILL_DIR/SKILL.md" ]; then
|
||||||
IMP_SRC=$(find "$IMP_STAGE" -type d -name impeccable -path "*skills*" 2>/dev/null | head -1)
|
IMP_SKILL_VER=$(sed -n 's/^version:[[:space:]]*//p' "$IMP_SKILL_DIR/SKILL.md" | head -1)
|
||||||
if [ -n "$IMP_SRC" ] && [ -f "$IMP_SRC/SKILL.md" ]; then
|
# -L: ~/.claude/agents is a symlink, and find would otherwise stop on it.
|
||||||
rm -rf "$IMP_DIR"
|
IMP_AGENTS=$(find -L "$HOME/.claude/agents" -maxdepth 1 -name 'impeccable-*.md' 2>/dev/null | wc -l)
|
||||||
mv "$IMP_SRC" "$IMP_DIR"
|
ok "impeccable installed (CLI ${IMP_USED}, skill ${IMP_SKILL_VER:-?}, ${IMP_AGENTS} agents)"
|
||||||
ok "impeccable synced to skills-external/ (CLI ${IMP_VER})"
|
if [ "$IMP_AGENTS" -eq 0 ]; then
|
||||||
|
warn "no impeccable-* agent landed in agents/ — the skill's finish/document verbs dispatch to them"
|
||||||
|
fi
|
||||||
|
if [ "$IMP_WAS_PARKED" = true ]; then
|
||||||
|
rm -rf "${IMP_PARKED:?}"
|
||||||
|
mv "$IMP_SKILL_DIR" "$IMP_PARKED"
|
||||||
|
info "impeccable was parked by a profile — refreshed copy returned to skills-disabled/"
|
||||||
|
fi
|
||||||
|
info "Per-project step, in the agent chat of each frontend project: /impeccable init"
|
||||||
|
info " (writes PRODUCT.md — the design context every impeccable verb reads)"
|
||||||
|
elif [ -f "$IMP_SKILL_DIR/SKILL.md" ] || [ -f "$IMP_PARKED/SKILL.md" ]; then
|
||||||
|
ok "impeccable already present (install failed: ${IMP_FAIL:-no SKILL.md written} — existing copy kept)"
|
||||||
else
|
else
|
||||||
warn "impeccable: installer ran but produced no skills/impeccable/SKILL.md — layout changed? Inspect: npx impeccable skills install"
|
warn "impeccable install failed (${IMP_FAIL:-no SKILL.md written}) — run manually: npx impeccable skills install -y --providers=claude --scope=global --no-hooks"
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
if [ -f "$IMP_DIR/SKILL.md" ]; then
|
|
||||||
ok "impeccable already present (installer failed — existing dist kept)"
|
|
||||||
else
|
|
||||||
warn "impeccable install failed — run manually: npx impeccable skills install -y --providers=claude --scope=project --no-hooks"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
rm -rf "$IMP_STAGE"
|
|
||||||
fi
|
|
||||||
if [ -L "$HOME/.claude/skills/impeccable" ]; then
|
|
||||||
ok "impeccable symlink OK"
|
|
||||||
else
|
|
||||||
info "Symlinking — will be created by link.sh"
|
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
@@ -892,42 +952,104 @@ done
|
|||||||
echo ""
|
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
|
# `@21st-dev/cli` (bin `21st`) supersedes the `@21st-dev/magic` MCP server:
|
||||||
# from 21st.dev. Toggled via lib/toggle-external.sh (same interface as
|
# same endpoint, one browser login (`21st login`, token in ~/.config/21st),
|
||||||
# gstack, emil-design-eng, etc.). Registered in Claude Code user scope.
|
# 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
|
# Machine-owned dist (impeccable pattern): `21st skills install` writes to
|
||||||
# into every Claude Code session and consume context tokens. Enable
|
# <HOME>/.claude/skills/<name>/ and REFUSES to follow a symlink anywhere on
|
||||||
# only when you're actively using Magic.
|
# 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.
|
# Default policy: pack DISABLED at install time — every skill description
|
||||||
# Template: $REPO/.env.example. Get a key at https://21st.dev/magic
|
# loads into every session. Enable on demand:
|
||||||
echo "── Step 8.7: Magic MCP (21st-dev) ──────────────────────────"
|
# 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 ""
|
echo ""
|
||||||
if [ -x "$REPO/lib/toggle-external.sh" ]; then
|
if command -v 21st &>/dev/null; then
|
||||||
MAGIC_STATUS="$(bash "$REPO/lib/toggle-external.sh" status magic 2>/dev/null || echo missing)"
|
ok "21st CLI already installed"
|
||||||
if [ "$MAGIC_STATUS" = "enabled" ]; then
|
else
|
||||||
info "Disabling magic MCP by default (enable on demand)..."
|
TFD_VER=$(pinned_version "21st")
|
||||||
bash "$REPO/lib/toggle-external.sh" disable magic >/dev/null
|
if [ "$TFD_VER" != "latest" ]; then
|
||||||
ok "magic MCP disabled — enable with: bash lib/toggle-external.sh enable magic"
|
info "Installing @21st-dev/cli@${TFD_VER} (pinned in plugins.lock.json)..."
|
||||||
|
npm install -g "@21st-dev/cli@${TFD_VER}"
|
||||||
else
|
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
|
fi
|
||||||
# The key lives in ~/.claude/.env (canonical, BDR-026), reached via the
|
if command -v 21st &>/dev/null; then
|
||||||
# repo/.env symlink that toggle-external.sh sources. Self-heal the common
|
ok "21st CLI installed"
|
||||||
# fresh-machine case: ~/.claude/.env was created AFTER link.sh ran, so the
|
else
|
||||||
# symlink is missing and the key looks absent though it's set.
|
err "21st CLI install failed — run manually: npm install -g @21st-dev/cli"
|
||||||
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)"
|
|
||||||
fi
|
fi
|
||||||
# Tolerate optional `export ` and leading whitespace; require a value.
|
fi
|
||||||
MAGIC_KEY_RE='^[[:space:]]*(export[[:space:]]+)?MAGIC_API_KEY=.'
|
|
||||||
if [ ! -f "$REPO/.env" ] || ! grep -qE "$MAGIC_KEY_RE" "$REPO/.env" 2>/dev/null; then
|
# Skill pack — staged install, then moved under skills-external/.
|
||||||
warn "MAGIC_API_KEY not set in ~/.claude/.env — add it (and run 'make link') before enabling magic"
|
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 <user> (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
|
fi
|
||||||
else
|
else
|
||||||
warn "lib/toggle-external.sh not found or not executable — skipping"
|
warn "lib/toggle-external.sh not found or not executable — skipping"
|
||||||
@@ -1040,7 +1162,7 @@ echo " 🔄 frontend-design — distinctive frontend interfaces, anti-AI-
|
|||||||
echo " 🔄 impeccable — /impeccable design verbs + 45-rule deterministic detector (npx impeccable detect)"
|
echo " 🔄 impeccable — /impeccable design verbs + 45-rule deterministic detector (npx impeccable detect)"
|
||||||
echo " 🔄 design-motion-principles — motion/animation design, 3-designer lens (kylezantos)"
|
echo " 🔄 design-motion-principles — motion/animation design, 3-designer lens (kylezantos)"
|
||||||
echo " 🔄 darwin-skill — autonomous skill optimizer (npx skills, ~/.agents/skills/)"
|
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 ""
|
||||||
echo " All plugins installed at: user scope (~/.claude/plugins/)"
|
echo " All plugins installed at: user scope (~/.claude/plugins/)"
|
||||||
echo " GStack skills symlinked individually into ~/.claude/skills/ (→ submodule)"
|
echo " GStack skills symlinked individually into ~/.claude/skills/ (→ submodule)"
|
||||||
|
|||||||
+40
-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
|
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,
|
tools** (the `# GATE-BLOCK:` allowlist in `design.profile`: ui-ux-pro-max,
|
||||||
frontend-design, emil-design-eng, design-motion-principles, impeccable, design-html,
|
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
|
browser/plan/shotgun tooling and graphify for convenience; those never trip the
|
||||||
gate. Motion (`design-motion-principles`) and static-HTML (`design-html`) are
|
gate. Motion (`design-motion-principles`) and static-HTML (`design-html`) are
|
||||||
already in the core set — checked regardless; their CLAUDE.md "+motion /
|
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
|
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 —
|
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
|
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.
|
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.
|
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.
|
🎨 DESIGN DETECTED — the design toolchain isn't fully active.
|
||||||
activate with /profile design: <skills / ui-ux-pro-max>
|
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.
|
→ run /profile design to activate it, then continue.
|
||||||
|
|
||||||
- **activate with /profile design** → skills + the plugin; `/profile design`
|
- **activate with /profile design** → skills + the plugin; `/profile design`
|
||||||
turns them on directly.
|
turns them on directly.
|
||||||
- **required + manual step** → required tools the profile can't flip silently.
|
- **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
|
**the `21st` CLI lands here: it TRIPS the gate** (it's required for Build),
|
||||||
a silent "optional". `/profile design` runs `toggle-external.sh` for magic,
|
it is NOT a silent "optional". `/profile design` symlinks the 21st skills,
|
||||||
which needs a valid `MAGIC_API_KEY` in `~/.claude/.env` — tell the user to verify it.
|
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.
|
- Do NOT hand-activate individual tools. The profile is the unit of activation.
|
||||||
- **11 / `READY BUT UNVERIFIED`** → `claude` was unreachable, so the design
|
- **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
|
plugin (ui-ux-pro-max) could NOT be checked. Do NOT report a plain "ready":
|
||||||
"ready": proceed only after telling the user that N tool(s) went unverified and
|
proceed only after telling the user that N tool(s) went unverified and having
|
||||||
having them confirm with `claude mcp list` / `claude plugin list`. Fail-visible,
|
them confirm with `claude plugin list`. Fail-visible, not fail-silent.
|
||||||
not fail-silent — the most important tool (magic) is exactly an unverifiable one.
|
|
||||||
|
|
||||||
### 4. Animation library — suggest-only (fires only on a real motion signal)
|
### 4. Animation library — suggest-only (fires only on a real motion signal)
|
||||||
|
|
||||||
@@ -138,6 +139,33 @@ count:
|
|||||||
toolchain check handles the skill; this step handles the lib. Don't conflate
|
toolchain check handles the skill; this step handles the lib. Don't conflate
|
||||||
them when talking to the user.
|
them when talking to the user.
|
||||||
|
|
||||||
|
### 5. Impeccable design context — suggest-only (one check, one line)
|
||||||
|
|
||||||
|
Same class as §4: a PROJECT-side prerequisite, not a tool. `impeccable`
|
||||||
|
installs globally, but every one of its verbs reads a per-project `PRODUCT.md`
|
||||||
|
that only `/impeccable init` writes. Without it the skill runs on invented
|
||||||
|
context, which is worse than not running it — and nothing else in the process
|
||||||
|
says so, because init has to happen in the agent chat, not in an installer.
|
||||||
|
|
||||||
|
**Fires when BOTH hold** — else stay silent:
|
||||||
|
|
||||||
|
1. impeccable is active (`skills/impeccable` present, i.e. it did not trip §3).
|
||||||
|
2. The project has no `PRODUCT.md` at its root.
|
||||||
|
|
||||||
|
Evaluate it on the same path as §4: after the toolchain resolves, never on the
|
||||||
|
INCOMPLETE stop path. One line, non-blocking:
|
||||||
|
|
||||||
|
🧭 impeccable has no project context here (no PRODUCT.md) — run `/impeccable init` first? (optional)
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
|
||||||
|
- Non-blocking, and never run `init` unprompted: it interviews the user about
|
||||||
|
the product, so it needs their attention, not their absence.
|
||||||
|
- One line per session at most. A refusal is an answer; do not re-ask inside
|
||||||
|
the same task.
|
||||||
|
- Skip entirely for a review/audit of a single component and for any non-UI
|
||||||
|
work. This is for Build and design-system tiers.
|
||||||
|
|
||||||
### Other toolchains
|
### Other toolchains
|
||||||
|
|
||||||
The script defaults to the `design` profile. A task needing another profile's
|
The script defaults to the `design` profile. A task needing another profile's
|
||||||
@@ -149,8 +177,8 @@ remedy is always `/profile <that>` — a profile, never a lone tool.
|
|||||||
|
|
||||||
- Remedy is ALWAYS a profile (`/profile design`), never an atomic tool toggle —
|
- 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.
|
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
|
- the `21st` CLI is REQUIRED (it trips the gate) and `/profile design` cannot
|
||||||
if `MAGIC_API_KEY` is in `~/.claude/.env` — the gate says so; surface that to the user.
|
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 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,
|
the `# GATE-BLOCK:` line(s) — edit there to add/remove a blocking design tool,
|
||||||
not in the script.
|
not in the script.
|
||||||
|
|||||||
+34
-9
@@ -29,12 +29,13 @@
|
|||||||
# required-manual required but the profile can't flip it silently (API
|
# required-manual required but the profile can't flip it silently (API
|
||||||
# key / external install) — the gate STILL trips, names
|
# key / external install) — the gate STILL trips, names
|
||||||
# it, and the remedy is `/profile design` + a manual step.
|
# 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
|
# Both classes trip the gate. Tools NOT on the GATE-BLOCK allowlist are
|
||||||
# ignored entirely (browser/plan/shotgun tooling, graphify).
|
# ignored entirely (browser/plan/shotgun tooling, graphify).
|
||||||
#
|
#
|
||||||
# disabledMcpServers is NEVER read — unreliable for bi-modal servers
|
# 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.
|
# Exit: 0 = ready · 11 = ready-but-unverified (proceed, say so) · 10 = incomplete (trips) · 2 = error.
|
||||||
# Usage: design-tool-gate.sh [profile] (default profile: design)
|
# Usage: design-tool-gate.sh [profile] (default profile: design)
|
||||||
@@ -79,6 +80,30 @@ ensure_claude_on_path() {
|
|||||||
}
|
}
|
||||||
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).
|
# Gate scope: the "# GATE-BLOCK:" allowlist (one or more lines, concatenated).
|
||||||
# Empty => fall back to "every gate-relevant entry is in scope" (coarse).
|
# Empty => fall back to "every gate-relevant entry is in scope" (coarse).
|
||||||
core_set="$(grep '^# GATE-BLOCK:' "$PROFILE_FILE" 2>/dev/null \
|
core_set="$(grep '^# GATE-BLOCK:' "$PROFILE_FILE" 2>/dev/null \
|
||||||
@@ -143,8 +168,8 @@ done <<< "$plain"
|
|||||||
# Verdict — three outcomes:
|
# Verdict — three outcomes:
|
||||||
# blocking/manual non-empty -> INCOMPLETE (exit 10): the gate trips.
|
# blocking/manual non-empty -> INCOMPLETE (exit 10): the gate trips.
|
||||||
# only unverified non-empty -> READY BUT UNVERIFIED (exit 11): fail-VISIBLE.
|
# only unverified non-empty -> READY BUT UNVERIFIED (exit 11): fail-VISIBLE.
|
||||||
# claude was unreachable, so the plugin/MCP (magic, ui-ux-pro-max) could
|
# claude was unreachable, so the plugin channel (ui-ux-pro-max) could not
|
||||||
# not be checked. Never pass this as a silent READY — proceed, but say so.
|
# be checked. Never pass this as a silent READY — proceed, but say so.
|
||||||
# nothing pending -> READY (exit 0).
|
# nothing pending -> READY (exit 0).
|
||||||
if [ "${#blocking[@]}" -gt 0 ] || [ "${#manual[@]}" -gt 0 ]; then
|
if [ "${#blocking[@]}" -gt 0 ] || [ "${#manual[@]}" -gt 0 ]; then
|
||||||
echo "design toolchain: INCOMPLETE"
|
echo "design toolchain: INCOMPLETE"
|
||||||
@@ -152,9 +177,9 @@ if [ "${#blocking[@]}" -gt 0 ] || [ "${#manual[@]}" -gt 0 ]; then
|
|||||||
echo " activate with /profile $PROFILE: ${blocking[*]}"
|
echo " activate with /profile $PROFILE: ${blocking[*]}"
|
||||||
fi
|
fi
|
||||||
if [ "${#manual[@]}" -gt 0 ]; then
|
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
|
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
|
esac
|
||||||
fi
|
fi
|
||||||
if [ "${#unverified[@]}" -gt 0 ]; then
|
if [ "${#unverified[@]}" -gt 0 ]; then
|
||||||
@@ -167,9 +192,9 @@ fi
|
|||||||
if [ "${#unverified[@]}" -gt 0 ]; then
|
if [ "${#unverified[@]}" -gt 0 ]; then
|
||||||
echo "design toolchain: READY BUT UNVERIFIED — ${#unverified[@]} tool(s) not checked"
|
echo "design toolchain: READY BUT UNVERIFIED — ${#unverified[@]} tool(s) not checked"
|
||||||
echo " unverified (claude CLI unreachable): ${unverified[*]}"
|
echo " unverified (claude CLI unreachable): ${unverified[*]}"
|
||||||
echo " the gate could NOT confirm the design plugin/MCP (e.g. magic,"
|
echo " the gate could NOT confirm the design plugin (ui-ux-pro-max) is"
|
||||||
echo " ui-ux-pro-max) are active. Proceed only after checking manually:"
|
echo " active. Proceed only after checking manually:"
|
||||||
echo " claude mcp list claude plugin list"
|
echo " claude plugin list"
|
||||||
exit 11
|
exit 11
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
+15
-20
@@ -11,7 +11,7 @@
|
|||||||
# Mechanism:
|
# Mechanism:
|
||||||
# - Skills (gstack/external/personal): symlink toggle skills/ ↔ skills-disabled/
|
# - Skills (gstack/external/personal): symlink toggle skills/ ↔ skills-disabled/
|
||||||
# - Plugins: `claude plugin enable|disable <name>@<marketplace>`
|
# - 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
|
# advisory otherwise
|
||||||
# - CLIs: advisory only (rtk, gsd, ctx7, graphify — installed externally)
|
# - CLIs: advisory only (rtk, gsd, ctx7, graphify — installed externally)
|
||||||
# - `set` is SYMMETRIC on managed items (BDR-079): plugins, external packs
|
# - `set` is SYMMETRIC on managed items (BDR-079): plugins, external packs
|
||||||
@@ -51,7 +51,6 @@ SKILLS_DIR="$REPO/skills"
|
|||||||
DISABLED_DIR="$REPO/skills-disabled"
|
DISABLED_DIR="$REPO/skills-disabled"
|
||||||
GSTACK_SRC="$REPO/skills-external/gstack" # gstack submodule — source of truth for gstack skills
|
GSTACK_SRC="$REPO/skills-external/gstack" # gstack submodule — source of truth for gstack skills
|
||||||
PROFILES_DIR="$REPO/lib/profiles"
|
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)
|
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
|
# Plugins that are toggle-managed by `set`. Anything NOT in this list is
|
||||||
@@ -73,13 +72,20 @@ MANAGED_EXTERNALS=(
|
|||||||
frontend-design
|
frontend-design
|
||||||
design-motion-principles
|
design-motion-principles
|
||||||
impeccable
|
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
|
# MCP servers that are toggle-managed by `set`, both ways (enable AND
|
||||||
# disable), delegated to lib/toggle-external.sh. Same allowlist doctrine.
|
# disable), delegated to lib/toggle-external.sh. Same allowlist doctrine.
|
||||||
MANAGED_MCPS=(
|
# Empty since 2026-09-22: `magic` was the only entry and 21st.dev replaced
|
||||||
magic
|
# 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
|
# Plugins that MUST stay enabled — `set` will refuse to disable these even if
|
||||||
# they're not in the profile. (Defensive: belt-and-suspenders alongside
|
# they're not in the profile. (Defensive: belt-and-suspenders alongside
|
||||||
@@ -324,15 +330,12 @@ enable_skill() {
|
|||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
mcp)
|
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
|
if [ "$(skill_status "$skill" mcp)" = "enabled" ]; then
|
||||||
: # already on
|
: # 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
|
else
|
||||||
info "MCP '$skill' not registered — run: claude mcp add $skill -- <command>"
|
info "MCP '$skill' not registered — run: claude mcp add $skill -- <command>"
|
||||||
fi
|
fi
|
||||||
@@ -394,15 +397,7 @@ disable_skill() {
|
|||||||
info "plugin '$skill' — manual: claude plugin disable $skill@<marketplace>"
|
info "plugin '$skill' — manual: claude plugin disable $skill@<marketplace>"
|
||||||
;;
|
;;
|
||||||
mcp)
|
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"
|
info "MCP '$skill' — manual: claude mcp remove $skill"
|
||||||
fi
|
|
||||||
;;
|
;;
|
||||||
cli)
|
cli)
|
||||||
: # never auto-uninstall CLIs
|
: # never auto-uninstall CLIs
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
# tooling, graphify) is bundled for convenience but never blocks. Keep these
|
# tooling, graphify) is bundled for convenience but never blocks. Keep these
|
||||||
# lines in sync when adding/removing a core design tool.
|
# 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: 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)
|
# Core design skills (gstack)
|
||||||
design-shotgun
|
design-shotgun
|
||||||
@@ -30,11 +31,19 @@ frontend-design external
|
|||||||
design-motion-principles external
|
design-motion-principles external
|
||||||
impeccable 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)
|
# Plugin (auto-toggle)
|
||||||
ui-ux-pro-max plugin@ui-ux-pro-max-skill
|
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)
|
# 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
|
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
|
# claude plugin enable pr-review-toolkit@claude-code-plugins
|
||||||
# or profile-based: bash lib/profile.sh apply audit (audit.profile keeps it;
|
# or profile-based: bash lib/profile.sh apply audit (audit.profile keeps it;
|
||||||
# a later `set full` re-disables it — MANAGED_PLUGINS lifecycle).
|
# 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) =================================================
|
# === CLIs (advisory) =================================================
|
||||||
|
21st cli
|
||||||
ctx7 cli
|
ctx7 cli
|
||||||
graphify cli
|
graphify cli
|
||||||
gsd cli
|
gsd cli
|
||||||
|
|||||||
@@ -49,9 +49,14 @@ emil-design-eng external
|
|||||||
frontend-design external
|
frontend-design external
|
||||||
design-motion-principles external
|
design-motion-principles external
|
||||||
impeccable 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
|
ui-ux-pro-max plugin@ui-ux-pro-max-skill
|
||||||
magic mcp
|
|
||||||
|
|
||||||
# === CLIs (advisory) =================================================
|
# === CLIs ============================================================
|
||||||
|
21st cli
|
||||||
ctx7 cli
|
ctx7 cli
|
||||||
graphify cli
|
graphify cli
|
||||||
|
|||||||
@@ -38,11 +38,19 @@ frontend-design external
|
|||||||
design-motion-principles external
|
design-motion-principles external
|
||||||
impeccable 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)
|
# Plugin: UI/UX intelligence (auto-toggle)
|
||||||
ui-ux-pro-max plugin@ui-ux-pro-max-skill
|
ui-ux-pro-max plugin@ui-ux-pro-max-skill
|
||||||
|
|
||||||
# MCP: 21st-dev Magic component generator
|
# CLI: 21st.dev component catalog + UI generation (needs `21st login`)
|
||||||
magic mcp
|
21st cli
|
||||||
|
|
||||||
# CLI: ctx7 (doc lookup for fast-evolving libs like Next.js)
|
# CLI: ctx7 (doc lookup for fast-evolving libs like Next.js)
|
||||||
ctx7 cli
|
ctx7 cli
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# lib/tests/profile-set-managed.test.sh — `set` symmetry on managed
|
# 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.
|
# Hermetic: fixture repo via *_REPO_OVERRIDE + fake `claude` on PATH.
|
||||||
set -u
|
set -u
|
||||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
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
|
FX="$(mktemp -d)"; trap 'rm -rf "$FX"' EXIT
|
||||||
mkdir -p "$FX/skills" "$FX/skills-disabled" "$FX/lib/profiles" "$FX/bin" \
|
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
|
for g in gs-a gs-b gs-c; do
|
||||||
mkdir -p "$FX/skills-external/gstack/$g"
|
mkdir -p "$FX/skills-external/gstack/$g"
|
||||||
touch "$FX/skills-external/gstack/$g/SKILL.md"
|
touch "$FX/skills-external/gstack/$g/SKILL.md"
|
||||||
done
|
done
|
||||||
cp "$ROOT/lib/profile.sh" "$ROOT/lib/toggle-external.sh" "$FX/lib/"
|
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.
|
# Non-managed external, enabled from the start — must never be touched.
|
||||||
ln -s "$FX/skills-external/other-ext" "$FX/skills/other-ext"
|
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-a
|
||||||
gs-b
|
gs-b
|
||||||
emil-design-eng external
|
emil-design-eng external
|
||||||
magic mcp
|
21st-ui-build external
|
||||||
EOF
|
EOF
|
||||||
cat > "$FX/lib/profiles/backendish.profile" <<'EOF'
|
cat > "$FX/lib/profiles/backendish.profile" <<'EOF'
|
||||||
gs-c
|
gs-c
|
||||||
EOF
|
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
|
cat > "$FX/bin/claude" <<EOF
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
FX="$FX"
|
FX="$FX"
|
||||||
echo "\$*" >> "\$FX/claude-calls.log"
|
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
|
exit 0
|
||||||
EOF
|
EOF
|
||||||
chmod +x "$FX/bin/claude"
|
chmod +x "$FX/bin/claude"
|
||||||
@@ -48,14 +47,14 @@ chmod +x "$FX/bin/claude"
|
|||||||
run() { PATH="$FX/bin:$PATH" PROFILE_REPO_OVERRIDE="$FX" \
|
run() { PATH="$FX/bin:$PATH" PROFILE_REPO_OVERRIDE="$FX" \
|
||||||
TOGGLE_EXTERNAL_REPO_OVERRIDE="$FX" bash "$FX/lib/profile.sh" "$@"; }
|
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
|
run set designish >/dev/null 2>&1
|
||||||
check T1-gsa-on "$([ -e "$FX/skills/gs-a" ] && echo on || echo off)" on
|
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 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 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 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 T5-21st-src "$([ -L "$FX/skills/21st-ui-build" ] && echo on || echo off)" on
|
||||||
check T6-add-call "$(grep -c '^mcp add magic' "$FX/claude-calls.log")" 1
|
check T6-no-mcp "$(grep -c '^mcp ' "$FX/claude-calls.log" || true)" 0
|
||||||
|
|
||||||
# --- set backendish: managed leftovers parked/unregistered ---
|
# --- set backendish: managed leftovers parked/unregistered ---
|
||||||
run set backendish >/dev/null 2>&1
|
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 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 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 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 T11-21st-off "$([ -e "$FX/skills/21st-ui-build" ] && echo on || echo off)" off
|
||||||
check T12-rm-call "$(grep -c '^mcp remove magic' "$FX/claude-calls.log")" 1
|
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
|
check T13-other-untouched "$([ -e "$FX/skills/other-ext" ] && echo on || echo off)" on
|
||||||
|
|
||||||
# --- back to designish: parked external restored (not re-sourced) ---
|
# --- back to designish: parked external restored (not re-sourced) ---
|
||||||
run set designish >/dev/null 2>&1
|
run set designish >/dev/null 2>&1
|
||||||
check T14-emil-back "$([ -e "$FX/skills/emil-design-eng" ] && echo on || echo off)" on
|
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 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 ]
|
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
|
||||||
|
|||||||
+59
-39
@@ -8,7 +8,7 @@
|
|||||||
# as symlinks inside skills/. This script moves those symlinks
|
# as symlinks inside skills/. This script moves those symlinks
|
||||||
# to/from skills-disabled/ so Claude Code stops/starts scanning them.
|
# 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:
|
# Usage:
|
||||||
# toggle-external.sh list
|
# toggle-external.sh list
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
# gstack — per-skill symlinks populated by gstack's own setup
|
# gstack — per-skill symlinks populated by gstack's own setup
|
||||||
# emil-design-eng — single symlink → skills-external/emil-design-eng
|
# emil-design-eng — single symlink → skills-external/emil-design-eng
|
||||||
# darwin-skill — single symlink → ~/.agents/skills/darwin-skill
|
# 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
|
# For fine-grained activation (only design skills, only qa skills, only
|
||||||
# audit skills, etc.) instead of all-or-nothing gstack toggling, use:
|
# 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"; }
|
err() { echo -e "${RED}✗${NC} $1"; }
|
||||||
|
|
||||||
# All non-plugin tools this script can toggle.
|
# 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.
|
# Prints the skill names that belong to the "21st" pack. Source of truth:
|
||||||
# Called only by the magic branch — other tools don't need env vars.
|
# skills-external/21st-* — the `21st skills install` run in install-plugins.sh
|
||||||
load_env() {
|
# owns that list, so adding a skill upstream needs no edit here.
|
||||||
if [ -z "${MAGIC_API_KEY:-}" ] && [ -f "$REPO/.env" ]; then
|
twentyfirst_skills() {
|
||||||
set -a
|
local d
|
||||||
# shellcheck source=/dev/null
|
for d in "$REPO"/skills-external/21st-*/; do
|
||||||
source "$REPO/.env"
|
[ -f "${d}SKILL.md" ] || continue
|
||||||
set +a
|
basename "$d"
|
||||||
fi
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# Prints the names (directory basenames) that belong to "gstack".
|
# Prints the names (directory basenames) that belong to "gstack".
|
||||||
@@ -84,13 +84,13 @@ status_tool() {
|
|||||||
[ -d "$HOME/.agents/skills/$tool" ] || { echo "missing"; return; }
|
[ -d "$HOME/.agents/skills/$tool" ] || { echo "missing"; return; }
|
||||||
[ -e "$SKILLS_DIR/$tool" ] && echo "enabled" || echo "disabled"
|
[ -e "$SKILLS_DIR/$tool" ] && echo "enabled" || echo "disabled"
|
||||||
;;
|
;;
|
||||||
magic)
|
21st)
|
||||||
command -v claude >/dev/null || { echo "missing"; return; }
|
local installed=0
|
||||||
if claude mcp list 2>/dev/null | grep -q '^magic:'; then
|
while read -r name; do
|
||||||
echo "enabled"
|
installed=1
|
||||||
else
|
[ -e "$SKILLS_DIR/$name" ] && { echo "enabled"; return; }
|
||||||
echo "disabled"
|
done < <(twentyfirst_skills)
|
||||||
fi
|
[ "$installed" -eq 1 ] && echo "disabled" || echo "missing"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "unknown"; return 1 ;;
|
echo "unknown"; return 1 ;;
|
||||||
@@ -124,12 +124,20 @@ disable_tool() {
|
|||||||
warn "$tool already disabled"
|
warn "$tool already disabled"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
magic)
|
21st)
|
||||||
if [ "$(status_tool magic)" = "enabled" ]; then
|
# Parked under the plain skill name — same convention as the other
|
||||||
claude mcp remove magic -s user >/dev/null
|
# externals, so profile.sh's park/restore path stays interoperable.
|
||||||
ok "magic disabled"
|
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
|
else
|
||||||
warn "magic already disabled"
|
warn "21st already disabled"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
*) err "Unknown tool: $tool"; return 1 ;;
|
*) err "Unknown tool: $tool"; return 1 ;;
|
||||||
@@ -177,25 +185,37 @@ enable_tool() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
magic)
|
21st)
|
||||||
load_env
|
local restored=0 linked=0
|
||||||
if [ -z "${MAGIC_API_KEY:-}" ]; then
|
while read -r name; do
|
||||||
err "MAGIC_API_KEY not set — add it to ~/.claude/.env (template: .env.example)"
|
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
|
return 1
|
||||||
fi
|
fi
|
||||||
if [ "$(status_tool magic)" = "enabled" ]; then
|
warn "21st already enabled"
|
||||||
warn "magic already enabled"
|
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
# Reference, not value: Claude Code expands ${VAR} in mcpServers.env at
|
ok "21st enabled ($((restored + linked)) skills: $restored restored, $linked linked)"
|
||||||
# launch (job7/BDR-026) — MAGIC_API_KEY itself never lands in
|
# The skills shell out to the CLI; without it (or without a session)
|
||||||
# ~/.claude.json. The check above still confirms the var IS set in
|
# they can only report failure. Warn, never block — the pack is still
|
||||||
# ~/.claude/.env before wiring the reference, so a missing key fails
|
# correctly wired and `make plugin` installs the CLI.
|
||||||
# here instead of silently at Claude Code startup.
|
if ! command -v 21st >/dev/null 2>&1; then
|
||||||
claude mcp add magic --scope user \
|
warn "the \`21st\` CLI is not on PATH — install it: npm i -g @21st-dev/cli"
|
||||||
--env 'API_KEY=${MAGIC_API_KEY}' \
|
elif ! 21st whoami 2>/dev/null | grep -q '^Logged in as '; then
|
||||||
-- npx -y @21st-dev/magic@latest
|
warn "not signed in to 21st — component retrieval and 21st AI need: 21st login"
|
||||||
ok "magic enabled (user scope)"
|
fi
|
||||||
;;
|
;;
|
||||||
*) err "Unknown tool: $tool"; return 1 ;;
|
*) err "Unknown tool: $tool"; return 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -71,7 +71,10 @@ if [ -d "$GSTACK_SRC/browse/dist" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
EXTERNAL_SKILLS=(emil-design-eng frontend-design design-motion-principles impeccable)
|
# impeccable is NOT here: its installer writes the skill straight into
|
||||||
|
# skills/ (and its agents into agents/) at --scope=global, so there is no
|
||||||
|
# skills-external/ copy to symlink. See install-plugins.sh Step 8d.
|
||||||
|
EXTERNAL_SKILLS=(emil-design-eng frontend-design design-motion-principles)
|
||||||
for _ext_skill in "${EXTERNAL_SKILLS[@]}"; do
|
for _ext_skill in "${EXTERNAL_SKILLS[@]}"; do
|
||||||
if [ -d "$REPO/skills-external/$_ext_skill" ]; then
|
if [ -d "$REPO/skills-external/$_ext_skill" ]; then
|
||||||
if [ -L "$CLAUDE/skills/$_ext_skill" ] && [ "$(readlink "$CLAUDE/skills/$_ext_skill")" = "$REPO/skills-external/$_ext_skill" ]; then
|
if [ -L "$CLAUDE/skills/$_ext_skill" ] && [ "$(readlink "$CLAUDE/skills/$_ext_skill")" = "$REPO/skills-external/$_ext_skill" ]; then
|
||||||
@@ -117,8 +120,6 @@ link_env() {
|
|||||||
echo " cp \"$REPO/.env.example\" \"$home_env\" && \"\${EDITOR:-nano}\" \"$home_env\""
|
echo " cp \"$REPO/.env.example\" \"$home_env\" && \"\${EDITOR:-nano}\" \"$home_env\""
|
||||||
return
|
return
|
||||||
fi
|
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
|
if [ -L "$repo_env" ]; then
|
||||||
[ "$(readlink "$repo_env")" = "$home_env" ] && return
|
[ "$(readlink "$repo_env")" = "$home_env" ] && return
|
||||||
ln -sf "$home_env" "$repo_env"; CHANGED=$((CHANGED + 1))
|
ln -sf "$home_env" "$repo_env"; CHANGED=$((CHANGED + 1))
|
||||||
|
|||||||
+7
-2
@@ -20,6 +20,11 @@
|
|||||||
"version": "latest",
|
"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\"."
|
"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": {
|
"graphifyy": {
|
||||||
"source": "pypi:graphifyy",
|
"source": "pypi:graphifyy",
|
||||||
"version": "latest",
|
"version": "latest",
|
||||||
@@ -40,7 +45,7 @@
|
|||||||
},
|
},
|
||||||
"impeccable": {
|
"impeccable": {
|
||||||
"source": "npm:impeccable",
|
"source": "npm:impeccable",
|
||||||
"version": "3.2.0",
|
"version": "4.1.0",
|
||||||
"note": "Design anti-pattern detector (45 deterministic rules, CLI 'impeccable detect', exit 0/2) + /impeccable skill (23 verbs) by pbakaus. Pin = CLI version; the skill dist has its own release track fetched by 'skills install'. Pinned for audit reproducibility (LRN-077 class: a rules update silently changes audit output). Requires Node >= 24 — install step skips gracefully below that. Machine-owned: synced to skills-external/impeccable/ (gitignored), symlinked by link.sh."
|
"note": "Design anti-pattern detector (45 deterministic rules, CLI 'impeccable detect', exit 0/2) + /impeccable skill (23 verbs) + 4 impeccable-* subagents, by pbakaus. Pin = CLI version ONLY: the skill dist and the engine binary have their own release tracks, fetched by 'skills install' at install time, so this pin does not freeze audit output the way a semgrep pin does. It still gates the CLI deliberately (LRN-077 class). BEWARE: the pin rots — the CLI downloads its skill dist at install time and an older release's artifact disappears upstream (3.2.0 -> 'Download failed: invalid zip data', 2026-09-22, which left 'make plugin' printing a run-it-yourself warning). install-plugins.sh Step 8d and update-all.sh therefore fall back to @latest on a pin failure and warn to bump this version. Requires Node >= 24. Installed at --scope=global: lands in ~/.claude/skills/impeccable + ~/.claude/agents/impeccable-*.md, both symlinks into this repo, both gitignored."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-6
@@ -235,11 +235,7 @@
|
|||||||
"WebFetch",
|
"WebFetch",
|
||||||
"Bash(git stash pop*)",
|
"Bash(git stash pop*)",
|
||||||
"Bash(git stash drop*)",
|
"Bash(git stash drop*)",
|
||||||
"Bash(git stash clear)",
|
"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"
|
|
||||||
],
|
],
|
||||||
"defaultMode": "auto",
|
"defaultMode": "auto",
|
||||||
"disableBypassPermissionsMode": "disable",
|
"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.",
|
"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.",
|
"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.",
|
"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 <pkg>`, `pnpm dlx` or `yarn dlx` of a package absent from the manifest and lockfile runs code fetched at call time; `npm install <name>` or `pnpm add <name>` adds a dependency the house rule requires naming first. Clear only when the user named the package in this turn."
|
"Undeclared node packages: `npx <pkg>`, `pnpm dlx` or `yarn dlx` of a package absent from the manifest and lockfile runs code fetched at call time; `npm install <name>` or `pnpm add <name>` 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": [
|
"hard_deny": [
|
||||||
"$defaults",
|
"$defaults",
|
||||||
|
|||||||
+15
-14
@@ -52,8 +52,7 @@ lists items + types:
|
|||||||
| `personal` | symlink move skills/ ↔ skills-disabled/\<name\> (no prefix) |
|
| `personal` | symlink move skills/ ↔ skills-disabled/\<name\> (no prefix) |
|
||||||
| `external` | symlink move skills/ ↔ skills-disabled/\<name\> |
|
| `external` | symlink move skills/ ↔ skills-disabled/\<name\> |
|
||||||
| `plugin@<marketplace>` | `claude plugin enable\|disable <name>@<marketplace>` (auto) |
|
| `plugin@<marketplace>` | `claude plugin enable\|disable <name>@<marketplace>` (auto) |
|
||||||
| `mcp` (known: magic) | delegate to `lib/toggle-external.sh` (uses `.env`) |
|
| `mcp` | advisory — prints manual `claude mcp add …` command (no server is managed today: `MANAGED_MCPS` is empty since 21st.dev moved to a CLI) |
|
||||||
| `mcp` (other) | advisory — prints manual `claude mcp add …` command |
|
|
||||||
| `cli` | advisory only — reports installed/not-installed |
|
| `cli` | advisory only — reports installed/not-installed |
|
||||||
|
|
||||||
**Always-on plugins** (`security-guidance`, `superpowers`) are
|
**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`,
|
`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.
|
`pr-review-toolkit@claude-code-plugins`. Other plugins are never auto-toggled.
|
||||||
**Managed externals** (`emil-design-eng`, `frontend-design`,
|
**Managed externals** (`emil-design-eng`, `frontend-design`,
|
||||||
`design-motion-principles`, `impeccable`) and **managed MCPs** (`magic`)
|
`design-motion-principles`, `impeccable`, and the five 21st design skills
|
||||||
follow the same symmetry (BDR-079): `set` enables them when the profile
|
`21st-ui-build`, `21st-ui-explore`, `21st-ui-review`, `21st-cli-use`,
|
||||||
lists them (from parked state, or from `skills-external/` if the symlink
|
`21st-ai`) follow the same symmetry (BDR-079): `set` enables them when the
|
||||||
never existed) and parks/unregisters them when it does not — e.g. `set
|
profile lists them (from parked state, or from `skills-external/` if the
|
||||||
backend` after design work turns emil and magic off. `darwin-skill` and any
|
symlink never existed) and parks them when it does not — e.g. `set backend`
|
||||||
other unlisted external are never auto-touched. gstack works the same
|
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
|
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.
|
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
|
update-check, learnings — script doesn't touch that infra. Disabled skills
|
||||||
are just hidden from Claude Code's scanner; the gstack repo stays installed.
|
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,
|
- 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,
|
plugin-dev, pr-review-toolkit) and the managed external packs
|
||||||
frontend-design, design-motion-principles, impeccable) and the `magic` MCP —
|
(emil-design-eng, frontend-design, design-motion-principles, impeccable,
|
||||||
in BOTH directions: `set` enables what the profile lists and disables the
|
the 21st design skills) — in BOTH directions: `set` enables what the profile
|
||||||
managed leftovers it doesn't (BDR-008, BDR-079). Anything outside those
|
lists and disables the managed leftovers it doesn't (BDR-008, BDR-079).
|
||||||
allowlists stays manual: `claude plugin enable|disable`, `claude mcp
|
Anything outside those allowlists stays manual: `claude plugin
|
||||||
add|remove`.
|
enable|disable`, `bash lib/toggle-external.sh enable|disable <tool>`.
|
||||||
- `set` is destructive in the sense that it disables non-listed gstack skills.
|
- `set` is destructive in the sense that it disables non-listed gstack skills.
|
||||||
Use `apply` if the user wants additive behavior.
|
Use `apply` if the user wants additive behavior.
|
||||||
|
|||||||
+98
-21
@@ -321,8 +321,6 @@ else
|
|||||||
info "bun not installed — skipping"
|
info "bun not installed — skipping"
|
||||||
fi
|
fi
|
||||||
# NOT updated here, deliberately (audit 2026-07-02):
|
# 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
|
# - graphify Claude integration (`graphify claude install`): rewrites curated
|
||||||
# CLAUDE.md / .claude/settings.json (BDR-028 guard territory) — re-run
|
# CLAUDE.md / .claude/settings.json (BDR-028 guard territory) — re-run
|
||||||
# MANUALLY only if a graphify upgrade changes its hook format.
|
# MANUALLY only if a graphify upgrade changes its hook format.
|
||||||
@@ -381,11 +379,34 @@ else
|
|||||||
info "design-motion-principles not installed — skipping"
|
info "design-motion-principles not installed — skipping"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Impeccable (design anti-pattern detector + skill) ──
|
# ── Impeccable (design detector + skill + subagents) ──
|
||||||
|
# Global scope: the installer writes through the ~/.claude/{skills,agents}
|
||||||
|
# symlinks straight into this repo (install-plugins.sh Step 8d explains why
|
||||||
|
# staging + project scope was wrong). The pin can rot upstream, so a pinned
|
||||||
|
# failure falls back to @latest rather than leaving the tool stale forever.
|
||||||
|
#
|
||||||
|
# One install attempt. $1 = "latest" or an exact version. On failure, IMP_FAIL
|
||||||
|
# holds the reason. Same helper as Step 8d: with a copy already in place a
|
||||||
|
# rotted pin exits 0 and says "Could not check for skill updates … left
|
||||||
|
# unchanged", so the exit code cannot tell it from an up-to-date no-op.
|
||||||
|
imp_install() {
|
||||||
|
local pkg="impeccable" out rc=0
|
||||||
|
[ "$1" != "latest" ] && pkg="impeccable@$1"
|
||||||
|
out=$(npx -y "$pkg" skills install -y --providers=claude --scope=global \
|
||||||
|
--no-hooks 2>&1) || rc=$?
|
||||||
|
IMP_FAIL=$(printf '%s\n' "$out" \
|
||||||
|
| grep -E 'Download failed|Could not check for skill updates' \
|
||||||
|
| head -1 || true)
|
||||||
|
if [ "$rc" -ne 0 ] && [ -z "$IMP_FAIL" ]; then
|
||||||
|
IMP_FAIL="installer exited $rc"
|
||||||
|
fi
|
||||||
|
[ -z "$IMP_FAIL" ]
|
||||||
|
}
|
||||||
echo ""
|
echo ""
|
||||||
echo "── Updating impeccable..."
|
echo "── Updating impeccable..."
|
||||||
IMP_DIR="$REPO/skills-external/impeccable"
|
IMP_SKILL_DIR="$HOME/.claude/skills/impeccable"
|
||||||
if [ ! -f "$IMP_DIR/SKILL.md" ]; then
|
IMP_PARKED="$REPO/skills-disabled/impeccable"
|
||||||
|
if [ ! -f "$IMP_SKILL_DIR/SKILL.md" ] && [ ! -f "$IMP_PARKED/SKILL.md" ]; then
|
||||||
info "impeccable not installed — skipping (run: make plugin)"
|
info "impeccable not installed — skipping (run: make plugin)"
|
||||||
else
|
else
|
||||||
IMP_VER=""
|
IMP_VER=""
|
||||||
@@ -399,29 +420,85 @@ print(d.get('impeccable',{}).get('version','latest'))
|
|||||||
fi
|
fi
|
||||||
IMP_NODE=$(node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1)
|
IMP_NODE=$(node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1)
|
||||||
if [ -z "${IMP_NODE:-}" ] || [ "$IMP_NODE" -lt 24 ]; then
|
if [ -z "${IMP_NODE:-}" ] || [ "$IMP_NODE" -lt 24 ]; then
|
||||||
info "impeccable update skipped — needs Node >= 24 (found ${IMP_NODE:-none}); existing dist kept"
|
info "impeccable update skipped — needs Node >= 24 (found ${IMP_NODE:-none}); existing copy kept"
|
||||||
else
|
else
|
||||||
IMP_PKG="impeccable"
|
IMP_WAS_PARKED=false
|
||||||
# Pin honored (LRN-077 class: a silent rules update changes audit
|
[ -d "$IMP_PARKED" ] && IMP_WAS_PARKED=true
|
||||||
# output on unchanged code) — bump the pin deliberately, then update.
|
# Pin honored (LRN-077 class: a silent rules update changes audit output
|
||||||
[ -n "$IMP_VER" ] && [ "$IMP_VER" != "latest" ] && IMP_PKG="impeccable@${IMP_VER}"
|
# on unchanged code) — bump the pin deliberately, then update.
|
||||||
IMP_STAGE=$(mktemp -d)
|
IMP_PIN="${IMP_VER:-latest}"
|
||||||
if (cd "$IMP_STAGE" && npx -y "$IMP_PKG" skills install -y --providers=claude --scope=project --no-hooks >/dev/null 2>&1); then
|
IMP_OK=false
|
||||||
IMP_SRC=$(find "$IMP_STAGE" -type d -name impeccable -path "*skills*" 2>/dev/null | head -1)
|
if imp_install "$IMP_PIN"; then
|
||||||
if [ -n "$IMP_SRC" ] && [ -f "$IMP_SRC/SKILL.md" ]; then
|
IMP_OK=true
|
||||||
rm -rf "$IMP_DIR"
|
elif [ "$IMP_PIN" != "latest" ]; then
|
||||||
mv "$IMP_SRC" "$IMP_DIR"
|
warn "impeccable@${IMP_PIN} did not install (${IMP_FAIL}) — that release's skill dist is gone upstream; trying @latest"
|
||||||
ok "impeccable refreshed (CLI ${IMP_VER:-latest})"
|
if imp_install latest; then
|
||||||
else
|
IMP_OK=true
|
||||||
warn "impeccable: installer produced no dist — existing kept"
|
warn "refreshed from @latest, not the pin — bump \"impeccable\".version in plugins.lock.json"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ "$IMP_OK" = true ] && [ -f "$IMP_SKILL_DIR/SKILL.md" ]; then
|
||||||
|
IMP_SKILL_VER=$(sed -n 's/^version:[[:space:]]*//p' "$IMP_SKILL_DIR/SKILL.md" | head -1)
|
||||||
|
ok "impeccable refreshed (CLI ${IMP_VER:-latest}, skill ${IMP_SKILL_VER:-?})"
|
||||||
|
if [ "$IMP_WAS_PARKED" = true ]; then
|
||||||
|
rm -rf "${IMP_PARKED:?}"
|
||||||
|
mv "$IMP_SKILL_DIR" "$IMP_PARKED"
|
||||||
|
info "impeccable was parked by a profile — refreshed copy returned to skills-disabled/"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
warn "impeccable refresh failed — existing dist kept"
|
warn "impeccable refresh failed (${IMP_FAIL:-no SKILL.md written}) — existing copy kept"
|
||||||
fi
|
fi
|
||||||
rm -rf "$IMP_STAGE"
|
|
||||||
fi
|
fi
|
||||||
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) ──
|
# ── 7.5. Update external skills (npx skills) ──
|
||||||
echo ""
|
echo ""
|
||||||
echo "── Updating external skills (npx skills)..."
|
echo "── Updating external skills (npx skills)..."
|
||||||
|
|||||||
Reference in New Issue
Block a user