Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:52:30 +08:00
commit d6c68e76fb
6 changed files with 797 additions and 0 deletions

View File

@@ -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"
]
}

3
README.md Normal file
View File

@@ -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

53
plugin.lock.json Normal file
View File

@@ -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": []
}
}

384
skills/gh-cli/SKILL.md Normal file
View File

@@ -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 <github-url>
```
**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 <pr-number-or-url> [options]
```
**Options:**
- `--list`: List changed files only (no content)
- `--file <path>`: 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`

View File

@@ -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 <github-url>
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()

View File

@@ -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 <pr-number-or-url> [options]
Options:
--list List changed files only (no content)
--file <path> 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()