diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7627062..63e844f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -12,7 +12,8 @@ ## Checklist -- [ ] `skillshare-hub.json` is valid JSON +- [ ] Added entry to the correct `skills/*.json` category file (see [CONTRIBUTING.md](../CONTRIBUTING.md#categories)) +- [ ] Ran `make build` and committed the updated `skillshare-hub.json` - [ ] `name` is lowercase with hyphens only - [ ] `description` is a clear one-liner - [ ] `source` repo is publicly accessible and contains a valid `SKILL.md` diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml index 596efe0..67480b0 100644 --- a/.github/workflows/validate-pr.yml +++ b/.github/workflows/validate-pr.yml @@ -2,6 +2,7 @@ name: Validate PR on: pull_request: paths: + - 'skills/**' - 'skillshare-hub.json' - 'scripts/**' - '.github/workflows/validate-pr.yml' @@ -16,6 +17,17 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Build hub.json from skills/ + run: ./scripts/build.sh + + - name: Check hub.json is up to date + run: | + if ! git diff --quiet skillshare-hub.json; then + echo "::error::skillshare-hub.json is out of date. Run 'make build' and commit the result." + git diff skillshare-hub.json + exit 1 + fi + - name: Validate hub.json run: ./scripts/validate.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8d1b628..e396ae8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,10 +5,29 @@ Thanks for sharing your skill with the community! ## How to Add a Skill 1. Fork this repo -2. Edit `skillshare-hub.json` — add your skill entry to the `skills` array -3. Open a Pull Request -4. CI will validate your entry automatically -5. A maintainer will review and merge +2. Find the right category file in `skills/` (see [Categories](#categories) below) +3. Add your skill entry to the JSON array in that file +4. Run `make build` to regenerate `skillshare-hub.json` +5. Commit **both** the `skills/*.json` change and `skillshare-hub.json` +6. Open a Pull Request +7. CI will validate and audit your entry automatically +8. A maintainer will review and merge + +> **Important**: Do not edit `skillshare-hub.json` directly — it is generated from `skills/*.json` by `make build`. CI will reject PRs where the hub file is out of sync. + +## Categories + +| File | Description | +|------|-------------| +| `skills/skill.json` | Hub meta-skills (skillshare, skill-creator, find-skills, template-skill) | +| `skills/anthropic.json` | Skills from `anthropics/skills` | +| `skills/marketing.json` | Skills from `coreyhaines31/marketingskills` | +| `skills/workflow.json` | Skills from `obra/superpowers` and `obra/episodic-memory` | +| `skills/expo.json` | Skills from `expo/skills` | +| `skills/vercel.json` | Skills from `vercel-labs/*` and `vercel/*` | +| `skills/community.json` | Everything else — independent authors and orgs | + +> **Not sure where to put your skill?** Just add it to `skills/community.json`. Maintainers will move it if needed. ## Skill Entry Format @@ -23,7 +42,18 @@ Single-skill repo: } ``` -Multi-skill repo (use `skill` to specify which one): +Multi-skill repo (use subpath in `source`): + +```json +{ + "name": "my-skill", + "description": "One-line description of what the skill does", + "source": "your-username/your-repo/my-skill", + "tags": ["relevant", "tags"] +} +``` + +Multi-skill repo (use `skill` field to specify which one): ```json { @@ -52,7 +82,7 @@ Non-GitHub platforms (GitLab, Bitbucket, self-hosted, etc.): |-------|----------|-------------| | `name` | Yes | Unique skill name — lowercase, hyphens only (e.g. `commit-helper`) | | `description` | Yes | One-line description of what the skill does | -| `source` | Yes | GitHub `owner/repo`, full git URL, or any platform URL | +| `source` | Yes | GitHub `owner/repo[/subpath]`, full git URL, or any platform URL | | `skill` | No | Specific skill name within a multi-skill repo (installs via `-s`) | | `tags` | No | 1-3 classification tags | @@ -71,6 +101,15 @@ Feel free to introduce new tags if none fit — maintainers may suggest changes - **Clear description** — explain what the skill does, not how it works - **No duplicates** — search existing entries before adding +## Local Development + +```bash +make build # Build skillshare-hub.json from skills/*.json +make validate # Validate format and rules +make audit # Audit new/changed skills against main +make ci # Run all three (build → validate → audit) +``` + ## Using Skills from the Hub ```bash diff --git a/Makefile b/Makefile index cad5348..c1923b0 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,9 @@ -.PHONY: format validate audit ci +.PHONY: build format validate audit ci -format: ## Format skillshare-hub.json (sort by name, 2-space indent) - @./scripts/format.sh +build: ## Build skillshare-hub.json from skills/*.json + @./scripts/build.sh + +format: build ## Alias for build (build already sorts and formats) validate: ## Validate skillshare-hub.json format and rules @./scripts/validate.sh @@ -9,4 +11,4 @@ validate: ## Validate skillshare-hub.json format and rules audit: ## Audit new/changed skills against main @./scripts/audit.sh main -ci: validate audit ## Run full CI locally +ci: build validate audit ## Run full CI locally diff --git a/scripts/audit.sh b/scripts/audit.sh index f3f53cc..acffb7c 100755 --- a/scripts/audit.sh +++ b/scripts/audit.sh @@ -11,110 +11,60 @@ log_group() { is_ci && echo "::group::$1" || echo "=== $1 ==="; } log_endgroup() { is_ci && echo "::endgroup::" || true; } log_error() { is_ci && echo "::error::$1" || echo "ERROR: $1"; } -# --- Find new/changed sources --- -base_sources=$(git show "${BASE_REF}:${HUB_FILE}" 2>/dev/null \ - | jq -r '.skills[] | "\(.name)|\(.source)"' | sort) || base_sources="" - -pr_sources=$(jq -r '.skills[] | "\(.name)|\(.source)"' "$HUB_FILE" | sort) - -new_entries=$(comm -13 <(echo "$base_sources") <(echo "$pr_sources")) -new_sources=$(echo "$new_entries" | cut -d'|' -f2 | sort -u) - -if [ -z "$new_entries" ]; then - echo "No new or changed skill sources to audit" - [ -n "$REPORT_FILE" ] && echo "No new or changed skill sources to audit." > "$REPORT_FILE" - exit 0 -fi - -echo "New/changed sources to audit:" -echo "$new_sources" -echo "" - -# --- Check skillshare is available --- -if ! command -v skillshare &>/dev/null; then - echo "ERROR: skillshare CLI not found. Install from https://github.com/runkids/skillshare" - exit 1 -fi +# --- Parse source into clone_url and subpath --- +# Output: clone_urlsubpath +parse_source() { + local source="$1" + local clone_url="" subpath="" -# --- Audit each source --- -failed=0 -results=() -details=() - -while IFS= read -r source; do - [ -z "$source" ] && continue - - # Parse source into clone_url + optional subpath - subpath="" if [[ "$source" =~ ^https?://github\.com/([^/]+/[^/]+)/tree/[^/]+/(.+)$ ]]; then - # GitHub browser URL: https://github.com/owner/repo/tree/branch/path clone_url="https://github.com/${BASH_REMATCH[1]}.git" subpath="${BASH_REMATCH[2]}" elif [[ "$source" =~ ^https?://github\.com/([^/]+/[^/]+)/?$ ]]; then - # GitHub repo URL (no subpath): https://github.com/owner/repo clone_url="https://github.com/${BASH_REMATCH[1]}.git" elif [[ "$source" =~ ^github\.com/([^/]+/[^/]+)(/(.+))?$ ]]; then - # github.com shorthand: github.com/owner/repo[/path] clone_url="https://github.com/${BASH_REMATCH[1]}.git" subpath="${BASH_REMATCH[3]}" elif [[ "$source" =~ ^(https?://[^/]+)/([^/]+/[^/]+)/src/branch/[^/]+/(.+)$ ]]; then - # Gitea/Forgejo browser URL: https://host/owner/repo/src/branch/main/path clone_url="${BASH_REMATCH[1]}/${BASH_REMATCH[2]}.git" subpath="${BASH_REMATCH[3]}" elif [[ "$source" =~ ^(https?://[^/]+)/([^/]+/[^/]+)/src/branch/ ]]; then - # Gitea/Forgejo repo URL (branch root, no subpath) clone_url="${BASH_REMATCH[1]}/${BASH_REMATCH[2]}.git" elif [[ "$source" == http* ]]; then - # Other HTTP URLs — use as-is clone_url="$source" else - # GitHub shorthand: owner/repo[/path] + local owner_repo owner_repo=$(echo "$source" | cut -d'/' -f1-2) subpath=$(echo "$source" | cut -d'/' -f3-) clone_url="https://github.com/${owner_repo}.git" fi - safe_name=$(echo "$source" | tr '/' '-') - clone_dir="/tmp/audit-${safe_name}" - - log_group "Auditing ${source}" + printf '%s\t%s' "$clone_url" "$subpath" +} - if ! clone_output=$(git clone --depth 1 "$clone_url" "$clone_dir" 2>&1); then - log_error "Failed to clone ${source}" - results+=("| \`${source}\` | :x: Clone failed | - |") - details+=("DETAIL_SEP### \`${source}\`"$'\n'"Clone failed:"$'\n'"\`\`\`"$'\n'"${clone_output}"$'\n'"\`\`\`") - failed=1 - log_endgroup - continue - fi - - # Audit subpath if specified, otherwise audit entire clone - audit_target="$clone_dir" - if [ -n "$subpath" ] && [ -d "$clone_dir/$subpath" ]; then - audit_target="$clone_dir/$subpath" - fi +# --- Audit a single target and record results --- +# Sets: results[], details[], failed +audit_target() { + local source="$1" audit_target="$2" + local audit_json audit_json=$(skillshare audit "$audit_target" --threshold high --json 2>/dev/null) || true - # Check if output is valid audit JSON if ! echo "$audit_json" | jq -e '.summary' >/dev/null 2>&1; then - # Not valid JSON — command errored (e.g. path not found) + local audit_err audit_err=$(skillshare audit "$audit_target" --threshold high 2>&1) || true echo "$audit_err" results+=("| \`${source}\` | :x: Error | - |") details+=("DETAIL_SEP### \`${source}\`"$'\n'"\`\`\`"$'\n'"${audit_err}"$'\n'"\`\`\`") failed=1 - rm -rf "$clone_dir" - log_endgroup - continue + return fi - # Extract risk info with jq + local risk_score risk_label risk findings_text audit_display risk_score=$(echo "$audit_json" | jq -r '.summary.riskScore') risk_label=$(echo "$audit_json" | jq -r '.summary.riskLabel' | tr '[:lower:]' '[:upper:]') risk="${risk_label} (${risk_score}/100)" - # Format findings for display findings_text=$(echo "$audit_json" | jq -r ' [.results[] | (.findings // [])[] | " \(.severity): \(.message) (\(.file):\(.line))\n \"\(.snippet)\""] | join("\n\n")') @@ -131,16 +81,108 @@ while IFS= read -r source; do results+=("| \`${source}\` | :white_check_mark: Passed | ${risk} |") details+=("DETAIL_SEP### \`${source}\`"$'\n'"\`\`\`"$'\n'"${audit_display}"$'\n'"\`\`\`") fi +} + +# --- Find new/changed sources --- +base_sources=$(git show "${BASE_REF}:${HUB_FILE}" 2>/dev/null \ + | jq -r '.skills[] | "\(.name)|\(.source)"' | sort) || base_sources="" + +pr_sources=$(jq -r '.skills[] | "\(.name)|\(.source)"' "$HUB_FILE" | sort) + +new_entries=$(comm -13 <(echo "$base_sources") <(echo "$pr_sources")) +new_sources=$(echo "$new_entries" | cut -d'|' -f2 | sort -u) + +if [ -z "$new_entries" ]; then + echo "No new or changed skill sources to audit" + [ -n "$REPORT_FILE" ] && echo "No new or changed skill sources to audit." > "$REPORT_FILE" + exit 0 +fi + +# --- Check skillshare is available --- +if ! command -v skillshare &>/dev/null; then + echo "ERROR: skillshare CLI not found. Install from https://github.com/runkids/skillshare" + exit 1 +fi + +# --- Group sources by clone_url (clone once per repo) --- +# Use temp dir instead of associative arrays (Bash 3.x compat) +group_dir=$(mktemp -d) +trap 'rm -rf "$group_dir"' EXIT +clone_order_file="$group_dir/_order" +touch "$clone_order_file" + +while IFS= read -r source; do + [ -z "$source" ] && continue + + parsed=$(parse_source "$source") + clone_url=$(echo "$parsed" | cut -f1) + subpath=$(echo "$parsed" | cut -f2) + + safe_name=$(echo "$clone_url" | sed 's|https://||;s|\.git$||' | tr '/:' '-') + if ! grep -qxF "$safe_name" "$clone_order_file" 2>/dev/null; then + echo "$safe_name" >> "$clone_order_file" + echo "$clone_url" > "$group_dir/${safe_name}.url" + fi + printf '%s\t%s\n' "$source" "$subpath" >> "$group_dir/${safe_name}.sources" +done <<< "$new_sources" + +unique_repos=$(wc -l < "$clone_order_file" | tr -d ' ') +echo "New/changed sources to audit: $(echo "$new_sources" | wc -l | tr -d ' ') sources across ${unique_repos} repos" +echo "" + +# --- Audit each repo --- +failed=0 +results=() +details=() + +while IFS= read -r safe_name; do + [ -z "$safe_name" ] && continue + clone_url=$(cat "$group_dir/${safe_name}.url") + clone_dir="/tmp/audit-${safe_name}" + rm -rf "$clone_dir" + + log_group "Cloning ${clone_url}" + + if ! clone_output=$(git clone --depth 1 "$clone_url" "$clone_dir" 2>&1); then + # Clone failed — mark all sources in this repo as failed + while IFS=$'\t' read -r source subpath; do + [ -z "$source" ] && continue + log_error "Failed to clone ${source}" + results+=("| \`${source}\` | :x: Clone failed | - |") + details+=("DETAIL_SEP### \`${source}\`"$'\n'"Clone failed:"$'\n'"\`\`\`"$'\n'"${clone_output}"$'\n'"\`\`\`") + failed=1 + done < "$group_dir/${safe_name}.sources" + rm -rf "$clone_dir" + log_endgroup + continue + fi + + echo "Cloned ${clone_url} — auditing subpaths..." + + # Audit each source/subpath in this repo + while IFS=$'\t' read -r source subpath; do + [ -z "$source" ] && continue + + local_target="$clone_dir" + if [ -n "$subpath" ] && [ -d "$clone_dir/$subpath" ]; then + local_target="$clone_dir/$subpath" + fi + + echo " Auditing: ${source}" + audit_target "$source" "$local_target" + done < "$group_dir/${safe_name}.sources" rm -rf "$clone_dir" log_endgroup -done <<< "$new_sources" +done < "$clone_order_file" # --- Generate report --- if [ -n "$REPORT_FILE" ]; then { echo "## Skill Audit Results" echo "" + echo "Audited $(echo "$new_sources" | wc -l | tr -d ' ') sources across ${unique_repos} repos." + echo "" echo "| Source | Status | Risk |" echo "|--------|--------|------|" for row in "${results[@]}"; do diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..d2ba0c8 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +SKILLS_DIR="${1:-skills}" +HUB_FILE="skillshare-hub.json" + +if [ ! -d "$SKILLS_DIR" ]; then + echo "ERROR: $SKILLS_DIR directory not found" + exit 1 +fi + +# Count source files +files=("$SKILLS_DIR"/*.json) +if [ ${#files[@]} -eq 0 ]; then + echo "ERROR: No .json files found in $SKILLS_DIR" + exit 1 +fi + +# Merge all arrays, sort by name, wrap in hub schema +jq -s '{ + schemaVersion: 1, + skills: ([.[][]] | sort_by(.name)) +}' "${files[@]}" > "$HUB_FILE" + +count=$(jq '.skills | length' "$HUB_FILE") +echo "Built: $HUB_FILE ($count skills from ${#files[@]} files)" diff --git a/skills/skill.json b/skills/skill.json new file mode 100644 index 0000000..560e3e0 --- /dev/null +++ b/skills/skill.json @@ -0,0 +1,21 @@ +[ + { + "name": "skill-creator", + "description": "Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.", + "source": "anthropics/skills", + "skill": "skill-creator", + "tags": [ + "skill" + ] + }, + { + "name": "skillshare", + "description": "Built-in AI skill for skillshare – helps install, search, and manage skills from the hub", + "source": "https://github.com/runkids/skillshare/tree/main/skills/skillshare", + "tags": [ + "skillshare", + "agent", + "skill" + ] + } +]