#!/usr/bin/env bash # install.sh — bootstrap installer for the skills library. # # Usage (one-liner, hosts without Task): # curl -fsSL https://git.d-ma.be/mathias/skills/raw/branch/main/install.sh | bash # # Pinned to a specific tag: # SKILLS_REF=v0.4.0 bash install.sh # # Idempotent: re-running pulls the latest changes for the configured ref # (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}" AGENTS_DIR="${AGENTS_SKILLS_DIR:-$HOME/.agents/skills}" CRUSH_DIR="${CRUSH_SKILLS_DIR:-$HOME/.config/crush/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 git -C "$CHECKOUT_DIR" checkout --quiet "$REF" if git -C "$CHECKOUT_DIR" symbolic-ref -q HEAD >/dev/null; then # on a branch — fast-forward git -C "$CHECKOUT_DIR" pull --ff-only --quiet fi else log "cloning $REPO_URL into $CHECKOUT_DIR (ref=$REF)" mkdir -p "$(dirname "$CHECKOUT_DIR")" git clone --quiet "$REPO_URL" "$CHECKOUT_DIR" git -C "$CHECKOUT_DIR" checkout --quiet "$REF" fi } count_skills() { find "$CHECKOUT_DIR/skills" -mindepth 2 -maxdepth 2 -name SKILL.md 2>/dev/null | wc -l | tr -d ' ' } # link_dir # 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 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" } main() { ensure_checkout # 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 "$@"