From d6c68e76fbbea5e039a0d5e00b340e812cf8ba62 Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sun, 30 Nov 2025 08:52:30 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 12 + README.md | 3 + plugin.lock.json | 53 +++ skills/gh-cli/SKILL.md | 384 ++++++++++++++++++++++ skills/gh-cli/scripts/view_github_file.py | 116 +++++++ skills/gh-cli/scripts/view_pr_files.py | 229 +++++++++++++ 6 files changed, 797 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 plugin.lock.json create mode 100644 skills/gh-cli/SKILL.md create mode 100755 skills/gh-cli/scripts/view_github_file.py create mode 100755 skills/gh-cli/scripts/view_pr_files.py diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..90be3df --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "gh-cli", + "description": "Interact with GitHub using the gh CLI for PR management, issues, repository operations, GitHub Actions, and viewing GitHub files", + "version": "1.0.0", + "author": { + "name": "robbyt", + "email": "robbyt@robbyt.net" + }, + "skills": [ + "./skills" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2a0f2a8 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# gh-cli + +Interact with GitHub using the gh CLI for PR management, issues, repository operations, GitHub Actions, and viewing GitHub files diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..48ebd64 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,53 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:robbyt/claude-skills:plugins/gh-cli", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "31bedece54f88faa493e519e039afee660ee3252", + "treeHash": "2c596b7c95295a291ed648c4ee2f5d15947980fcbaae144c0f6a69dd2a457b4d", + "generatedAt": "2025-11-28T10:28:01.732472Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "gh-cli", + "description": "Interact with GitHub using the gh CLI for PR management, issues, repository operations, GitHub Actions, and viewing GitHub files", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "dad1138cfbfe77cbe93eff3bf97eb70b0627c9abb75c199da4686f9b0b4928a3" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "ead0a4890669649f5907fd1075f86e44834f5df4d51f9917ed9b379e8247a330" + }, + { + "path": "skills/gh-cli/SKILL.md", + "sha256": "5890c30aca591b2577172ad8125a6ddab31165497decb906d35829511a9d2c1a" + }, + { + "path": "skills/gh-cli/scripts/view_pr_files.py", + "sha256": "04241cc60911da682b60ce6c225250b21b5c49615ec21991ff28f2e463432e61" + }, + { + "path": "skills/gh-cli/scripts/view_github_file.py", + "sha256": "1b1515e9930568b9a537503a8ea910404b50587c483cdbec56772473ebf3aaa1" + } + ], + "dirSha256": "2c596b7c95295a291ed648c4ee2f5d15947980fcbaae144c0f6a69dd2a457b4d" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/gh-cli/SKILL.md b/skills/gh-cli/SKILL.md new file mode 100644 index 0000000..b4cd9b8 --- /dev/null +++ b/skills/gh-cli/SKILL.md @@ -0,0 +1,384 @@ +--- +name: gh-cli +description: Comprehensive GitHub CLI integration for PR management, issues, repository operations, GitHub Actions, and viewing GitHub file links. Triggers on explicit `gh` mentions ("use gh to create PR", "run gh issue list") or natural language GitHub operations ("show me open issues", "create a pull request", "view this GitHub file"). Handles GitHub URLs to view raw file content without HTML/JS clutter. +--- + +# GitHub CLI Integration + +Interact with GitHub repositories using the `gh` CLI for pull requests, issues, repository operations, GitHub Actions, and viewing GitHub files. + +## Prerequisites + +⚠️ **This skill assumes GitHub CLI (`gh`) is already installed and authenticated.** + +If you haven't completed setup: +1. Install: https://github.com/cli/cli#installation +2. Authenticate: `gh auth login` +3. Verify: `gh auth status` + +**Claude Code will NOT configure authentication for you.** This must be done manually before using this skill. + +## Quick Start + +Verify installation: +```bash +command -v gh +gh --version +``` + +Check authentication: +```bash +gh auth status +``` + +Basic patterns: +```bash +gh pr list # List pull requests +gh issue create --title "Bug" # Create issue +gh repo view owner/repo # View repository info +gh run list # List workflow runs +``` + +## Core Capabilities + +### 1. Pull Requests + +**Common operations:** +```bash +# List PRs +gh pr list --state open +gh pr list --author @me + +# View PR details +gh pr view 123 +gh pr view 123 --json title,body,state + +# Create PR +gh pr create --title "Feature" --body "Description" +gh pr create --fill # Use git commit messages + +# Review and merge +gh pr review 123 --approve +gh pr merge 123 --squash + +# View PR diff +gh pr diff 123 +gh pr diff 123 -- path/to/file.go # Specific file +``` + +**Helper script - View PR files:** +```bash +# List all files changed in a PR +python3 scripts/view_pr_files.py 123 --list +python3 scripts/view_pr_files.py https://github.com/user/repo/pull/123 --list + +# Show full diff +python3 scripts/view_pr_files.py 123 --diff + +# Show specific file from PR +python3 scripts/view_pr_files.py 123 --file path/to/file.go +``` + +### 2. Issues + +**Common operations:** +```bash +# List issues +gh issue list +gh issue list --label bug --state open +gh issue list --assignee @me + +# View issue +gh issue view 456 +gh issue view 456 --json title,body,labels + +# Create issue +gh issue create --title "Bug report" --body "Details" +gh issue create --label bug --assignee @me + +# Update issue +gh issue edit 456 --add-label "needs-review" +gh issue close 456 +gh issue reopen 456 +``` + +### 3. Repository Operations + +**Common operations:** +```bash +# View repository +gh repo view owner/repo +gh repo view owner/repo --json name,description,stargazersCount + +# Clone repository +gh repo clone owner/repo + +# Fork repository +gh repo fork owner/repo + +# List repositories +gh repo list owner +gh repo list owner --limit 50 --json name,description +``` + +### 4. GitHub Actions + +**Common operations:** +```bash +# List workflow runs +gh run list +gh run list --workflow ci.yml + +# View run details +gh run view 789 +gh run view 789 --log + +# Watch run in progress +gh run watch 789 + +# Re-run workflow +gh run rerun 789 +``` + +### 5. Viewing GitHub File URLs + +When the user provides a GitHub file URL (e.g., `https://github.com/user/repo/blob/main/path/to/file.go`), use the helper script to fetch raw file content instead of using WebFetch (which returns HTML/JS). + +**Helper script - View GitHub file:** +```bash +python3 scripts/view_github_file.py https://github.com/user/repo/blob/main/path/to/file.go +``` + +This script: +- Parses the GitHub URL to extract owner, repo, ref (branch/tag/commit), and file path +- Uses `gh api` to fetch the file content from GitHub's API +- Decodes the base64-encoded content +- Returns clean source code without HTML wrapper + +**Why use this instead of WebFetch:** +- WebFetch on GitHub URLs returns HTML with navigation, JavaScript, and GitHub UI +- This script returns only the raw file content +- Works for any branch, tag, or commit SHA +- Automatically handles base64 decoding + +## GitHub API Usage + +The `gh api` command provides direct access to GitHub's REST API. + +**Basic pattern:** +```bash +gh api repos/{owner}/{repo}/contents/{path} +gh api repos/{owner}/{repo}/contents/{path}?ref=branch-name +``` + +**Using placeholders:** +The `gh` CLI automatically replaces `{owner}`, `{repo}`, and `{branch}` placeholders based on the current repository context or `GH_REPO` environment variable. + +**Example - Get file content:** +```bash +# Returns JSON with base64-encoded content +gh api repos/owner/repo/contents/README.md + +# Extract and decode content +gh api repos/owner/repo/contents/README.md --jq '.content' | base64 --decode +``` + +**Important API details:** +- API paths should NOT start with `/` (use `repos/...` not `/repos/...`) +- Use query parameters for optional fields: `?ref=branch&per_page=100` +- Use `--jq` to extract specific fields from JSON responses +- File content is base64-encoded in the API response + +## Helper Scripts + +### scripts/view_github_file.py + +Fetches raw file content from GitHub URLs. + +**Usage:** +```bash +python3 scripts/view_github_file.py +``` + +**Example:** +```bash +python3 scripts/view_github_file.py https://github.com/golang/go/blob/master/README.md +``` + +**Supported URL formats:** +- `https://github.com/{owner}/{repo}/blob/{ref}/{path}` +- `https://github.com/{owner}/{repo}/tree/{ref}/{path}` + +**Output:** +- Stderr: Status message indicating what's being fetched +- Stdout: Raw file content + +### scripts/view_pr_files.py + +Views files changed in a pull request. + +**Usage:** +```bash +python3 scripts/view_pr_files.py [options] +``` + +**Options:** +- `--list`: List changed files only (no content) +- `--file `: Show content for specific file +- `--diff`: Show full diff (default) + +**Examples:** +```bash +# List files changed in PR +python3 scripts/view_pr_files.py 123 --list + +# View full diff +python3 scripts/view_pr_files.py https://github.com/user/repo/pull/123 + +# View specific file from PR +python3 scripts/view_pr_files.py 123 --file src/main.go +``` + +## Fallback Strategies + +If helper scripts fail (Python not available, import errors, etc.), fall back to direct `gh` CLI commands or HTTP requests. + +### Viewing GitHub Files (Fallback) + +**If `view_github_file.py` fails, use `gh api` directly:** +```bash +# Parse URL manually to extract: owner, repo, ref, path +# Then fetch with gh api +gh api repos/{owner}/{repo}/contents/{path}?ref={ref} --jq '.content' | base64 --decode +``` + +**Example:** +```bash +# URL: https://github.com/golang/go/blob/master/README.md +# Becomes: +gh api repos/golang/go/contents/README.md?ref=master --jq '.content' | base64 --decode +``` + +**If `gh` CLI fails, use HTTP requests:** +```bash +# Use curl or WebFetch to raw.githubusercontent.com +curl https://raw.githubusercontent.com/{owner}/{repo}/{ref}/{path} +``` + +**Example:** +```bash +curl https://raw.githubusercontent.com/golang/go/master/README.md +``` + +### Viewing PR Files (Fallback) + +**If `view_pr_files.py` fails, use `gh pr` commands directly:** +```bash +# List changed files +gh pr view 123 --json files --jq '.files[].path' + +# View diff +gh pr diff 123 + +# View specific file diff +gh pr diff 123 -- path/to/file.go +``` + +**To get file content from PR branch:** +```bash +# Get the head ref (branch name) +gh pr view 123 --json headRefName --jq '.headRefName' + +# Fetch file content from that ref +gh api repos/{owner}/{repo}/contents/{path}?ref={head_ref} --jq '.content' | base64 --decode +``` + +### General Guidelines + +When helper scripts fail: +1. **Try `gh` CLI directly** - Most script functionality can be replicated with `gh` commands +2. **Use `gh api` for API access** - Direct GitHub REST API access +3. **Fall back to HTTP requests** - Use `curl` or raw.githubusercontent.com URLs +4. **Report the error** - Let the user know the helper script failed and which fallback was used + +**Common failure reasons:** +- Python not in PATH +- Missing Python dependencies (should be stdlib only, but environment issues can occur) +- Script file permissions +- Incorrect script path + +## Common Workflows + +### Reviewing a Pull Request + +```bash +# 1. View PR details +gh pr view 123 + +# 2. List changed files +python3 scripts/view_pr_files.py 123 --list + +# 3. Review specific files +python3 scripts/view_pr_files.py 123 --file path/to/file.go + +# 4. View full diff +gh pr diff 123 + +# 5. Leave review +gh pr review 123 --approve --body "LGTM" +``` + +### Investigating GitHub File Links + +When a user shares a GitHub URL like `https://github.com/user/repo/blob/main/src/server.go`: + +```bash +# Fetch and analyze the file +python3 scripts/view_github_file.py https://github.com/user/repo/blob/main/src/server.go + +# Then analyze the content as needed +``` + +### Checking CI/CD Status + +```bash +# View recent workflow runs +gh run list --limit 5 + +# Check specific run +gh run view 789 + +# View logs if failed +gh run view 789 --log +``` + +## Troubleshooting + +### gh: command not found + +**Solution:** Install GitHub CLI from https://github.com/cli/cli#installation + +### gh: Not Authenticated + +**Solution:** Run `gh auth login` and follow the prompts + +### API Rate Limiting + +**Problem:** `gh api` returns 403 or rate limit errors + +**Solution:** +- Authenticated requests have higher rate limits (5000/hour vs 60/hour) +- Check remaining quota: `gh api rate_limit` +- Wait or use a different authentication token + +### File Not Found (404) + +**Problem:** `gh api` returns 404 for file paths + +**Possible causes:** +- File path is incorrect +- Branch/ref doesn't exist +- Private repository without access +- File was deleted or moved + +**Solution:** Verify the file exists at the specified ref using GitHub web UI or `gh repo view` diff --git a/skills/gh-cli/scripts/view_github_file.py b/skills/gh-cli/scripts/view_github_file.py new file mode 100755 index 0000000..081ca4c --- /dev/null +++ b/skills/gh-cli/scripts/view_github_file.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +""" +View raw file content from a GitHub URL using the gh CLI. + +Usage: + python view_github_file.py + +Example: + python view_github_file.py https://github.com/user/repo/blob/main/path/to/file.go +""" + +import sys +import re +import subprocess +import shutil +from typing import Optional, Tuple + + +def parse_github_url(url: str) -> Optional[Tuple[str, str, str, str]]: + """ + Parse a GitHub file URL and extract components. + + Supports formats: + - https://github.com/{owner}/{repo}/blob/{ref}/{path} + - https://github.com/{owner}/{repo}/tree/{ref}/{path} + + Note: This regex has limitations with branch names containing slashes + (e.g., feature/branch). The regex captures the first path segment after + blob/tree as the ref, which may not work for all branch naming patterns. + + Returns: + tuple: (owner, repo, ref, path) or None if invalid + """ + pattern = r"https?://github\.com/([^/]+)/([^/]+)/(?:blob|tree)/([^/]+)/(.+)" + match = re.match(pattern, url) + + if not match: + return None + + owner, repo, ref, path = match.groups() + return owner, repo, ref, path + + +def fetch_file_content(owner: str, repo: str, ref: str, path: str) -> Optional[str]: + """ + Fetch file content from GitHub using gh CLI with raw media type. + + Args: + owner: Repository owner + repo: Repository name + ref: Branch, tag, or commit SHA + path: File path within repository + + Returns: + str: File content or None on error + """ + if not shutil.which("gh"): + print("Error: 'gh' CLI tool is not installed or not in PATH.", file=sys.stderr) + return None + + api_path = f"repos/{owner}/{repo}/contents/{path}?ref={ref}" + + try: + result = subprocess.run( + ["gh", "api", "-H", "Accept: application/vnd.github.v3.raw", api_path], + capture_output=True, + check=True, + ) + + return result.stdout.decode("utf-8") + + except UnicodeDecodeError: + print( + f"Error: File at '{path}' appears to be binary or non-UTF-8 text.", + file=sys.stderr, + ) + return None + except subprocess.CalledProcessError as e: + error_msg = e.stderr.decode("utf-8").strip() if e.stderr else "Unknown error" + print(f"Error calling gh API: {error_msg}", file=sys.stderr) + return None + except Exception as e: + print(f"Unexpected error: {e}", file=sys.stderr) + return None + + +def main() -> None: + if len(sys.argv) != 2: + print(__doc__) + sys.exit(1) + + url = sys.argv[1] + + parsed = parse_github_url(url) + if not parsed: + print(f"Error: Invalid GitHub URL format: {url}", file=sys.stderr) + print( + "Expected format: https://github.com/owner/repo/blob/ref/path/to/file", + file=sys.stderr, + ) + sys.exit(1) + + owner, repo, ref, path = parsed + + print(f"Fetching: {owner}/{repo} @ {ref}:{path}", file=sys.stderr) + + content = fetch_file_content(owner, repo, ref, path) + + if content is None: + sys.exit(1) + + print(content) + + +if __name__ == "__main__": + main() diff --git a/skills/gh-cli/scripts/view_pr_files.py b/skills/gh-cli/scripts/view_pr_files.py new file mode 100755 index 0000000..30af209 --- /dev/null +++ b/skills/gh-cli/scripts/view_pr_files.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +""" +View files changed in a GitHub Pull Request using the gh CLI. + +Usage: + python view_pr_files.py [options] + +Options: + --list List changed files only (no content) + --file Show content for specific file + --diff Show full diff (default) + +Examples: + python view_pr_files.py 123 --list + python view_pr_files.py https://github.com/user/repo/pull/123 --file path/to/file.go + python view_pr_files.py 123 --diff +""" + +import sys +import re +import subprocess +import argparse +import json +import base64 +import urllib.parse +from typing import Optional, List + + +def parse_pr_reference(pr_ref: str) -> Optional[int]: + """ + Parse PR reference (number or URL) and extract PR number. + + Args: + pr_ref: PR number (e.g., "123") or URL (e.g., "https://github.com/user/repo/pull/123") + + Returns: + int: PR number or None if invalid + """ + if pr_ref.isdigit(): + return int(pr_ref) + + pattern = r"https?://github\.com/[^/]+/[^/]+/pull/(\d+)" + match = re.match(pattern, pr_ref) + + if match: + return int(match.group(1)) + + return None + + +def list_pr_files(pr_number: int) -> Optional[List[str]]: + """ + List all files changed in a PR. + + Args: + pr_number: PR number + + Returns: + list: List of changed file paths or None on error + """ + try: + result = subprocess.run( + [ + "gh", + "pr", + "view", + str(pr_number), + "--json", + "files", + "--jq", + ".files[].path", + ], + capture_output=True, + text=True, + check=True, + ) + + files = result.stdout.strip().split("\n") + return [f for f in files if f] + + except subprocess.CalledProcessError as e: + print(f"Error fetching PR files: {e.stderr}", file=sys.stderr) + return None + + +def show_pr_diff(pr_number: int, file_path: Optional[str] = None) -> Optional[str]: + """ + Show PR diff, optionally filtered to a specific file. + + Args: + pr_number: PR number + file_path: Optional file path to filter diff + + Returns: + str: Diff content or None on error + """ + try: + cmd = ["gh", "pr", "diff", str(pr_number)] + + if file_path: + cmd.extend(["--", file_path]) + + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + + return result.stdout + + except subprocess.CalledProcessError as e: + print(f"Error fetching PR diff: {e.stderr}", file=sys.stderr) + return None + + +def show_file_content(pr_number: int, file_path: str) -> Optional[str]: + """ + Show the new version of a file from a PR. + + Args: + pr_number: PR number + file_path: File path to view + + Returns: + str: File content or None on error + """ + try: + result = subprocess.run( + ["gh", "pr", "view", str(pr_number), "--json", "headRefName"], + capture_output=True, + text=True, + check=True, + ) + + head_ref = json.loads(result.stdout)["headRefName"] + + safe_path = urllib.parse.quote(file_path, safe="") + + result = subprocess.run( + [ + "gh", + "api", + f"repos/{{owner}}/{{repo}}/contents/{safe_path}?ref={head_ref}", + ], + capture_output=True, + text=True, + check=True, + ) + + response = json.loads(result.stdout) + + if "content" not in response: + print("Error: File not found or is a directory", file=sys.stderr) + return None + + content_b64 = response["content"].replace("\n", "") + + try: + content = base64.b64decode(content_b64).decode("utf-8") + return content + except UnicodeDecodeError: + print( + f"Error: Unable to decode file content (likely binary or non-UTF-8).", + file=sys.stderr, + ) + return None + + except subprocess.CalledProcessError as e: + print(f"Error fetching file content: {e.stderr}", file=sys.stderr) + return None + except Exception as e: + print(f"Unexpected error: {e}", file=sys.stderr) + return None + + +def main() -> None: + parser = argparse.ArgumentParser( + description="View files changed in a GitHub Pull Request", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + + parser.add_argument("pr_ref", help="PR number or URL") + + mode_group = parser.add_mutually_exclusive_group() + mode_group.add_argument( + "--list", action="store_true", help="List changed files only" + ) + mode_group.add_argument( + "--file", metavar="PATH", help="Show content for specific file" + ) + mode_group.add_argument( + "--diff", action="store_true", help="Show full diff (default)" + ) + + args = parser.parse_args() + + pr_number = parse_pr_reference(args.pr_ref) + if pr_number is None: + print(f"Error: Invalid PR reference: {args.pr_ref}", file=sys.stderr) + print( + "Expected: PR number (e.g., '123') or URL (e.g., 'https://github.com/user/repo/pull/123')", + file=sys.stderr, + ) + sys.exit(1) + + if args.list: + files = list_pr_files(pr_number) + if files is None: + sys.exit(1) + + print(f"Files changed in PR #{pr_number}:", file=sys.stderr) + for f in files: + print(f) + + elif args.file: + content = show_file_content(pr_number, args.file) + if content is None: + sys.exit(1) + + print(f"File: {args.file} (from PR #{pr_number})", file=sys.stderr) + print(content) + + else: + diff = show_pr_diff(pr_number, args.file) + if diff is None: + sys.exit(1) + + print(diff) + + +if __name__ == "__main__": + main()