Upstream supersedes `@21st-dev/magic` with `@21st-dev/cli` (bin `21st`): same endpoint, `21st login` in place of an API key, no MCP process loaded into every session. - install-plugins.sh Step 8.7: `npm i -g @21st-dev/cli` (pinned in plugins.lock.json), staged `21st skills install`, TTY-only login offer, pack disabled by default. update-all.sh 7.4 refreshes both. - The documented `21st install-skill` cannot be used: the installer refuses to follow a symlink on the target path and `~/.claude/skills` is one. The install runs under a throwaway HOME and the result moves into skills-external/21st-* (gitignored), symlinked on demand. - toggle-external.sh manages `21st` as a pack (names globbed from skills-external/21st-*, parked under plain names). `magic` is gone. - The 5 design skills join design/web/web-full/full and MANAGED_EXTERNALS; 21st-registry and 21st-design-sync stay parked. MANAGED_MCPS is now empty and profile.sh's dead magic branches are removed. - Design gate: GATE-BLOCK gains `21st` (required-manual, magic's old slot) and `21st-ui-build`; PATH repair extended to the npm global bin. - settings.json: the 4 mcp__magic__* ask entries go; the outward-facing 21st verbs land in autoMode.soft_deny, the tier that holds under auto mode (LRN-153). - Docs: README, CLAUDE.global.md, design-gate.md, profile SKILL.md, .env.example, .gitleaks.toml, link.sh. BDR-093, LRN-158. Tests: profile-set-managed 17/17, make test green except 2 pre-existing gitflow FAILs (gitleaks binary absent on this host), shellcheck clean.
139 lines
4.9 KiB
Bash
139 lines
4.9 KiB
Bash
#!/usr/bin/env bash
|
|
# Symlink this repo into ~/.claude/
|
|
set -euo pipefail
|
|
|
|
REPO="$(cd "$(dirname "$0")" && pwd)"
|
|
CLAUDE="$HOME/.claude"
|
|
CHANGED=0
|
|
|
|
mkdir -p "$CLAUDE"
|
|
|
|
link_file() {
|
|
local src="$1" dst="$2"
|
|
if [ -L "$dst" ] && [ "$(readlink "$dst")" = "$src" ]; then
|
|
return # already correct
|
|
fi
|
|
ln -sf "$src" "$dst"
|
|
CHANGED=$((CHANGED + 1))
|
|
}
|
|
|
|
link_file "$REPO/CLAUDE.global.md" "$CLAUDE/CLAUDE.md"
|
|
link_file "$REPO/settings.json" "$CLAUDE/settings.json"
|
|
|
|
for item in hooks agents skills lib templates rules; do
|
|
target="$CLAUDE/$item"
|
|
if [ -L "$target" ]; then
|
|
if [ "$(readlink "$target")" = "$REPO/$item" ]; then
|
|
continue # already correct
|
|
fi
|
|
rm -f "$target"
|
|
elif [ -d "$target" ]; then
|
|
echo "⚠️ ~/.claude/$item is a real directory. Rename or remove it, then re-run link.sh."
|
|
continue
|
|
fi
|
|
ln -sf "$REPO/$item" "$target"
|
|
CHANGED=$((CHANGED + 1))
|
|
done
|
|
|
|
# GStack is exposed via per-skill symlinks under skills/ (browse,
|
|
# canary, autoplan, design-review, …) created by gstack's own
|
|
# `./setup`. A global `skills/gstack -> skills-external/gstack/`
|
|
# symlink duplicated the top-level gstack SKILL.md alongside those
|
|
# individual skills, producing two entries with the same description
|
|
# ("Fast headless browser for QA testing…"). Remove any stale global
|
|
# link — only per-skill entries remain.
|
|
if [ -L "$REPO/skills/gstack" ] || [ -L "$CLAUDE/skills/gstack" ]; then
|
|
rm -f "$REPO/skills/gstack" "$CLAUDE/skills/gstack"
|
|
CHANGED=$((CHANGED + 1))
|
|
fi
|
|
if [ ! -d "$REPO/skills-external/gstack" ]; then
|
|
echo "⚠️ GStack submodule not found — run: git submodule update --init"
|
|
fi
|
|
|
|
# GStack shared infrastructure: bin/ (CLI tools, config, analytics) and
|
|
# browse/dist/ (compiled browse binary). Per-skill SKILL.md symlinks don't
|
|
# expose these, but multiple skills hardcode ~/.claude/skills/gstack/bin/
|
|
# and ~/.claude/skills/gstack/browse/dist/. Create targeted symlinks.
|
|
GSTACK_SRC="$REPO/skills-external/gstack"
|
|
GSTACK_DST="$CLAUDE/skills/gstack"
|
|
if [ -d "$GSTACK_SRC/bin" ]; then
|
|
mkdir -p "$GSTACK_DST"
|
|
if [ ! -L "$GSTACK_DST/bin" ]; then
|
|
ln -sf "$GSTACK_SRC/bin" "$GSTACK_DST/bin"
|
|
CHANGED=$((CHANGED + 1))
|
|
fi
|
|
fi
|
|
if [ -d "$GSTACK_SRC/browse/dist" ]; then
|
|
mkdir -p "$GSTACK_DST/browse"
|
|
if [ ! -L "$GSTACK_DST/browse/dist" ]; then
|
|
ln -sf "$GSTACK_SRC/browse/dist" "$GSTACK_DST/browse/dist"
|
|
CHANGED=$((CHANGED + 1))
|
|
fi
|
|
fi
|
|
|
|
EXTERNAL_SKILLS=(emil-design-eng frontend-design design-motion-principles impeccable)
|
|
for _ext_skill in "${EXTERNAL_SKILLS[@]}"; do
|
|
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
|
|
: # already correct
|
|
else
|
|
ln -sf "$REPO/skills-external/$_ext_skill" "$CLAUDE/skills/$_ext_skill"
|
|
CHANGED=$((CHANGED + 1))
|
|
fi
|
|
else
|
|
echo "⚠️ $_ext_skill not found — run: make plugin"
|
|
fi
|
|
done
|
|
|
|
# External skills installed via `npx skills add` live under
|
|
# $HOME/.agents/skills/. We symlink them into $REPO/skills/ with
|
|
# absolute paths so the link stays valid regardless of where the
|
|
# repo is cloned (relative ../../ paths broke on repos deeper than
|
|
# one level below $HOME).
|
|
NPX_EXTERNAL_SKILLS=(darwin-skill)
|
|
for _ext in "${NPX_EXTERNAL_SKILLS[@]}"; do
|
|
_target="$HOME/.agents/skills/$_ext"
|
|
_link="$REPO/skills/$_ext"
|
|
if [ ! -d "$_target" ]; then
|
|
echo "⚠️ $_ext not installed at $_target — run: make plugin"
|
|
continue
|
|
fi
|
|
if [ -L "$_link" ] && [ "$(readlink "$_link")" = "$_target" ]; then
|
|
continue
|
|
fi
|
|
rm -f "$_link"
|
|
ln -sf "$_target" "$_link"
|
|
CHANGED=$((CHANGED + 1))
|
|
done
|
|
|
|
# ── Local secrets: repo/.env -> ~/.claude/.env ──────────────
|
|
# Real key lives in ~/.claude/.env (source of truth, outside the repo so the
|
|
# secret never enters the git tree). The repo reaches it via a symlink that
|
|
# `source "$REPO/.env"` follows transparently. Never creates/copies/prints it.
|
|
link_env() {
|
|
local home_env="$CLAUDE/.env" repo_env="$REPO/.env"
|
|
if [ ! -f "$home_env" ]; then
|
|
echo "⚠️ $home_env missing — create it (the repo never stores the secret):"
|
|
echo " cp \"$REPO/.env.example\" \"$home_env\" && \"\${EDITOR:-nano}\" \"$home_env\""
|
|
return
|
|
fi
|
|
if [ -L "$repo_env" ]; then
|
|
[ "$(readlink "$repo_env")" = "$home_env" ] && return
|
|
ln -sf "$home_env" "$repo_env"; CHANGED=$((CHANGED + 1))
|
|
elif [ ! -e "$repo_env" ]; then
|
|
ln -sf "$home_env" "$repo_env"; CHANGED=$((CHANGED + 1))
|
|
else
|
|
echo "⚠️ $repo_env is a real file, not a symlink."
|
|
echo " If it holds your secret: mv \"$repo_env\" \"$home_env\" then re-run link.sh"
|
|
echo " Otherwise remove it so link.sh can link to $home_env."
|
|
fi
|
|
}
|
|
link_env
|
|
|
|
if [ "$CHANGED" -eq 0 ]; then
|
|
echo "✅ All symlinks already up to date."
|
|
else
|
|
echo "✅ $CHANGED symlink(s) updated in ~/.claude/"
|
|
fi
|
|
echo " Next: bash install-plugins.sh"
|