#!/bin/sh # claudeskills.page installer — installs Claude Code skills from https://claudeskills.page # # curl -fsSL https://claudeskills.page/install.sh | sh -s -- [ ...] # # Options: # --project install into ./.claude/skills (commit it to share with your team) # --dir install into a custom skills directory # --category install every skill in a category (e.g. frontend, security) # --list [query] list available skills, optionally filtered # -h, --help show this help # # Each skill is unpacked to //. An existing copy of that one # skill folder is replaced so updates never leave stale files behind. set -eu BASE="${CLAUDESKILLS_BASE:-https://claudeskills.page}" DEST="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills" SCOPE="personal" NAMES="" CATEGORY="" LIST=0 QUERY="" if [ -t 1 ]; then B="$(printf '\033[1m')"; D="$(printf '\033[2m')"; C="$(printf '\033[38;5;173m')"; G="$(printf '\033[32m')"; R="$(printf '\033[31m')"; N="$(printf '\033[0m')"; else B=""; D=""; C=""; G=""; R=""; N=""; fi say() { printf '%s\n' "$*"; } die() { printf '%serror:%s %s\n' "$R" "$N" "$*" >&2; exit 1; } usage() { sed -n '2,16p' "$0" 2>/dev/null | sed 's/^# \{0,1\}//' || true; say "Usage: sh install.sh [--project|--dir DIR] [--category NAME] [--list [QUERY]] ..."; } while [ $# -gt 0 ]; do case "$1" in --project) DEST="$(pwd)/.claude/skills"; SCOPE="project" ;; --dir) [ $# -ge 2 ] || die "--dir needs a path"; DEST="$2"; SCOPE="custom"; shift ;; --category) [ $# -ge 2 ] || die "--category needs a name"; CATEGORY="$2"; shift ;; --list) LIST=1; if [ $# -ge 2 ] && [ "${2#-}" = "$2" ]; then QUERY="$2"; shift; fi ;; -h|--help) usage; exit 0 ;; -*) die "unknown option: $1" ;; *) NAMES="$NAMES $1" ;; esac shift done if command -v curl >/dev/null 2>&1; then fetch() { curl -fsSL "$1" -o "$2"; } elif command -v wget >/dev/null 2>&1; then fetch() { wget -qO "$2" "$1"; } else die "curl or wget is required"; fi if command -v unzip >/dev/null 2>&1; then extract() { unzip -qo "$1" -d "$2"; } elif command -v python3 >/dev/null 2>&1; then extract() { python3 -m zipfile -e "$1" "$2"; } else die "unzip (or python3) is required"; fi TMP="$(mktemp -d 2>/dev/null || mktemp -d -t claudeskills)" trap 'rm -rf "$TMP"' EXIT INT TERM fetch "$BASE/skills.txt" "$TMP/index.txt" || die "could not download the skill index from $BASE" if [ "$LIST" -eq 1 ]; then say "${B}Available skills${N} ${D}($BASE)${N}" if [ -n "$QUERY" ]; then grep -i -- "$QUERY" "$TMP/index.txt" || say "No skills match \"$QUERY\"." else cat "$TMP/index.txt"; fi | awk -F '\t' -v c="$C" -v d="$D" -v n="$N" 'NF==3 { printf " %s%-32s%s %s%-14s%s %s\n", c, $1, n, d, $2, n, $3 }' exit 0 fi if [ -n "$CATEGORY" ]; then EXTRA="$(awk -F '\t' -v cat="$CATEGORY" '$2 == cat { printf "%s ", $1 }' "$TMP/index.txt")" [ -n "$EXTRA" ] || die "unknown category: $CATEGORY" NAMES="$NAMES $EXTRA" fi [ -n "$(printf '%s' "$NAMES" | tr -d ' ')" ] || { usage; say; die "name at least one skill, e.g. sh -s -- systematic-debugging"; } mkdir -p "$DEST" || die "cannot create $DEST" say "${B}Installing to${N} $DEST ${D}($SCOPE)${N}" OK=0; FAIL=0 for name in $NAMES; do case "$name" in *[!a-z0-9-]*|-*|"") say " ${R}✗${N} $name ${D}— invalid name${N}"; FAIL=$((FAIL + 1)); continue ;; esac if ! awk -F '\t' -v n="$name" '$1 == n { found = 1 } END { exit !found }' "$TMP/index.txt"; then say " ${R}✗${N} $name ${D}— not found (try --list $name)${N}"; FAIL=$((FAIL + 1)); continue fi if ! fetch "$BASE/downloads/$name.zip" "$TMP/$name.zip"; then say " ${R}✗${N} $name ${D}— download failed${N}"; FAIL=$((FAIL + 1)); continue fi mkdir -p "$TMP/x-$name" extract "$TMP/$name.zip" "$TMP/x-$name" [ -f "$TMP/x-$name/$name/SKILL.md" ] || { say " ${R}✗${N} $name ${D}— archive is missing SKILL.md${N}"; FAIL=$((FAIL + 1)); continue; } status="installed" [ -d "$DEST/$name" ] && { rm -rf "${DEST:?}/$name"; status="updated"; } mv "$TMP/x-$name/$name" "$DEST/$name" say " ${G}✓${N} $name ${D}— $status${N}" OK=$((OK + 1)) done say "" if [ "$OK" -gt 0 ]; then say "${G}${B}Done.${N} $OK skill(s) ready. Claude Code picks them up automatically — run ${C}/skills${N} to confirm." say "${D}Browse more at $BASE/skills/${N}" fi [ "$FAIL" -eq 0 ] || exit 1