
Github
- 184 installs
- 12 repo stars
- Updated August 4, 2026
- odyssey4me/agent-skills
Wire coding agents to GitHub for repo browsing, issue triage, PR creation/review, and workflow automation inside software delivery pipelines.
About
Provides agent-oriented GitHub integration for repositories, issues, pull requests, and Actions-aware workflows. Enables coding agents to participate in real software delivery loops rather than isolated local edits without version-control context.
- Repository and branch inspection
- Issue and PR automation hooks
- Review and comment assistance
- Workflow-triggered agent actions
- OAuth/token-aware API usage patterns
Github by the numbers
- 184 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #3,019 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/odyssey4me/agent-skills --skill githubAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 184 |
|---|---|
| repo stars | ★ 12 |
| Last updated | August 4, 2026 |
| Repository | odyssey4me/agent-skills ↗ |
What it does
Wire coding agents to GitHub for repo browsing, issue triage, PR creation/review, and workflow automation inside software delivery pipelines.
Files
GitHub Skill
This skill provides GitHub integration using the official gh CLI tool. A Python wrapper script produces markdown-formatted output for read/view operations. Action commands (create, merge, close, comment) should use gh directly.
Prerequisites
Install gh CLI: installation guide
Authentication
# Authenticate with GitHub
gh auth login
# Verify authentication
gh auth statusSee GitHub CLI Authentication for details.
Script Usage
The wrapper script (scripts/github.py) formats output as markdown. Use it for read/view operations to get agent-consumable output. Use gh directly for action commands (create, merge, close, comment). See permissions.md for read/write classification of each command.
# Check gh CLI is installed and authenticated
$SKILL_DIR/scripts/github.py check
# Issues
$SKILL_DIR/scripts/github.py issues list --repo OWNER/REPO
$SKILL_DIR/scripts/github.py issues view 123 --repo OWNER/REPO
# Pull Requests
$SKILL_DIR/scripts/github.py prs list --repo OWNER/REPO
$SKILL_DIR/scripts/github.py prs view 456 --repo OWNER/REPO
$SKILL_DIR/scripts/github.py prs checks 456 --repo OWNER/REPO
$SKILL_DIR/scripts/github.py prs status --repo OWNER/REPO
# Workflow Runs
$SKILL_DIR/scripts/github.py runs list --repo OWNER/REPO
$SKILL_DIR/scripts/github.py runs view 12345 --repo OWNER/REPO
# Repositories
$SKILL_DIR/scripts/github.py repos list
$SKILL_DIR/scripts/github.py repos view OWNER/REPO
# Search
$SKILL_DIR/scripts/github.py search repos "machine learning"
$SKILL_DIR/scripts/github.py search issues "label:bug is:open"
$SKILL_DIR/scripts/github.py search prs "is:open review:required"All commands support --limit N for list commands (default 30).
Commands (Direct gh Usage)
For action commands, use gh directly:
Issues
gh issue list # List issues
gh issue view 123 # View issue details
gh issue create # Create new issue
gh issue comment 123 # Add comment
gh issue close 123 # Close issue
gh issue edit 123 --add-label bug # Edit issueFull reference: gh issue
Pull Requests
gh pr list # List PRs
gh pr view 456 # View PR details
gh pr create # Create new PR
gh pr review 456 --approve # Approve PR
gh pr merge 456 --squash # Merge PR
gh pr checkout 456 # Checkout PR branch
gh pr diff 456 # View PR diff
gh pr checks 456 # View CI statusFull reference: gh pr
Workflows & Actions
gh workflow list # List workflows
gh workflow run "CI" # Trigger workflow
gh run list # List workflow runs
gh run view 123456 # View run details
gh run watch 123456 # Watch run progress
gh run download 123456 # Download artifacts
gh run rerun 123456 --failed # Rerun failed jobsFull references:
Repositories
gh repo list # List repositories
gh repo view OWNER/REPO # View repository
gh repo create # Create repository
gh repo clone OWNER/REPO # Clone repository
gh repo fork OWNER/REPO # Fork repositoryFull reference: gh repo
Search
gh search repos "machine learning" # Search repositories
gh search issues "is:open label:bug" # Search issues
gh search prs "is:open" # Search pull requests
gh search code "function auth" # Search codeFull reference: gh search
Examples
Daily PR Review
# Show PRs needing your attention
$SKILL_DIR/scripts/github.py prs status
# Review a specific PR
$SKILL_DIR/scripts/github.py prs view 456
$SKILL_DIR/scripts/github.py prs checks 456
gh pr diff 456
gh pr review 456 --approve
# Verify approval was recorded
$SKILL_DIR/scripts/github.py prs view 456 # check review statusCreate Issue and Link PR
# Create issue
gh issue create --title "Bug: Login fails" --body "Description" --label bug
# Verify: note the issue number from output
# Create PR that fixes it (use issue number from above)
gh pr create --title "Fix login bug (#123)" --body "Fixes #123"
# Verify PR was created and linked
$SKILL_DIR/scripts/github.py prs view <number>Monitor CI Pipeline
# Watch latest workflow run
gh run watch $(gh run list --limit 1 --json databaseId --jq '.[0].databaseId')
# Check failed runs
gh run list --status failure
# Rerun failed jobs
gh run rerun RUNID --failed
# Verify rerun started
$SKILL_DIR/scripts/github.py runs view RUNIDSee common-workflows.md for more examples.
Advanced Usage
JSON Output for Scripting
# Get specific fields
gh issue list --json number,title,author
# Process with jq
gh pr list --json number,title | jq '.[] | "\(.number): \(.title)"'
# Export to CSV
gh issue list --json number,title,author | jq -r '.[] | @csv'GitHub API Access
For operations not covered by gh commands:
# Make authenticated API request
gh api repos/OWNER/REPO/issues
# POST request
gh api repos/OWNER/REPO/issues -X POST -f title="Issue" -f body="Text"
# Process response
gh api repos/OWNER/REPO | jq '.stargazers_count'Full reference: gh api
Aliases for Frequent Operations
# Create shortcuts
gh alias set prs 'pr list --author @me'
gh alias set issues 'issue list --assignee @me'
gh alias set review 'pr list --search "review-requested:@me"'
# Use them
gh prs
gh issues
gh reviewConfiguration
# View configuration
gh config list
# Set default editor
gh config set editor vim
# Set git protocol
gh config set git_protocol sshConfiguration stored in ~/.config/gh/config.yml
Model Guidance
This skill wraps an official CLI. A fast, lightweight model is sufficient.
Troubleshooting
# Check authentication
gh auth status
# Re-authenticate
gh auth login
# Enable debug logging
GH_DEBUG=1 gh issue list
# Check gh version
gh --versionOfficial Documentation
- GitHub CLI Manual: <https://cli.github.com/manual/>
- GitHub CLI Repository: <https://github.com/cli/cli>
- GitHub API Documentation: <https://docs.github.com/en/rest>
- GitHub Actions: <https://docs.github.com/en/actions>
Summary
The GitHub skill uses the official gh CLI with a Python wrapper for markdown-formatted output on read/view commands.
Quick start: 1. Install: brew install gh (or equivalent for your OS) 2. Authenticate: gh auth login 3. Verify: $SKILL_DIR/scripts/github.py check 4. Read: $SKILL_DIR/scripts/github.py issues list --repo OWNER/REPO 5. Write: gh issue create, gh pr create, etc. (use gh directly)
For detailed command reference, use gh <command> --help or visit <https://cli.github.com/manual/>.
Common GitHub Workflows with gh CLI
This document provides practical examples of common GitHub workflows using the gh CLI.
Table of Contents
- Issue Management
- Pull Request Workflow
- CI/CD and Actions
- Repository Management
- Team Collaboration
- Automation Examples
Issue Management
Daily Issue Triage
Review and label new issues:
#!/bin/bash
# List unlabeled issues
gh issue list --label "" --json number,title,author
# For each issue, view and add labels
gh issue list --label "" --limit 10 | while read issue; do
issue_num=$(echo $issue | awk '{print $1}' | tr -d '#')
gh issue view $issue_num
echo "Add label (bug/enhancement/question/documentation/skip):"
read label
if [ "$label" != "skip" ]; then
gh issue edit $issue_num --add-label $label
fi
doneClose Stale Issues
Close issues inactive for over 90 days:
# List issues older than 90 days
gh issue list --state open --json number,title,updatedAt | \
jq -r --arg date "$(date -d '90 days ago' +%Y-%m-%d)" \
'.[] | select(.updatedAt < $date) | "\(.number) \(.title)"'
# Close them with a comment
gh issue list --state open --json number,updatedAt | \
jq -r --arg date "$(date -d '90 days ago' +%Y-%m-%d)" \
'.[] | select(.updatedAt < $date) | .number' | \
while read num; do
gh issue close $num --comment "Closing due to inactivity. Please reopen if still relevant."
doneCreate Issue from Template
# List available templates
ls .github/ISSUE_TEMPLATE/
# Create issue from template
gh issue create --template bug_report.md \
--title "Login fails with OAuth" \
--body "Steps to reproduce: ..." \
--label bug \
--assignee @meBulk Issue Operations
Add milestone to multiple issues:
# Find issues with label "v2.0"
gh issue list --label "v2.0" --json number | \
jq -r '.[].number' | \
while read num; do
gh issue edit $num --milestone "2.0 Release"
donePull Request Workflow
Create PR from Feature Branch
# Ensure you're on feature branch
git checkout -b feature/new-login
# Make changes and commit
git add .
git commit -m "feat: implement new login flow"
# Push branch
git push -u origin feature/new-login
# Create PR with auto-filled title/body from commits
gh pr create --fill
# Or with custom details
gh pr create \
--title "Implement new login flow" \
--body "This PR implements OAuth 2.0 login.
## Changes
- Add OAuth provider
- Update login UI
- Add tests
Fixes #123" \
--label enhancement \
--reviewer teammate1,teammate2Review PRs Assigned to You
#!/bin/bash
# Daily PR review workflow
echo "=== PRs waiting for your review ==="
gh pr list --search "review-requested:@me" --json number,title,author
# Review each PR
gh pr list --search "review-requested:@me" --json number | \
jq -r '.[].number' | \
while read pr; do
echo -e "\n=== Reviewing PR #$pr ==="
# View PR details
gh pr view $pr
# Check CI status
gh pr checks $pr
# View diff
gh pr diff $pr
# Checkout locally to test
echo "Checkout and test locally? (y/n)"
read checkout
if [ "$checkout" = "y" ]; then
gh pr checkout $pr
# Run tests
make test
# Switch back
git checkout -
fi
# Submit review
echo "Action: (approve/comment/request-changes/skip)"
read action
case $action in
approve)
gh pr review $pr --approve --body "LGTM! ✅"
;;
comment)
echo "Enter comment:"
read comment
gh pr review $pr --comment --body "$comment"
;;
request-changes)
echo "Enter requested changes:"
read changes
gh pr review $pr --request-changes --body "$changes"
;;
esac
doneAuto-merge When Checks Pass
# Enable auto-merge with squash
gh pr merge 456 --auto --squash --delete-branch
# The PR will automatically merge when:
# - All required checks pass
# - Required reviews are approved
# - No merge conflictsUpdate PR Based on Review Comments
# View PR with comments
gh pr view 456 --comments
# Make changes
git add .
git commit -m "fix: address review comments"
git push
# Add comment to PR
gh pr comment 456 --body "Updated per review feedback"
# Request re-review
gh pr review 456 --comment --body "@reviewer1 please re-review"CI/CD and Actions
Monitor Workflow Status
#!/bin/bash
# Watch latest CI run for current branch
# Get latest run for current branch
BRANCH=$(git branch --show-current)
RUN_ID=$(gh run list --branch $BRANCH --limit 1 --json databaseId --jq '.[0].databaseId')
# Watch it in real-time
gh run watch $RUN_IDTrigger Deployment
# Trigger production deployment
gh workflow run "Deploy to Production" \
--ref main \
-f environment=production \
-f version=v2.1.0
# Monitor deployment
gh run list --workflow="Deploy to Production" --limit 1
DEPLOY_RUN=$(gh run list --workflow="Deploy to Production" --limit 1 --json databaseId --jq '.[0].databaseId')
gh run watch $DEPLOY_RUNRerun Failed CI Jobs
# List recent failed runs
gh run list --status failure --limit 10
# Rerun failed jobs only
gh run rerun 123456 --failed
# Or rerun all jobs
gh run rerun 123456Download Build Artifacts
# List artifacts for a run
gh run view 123456 --json artifacts
# Download all artifacts
gh run download 123456
# Download specific artifact
gh run download 123456 --name "build-artifacts"
# Download to specific directory
gh run download 123456 --dir ./artifactsCancel In-Progress Runs
# Cancel specific run
gh run cancel 123456
# Cancel all in-progress runs for a branch
gh run list --branch feature/test --status in_progress --json databaseId | \
jq -r '.[].databaseId' | \
while read run; do
gh run cancel $run
doneRepository Management
Create New Repository from Template
# Fork a template repository
gh repo fork template-org/template-repo --clone
# Or create from scratch
gh repo create my-new-project \
--public \
--description "My awesome project" \
--gitignore Python \
--license MIT
# Create from current directory
gh repo create --source=. --public --pushSync Fork with Upstream
# Add upstream if not already added
git remote add upstream https://github.com/original-owner/repo.git
# Fetch upstream changes
git fetch upstream
# Sync via gh
gh repo sync --branch main
# Or manually merge
git checkout main
git merge upstream/main
git push origin mainArchive Old Repositories
# List repositories to archive
gh repo list --json name,pushedAt,isArchived | \
jq -r --arg date "$(date -d '2 years ago' +%Y-%m-%d)" \
'.[] | select(.isArchived == false and .pushedAt < $date) | .name'
# Archive them
gh repo list --json name,pushedAt,isArchived | \
jq -r --arg date "$(date -d '2 years ago' +%Y-%m-%d)" \
'.[] | select(.isArchived == false and .pushedAt < $date) | .name' | \
while read repo; do
gh repo archive $repo --yes
doneTeam Collaboration
Assign Code Review to Team
# Create PR and assign to team
gh pr create --fill --reviewer team-name
# Or add reviewers to existing PR
gh pr edit 456 --add-reviewer @teammate1,@teammate2Track Team's PR Status
#!/bin/bash
# Team PR dashboard
echo "=== Team PRs Status ==="
# List PRs by team members
for member in alice bob carol; do
echo -e "\n$member's PRs:"
gh pr list --author $member --json number,title,reviews,checks | \
jq -r '.[] | " #\(.number): \(.title) | Reviews: \(.reviews | length) | Checks: \(.checks[0].status)"'
doneCreate Release
# Create a release
gh release create v2.1.0 \
--title "Version 2.1.0" \
--notes "## What's New
- Feature 1
- Feature 2
## Bug Fixes
- Fix 1
- Fix 2" \
--target main \
./dist/app-v2.1.0.tar.gz
# Or generate notes automatically from PRs
gh release create v2.1.0 --generate-notes --target mainAutomation Examples
Daily Standup Report
#!/bin/bash
# Generate daily activity report
TODAY=$(date +%Y-%m-%d)
YESTERDAY=$(date -d '1 day ago' +%Y-%m-%d)
echo "=== Activity Report for $TODAY ==="
echo -e "\nIssues closed:"
gh issue list --state closed --search "closed:>=$YESTERDAY" --json number,title | \
jq -r '.[] | " #\(.number): \(.title)"'
echo -e "\nPRs merged:"
gh pr list --state merged --search "merged:>=$YESTERDAY" --json number,title | \
jq -r '.[] | " #\(.number): \(.title)"'
echo -e "\nNew issues:"
gh issue list --search "created:>=$YESTERDAY" --json number,title,author | \
jq -r '.[] | " #\(.number): \(.title) (@\(.author.login))"'
echo -e "\nNew PRs:"
gh pr list --search "created:>=$YESTERDAY" --json number,title,author | \
jq -r '.[] | " #\(.number): \(.title) (@\(.author.login))"'Auto-label PRs Based on Files Changed
#!/bin/bash
# Auto-label PRs based on changed files
for pr in $(gh pr list --state open --json number --jq '.[].number'); do
# Get changed files
FILES=$(gh pr view $pr --json files --jq '.files[].path')
# Add labels based on file patterns
if echo "$FILES" | grep -q "^docs/"; then
gh pr edit $pr --add-label documentation
fi
if echo "$FILES" | grep -q "test"; then
gh pr edit $pr --add-label tests
fi
if echo "$FILES" | grep -q "\.py$"; then
gh pr edit $pr --add-label python
fi
if echo "$FILES" | grep -q "\.js$\|\.ts$"; then
gh pr edit $pr --add-label javascript
fi
doneNotify on Failed CI
#!/bin/bash
# Check for failed CI and notify
# Get failed runs from last hour
gh run list --status failure --created "$(date -d '1 hour ago' +%Y-%m-%dT%H:%M:%S)" \
--json number,displayTitle,workflowName,url | \
jq -r '.[] | "❌ \(.workflowName): \(.displayTitle)\n \(.url)"' | \
while read -r line; do
# Send notification (example using curl to a webhook)
curl -X POST https://hooks.slack.com/services/YOUR/WEBHOOK/URL \
-H 'Content-Type: application/json' \
-d "{\"text\": \"$line\"}"
doneBulk PR Cleanup
Close draft PRs older than 30 days:
# List old draft PRs
gh pr list --state open --draft --json number,title,createdAt | \
jq -r --arg date "$(date -d '30 days ago' +%Y-%m-%d)" \
'.[] | select(.createdAt < $date) | "\(.number) \(.title)"'
# Close them
gh pr list --state open --draft --json number,createdAt | \
jq -r --arg date "$(date -d '30 days ago' +%Y-%m-%d)" \
'.[] | select(.createdAt < $date) | .number' | \
while read pr; do
gh pr close $pr --comment "Closing stale draft PR. Please reopen if still working on this."
gh pr edit $pr --add-label "stale"
doneAdvanced: Using GitHub API
For operations not covered by gh commands, use the API:
# List repository languages
gh api repos/OWNER/REPO/languages | jq '.'
# Get repository statistics
gh api repos/OWNER/REPO | jq '{
stars: .stargazers_count,
forks: .forks_count,
watchers: .watchers_count,
open_issues: .open_issues_count
}'
# List contributors
gh api repos/OWNER/REPO/contributors --paginate | \
jq -r '.[] | "\(.contributions)\t\(.login)"' | \
sort -rn | head -10
# Create a project card
gh api projects/columns/COLUMN_ID/cards \
-X POST \
-f note='Task description' \
-f content_type='Issue' \
-f content_id=123
# Search code across organization
gh api search/code \
-f q='org:myorg function authenticate language:python' | \
jq -r '.items[] | "\(.repository.full_name):\(.path)"'Tips for Efficient Workflows
Use Aliases
Create shortcuts for common operations:
# Save frequently used commands as aliases
gh alias set prs 'pr list --author @me'
gh alias set issues 'issue list --assignee @me'
gh alias set review 'pr list --search "review-requested:@me"'
# Use them
gh prs
gh issues
gh reviewJSON Output for Scripting
Use --json flag for programmatic processing:
# Get specific fields only
gh pr list --json number,title,author,labels
# Process with jq
gh issue list --json number,title,labels | \
jq '.[] | select(.labels | map(.name) | index("bug"))'
# Export to CSV
gh pr list --json number,title,author,createdAt | \
jq -r '.[] | [.number, .title, .author.login, .createdAt] | @csv'Environment Variables
Control gh behavior with environment variables:
# Enable debug logging
GH_DEBUG=1 gh pr list
# Use different editor
GH_EDITOR=vim gh issue create
# Disable interactive prompts
GH_PROMPT_DISABLED=1 gh pr create --fill
# Use specific PAT
GH_TOKEN=ghp_custom_token gh repo listAdditional Resources
Command Permissions
This reference classifies commands by access level to help agents enforce appropriate permission controls.
- read: Safe to execute without user confirmation. These commands
only retrieve or display information.
- write: Requires user confirmation before execution. These
commands create, modify, or delete data.
Note: This skill's script only provides read operations. Write operations (creating issues, PRs, merging) use the gh CLI directly and are not covered here.
| Command | Access | Description |
|---|---|---|
| check | read | Verify setup and connectivity |
| issues list | read | List repository issues |
| issues view | read | View issue details |
| prs list | read | List pull requests |
| prs view | read | View pull request details |
| prs checks | read | View PR check status |
| prs status | read | View PR review status |
| runs list | read | List workflow runs |
| runs view | read | View workflow run details |
| repos list | read | List repositories |
| repos view | read | View repository details |
| search repos | read | Search repositories |
| search issues | read | Search issues |
| search prs | read | Search pull requests |
#!/usr/bin/env python3
"""GitHub wrapper skill for AI agents.
Wraps the gh CLI to produce markdown-formatted output for read/view commands.
Action commands (create, merge, close, comment) should use gh directly.
Usage:
python github.py check
python github.py issues list --repo OWNER/REPO
python github.py issues view 123 --repo OWNER/REPO
python github.py prs list --repo OWNER/REPO
python github.py prs view 456 --repo OWNER/REPO
python github.py prs checks 456 --repo OWNER/REPO
python github.py prs status --repo OWNER/REPO
python github.py runs list --repo OWNER/REPO
python github.py runs view 123456 --repo OWNER/REPO
python github.py repos list
python github.py repos view OWNER/REPO
python github.py search repos "machine learning"
python github.py search issues "label:bug is:open"
python github.py search prs "is:open review:required"
Requirements:
gh CLI (https://cli.github.com/)
"""
from __future__ import annotations
import argparse
import json
import shutil
import subprocess
import sys
from typing import Any
# ============================================================================
# CONSTANTS — JSON field lists per entity type
# ============================================================================
ISSUE_LIST_FIELDS = "number,title,state,author,assignees,labels,createdAt,updatedAt"
ISSUE_VIEW_FIELDS = (
"number,title,state,body,author,assignees,labels,milestone,createdAt,updatedAt,comments,url"
)
PR_LIST_FIELDS = "number,title,state,author,assignees,labels,createdAt,updatedAt,isDraft"
PR_VIEW_FIELDS = (
"number,title,state,body,author,assignees,labels,milestone,"
"createdAt,updatedAt,comments,url,isDraft,mergeable,reviewDecision,"
"additions,deletions,changedFiles,headRefName,baseRefName"
)
PR_STATUS_FIELDS = "number,title,state,headRefName,baseRefName,isDraft,reviewDecision,author,url"
PR_CHECKS_FIELDS = "name,status,conclusion,startedAt,completedAt,detailsUrl"
RUN_LIST_FIELDS = "databaseId,displayTitle,status,conclusion,event,createdAt,updatedAt,url"
RUN_VIEW_FIELDS = (
"databaseId,displayTitle,status,conclusion,event,"
"createdAt,updatedAt,url,workflowName,headBranch,headSha,jobs"
)
REPO_LIST_FIELDS = "name,owner,description,isPrivate,isFork,stargazerCount,updatedAt,url"
REPO_VIEW_FIELDS = (
"name,owner,description,isPrivate,isFork,stargazerCount,"
"forkCount,watchers,updatedAt,url,defaultBranchRef,homepageUrl,"
"primaryLanguage,licenseInfo,isArchived"
)
SEARCH_REPOS_FIELDS = "fullName,description,isPrivate,stargazersCount,updatedAt,url"
SEARCH_ISSUES_FIELDS = (
"repository,number,title,state,author,assignees,labels,createdAt,updatedAt,url"
)
SEARCH_PRS_FIELDS = (
"repository,number,title,state,author,assignees,labels,createdAt,updatedAt,url,isDraft"
)
# ============================================================================
# gh CLI HELPER
# ============================================================================
def run_gh(args: list[str], json_fields: str | None = None) -> dict[str, Any] | list[Any] | str:
"""Run a gh CLI command and return parsed output.
Args:
args: Arguments to pass to gh (e.g., ["issue", "list"]).
json_fields: Comma-separated field list for --json output.
Returns:
Parsed JSON data (dict or list), or raw string output.
Raises:
SystemExit: If gh command fails.
"""
cmd = ["gh", *args]
if json_fields:
cmd.extend(["--json", json_fields])
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"Error: {result.stderr.strip()}", file=sys.stderr)
sys.exit(1)
output = result.stdout.strip()
if json_fields and output:
return json.loads(output)
return output
# ============================================================================
# DATE FORMATTING
# ============================================================================
def format_date(iso_date: str | None) -> str:
"""Format ISO 8601 date to YYYY-MM-DD HH:MM.
Args:
iso_date: ISO 8601 date string (e.g., "2024-01-15T10:30:00Z").
Returns:
Formatted date string, or "N/A" if input is None/empty.
"""
if not iso_date:
return "N/A"
# ISO 8601: 2024-01-15T10:30:00Z → 2024-01-15 10:30
return iso_date[:10] + " " + iso_date[11:16] if len(iso_date) >= 16 else iso_date[:10]
# ============================================================================
# FORMAT FUNCTIONS — one per entity type (markdown output)
# ============================================================================
def format_issue_summary(issue: dict[str, Any]) -> str:
"""Format a GitHub issue for markdown display.
Args:
issue: Issue dictionary from gh --json output.
Returns:
Markdown-formatted string.
"""
number = issue.get("number", "?")
title = issue.get("title", "(No title)")
state = issue.get("state", "UNKNOWN")
author = _get_login(issue.get("author"))
assignees = _get_logins(issue.get("assignees", []))
labels = _get_label_names(issue.get("labels", []))
created = format_date(issue.get("createdAt"))
lines = [
f"### #{number}: {title}",
f"- **State:** {state}",
f"- **Author:** {author}",
]
if assignees:
lines.append(f"- **Assignees:** {assignees}")
if labels:
lines.append(f"- **Labels:** {labels}")
lines.append(f"- **Created:** {created}")
body = issue.get("body")
if body:
lines.append(f"\n{body.strip()}")
url = issue.get("url")
if url:
lines.append(f"\n- **URL:** {url}")
return "\n".join(lines)
def format_issue_row(issue: dict[str, Any]) -> str:
"""Format a GitHub issue as a compact markdown entry for lists.
Args:
issue: Issue dictionary from gh --json output.
Returns:
Markdown-formatted string.
"""
number = issue.get("number", "?")
title = issue.get("title", "(No title)")
state = issue.get("state", "UNKNOWN")
author = _get_login(issue.get("author"))
labels = _get_label_names(issue.get("labels", []))
created = format_date(issue.get("createdAt"))
lines = [
f"### #{number}: {title}",
f"- **State:** {state}",
f"- **Author:** {author}",
]
if labels:
lines.append(f"- **Labels:** {labels}")
lines.append(f"- **Created:** {created}")
return "\n".join(lines)
def format_pr_summary(pr: dict[str, Any]) -> str:
"""Format a GitHub pull request for markdown display.
Args:
pr: PR dictionary from gh --json output.
Returns:
Markdown-formatted string.
"""
number = pr.get("number", "?")
title = pr.get("title", "(No title)")
state = pr.get("state", "UNKNOWN")
draft = " (Draft)" if pr.get("isDraft") else ""
author = _get_login(pr.get("author"))
assignees = _get_logins(pr.get("assignees", []))
labels = _get_label_names(pr.get("labels", []))
created = format_date(pr.get("createdAt"))
lines = [
f"### #{number}: {title}{draft}",
f"- **State:** {state}",
f"- **Author:** {author}",
]
if assignees:
lines.append(f"- **Assignees:** {assignees}")
if labels:
lines.append(f"- **Labels:** {labels}")
head = pr.get("headRefName")
base = pr.get("baseRefName")
if head and base:
lines.append(f"- **Branch:** {head} → {base}")
review = pr.get("reviewDecision")
if review:
lines.append(f"- **Review:** {review}")
additions = pr.get("additions")
deletions = pr.get("deletions")
changed = pr.get("changedFiles")
if additions is not None:
lines.append(f"- **Changes:** +{additions} -{deletions} ({changed} files)")
lines.append(f"- **Created:** {created}")
body = pr.get("body")
if body:
lines.append(f"\n{body.strip()}")
url = pr.get("url")
if url:
lines.append(f"\n- **URL:** {url}")
return "\n".join(lines)
def format_pr_row(pr: dict[str, Any]) -> str:
"""Format a GitHub PR as a compact markdown entry for lists.
Args:
pr: PR dictionary from gh --json output.
Returns:
Markdown-formatted string.
"""
number = pr.get("number", "?")
title = pr.get("title", "(No title)")
state = pr.get("state", "UNKNOWN")
draft = " (Draft)" if pr.get("isDraft") else ""
author = _get_login(pr.get("author"))
labels = _get_label_names(pr.get("labels", []))
created = format_date(pr.get("createdAt"))
lines = [
f"### #{number}: {title}{draft}",
f"- **State:** {state}",
f"- **Author:** {author}",
]
if labels:
lines.append(f"- **Labels:** {labels}")
lines.append(f"- **Created:** {created}")
return "\n".join(lines)
def format_pr_status(data: dict[str, Any]) -> str:
"""Format gh pr status output as markdown.
Args:
data: Dictionary with 'currentBranch', 'createdBy', 'needsReview' keys.
Returns:
Markdown-formatted string.
"""
lines = ["## PR Status"]
current = data.get("currentBranch")
lines.append("\n### Current Branch")
if current:
pr = current
number = pr.get("number", "?")
title = pr.get("title", "(No title)")
state = pr.get("state", "UNKNOWN")
draft = " (Draft)" if pr.get("isDraft") else ""
review = pr.get("reviewDecision", "")
url = pr.get("url", "")
lines.append(f"- **#{number}: {title}**{draft}")
line_parts = [f" {state}"]
if review:
line_parts.append(f"Review: {review}")
lines.append(" | ".join(line_parts))
if url:
lines.append(f" {url}")
else:
lines.append("No PR for current branch")
for section_key, section_title in [
("createdBy", "Created by You"),
("needsReview", "Requesting Your Review"),
]:
prs = data.get(section_key, [])
lines.append(f"\n### {section_title}")
if not prs:
lines.append("None")
else:
for pr in prs:
number = pr.get("number", "?")
title = pr.get("title", "(No title)")
state = pr.get("state", "UNKNOWN")
draft = " (Draft)" if pr.get("isDraft") else ""
head = pr.get("headRefName", "")
base = pr.get("baseRefName", "")
review = pr.get("reviewDecision", "")
branch = f" ({head} → {base})" if head and base else ""
entry = f"- **#{number}: {title}**{draft}{branch}"
parts = [f" {state}"]
if review:
parts.append(f"Review: {review}")
lines.append(entry)
lines.append(" | ".join(parts))
return "\n".join(lines)
def format_check(check: dict[str, Any]) -> str:
"""Format a PR check/status for markdown display.
Args:
check: Check dictionary from gh --json output.
Returns:
Markdown-formatted string.
"""
name = check.get("name", "(Unknown)")
status = check.get("status", "UNKNOWN")
conclusion = check.get("conclusion", "")
started = format_date(check.get("startedAt"))
state = conclusion if conclusion else status
return f"- **{name}:** {state} (started {started})"
def format_run_summary(run: dict[str, Any]) -> str:
"""Format a GitHub Actions run for markdown display.
Args:
run: Run dictionary from gh --json output.
Returns:
Markdown-formatted string.
"""
run_id = run.get("databaseId", "?")
title = run.get("displayTitle", "(No title)")
status = run.get("status", "UNKNOWN")
conclusion = run.get("conclusion", "")
event = run.get("event", "")
created = format_date(run.get("createdAt"))
state = f"{status}" if not conclusion else f"{conclusion}"
lines = [
f"### {title} (#{run_id})",
f"- **Status:** {state}",
]
if event:
lines.append(f"- **Event:** {event}")
lines.append(f"- **Created:** {created}")
workflow = run.get("workflowName")
if workflow:
lines.append(f"- **Workflow:** {workflow}")
branch = run.get("headBranch")
if branch:
lines.append(f"- **Branch:** {branch}")
sha = run.get("headSha")
if sha:
lines.append(f"- **Commit:** {sha[:8]}")
url = run.get("url")
if url:
lines.append(f"- **URL:** {url}")
jobs = run.get("jobs", [])
if jobs:
lines.append("\n**Jobs:**")
for job in jobs:
job_name = job.get("name", "?")
job_status = job.get("conclusion") or job.get("status", "?")
lines.append(f"- **{job_name}:** {job_status}")
return "\n".join(lines)
def format_run_row(run: dict[str, Any]) -> str:
"""Format a GitHub Actions run as a compact markdown entry for lists.
Args:
run: Run dictionary from gh --json output.
Returns:
Markdown-formatted string.
"""
run_id = run.get("databaseId", "?")
title = run.get("displayTitle", "(No title)")
status = run.get("status", "UNKNOWN")
conclusion = run.get("conclusion", "")
event = run.get("event", "")
created = format_date(run.get("createdAt"))
state = f"{status}" if not conclusion else f"{conclusion}"
lines = [
f"### {title} (#{run_id})",
f"- **Status:** {state}",
]
if event:
lines.append(f"- **Event:** {event}")
lines.append(f"- **Created:** {created}")
return "\n".join(lines)
def format_repo_summary(repo: dict[str, Any]) -> str:
"""Format a GitHub repository for markdown display.
Args:
repo: Repository dictionary from gh --json output.
Returns:
Markdown-formatted string.
"""
owner = repo.get("owner", {})
owner_login = owner.get("login", "") if isinstance(owner, dict) else str(owner)
name = repo.get("name", "(Unknown)")
full_name = f"{owner_login}/{name}" if owner_login else name
description = repo.get("description") or "(No description)"
private = "Private" if repo.get("isPrivate") else "Public"
stars = repo.get("stargazerCount", 0)
forks = repo.get("forkCount", 0)
updated = format_date(repo.get("updatedAt"))
lines = [
f"### {full_name}",
f"- **Description:** {description}",
f"- **Visibility:** {private}",
f"- **Stars:** {stars}",
]
if forks:
lines.append(f"- **Forks:** {forks}")
lang = repo.get("primaryLanguage")
if lang:
lang_name = lang.get("name", "") if isinstance(lang, dict) else str(lang)
if lang_name:
lines.append(f"- **Language:** {lang_name}")
license_info = repo.get("licenseInfo")
if license_info:
lic_name = license_info.get("name", "") if isinstance(license_info, dict) else ""
if lic_name:
lines.append(f"- **License:** {lic_name}")
default_branch = repo.get("defaultBranchRef")
if default_branch:
branch_name = default_branch.get("name", "") if isinstance(default_branch, dict) else ""
if branch_name:
lines.append(f"- **Default Branch:** {branch_name}")
lines.append(f"- **Updated:** {updated}")
url = repo.get("url")
if url:
lines.append(f"- **URL:** {url}")
return "\n".join(lines)
def format_repo_row(repo: dict[str, Any]) -> str:
"""Format a GitHub repository as a compact markdown entry for lists.
Args:
repo: Repository dictionary from gh --json output.
Returns:
Markdown-formatted string.
"""
owner = repo.get("owner", {})
owner_login = owner.get("login", "") if isinstance(owner, dict) else str(owner)
name = repo.get("name", "(Unknown)")
full_name = f"{owner_login}/{name}" if owner_login else name
description = repo.get("description") or "(No description)"
private = "Private" if repo.get("isPrivate") else "Public"
stars = repo.get("stargazerCount", 0)
lines = [
f"### {full_name}",
f"- **Description:** {description}",
f"- **Visibility:** {private}",
f"- **Stars:** {stars}",
]
return "\n".join(lines)
def format_search_repo(repo: dict[str, Any]) -> str:
"""Format a search result repository for markdown display.
Args:
repo: Search result dictionary from gh search --json output.
Returns:
Markdown-formatted string.
"""
full_name = repo.get("fullName", "(Unknown)")
description = repo.get("description") or "(No description)"
private = "Private" if repo.get("isPrivate") else "Public"
stars = repo.get("stargazersCount", 0)
updated = format_date(repo.get("updatedAt"))
lines = [
f"### {full_name}",
f"- **Description:** {description}",
f"- **Visibility:** {private}",
f"- **Stars:** {stars}",
f"- **Updated:** {updated}",
]
url = repo.get("url")
if url:
lines.append(f"- **URL:** {url}")
return "\n".join(lines)
def format_search_issue(issue: dict[str, Any]) -> str:
"""Format a search result issue for markdown display.
Args:
issue: Search result dictionary from gh search --json output.
Returns:
Markdown-formatted string.
"""
repo = issue.get("repository", {})
repo_name = repo.get("nameWithOwner", "") if isinstance(repo, dict) else str(repo)
number = issue.get("number", "?")
title = issue.get("title", "(No title)")
state = issue.get("state", "UNKNOWN")
author = _get_login(issue.get("author"))
labels = _get_label_names(issue.get("labels", []))
created = format_date(issue.get("createdAt"))
prefix = f"{repo_name}#" if repo_name else "#"
lines = [
f"### {prefix}{number}: {title}",
f"- **State:** {state}",
f"- **Author:** {author}",
]
if labels:
lines.append(f"- **Labels:** {labels}")
lines.append(f"- **Created:** {created}")
return "\n".join(lines)
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
def _get_login(author: dict[str, Any] | None) -> str:
"""Extract login name from an author/user dict.
Args:
author: User dictionary with 'login' key.
Returns:
Login string or "Unknown".
"""
if not author:
return "Unknown"
if isinstance(author, dict):
return author.get("login", "Unknown")
return str(author)
def _get_logins(users: list[dict[str, Any]]) -> str:
"""Extract comma-separated login names from a list of user dicts.
Args:
users: List of user dictionaries.
Returns:
Comma-separated login names, or empty string if none.
"""
if not users:
return ""
names = []
for u in users:
if isinstance(u, dict):
names.append(u.get("login", "?"))
else:
names.append(str(u))
return ", ".join(names)
def _get_label_names(labels: list[dict[str, Any]]) -> str:
"""Extract comma-separated label names from a list of label dicts.
Args:
labels: List of label dictionaries.
Returns:
Comma-separated label names, or empty string if none.
"""
if not labels:
return ""
names = []
for label in labels:
if isinstance(label, dict):
names.append(label.get("name", "?"))
else:
names.append(str(label))
return ", ".join(names)
# ============================================================================
# COMMAND HANDLERS — one per subcommand, return exit code
# ============================================================================
def cmd_check(_args: argparse.Namespace) -> int:
"""Verify gh CLI is installed and authenticated.
Args:
_args: Parsed arguments (unused).
Returns:
Exit code (0 success, 1 error).
"""
if not shutil.which("gh"):
print("Error: gh CLI not found. Install from https://cli.github.com/", file=sys.stderr)
return 1
result = subprocess.run(["gh", "auth", "status"], capture_output=True, text=True)
if result.returncode != 0:
print("Error: gh CLI not authenticated.", file=sys.stderr)
print(result.stderr.strip(), file=sys.stderr)
print("\nRun: gh auth login", file=sys.stderr)
return 1
print("✓ gh CLI is installed and authenticated")
# Show auth details (stderr from gh auth status contains the info)
for line in result.stderr.strip().splitlines():
print(f" {line.strip()}")
return 0
def cmd_issues_list(args: argparse.Namespace) -> int:
"""List issues for a repository.
Args:
args: Parsed arguments with repo, limit, json flags.
Returns:
Exit code.
"""
gh_args = ["issue", "list"]
if args.repo:
gh_args.extend(["-R", args.repo])
gh_args.extend(["--limit", str(args.limit)])
if args.json:
data = run_gh(gh_args, ISSUE_LIST_FIELDS)
print(json.dumps(data, indent=2))
else:
data = run_gh(gh_args, ISSUE_LIST_FIELDS)
items = data if isinstance(data, list) else []
if not items:
print("No issues found")
else:
print(f"## Issues\n\nFound {len(items)} issue(s):\n")
print("\n\n".join(format_issue_row(i) for i in items))
return 0
def cmd_issues_view(args: argparse.Namespace) -> int:
"""View a single issue.
Args:
args: Parsed arguments with issue number, repo, json flags.
Returns:
Exit code.
"""
gh_args = ["issue", "view", str(args.number)]
if args.repo:
gh_args.extend(["-R", args.repo])
if args.json:
data = run_gh(gh_args, ISSUE_VIEW_FIELDS)
print(json.dumps(data, indent=2))
else:
data = run_gh(gh_args, ISSUE_VIEW_FIELDS)
if isinstance(data, dict):
print(format_issue_summary(data))
return 0
def cmd_prs_list(args: argparse.Namespace) -> int:
"""List pull requests for a repository.
Args:
args: Parsed arguments with repo, limit, json flags.
Returns:
Exit code.
"""
gh_args = ["pr", "list"]
if args.repo:
gh_args.extend(["-R", args.repo])
gh_args.extend(["--limit", str(args.limit)])
if args.json:
data = run_gh(gh_args, PR_LIST_FIELDS)
print(json.dumps(data, indent=2))
else:
data = run_gh(gh_args, PR_LIST_FIELDS)
items = data if isinstance(data, list) else []
if not items:
print("No pull requests found")
else:
print(f"## Pull Requests\n\nFound {len(items)} PR(s):\n")
print("\n\n".join(format_pr_row(pr) for pr in items))
return 0
def cmd_prs_view(args: argparse.Namespace) -> int:
"""View a single pull request.
Args:
args: Parsed arguments with PR number, repo, json flags.
Returns:
Exit code.
"""
gh_args = ["pr", "view", str(args.number)]
if args.repo:
gh_args.extend(["-R", args.repo])
if args.json:
data = run_gh(gh_args, PR_VIEW_FIELDS)
print(json.dumps(data, indent=2))
else:
data = run_gh(gh_args, PR_VIEW_FIELDS)
if isinstance(data, dict):
print(format_pr_summary(data))
return 0
def cmd_prs_checks(args: argparse.Namespace) -> int:
"""View CI checks for a pull request.
Args:
args: Parsed arguments with PR number, repo, json flags.
Returns:
Exit code.
"""
gh_args = ["pr", "checks", str(args.number)]
if args.repo:
gh_args.extend(["-R", args.repo])
if args.json:
data = run_gh(gh_args, PR_CHECKS_FIELDS)
print(json.dumps(data, indent=2))
else:
data = run_gh(gh_args, PR_CHECKS_FIELDS)
items = data if isinstance(data, list) else []
if not items:
print("No checks found")
else:
print(f"## PR #{args.number} Checks\n")
print("\n".join(format_check(c) for c in items))
return 0
def cmd_prs_status(args: argparse.Namespace) -> int:
"""Show status of relevant pull requests.
Args:
args: Parsed arguments with repo, json flags.
Returns:
Exit code.
"""
gh_args = ["pr", "status"]
if args.repo:
gh_args.extend(["-R", args.repo])
if args.json:
data = run_gh(gh_args, PR_STATUS_FIELDS)
print(json.dumps(data, indent=2))
else:
data = run_gh(gh_args, PR_STATUS_FIELDS)
if isinstance(data, dict):
print(format_pr_status(data))
return 0
def cmd_runs_list(args: argparse.Namespace) -> int:
"""List workflow runs for a repository.
Args:
args: Parsed arguments with repo, limit, json flags.
Returns:
Exit code.
"""
gh_args = ["run", "list"]
if args.repo:
gh_args.extend(["-R", args.repo])
gh_args.extend(["--limit", str(args.limit)])
if args.json:
data = run_gh(gh_args, RUN_LIST_FIELDS)
print(json.dumps(data, indent=2))
else:
data = run_gh(gh_args, RUN_LIST_FIELDS)
items = data if isinstance(data, list) else []
if not items:
print("No workflow runs found")
else:
print(f"## Workflow Runs\n\nFound {len(items)} run(s):\n")
print("\n\n".join(format_run_row(r) for r in items))
return 0
def cmd_runs_view(args: argparse.Namespace) -> int:
"""View a single workflow run.
Args:
args: Parsed arguments with run ID, repo, json flags.
Returns:
Exit code.
"""
gh_args = ["run", "view", str(args.run_id)]
if args.repo:
gh_args.extend(["-R", args.repo])
if args.json:
data = run_gh(gh_args, RUN_VIEW_FIELDS)
print(json.dumps(data, indent=2))
else:
data = run_gh(gh_args, RUN_VIEW_FIELDS)
if isinstance(data, dict):
print(format_run_summary(data))
return 0
def cmd_repos_list(args: argparse.Namespace) -> int:
"""List repositories for the authenticated user.
Args:
args: Parsed arguments with limit, json flags.
Returns:
Exit code.
"""
gh_args = ["repo", "list"]
gh_args.extend(["--limit", str(args.limit)])
if args.json:
data = run_gh(gh_args, REPO_LIST_FIELDS)
print(json.dumps(data, indent=2))
else:
data = run_gh(gh_args, REPO_LIST_FIELDS)
items = data if isinstance(data, list) else []
if not items:
print("No repositories found")
else:
print(f"## Repositories\n\nFound {len(items)} repository(ies):\n")
print("\n\n".join(format_repo_row(r) for r in items))
return 0
def cmd_repos_view(args: argparse.Namespace) -> int:
"""View a single repository.
Args:
args: Parsed arguments with repo name, json flag.
Returns:
Exit code.
"""
gh_args = ["repo", "view", args.repo]
if args.json:
data = run_gh(gh_args, REPO_VIEW_FIELDS)
print(json.dumps(data, indent=2))
else:
data = run_gh(gh_args, REPO_VIEW_FIELDS)
if isinstance(data, dict):
print(format_repo_summary(data))
return 0
def cmd_search_repos(args: argparse.Namespace) -> int:
"""Search repositories.
Args:
args: Parsed arguments with query, limit, json flags.
Returns:
Exit code.
"""
gh_args = ["search", "repos", args.query, "--limit", str(args.limit)]
if args.json:
data = run_gh(gh_args, SEARCH_REPOS_FIELDS)
print(json.dumps(data, indent=2))
else:
data = run_gh(gh_args, SEARCH_REPOS_FIELDS)
items = data if isinstance(data, list) else []
if not items:
print("No repositories found")
else:
print(f"## Search Results\n\nFound {len(items)} repository(ies):\n")
print("\n\n".join(format_search_repo(r) for r in items))
return 0
def cmd_search_issues(args: argparse.Namespace) -> int:
"""Search issues.
Args:
args: Parsed arguments with query, limit, json flags.
Returns:
Exit code.
"""
gh_args = ["search", "issues", args.query, "--limit", str(args.limit)]
if args.json:
data = run_gh(gh_args, SEARCH_ISSUES_FIELDS)
print(json.dumps(data, indent=2))
else:
data = run_gh(gh_args, SEARCH_ISSUES_FIELDS)
items = data if isinstance(data, list) else []
if not items:
print("No issues found")
else:
print(f"## Search Results\n\nFound {len(items)} issue(s):\n")
print("\n\n".join(format_search_issue(i) for i in items))
return 0
def cmd_search_prs(args: argparse.Namespace) -> int:
"""Search pull requests.
Args:
args: Parsed arguments with query, limit, json flags.
Returns:
Exit code.
"""
gh_args = ["search", "prs", args.query, "--limit", str(args.limit)]
if args.json:
data = run_gh(gh_args, SEARCH_PRS_FIELDS)
print(json.dumps(data, indent=2))
else:
data = run_gh(gh_args, SEARCH_PRS_FIELDS)
items = data if isinstance(data, list) else []
if not items:
print("No pull requests found")
else:
print(f"## Search Results\n\nFound {len(items)} PR(s):\n")
print("\n\n".join(format_search_issue(i) for i in items))
return 0
# ============================================================================
# ARGUMENT PARSER
# ============================================================================
def build_parser() -> argparse.ArgumentParser:
"""Build the argument parser with nested subcommands.
Returns:
Configured ArgumentParser.
"""
parser = argparse.ArgumentParser(
description="GitHub wrapper for AI agents — markdown-formatted gh output",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
subparsers = parser.add_subparsers(dest="command", help="Command to execute")
# check
subparsers.add_parser("check", help="Verify gh CLI is installed and authenticated")
# issues
issues_parser = subparsers.add_parser("issues", help="Issue operations")
issues_sub = issues_parser.add_subparsers(dest="issues_command")
issues_list = issues_sub.add_parser("list", help="List issues")
issues_list.add_argument("--repo", "-R", help="Repository (OWNER/REPO)")
issues_list.add_argument("--limit", type=int, default=30, help="Max results (default 30)")
issues_list.add_argument("--json", action="store_true", help="Output raw JSON")
issues_view = issues_sub.add_parser("view", help="View issue details")
issues_view.add_argument("number", type=int, help="Issue number")
issues_view.add_argument("--repo", "-R", help="Repository (OWNER/REPO)")
issues_view.add_argument("--json", action="store_true", help="Output raw JSON")
# prs
prs_parser = subparsers.add_parser("prs", help="Pull request operations")
prs_sub = prs_parser.add_subparsers(dest="prs_command")
prs_list = prs_sub.add_parser("list", help="List pull requests")
prs_list.add_argument("--repo", "-R", help="Repository (OWNER/REPO)")
prs_list.add_argument("--limit", type=int, default=30, help="Max results (default 30)")
prs_list.add_argument("--json", action="store_true", help="Output raw JSON")
prs_view = prs_sub.add_parser("view", help="View PR details")
prs_view.add_argument("number", type=int, help="PR number")
prs_view.add_argument("--repo", "-R", help="Repository (OWNER/REPO)")
prs_view.add_argument("--json", action="store_true", help="Output raw JSON")
prs_checks = prs_sub.add_parser("checks", help="View PR checks")
prs_checks.add_argument("number", type=int, help="PR number")
prs_checks.add_argument("--repo", "-R", help="Repository (OWNER/REPO)")
prs_checks.add_argument("--json", action="store_true", help="Output raw JSON")
prs_status = prs_sub.add_parser("status", help="Show status of relevant PRs")
prs_status.add_argument("--repo", "-R", help="Repository (OWNER/REPO)")
prs_status.add_argument("--json", action="store_true", help="Output raw JSON")
# runs
runs_parser = subparsers.add_parser("runs", help="Workflow run operations")
runs_sub = runs_parser.add_subparsers(dest="runs_command")
runs_list = runs_sub.add_parser("list", help="List workflow runs")
runs_list.add_argument("--repo", "-R", help="Repository (OWNER/REPO)")
runs_list.add_argument("--limit", type=int, default=30, help="Max results (default 30)")
runs_list.add_argument("--json", action="store_true", help="Output raw JSON")
runs_view = runs_sub.add_parser("view", help="View workflow run details")
runs_view.add_argument("run_id", type=int, help="Run ID")
runs_view.add_argument("--repo", "-R", help="Repository (OWNER/REPO)")
runs_view.add_argument("--json", action="store_true", help="Output raw JSON")
# repos
repos_parser = subparsers.add_parser("repos", help="Repository operations")
repos_sub = repos_parser.add_subparsers(dest="repos_command")
repos_list = repos_sub.add_parser("list", help="List repositories")
repos_list.add_argument("--limit", type=int, default=30, help="Max results (default 30)")
repos_list.add_argument("--json", action="store_true", help="Output raw JSON")
repos_view = repos_sub.add_parser("view", help="View repository details")
repos_view.add_argument("repo", help="Repository (OWNER/REPO)")
repos_view.add_argument("--json", action="store_true", help="Output raw JSON")
# search
search_parser = subparsers.add_parser("search", help="Search operations")
search_sub = search_parser.add_subparsers(dest="search_command")
search_repos = search_sub.add_parser("repos", help="Search repositories")
search_repos.add_argument("query", help="Search query")
search_repos.add_argument("--limit", type=int, default=30, help="Max results (default 30)")
search_repos.add_argument("--json", action="store_true", help="Output raw JSON")
search_issues = search_sub.add_parser("issues", help="Search issues")
search_issues.add_argument("query", help="Search query")
search_issues.add_argument("--limit", type=int, default=30, help="Max results (default 30)")
search_issues.add_argument("--json", action="store_true", help="Output raw JSON")
search_prs = search_sub.add_parser("prs", help="Search pull requests")
search_prs.add_argument("query", help="Search query")
search_prs.add_argument("--limit", type=int, default=30, help="Max results (default 30)")
search_prs.add_argument("--json", action="store_true", help="Output raw JSON")
return parser
# ============================================================================
# MAIN
# ============================================================================
def main() -> int:
"""Main entry point.
Returns:
Exit code.
"""
parser = build_parser()
args = parser.parse_args()
if not args.command:
parser.print_help()
return 1
if args.command == "check":
return cmd_check(args)
elif args.command == "issues":
if not hasattr(args, "issues_command") or not args.issues_command:
parser.parse_args(["issues", "--help"])
return 1
if args.issues_command == "list":
return cmd_issues_list(args)
elif args.issues_command == "view":
return cmd_issues_view(args)
elif args.command == "prs":
if not hasattr(args, "prs_command") or not args.prs_command:
parser.parse_args(["prs", "--help"])
return 1
if args.prs_command == "list":
return cmd_prs_list(args)
elif args.prs_command == "view":
return cmd_prs_view(args)
elif args.prs_command == "checks":
return cmd_prs_checks(args)
elif args.prs_command == "status":
return cmd_prs_status(args)
elif args.command == "runs":
if not hasattr(args, "runs_command") or not args.runs_command:
parser.parse_args(["runs", "--help"])
return 1
if args.runs_command == "list":
return cmd_runs_list(args)
elif args.runs_command == "view":
return cmd_runs_view(args)
elif args.command == "repos":
if not hasattr(args, "repos_command") or not args.repos_command:
parser.parse_args(["repos", "--help"])
return 1
if args.repos_command == "list":
return cmd_repos_list(args)
elif args.repos_command == "view":
return cmd_repos_view(args)
elif args.command == "search":
if not hasattr(args, "search_command") or not args.search_command:
parser.parse_args(["search", "--help"])
return 1
if args.search_command == "repos":
return cmd_search_repos(args)
elif args.search_command == "issues":
return cmd_search_issues(args)
elif args.search_command == "prs":
return cmd_search_prs(args)
parser.print_help()
return 1
if __name__ == "__main__":
sys.exit(main())