refactor(plugin)!: ship as a Claude Code plugin, one dir link per harness
Skills move to skills/<name>/SKILL.md and the repo gains both manifests, so it is now a marketplace hosting exactly one plugin whose root is the repo root (source: "."). Claude Code installs it from git; no symlinks involved. Why: install.sh wired the library into every consuming repo as an ABSOLUTE symlink to $CHECKOUT_DIR. The checkout moved three times, most recently when local-dev@1966d5a dropped the embedded .skills/ copy, and nothing migrated the links — 243 dangling links across 12 repos, hidden from git by the **/.claude/skills/ ignore rule. Worse, ~/dev spans two path roots (/Users/mathias on the iCloud-shared Macs, /home/mathias on koala), so an absolute link stored there is broken on one side by construction. Re-running the installer cannot fix that; only removing the mechanism can. install.sh keeps the harnesses with no plugin system and drops from 42 links per harness to one directory link, via the convergent ~/.agents/skills convention that Pi and OpenCode both read: ~/.agents/skills -> $CHECKOUT_DIR/skills ~/.config/crush/skills -> ~/.agents/skills ~/.config/opencode/skills -> ~/.agents/skills wire_claude_repo is deleted outright rather than gated behind a flag — on a mixed-root fleet it cannot be made correct. Antigravity and Vibe targets go too; both unused. CHECKOUT_DIR keeps its host-local default deliberately: pointing it into ~/dev would put a git working tree inside iCloud with two concurrent writers. Taskfile drops five near-identical wiring loops (they had already drifted) and delegates to install.sh. `task check` gains frontmatter validation, a stray-root-entry gate, and manifest/tag version parity — the loose secret-hygiene.md sat unloadable for months because nothing checked layout. Verified: claude plugin validate . passes clean; a probe skill loaded as skills:zzprobe through --plugin-dir; task check green at 42 skills. Refs: #23 Bump-Type: minor Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "mathias",
|
||||
"description": "Mathias's private plugin marketplace, hosted on gitea.d-ma.be. Self-hosted by design — nothing here transits GitHub.",
|
||||
"owner": {
|
||||
"name": "Mathias",
|
||||
"url": "https://git.d-ma.be/mathias"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "skills",
|
||||
"source": ".",
|
||||
"description": "Engineering skills, the pre-task-ritual and secret-guard hooks, and the brain/gitea/infra MCP servers as one versioned unit."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "skills",
|
||||
"description": "Mathias's engineering skills library, plus the hooks and MCP servers that make it enforceable: TDD, clean code, debugging, specs, discovery, CI, and secret hygiene.",
|
||||
"version": "0.3.3",
|
||||
"author": {
|
||||
"name": "Mathias",
|
||||
"url": "https://git.d-ma.be/mathias"
|
||||
},
|
||||
"homepage": "https://git.d-ma.be/mathias/skills",
|
||||
"repository": "https://git.d-ma.be/mathias/skills.git"
|
||||
}
|
||||
+89
-154
@@ -2,22 +2,19 @@ version: '3'
|
||||
|
||||
# Skills library Taskfile.
|
||||
#
|
||||
# Primary entry point for project + host installation. Consumer projects
|
||||
# do NOT depend on this file directly — they ship their own
|
||||
# `skills:install` target that delegates here via `install.sh`. This
|
||||
# Taskfile is invoked by maintainers of the skills repo itself, plus by
|
||||
# install.sh during the post-clone wire step.
|
||||
# This repo is two things at once:
|
||||
# 1. A Claude Code PLUGIN (skills + hooks + MCP servers), distributed through
|
||||
# the marketplace this same repo hosts at .claude-plugin/marketplace.json.
|
||||
# 2. A plain skills library for harnesses with no plugin system — Crush, Pi,
|
||||
# OpenCode — wired by install.sh as ONE directory symlink per harness.
|
||||
#
|
||||
# The wiring loops that used to live here were five near-identical copies that
|
||||
# drifted; install.sh owns wiring now and this file delegates to it.
|
||||
|
||||
vars:
|
||||
REPO_URL: 'https://git.d-ma.be/mathias/skills.git'
|
||||
CHECKOUT_DIR: '{{.HOME}}/.local/share/skills'
|
||||
CLAUDE_GLOBAL_DIR: '{{.HOME}}/.claude/skills'
|
||||
CRUSH_DIR: '{{.HOME}}/.config/crush/skills'
|
||||
ANTIGRAVITY_DIR: '{{.HOME}}/.gemini/antigravity/skills'
|
||||
VIBE_DIR: '{{.HOME}}/.vibe/skills'
|
||||
# Anything at the repo root that is NOT a skill directory. Both
|
||||
# Taskfile + install.sh share this exclusion regex.
|
||||
NON_SKILL_ENTRIES: '^(Taskfile.yml|install.sh|README.md|SKILLS_INDEX.md|.gitea|.git$)$'
|
||||
SKILLS_DIR: '{{.TASKFILE_DIR}}/skills'
|
||||
PLUGIN_MANIFEST: '{{.TASKFILE_DIR}}/.claude-plugin/plugin.json'
|
||||
|
||||
tasks:
|
||||
|
||||
@@ -28,134 +25,95 @@ tasks:
|
||||
list:
|
||||
desc: 'Print every skill in this repo'
|
||||
cmds:
|
||||
- 'ls -1 {{.TASKFILE_DIR}} | grep -Ev "^(Taskfile.yml|install.sh|README.md|SKILLS_INDEX.md|.gitea|.git$)$" || true'
|
||||
- 'ls -1 {{.SKILLS_DIR}}'
|
||||
silent: true
|
||||
|
||||
install:
|
||||
desc: 'Wire skills into every supported harness (Claude Code, Crush, Antigravity, Mistral Vibe).'
|
||||
desc: 'Wire THIS checkout into Crush / Pi / OpenCode. Claude Code uses the plugin instead.'
|
||||
cmds:
|
||||
- task: install:claude:global
|
||||
- task: install:claude:repo
|
||||
- task: install:crush
|
||||
- task: install:antigravity
|
||||
- task: install:vibe
|
||||
|
||||
install:claude:global:
|
||||
desc: 'Per-skill symlinks under ~/.claude/skills/<name> — visible in every Claude Code project on this host.'
|
||||
cmds:
|
||||
- mkdir -p {{.CLAUDE_GLOBAL_DIR}}
|
||||
- |
|
||||
for skill in $(ls -1 "{{.TASKFILE_DIR}}" | grep -Ev "{{.NON_SKILL_ENTRIES}}"); do
|
||||
target="{{.TASKFILE_DIR}}/${skill}"
|
||||
link="{{.CLAUDE_GLOBAL_DIR}}/${skill}"
|
||||
if [ -L "$link" ] || [ -e "$link" ]; then
|
||||
current=$(readlink "$link" 2>/dev/null || true)
|
||||
if [ "$current" = "$target" ]; then
|
||||
continue
|
||||
fi
|
||||
rm -rf "$link"
|
||||
fi
|
||||
ln -s "$target" "$link"
|
||||
echo "linked $link → $target"
|
||||
done
|
||||
|
||||
install:claude:repo:
|
||||
desc: 'Per-skill symlinks under $(pwd)/.claude/skills/<name> when invoked inside a git repo.'
|
||||
cmds:
|
||||
- |
|
||||
if ! git rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||
echo "not in a git repo — skipping per-repo wire"; exit 0
|
||||
fi
|
||||
repo=$(git rev-parse --show-toplevel)
|
||||
if [ "$repo" = "{{.TASKFILE_DIR}}" ]; then
|
||||
echo "running from skills repo itself — skipping per-repo wire"; exit 0
|
||||
fi
|
||||
target_dir="$repo/.claude/skills"
|
||||
mkdir -p "$target_dir"
|
||||
for skill in $(ls -1 "{{.TASKFILE_DIR}}" | grep -Ev "{{.NON_SKILL_ENTRIES}}"); do
|
||||
target="{{.TASKFILE_DIR}}/${skill}"
|
||||
link="${target_dir}/${skill}"
|
||||
if [ -L "$link" ] || [ -e "$link" ]; then
|
||||
current=$(readlink "$link" 2>/dev/null || true)
|
||||
if [ "$current" = "$target" ]; then
|
||||
continue
|
||||
fi
|
||||
rm -rf "$link"
|
||||
fi
|
||||
ln -s "$target" "$link"
|
||||
echo "linked $link → $target"
|
||||
done
|
||||
|
||||
install:crush:
|
||||
desc: 'Per-skill symlinks under ~/.config/crush/skills/<name> — visible in every Crush session on this host.'
|
||||
cmds:
|
||||
- mkdir -p {{.CRUSH_DIR}}
|
||||
- |
|
||||
for skill in $(ls -1 "{{.TASKFILE_DIR}}" | grep -Ev "{{.NON_SKILL_ENTRIES}}"); do
|
||||
target="{{.TASKFILE_DIR}}/${skill}"
|
||||
link="{{.CRUSH_DIR}}/${skill}"
|
||||
if [ -L "$link" ] || [ -e "$link" ]; then
|
||||
current=$(readlink "$link" 2>/dev/null || true)
|
||||
if [ "$current" = "$target" ]; then
|
||||
continue
|
||||
fi
|
||||
rm -rf "$link"
|
||||
fi
|
||||
ln -s "$target" "$link"
|
||||
echo "linked $link → $target"
|
||||
done
|
||||
|
||||
install:antigravity:
|
||||
desc: 'Per-skill symlinks under ~/.gemini/antigravity/skills/<name>. SKILL.md frontmatter (name + description) is already antigravity-compatible.'
|
||||
cmds:
|
||||
- mkdir -p {{.ANTIGRAVITY_DIR}}
|
||||
- |
|
||||
for skill in $(ls -1 "{{.TASKFILE_DIR}}" | grep -Ev "{{.NON_SKILL_ENTRIES}}"); do
|
||||
target="{{.TASKFILE_DIR}}/${skill}"
|
||||
link="{{.ANTIGRAVITY_DIR}}/${skill}"
|
||||
if [ -L "$link" ] || [ -e "$link" ]; then
|
||||
current=$(readlink "$link" 2>/dev/null || true)
|
||||
if [ "$current" = "$target" ]; then
|
||||
continue
|
||||
fi
|
||||
rm -rf "$link"
|
||||
fi
|
||||
ln -s "$target" "$link"
|
||||
echo "linked $link → $target"
|
||||
done
|
||||
|
||||
install:vibe:
|
||||
desc: 'Per-skill file symlinks under ~/.vibe/skills/<name>.md → <skill>/SKILL.md for Mistral Vibe (flat .md layout, not a directory).'
|
||||
cmds:
|
||||
- mkdir -p {{.VIBE_DIR}}
|
||||
- |
|
||||
for skill in $(ls -1 "{{.TASKFILE_DIR}}" | grep -Ev "{{.NON_SKILL_ENTRIES}}"); do
|
||||
target="{{.TASKFILE_DIR}}/${skill}/SKILL.md"
|
||||
[ -f "$target" ] || continue
|
||||
link="{{.VIBE_DIR}}/${skill}.md"
|
||||
if [ -L "$link" ] || [ -e "$link" ]; then
|
||||
current=$(readlink "$link" 2>/dev/null || true)
|
||||
if [ "$current" = "$target" ]; then
|
||||
continue
|
||||
fi
|
||||
rm -rf "$link"
|
||||
fi
|
||||
ln -s "$target" "$link"
|
||||
echo "linked $link → $target"
|
||||
done
|
||||
- SKILLS_SKIP_FETCH=1 SKILLS_CHECKOUT_DIR='{{.TASKFILE_DIR}}' bash '{{.TASKFILE_DIR}}/install.sh'
|
||||
|
||||
update:
|
||||
desc: 'git pull the canonical checkout then re-run install.'
|
||||
desc: 'Fast-forward this checkout and re-wire.'
|
||||
cmds:
|
||||
- |
|
||||
if [ ! -d "{{.CHECKOUT_DIR}}/.git" ]; then
|
||||
echo "no canonical checkout at {{.CHECKOUT_DIR}} — run install.sh first"; exit 1
|
||||
fi
|
||||
git -C "{{.CHECKOUT_DIR}}" pull --ff-only
|
||||
- git -C '{{.TASKFILE_DIR}}' pull --ff-only
|
||||
- task: install
|
||||
|
||||
doctor:
|
||||
desc: 'Report drift across every harness on this host. Non-zero exit means something is wrong.'
|
||||
cmds:
|
||||
- '{{.TASKFILE_DIR}}/bin/doctor'
|
||||
|
||||
validate:
|
||||
desc: 'Validate the plugin + marketplace manifests with the Claude Code CLI.'
|
||||
cmds:
|
||||
- claude plugin validate '{{.TASKFILE_DIR}}'
|
||||
|
||||
test:
|
||||
desc: 'Run the shell test suite (hooks + doctor).'
|
||||
cmds:
|
||||
- |
|
||||
fail=0
|
||||
for t in '{{.TASKFILE_DIR}}'/tests/*_test.sh; do
|
||||
[ -f "$t" ] || continue
|
||||
echo "--- $(basename "$t")"
|
||||
sh "$t" || fail=1
|
||||
done
|
||||
exit $fail
|
||||
|
||||
check:
|
||||
desc: 'Structure gate — layout, frontmatter, manifest/tag parity. Runs in CI.'
|
||||
cmds:
|
||||
- |
|
||||
fail=0
|
||||
|
||||
# 1. No stray entries at the repo root. A loose foo.md where
|
||||
# skills/foo/SKILL.md was meant loads in NO harness and errors in
|
||||
# none either — it just silently does nothing. (See #23.)
|
||||
for entry in "{{.TASKFILE_DIR}}"/*; do
|
||||
base=$(basename "$entry")
|
||||
case "$base" in
|
||||
install.sh|Taskfile.yml|README.md|SKILLS_INDEX.md|skills|hooks|bin|tests) continue ;;
|
||||
esac
|
||||
echo "ERROR: unexpected entry at repo root: $base" >&2
|
||||
fail=1
|
||||
done
|
||||
|
||||
# 2. Every skill is a directory with a SKILL.md carrying name + description.
|
||||
count=0
|
||||
for dir in "{{.SKILLS_DIR}}"/*; do
|
||||
name=$(basename "$dir")
|
||||
if [ ! -d "$dir" ]; then
|
||||
echo "ERROR: skills/$name is not a directory" >&2; fail=1; continue
|
||||
fi
|
||||
if [ ! -f "$dir/SKILL.md" ]; then
|
||||
echo "ERROR: skills/$name missing SKILL.md" >&2; fail=1; continue
|
||||
fi
|
||||
head -n 20 "$dir/SKILL.md" | grep -q '^name:' || { echo "ERROR: skills/$name SKILL.md has no 'name:' frontmatter" >&2; fail=1; }
|
||||
head -n 20 "$dir/SKILL.md" | grep -q '^description:' || { echo "ERROR: skills/$name SKILL.md has no 'description:' frontmatter" >&2; fail=1; }
|
||||
count=$((count + 1))
|
||||
done
|
||||
|
||||
# 3. Plugin manifest version must match the latest tag, or installers
|
||||
# never see the update (Claude Code gates updates on this field).
|
||||
if [ -f '{{.PLUGIN_MANIFEST}}' ]; then
|
||||
manifest_v=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' '{{.PLUGIN_MANIFEST}}' | head -1)
|
||||
tag_v=$(git -C '{{.TASKFILE_DIR}}' describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo '')
|
||||
if [ -n "$tag_v" ] && [ "$manifest_v" != "$tag_v" ]; then
|
||||
echo "ERROR: plugin.json version ($manifest_v) != latest tag ($tag_v)" >&2
|
||||
fail=1
|
||||
fi
|
||||
else
|
||||
echo "ERROR: missing .claude-plugin/plugin.json" >&2; fail=1
|
||||
fi
|
||||
|
||||
if [ $fail -ne 0 ]; then
|
||||
echo "check FAILED" >&2; exit 1
|
||||
fi
|
||||
echo "check OK — $count skills, manifest matches tag"
|
||||
|
||||
release:
|
||||
desc: 'Tag HEAD per Bump-Type in the latest commit footer. Normally CI handles this — run locally only when CI is unavailable.'
|
||||
desc: 'Tag HEAD per Bump-Type in the latest commit footer. Normally CI handles this.'
|
||||
cmds:
|
||||
- |
|
||||
latest=$(git -C "{{.TASKFILE_DIR}}" describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
||||
@@ -172,26 +130,3 @@ tasks:
|
||||
echo "tagging $next (previous $latest, bump=${bump:-patch})"
|
||||
git -C "{{.TASKFILE_DIR}}" tag -a "$next" -m "release $next"
|
||||
echo "now: git push origin $next"
|
||||
|
||||
check:
|
||||
desc: 'Sanity check — every skill dir has a SKILL.md, no stray files at root.'
|
||||
cmds:
|
||||
- |
|
||||
missing=0
|
||||
for skill in $(ls -1 "{{.TASKFILE_DIR}}" | grep -Ev "^(Taskfile.yml|install.sh|README.md|SKILLS_INDEX.md|.gitea|.git$)$"); do
|
||||
dir="{{.TASKFILE_DIR}}/${skill}"
|
||||
if [ ! -d "$dir" ]; then
|
||||
echo "ERROR: $skill is not a directory" >&2
|
||||
missing=$((missing + 1))
|
||||
continue
|
||||
fi
|
||||
if [ ! -f "$dir/SKILL.md" ]; then
|
||||
echo "ERROR: $dir missing SKILL.md" >&2
|
||||
missing=$((missing + 1))
|
||||
fi
|
||||
done
|
||||
if [ $missing -gt 0 ]; then
|
||||
echo "$missing skill(s) failed structure check" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "all skills have SKILL.md"
|
||||
|
||||
+67
-99
@@ -5,25 +5,55 @@
|
||||
# curl -fsSL https://git.d-ma.be/mathias/skills/raw/branch/main/install.sh | bash
|
||||
#
|
||||
# Pinned to a specific tag:
|
||||
# SKILLS_REF=v0.1.0 bash install.sh
|
||||
# SKILLS_REF=v0.4.0 bash install.sh
|
||||
#
|
||||
# Idempotent: re-running pulls the latest changes for the configured ref
|
||||
# (default: main) and re-wires symlinks. Existing correct links are
|
||||
# left alone; mismatched ones are replaced.
|
||||
# (default: main) and re-wires the links.
|
||||
#
|
||||
# Claude Code is NOT wired here. It installs this repo as a plugin from the
|
||||
# marketplace this repo also hosts:
|
||||
#
|
||||
# /plugin marketplace add https://git.d-ma.be/mathias/skills.git
|
||||
# /plugin install skills@mathias
|
||||
#
|
||||
# That path carries the hooks and MCP server definitions too, which symlinks
|
||||
# cannot. This script covers the harnesses with no plugin system: Crush, Pi
|
||||
# and OpenCode.
|
||||
#
|
||||
# Wiring model — one directory link per harness, never one per skill:
|
||||
#
|
||||
# ~/.agents/skills -> $CHECKOUT_DIR/skills (canonical; Pi reads it directly)
|
||||
# ~/.config/crush/skills -> ~/.agents/skills
|
||||
# ~/.config/opencode/skills -> ~/.agents/skills
|
||||
#
|
||||
# Adding a skill to the repo therefore wires it everywhere with no re-run,
|
||||
# and a broken link is one broken link rather than 42.
|
||||
#
|
||||
# CHECKOUT_DIR deliberately defaults OUTSIDE any file-synced or
|
||||
# cross-platform-shared directory. Do not "fix" it to point into ~/dev: on a
|
||||
# fleet whose workspace is shared between macOS (/Users/...) and Linux
|
||||
# (/home/...), an absolute link stored there is broken on one side by
|
||||
# construction. See mathias/skills#23.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_URL="${SKILLS_REPO_URL:-https://git.d-ma.be/mathias/skills.git}"
|
||||
REF="${SKILLS_REF:-main}"
|
||||
CHECKOUT_DIR="${SKILLS_CHECKOUT_DIR:-$HOME/.local/share/skills}"
|
||||
CLAUDE_GLOBAL_DIR="${CLAUDE_SKILLS_DIR:-$HOME/.claude/skills}"
|
||||
AGENTS_DIR="${AGENTS_SKILLS_DIR:-$HOME/.agents/skills}"
|
||||
CRUSH_DIR="${CRUSH_SKILLS_DIR:-$HOME/.config/crush/skills}"
|
||||
ANTIGRAVITY_DIR="${ANTIGRAVITY_SKILLS_DIR:-$HOME/.gemini/antigravity/skills}"
|
||||
VIBE_SKILLS_DIR="${VIBE_SKILLS_DIR:-$HOME/.vibe/skills}"
|
||||
OPENCODE_DIR="${OPENCODE_SKILLS_DIR:-$HOME/.config/opencode/skills}"
|
||||
|
||||
log() { printf '[skills] %s\n' "$*"; }
|
||||
|
||||
ensure_checkout() {
|
||||
# Wiring an existing working checkout (task install) must not fetch or
|
||||
# move HEAD — that would discard in-progress work.
|
||||
if [ "${SKILLS_SKIP_FETCH:-}" = "1" ]; then
|
||||
[ -d "$CHECKOUT_DIR/skills" ] || { log "no skills/ under $CHECKOUT_DIR"; exit 1; }
|
||||
log "using $CHECKOUT_DIR as-is (SKILLS_SKIP_FETCH=1)"
|
||||
return
|
||||
fi
|
||||
if [ -d "$CHECKOUT_DIR/.git" ]; then
|
||||
log "updating $CHECKOUT_DIR"
|
||||
git -C "$CHECKOUT_DIR" fetch --tags --quiet
|
||||
@@ -40,109 +70,47 @@ ensure_checkout() {
|
||||
fi
|
||||
}
|
||||
|
||||
list_skills() {
|
||||
# Every top-level entry that isn't a known meta file.
|
||||
(cd "$CHECKOUT_DIR" && ls -1) | grep -Ev '^(Taskfile\.yml|install\.sh|README\.md|SKILLS_INDEX\.md|\.gitea|\.git)$' || true
|
||||
count_skills() {
|
||||
find "$CHECKOUT_DIR/skills" -mindepth 2 -maxdepth 2 -name SKILL.md 2>/dev/null | wc -l | tr -d ' '
|
||||
}
|
||||
|
||||
link_skill() {
|
||||
# link_skill <target_dir> <skill_name>
|
||||
local target_dir="$1"
|
||||
local skill="$2"
|
||||
local target="$CHECKOUT_DIR/$skill"
|
||||
local link="$target_dir/$skill"
|
||||
|
||||
if [ -L "$link" ] || [ -e "$link" ]; then
|
||||
local current
|
||||
current=$(readlink "$link" 2>/dev/null || true)
|
||||
if [ "$current" = "$target" ]; then
|
||||
return
|
||||
# link_dir <link_path> <target_path>
|
||||
# Replaces whatever is there — including a directory left behind by the old
|
||||
# per-skill wiring — with a single symlink.
|
||||
link_dir() {
|
||||
local link="$1" target="$2"
|
||||
if [ -L "$link" ]; then
|
||||
[ "$(readlink "$link")" = "$target" ] && return
|
||||
rm -f "$link"
|
||||
elif [ -d "$link" ]; then
|
||||
# Refuse to discard a directory holding anything but symlinks: that
|
||||
# would be someone's real skills, not our old wiring.
|
||||
if find "$link" -mindepth 1 -maxdepth 1 ! -type l | grep -q .; then
|
||||
log "REFUSING to replace $link — it holds non-symlink entries; move them and re-run"
|
||||
return 1
|
||||
fi
|
||||
rm -rf "$link"
|
||||
find "$link" -mindepth 1 -maxdepth 1 -type l -delete
|
||||
rmdir "$link"
|
||||
elif [ -e "$link" ]; then
|
||||
rm -f "$link"
|
||||
fi
|
||||
mkdir -p "$(dirname "$link")"
|
||||
ln -s "$target" "$link"
|
||||
log "linked $link → $target"
|
||||
}
|
||||
|
||||
wire_claude_global() {
|
||||
mkdir -p "$CLAUDE_GLOBAL_DIR"
|
||||
while IFS= read -r skill; do
|
||||
[ -n "$skill" ] || continue
|
||||
link_skill "$CLAUDE_GLOBAL_DIR" "$skill"
|
||||
done < <(list_skills)
|
||||
}
|
||||
|
||||
wire_crush() {
|
||||
# Crush reads skills from ~/.config/crush/skills/<name>/SKILL.md.
|
||||
# Pre-creating the dir is cheap even when Crush isn't installed.
|
||||
mkdir -p "$CRUSH_DIR"
|
||||
while IFS= read -r skill; do
|
||||
[ -n "$skill" ] || continue
|
||||
link_skill "$CRUSH_DIR" "$skill"
|
||||
done < <(list_skills)
|
||||
}
|
||||
|
||||
wire_antigravity() {
|
||||
# Antigravity (Google's VS Code extension) reads global skills from
|
||||
# ~/.gemini/antigravity/skills/<name>/SKILL.md. Our SKILL.md files
|
||||
# already carry the required `name` + `description` YAML frontmatter,
|
||||
# so symlinks are sufficient — no manifest translation step.
|
||||
mkdir -p "$ANTIGRAVITY_DIR"
|
||||
while IFS= read -r skill; do
|
||||
[ -n "$skill" ] || continue
|
||||
link_skill "$ANTIGRAVITY_DIR" "$skill"
|
||||
done < <(list_skills)
|
||||
}
|
||||
|
||||
wire_claude_repo() {
|
||||
# Only wire per-repo when invoked from inside a git repo and it isn't
|
||||
# the skills repo itself.
|
||||
if ! git rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||
return
|
||||
fi
|
||||
local repo
|
||||
repo=$(git rev-parse --show-toplevel)
|
||||
if [ "$repo" = "$CHECKOUT_DIR" ]; then
|
||||
return
|
||||
fi
|
||||
local target_dir="$repo/.claude/skills"
|
||||
mkdir -p "$target_dir"
|
||||
while IFS= read -r skill; do
|
||||
[ -n "$skill" ] || continue
|
||||
link_skill "$target_dir" "$skill"
|
||||
done < <(list_skills)
|
||||
}
|
||||
|
||||
wire_vibe() {
|
||||
# Mistral Vibe reads a flat ~/.vibe/skills/<name>.md — link the SKILL.md
|
||||
# file directly (not the skill directory, as the other harnesses do).
|
||||
mkdir -p "$VIBE_SKILLS_DIR"
|
||||
while IFS= read -r skill; do
|
||||
[ -n "$skill" ] || continue
|
||||
local target="$CHECKOUT_DIR/$skill/SKILL.md"
|
||||
[ -f "$target" ] || continue
|
||||
local link="$VIBE_SKILLS_DIR/$skill.md"
|
||||
if [ -L "$link" ] || [ -e "$link" ]; then
|
||||
local current
|
||||
current=$(readlink "$link" 2>/dev/null || true)
|
||||
if [ "$current" = "$target" ]; then
|
||||
continue
|
||||
fi
|
||||
rm -rf "$link"
|
||||
fi
|
||||
ln -s "$target" "$link"
|
||||
log "linked $link → $target"
|
||||
done < <(list_skills)
|
||||
}
|
||||
|
||||
main() {
|
||||
ensure_checkout
|
||||
wire_claude_global
|
||||
wire_claude_repo
|
||||
wire_crush
|
||||
wire_antigravity
|
||||
wire_vibe
|
||||
log "done — $(list_skills | wc -l | tr -d ' ') skill(s) wired at ref=$REF"
|
||||
|
||||
# Canonical location. Pi reads ~/.agents/skills directly, so it needs no
|
||||
# harness-specific link of its own.
|
||||
link_dir "$AGENTS_DIR" "$CHECKOUT_DIR/skills"
|
||||
|
||||
link_dir "$CRUSH_DIR" "$AGENTS_DIR"
|
||||
link_dir "$OPENCODE_DIR" "$AGENTS_DIR"
|
||||
|
||||
log "done — $(count_skills) skill(s) at ref=$REF, 3 links total"
|
||||
log "Claude Code: install the plugin instead — /plugin marketplace add $REPO_URL"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
Reference in New Issue
Block a user