++
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: pull_brainskill
|
||||
description: Pulls valid Claude Skills from a ZIP URL (preferred), Git repository, or local directory. Only folders containing a strictly named SKILL.md file are imported.
|
||||
allowed-tools: Bash
|
||||
---
|
||||
|
||||
# Pull BRAIN Skill
|
||||
|
||||
This skill imports skill folders from a remote source or local directory.
|
||||
|
||||
**Strict Validation**: A folder is considered a valid skill ONLY if it contains a `SKILL.md` file (strictly case-sensitive). Folders with `skill.md` or `Skill.md` will be skipped.
|
||||
|
||||
## How to use
|
||||
|
||||
1. **Locate the script**:
|
||||
- Project path: `.claude/skills/pull_BRAINSkill/scripts/pull_skills.py`
|
||||
- Global path (Windows): `%USERPROFILE%\.claude\skills\pull_BRAINSkill\scripts\pull_skills.py`
|
||||
|
||||
2. **Run the script**: Provide a ZIP URL (Recommended), Git URL, or local path.
|
||||
|
||||
### Example 1: Pull via ZIP (Recommended First Choice)
|
||||
This method is faster and works best in restricted network environments. To do this, you must firstly parse the repository into a ZIP file URL by appending `/archive/refs/heads/main.zip` to the repo URL. For example, for a repository at `https://github.com/GitRepoAuthorName/RepoName`, the ZIP URL would be `https://github.com/GitRepoAuthorName/RepoName/archive/refs/heads/main.zip`.
|
||||
```bash
|
||||
python ".claude/skills/pull_BRAINSkill/scripts/pull_skills.py" "https://github.com/GitRepoAuthorName/RepoName/archive/refs/heads/main.zip" --overwrite
|
||||
```
|
||||
|
||||
### Example 2: Pull via Git
|
||||
Use this if you need a specific branch or have git configured.
|
||||
```bash
|
||||
python ".claude/skills/pull_BRAINSkill/scripts/pull_skills.py" "https://github.com/GitRepoAuthorName/RepoName.git"
|
||||
```
|
||||
|
||||
### Example 3: Import from Local Directory
|
||||
```bash
|
||||
python ".claude/skills/pull_BRAINSkill/scripts/pull_skills.py" "C:/Downloads/my-skills-repo"
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--dest <path>`: Destination directory for skills (default: `.claude/skills` relative to current project).
|
||||
- `--branch <branch>`: Specify branch to checkout.
|
||||
- `--overwrite`: Overwrite existing skill folders with the same name.
|
||||
|
||||
## Behavior
|
||||
- Clones the repository shallowly (`--depth 1`) into a temp directory.
|
||||
- Scans top-level folders for `SKILL.md`.
|
||||
- Copies valid skill folders into the destination `.claude/skills` directory.
|
||||
|
||||
## Notes
|
||||
- Paths use forward slashes for compatibility.
|
||||
- Requires `git` in PATH.
|
||||
- Only checks for the presence of `SKILL.md`, not content validity.
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Pull Skill Folders from a Git repository into a local .claude/skills directory.
|
||||
|
||||
Usage:
|
||||
python pull_skills.py <git_repo_url> [--dest <dest_path>] [--branch <branch>] [--overwrite]
|
||||
|
||||
- git_repo_url: HTTPS git URL, e.g., https://github.com/GitRepoAuthorName/-.git
|
||||
- dest_path: Destination folder for skills (default: <cwd>/.claude/skills)
|
||||
- branch: Branch to checkout (default: auto, uses repo default)
|
||||
- overwrite: If set, overwrite existing skill folders (default: off)
|
||||
|
||||
Behavior:
|
||||
- Clones the repo into a temporary directory (depth=1)
|
||||
- Scans top-level subfolders for a SKILL.md file
|
||||
- Copies those folders into the destination .claude/skills directory
|
||||
|
||||
Notes:
|
||||
- Requires Git installed and available in PATH.
|
||||
- Only validates existence of SKILL.md, does not validate YAML content.
|
||||
- Copies one level deep (top-level folders only).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
import subprocess
|
||||
from typing import List, Dict
|
||||
|
||||
|
||||
def run(cmd: List[str], cwd: str | None = None) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
|
||||
|
||||
|
||||
def ensure_dir(path: str) -> None:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
|
||||
def copy_skill_folder(src_folder: str, dest_root: str, overwrite: bool) -> Dict[str, str]:
|
||||
name = os.path.basename(src_folder.rstrip("/\\"))
|
||||
dest_folder = os.path.join(dest_root, name)
|
||||
|
||||
# STRICT check for SKILL.md existence (case-sensitive)
|
||||
# os.listdir returns the actual filename on the filesystem
|
||||
if "SKILL.md" not in os.listdir(src_folder):
|
||||
return {"status": "skipped", "folder": name, "reason": "no SKILL.md found (strictly case-sensitive)"}
|
||||
|
||||
if os.path.exists(dest_folder):
|
||||
if overwrite:
|
||||
shutil.rmtree(dest_folder)
|
||||
else:
|
||||
return {"status": "skipped", "folder": name, "reason": "exists"}
|
||||
|
||||
shutil.copytree(src_folder, dest_folder)
|
||||
|
||||
return {"status": "copied", "folder": name, "dest": dest_folder}
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(json.dumps({
|
||||
"ok": False,
|
||||
"error": "Missing git repo URL",
|
||||
"usage": "python pull_skills.py <git_repo_url> [--dest <dest_path>] [--branch <branch>] [--overwrite]"
|
||||
}, ensure_ascii=False))
|
||||
sys.exit(1)
|
||||
|
||||
repo_url = sys.argv[1]
|
||||
dest = None
|
||||
branch = None
|
||||
overwrite = False
|
||||
|
||||
# Parse optional args
|
||||
args = sys.argv[2:]
|
||||
i = 0
|
||||
while i < len(args):
|
||||
a = args[i]
|
||||
if a == "--dest" and i + 1 < len(args):
|
||||
dest = args[i + 1]
|
||||
i += 2
|
||||
elif a == "--branch" and i + 1 < len(args):
|
||||
branch = args[i + 1]
|
||||
i += 2
|
||||
elif a == "--overwrite":
|
||||
overwrite = True
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
|
||||
if not dest:
|
||||
dest = os.path.join(os.getcwd(), ".claude", "skills")
|
||||
ensure_dir(dest)
|
||||
|
||||
tempdir = tempfile.mkdtemp(prefix="pull_skills_")
|
||||
repo_dir = os.path.join(tempdir, "repo")
|
||||
|
||||
|
||||
# Handle local directory vs git URL vs ZIP URL
|
||||
if os.path.isdir(repo_url):
|
||||
# Case 1: Local directory
|
||||
try:
|
||||
shutil.copytree(repo_url, repo_dir, dirs_exist_ok=True)
|
||||
except Exception as e:
|
||||
print(json.dumps({"ok": False, "stage": "copy_local", "error": str(e)}, ensure_ascii=False))
|
||||
sys.exit(2)
|
||||
|
||||
elif repo_url.lower().endswith(".zip"):
|
||||
# Case 2: ZIP URL (direct download)
|
||||
import urllib.request
|
||||
import zipfile
|
||||
|
||||
try:
|
||||
zip_path = os.path.join(tempdir, "repo.zip")
|
||||
# print(f"Downloading {repo_url}...", file=sys.stderr)
|
||||
urllib.request.urlretrieve(repo_url, zip_path)
|
||||
|
||||
extract_dir = os.path.join(tempdir, "extracted")
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||
zip_ref.extractall(extract_dir)
|
||||
|
||||
# Handle GitHub zip structure: usually extract_dir/<repo-branch>/...
|
||||
# We want repo_dir to point to the folder containing the skills
|
||||
contents = os.listdir(extract_dir)
|
||||
if len(contents) == 1 and os.path.isdir(os.path.join(extract_dir, contents[0])):
|
||||
# Move the inner content to repo_dir
|
||||
shutil.move(os.path.join(extract_dir, contents[0]), repo_dir)
|
||||
else:
|
||||
shutil.move(extract_dir, repo_dir)
|
||||
|
||||
except Exception as e:
|
||||
print(json.dumps({
|
||||
"ok": False,
|
||||
"stage": "download_zip",
|
||||
"error": str(e),
|
||||
"url": repo_url
|
||||
}, ensure_ascii=False))
|
||||
sys.exit(2)
|
||||
|
||||
else:
|
||||
# Case 3: Git Clone
|
||||
clone_cmd = ["git", "clone", "--depth", "1"]
|
||||
if branch:
|
||||
clone_cmd += ["--branch", branch]
|
||||
clone_cmd += [repo_url, repo_dir]
|
||||
|
||||
cp = run(clone_cmd)
|
||||
if cp.returncode != 0:
|
||||
print(json.dumps({
|
||||
"ok": False,
|
||||
"stage": "clone",
|
||||
"cmd": " ".join(clone_cmd),
|
||||
"stderr": cp.stderr.strip(),
|
||||
"stdout": cp.stdout.strip()
|
||||
}, ensure_ascii=False))
|
||||
sys.exit(2)
|
||||
|
||||
# Scan top-level subfolders
|
||||
copied: List[Dict[str, str]] = []
|
||||
skipped: List[Dict[str, str]] = []
|
||||
|
||||
for entry in os.listdir(repo_dir):
|
||||
sub = os.path.join(repo_dir, entry)
|
||||
if os.path.isdir(sub):
|
||||
# Check for skill.md case-insensitively
|
||||
res = copy_skill_folder(sub, dest, overwrite)
|
||||
if res["status"] == "copied":
|
||||
copied.append(res)
|
||||
else:
|
||||
# If "no SKILL.md found", we just skip silently effectively (or log as skipped)
|
||||
# But to avoid cluttering output with non-skill folders, strict filtering is good.
|
||||
pass
|
||||
|
||||
result = {
|
||||
"ok": True,
|
||||
"repo": repo_url,
|
||||
"dest": dest,
|
||||
"copied": copied,
|
||||
"skipped": skipped
|
||||
}
|
||||
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user