From 387fdcce51d3b87bbcd06458dc8fd987c854e631 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 29 Oct 2025 16:38:45 +0100 Subject: [PATCH 1/2] feat: add support for repositories with individual SKILL.md files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This enhancement allows upskill to work with repositories like anthropics/skills that organize skills as individual SKILL.md files instead of using a .claude/skills directory structure. New features: - Auto-detect whether repo uses .claude/skills or individual SKILL.md files - --list flag to display available skills before installing - --skill flag (multi-use) to selectively install specific skills - --all flag to install all discovered skills from SKILL.md files - Backward compatible: repos with .claude/skills work as before For repos without .claude/skills: - Discovers all SKILL.md files recursively - Extracts skill name and description from YAML frontmatter - Allows selective installation by skill name - Prompts user to choose skills when neither --skill nor --all is provided Examples: upskill anthropics/skills --list upskill anthropics/skills --skill pdf --skill xlsx upskill anthropics/skills --all 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Signed-off-by: Lars Trieloff --- README.md | 33 ++++++++-- upskill | 179 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 203 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4f01a8e..9f70e7f 100644 --- a/README.md +++ b/README.md @@ -16,22 +16,47 @@ Quickly install Claude/Agent skills from another repository. Works standalone an ## Usage -Install skills from another repo (same syntax as `gh repo clone`): +### Install from repositories with `.claude/skills` directory + +Install all skills from another repo (same syntax as `gh repo clone`): ``` upskill adobe/helix-website -b agent-skills ``` -What this does: +### Install from repositories with individual SKILL.md files + +For repositories like `anthropics/skills` that don't have a `.claude/skills` directory, you can: + +**List available skills:** +``` +upskill anthropics/skills --list +``` + +**Install specific skills:** +``` +upskill anthropics/skills --skill pdf --skill xlsx +``` + +**Install all skills:** +``` +upskill anthropics/skills --all +``` + +### What this does: - Creates a temp directory and `gh repo clone`s the source repository -- Copies everything from `source/.claude/skills` into `./.claude/skills` +- For repos with `.claude/skills`: copies everything into `./.claude/skills` +- For repos with `SKILL.md` files: discovers and copies selected skills - Creates `./.agents/discover-skills` (robust, shellcheck-friendly) - Adds or updates a Skills section in `./AGENTS.md` with clear start/end markers - Ensures repeated runs do not duplicate the section -Options: +### Options: - `-b, --branch `: use a specific branch, tag, or commit - `--skills-path `: change source skills path (default: `.claude/skills`) +- `--list`: list available skills without installing +- `--skill `: install specific skill(s) (can be used multiple times) +- `--all`: install all skills from SKILL.md files - `-i`: add created files to `.gitignore` (`.claude/skills/` and `.agents/discover-skills`), idempotent via markers ## Idempotent AGENTS.md updates diff --git a/upskill b/upskill index 7eba6bf..ffca5fa 100755 --- a/upskill +++ b/upskill @@ -11,15 +11,21 @@ ${PROGRAM_NAME} ${VERSION} Install Claude/Agent skills from another GitHub repository. Usage: - ${PROGRAM_NAME} [-i] [-b ] [--skills-path ] + ${PROGRAM_NAME} [-i] [-b ] [--skills-path ] [--list] [--skill ] [--all] Examples: ${PROGRAM_NAME} adobe/helix-website -b agent-skills + ${PROGRAM_NAME} anthropics/skills --list + ${PROGRAM_NAME} anthropics/skills --skill pdf --skill xlsx + ${PROGRAM_NAME} anthropics/skills --all Options: -i Add created files to .gitignore -b, --branch Branch, tag, or commit to clone (default: repo default) --skills-path Path to skills in source repo (default: .claude/skills) + --list List available skills without installing + --skill Install specific skill(s) (can be used multiple times) + --all Install all skills from SKILL.md files -q, --quiet Reduce output -h, --help Show help -v, --version Show version @@ -156,11 +162,85 @@ copy_tree() { fi } +discover_skill_files() { + # $1: directory to search + # Outputs: skill_name|skill_path pairs, one per line + local search_dir="$1" + + while IFS= read -r -d '' skill_file; do + local skill_dir skill_name name description + skill_dir=$(dirname "$skill_file") + skill_name=$(basename "$skill_dir") + + # Try to extract name from YAML frontmatter + if head -n 1 "$skill_file" | grep -q "^---$"; then + frontmatter=$(awk 'BEGIN{inside=0; c=0} /^---$/ {inside=!inside; if(++c==3) exit} inside==1 {print}' "$skill_file") + name=$(printf '%s\n' "$frontmatter" | awk -F': *' '/^name:/ {sub(/^name: */,"",$0); print substr($0, index($0,$2))}' 2>/dev/null) + description=$(printf '%s\n' "$frontmatter" | awk -F': *' '/^description:/ {sub(/^description: */,"",$0); print substr($0, index($0,$2))}' 2>/dev/null) + fi + + # Output: skill_name|skill_path|description + printf '%s|%s|%s\n' "${name:-$skill_name}" "$skill_file" "$description" + done < <(find "$search_dir" -type f -name 'SKILL.md' -print0) +} + +list_skills() { + # $1: directory to search + local search_dir="$1" + + echo "Available skills in repository:" + echo "===============================" + echo "" + + local count=0 + while IFS='|' read -r name path description; do + ((count++)) + echo "Skill: $name" + echo "Path: $path" + if [[ -n "$description" ]]; then + echo "Desc: $description" + fi + echo "" + done < <(discover_skill_files "$search_dir") + + if [[ $count -eq 0 ]]; then + echo "No SKILL.md files found in $search_dir" + return 1 + fi + + echo "Found $count skill(s)" + echo "" + echo "To install specific skills:" + echo " $PROGRAM_NAME --skill [--skill ...]" + echo "" + echo "To install all skills:" + echo " $PROGRAM_NAME --all" +} + +copy_skill() { + # $1: skill file path, $2: destination skills dir + local skill_file="$1" + local dest_dir="$2" + local skill_dir skill_name + + skill_dir=$(dirname "$skill_file") + skill_name=$(basename "$skill_dir") + + log "Installing skill: $skill_name" + + # Copy the entire skill directory + mkdir -p "$dest_dir/$skill_name" + copy_tree "$skill_dir" "$dest_dir/$skill_name" +} + main() { local repo="" local branch="" local skills_rel_path=".claude/skills" local add_to_gitignore="" + local list_only="" + local install_all="" + local -a selected_skills=() while [[ $# -gt 0 ]]; do case "$1" in @@ -170,6 +250,9 @@ main() { -i) add_to_gitignore=1; shift ;; -b|--branch) branch="$2"; shift 2 ;; --skills-path) skills_rel_path="$2"; shift 2 ;; + --list) list_only=1; shift ;; + --all) install_all=1; shift ;; + --skill) selected_skills+=("$2"); shift 2 ;; -*) die "Unknown option: $1" ;; *) repo="$1"; shift ;; esac @@ -177,6 +260,17 @@ main() { [[ -n "$repo" ]] || { usage; die "Missing required "; } + # Validate mutually exclusive options + if [[ -n "$list_only" && -n "$install_all" ]]; then + die "--list and --all are mutually exclusive" + fi + if [[ -n "$list_only" && ${#selected_skills[@]} -gt 0 ]]; then + die "--list and --skill are mutually exclusive" + fi + if [[ -n "$install_all" && ${#selected_skills[@]} -gt 0 ]]; then + die "--all and --skill are mutually exclusive" + fi + require_cmd gh require_cmd git require_cmd awk @@ -197,15 +291,90 @@ main() { gh repo clone "$repo" "$clone_dir" >/dev/null fi - local src_skills_dir + # Check if source has .claude/skills directory + local src_skills_dir has_claude_dir src_skills_dir="$clone_dir/$skills_rel_path" - [[ -d "$src_skills_dir" ]] || die "Source skills directory not found: $src_skills_dir" + has_claude_dir="" + + if [[ -d "$src_skills_dir" ]]; then + has_claude_dir=1 + log "Found .claude/skills directory in repository" + else + log "No .claude/skills directory found, looking for SKILL.md files..." + fi + + # Handle --list flag + if [[ -n "$list_only" ]]; then + if [[ -n "$has_claude_dir" ]]; then + list_skills "$src_skills_dir" + else + list_skills "$clone_dir" + fi + exit 0 + fi local dest_skills_dir dest_skills_dir=".claude/skills" mkdir -p "$dest_skills_dir" - log "Copying skills from $src_skills_dir to $dest_skills_dir ..." - copy_tree "$src_skills_dir" "$dest_skills_dir" + + # Install skills based on mode + if [[ -n "$has_claude_dir" ]]; then + # Traditional mode: copy entire .claude/skills directory + log "Copying skills from $src_skills_dir to $dest_skills_dir ..." + copy_tree "$src_skills_dir" "$dest_skills_dir" + else + # New mode: find and copy individual SKILL.md files + local skill_count=0 + local tmpfile + tmpfile=$(mktemp) + + # Build list of available skills + discover_skill_files "$clone_dir" >"$tmpfile" + + if [[ ! -s "$tmpfile" ]]; then + rm -f "$tmpfile" + die "No skills found in repository (no .claude/skills directory and no SKILL.md files)" + fi + + # Determine which skills to install + if [[ -n "$install_all" ]]; then + local total_count + total_count=$(wc -l <"$tmpfile") + log "Installing all $total_count skills..." + while IFS='|' read -r name path description; do + copy_skill "$path" "$dest_skills_dir" + ((skill_count++)) + done <"$tmpfile" + elif [[ ${#selected_skills[@]} -gt 0 ]]; then + log "Installing ${#selected_skills[@]} selected skill(s)..." + for skill_name in "${selected_skills[@]}"; do + local found="" + while IFS='|' read -r name path description; do + if [[ "$name" == "$skill_name" ]]; then + copy_skill "$path" "$dest_skills_dir" + ((skill_count++)) + found=1 + break + fi + done <"$tmpfile" + if [[ -z "$found" ]]; then + rm -f "$tmpfile" + die "Skill not found: $skill_name (use --list to see available skills)" + fi + done + else + # No .claude directory and no flags - show available skills + echo "No .claude/skills directory found in repository." + echo "" + list_skills "$clone_dir" + echo "" + rm -f "$tmpfile" + die "Please specify which skills to install using --skill or --all" + fi + + rm -f "$tmpfile" + log "Installed $skill_count skill(s)" + fi # Write .agents/discover-skills mkdir -p .agents From c9daae894cb4f3ad36e3999acc9a89da1a660d91 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 29 Oct 2025 16:49:17 +0100 Subject: [PATCH 2/2] refactor: improve skill listing UX and formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Based on user feedback, this commit improves the skill listing experience: - Enhanced formatting: bold skill names, cyan relative paths in parentheses - Smart examples: use actual discovered skill names in usage examples - Better layout: description on separate line below skill name - Cleaner paths: show relative paths instead of absolute paths - Verified temp directory cleanup works correctly (trap already in place) Example output: **pdf** (document-skills/pdf/SKILL.md) Comprehensive PDF manipulation toolkit... To install specific skills: upskill anthropics/skills --skill pdf --skill xlsx 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Signed-off-by: Lars Trieloff --- upskill | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/upskill b/upskill index ffca5fa..fcc7a35 100755 --- a/upskill +++ b/upskill @@ -186,19 +186,34 @@ discover_skill_files() { list_skills() { # $1: directory to search + # $2: optional repo name for examples local search_dir="$1" + local repo="${2:-}" + + # ANSI color codes + local bold='\033[1m' + local cyan='\033[36m' + local reset='\033[0m' echo "Available skills in repository:" echo "===============================" echo "" local count=0 + local -a skill_names=() while IFS='|' read -r name path description; do ((count++)) - echo "Skill: $name" - echo "Path: $path" + skill_names+=("$name") + + # Make path relative to search_dir + local rel_path="${path#"$search_dir"/}" + + # Bold skill name, cyan relative path in parentheses + printf "${bold}%s${reset} ${cyan}(%s)${reset}\n" "$name" "$rel_path" + + # Description on next line if present if [[ -n "$description" ]]; then - echo "Desc: $description" + echo "$description" fi echo "" done < <(discover_skill_files "$search_dir") @@ -211,10 +226,17 @@ list_skills() { echo "Found $count skill(s)" echo "" echo "To install specific skills:" - echo " $PROGRAM_NAME --skill [--skill ...]" + # Use first two skills in example if available + if [[ ${#skill_names[@]} -ge 2 ]]; then + echo " $PROGRAM_NAME $repo --skill ${skill_names[0]} --skill ${skill_names[1]}" + elif [[ ${#skill_names[@]} -eq 1 ]]; then + echo " $PROGRAM_NAME $repo --skill ${skill_names[0]}" + else + echo " $PROGRAM_NAME $repo --skill [--skill ...]" + fi echo "" echo "To install all skills:" - echo " $PROGRAM_NAME --all" + echo " $PROGRAM_NAME $repo --all" } copy_skill() { @@ -306,9 +328,9 @@ main() { # Handle --list flag if [[ -n "$list_only" ]]; then if [[ -n "$has_claude_dir" ]]; then - list_skills "$src_skills_dir" + list_skills "$src_skills_dir" "$repo" else - list_skills "$clone_dir" + list_skills "$clone_dir" "$repo" fi exit 0 fi @@ -366,7 +388,7 @@ main() { # No .claude directory and no flags - show available skills echo "No .claude/skills directory found in repository." echo "" - list_skills "$clone_dir" + list_skills "$clone_dir" "$repo" echo "" rm -f "$tmpfile" die "Please specify which skills to install using --skill or --all"