From b999e5074ad1276803f9d67a4866c7f4aa68440f Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sun, 30 Nov 2025 08:39:15 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 14 + README.md | 3 + commands/download-issues.md | 145 +++ commands/fix-issues.md | 107 ++ commands/pr-status.md | 82 ++ plugin.lock.json | 85 ++ skills/pull-request-skill/.env.example | 19 + skills/pull-request-skill/.gitignore | 19 + skills/pull-request-skill/SKILL.md | 359 ++++++ skills/pull-request-skill/package.json | 35 + skills/pull-request-skill/pr-review.ts | 1101 +++++++++++++++++ skills/pull-request-skill/read-pr-issues.sh | 139 +++ .../pull-request-skill/resolve-pr-issues.sh | 179 +++ skills/pull-request-skill/run.js | 173 +++ 14 files changed, 2460 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 commands/download-issues.md create mode 100644 commands/fix-issues.md create mode 100644 commands/pr-status.md create mode 100644 plugin.lock.json create mode 100644 skills/pull-request-skill/.env.example create mode 100644 skills/pull-request-skill/.gitignore create mode 100644 skills/pull-request-skill/SKILL.md create mode 100644 skills/pull-request-skill/package.json create mode 100644 skills/pull-request-skill/pr-review.ts create mode 100755 skills/pull-request-skill/read-pr-issues.sh create mode 100755 skills/pull-request-skill/resolve-pr-issues.sh create mode 100755 skills/pull-request-skill/run.js diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..155ccef --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,14 @@ +{ + "name": "reviewer", + "description": "Claude Code Skill for exporting and fixing CodeRabbit AI review comments from GitHub Pull Requests. Auto-downloads PR reviews, organizes them by severity, and provides commands to systematically resolve issues.", + "version": "1.0.0", + "author": { + "name": "marcio" + }, + "skills": [ + "./skills" + ], + "commands": [ + "./commands" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9373a9e --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# reviewer + +Claude Code Skill for exporting and fixing CodeRabbit AI review comments from GitHub Pull Requests. Auto-downloads PR reviews, organizes them by severity, and provides commands to systematically resolve issues. diff --git a/commands/download-issues.md b/commands/download-issues.md new file mode 100644 index 0000000..a1887db --- /dev/null +++ b/commands/download-issues.md @@ -0,0 +1,145 @@ +--- +description: Download CodeRabbit AI review comments for a Pull Request +--- + +## Download PR Reviews + +This command downloads CodeRabbit AI review comments from a GitHub Pull Request and organizes them by severity for systematic resolution. + +**This command works from any directory** - it will save reviews to the current working directory's `.reviews/` folder. + +## Usage + +```bash +# With PR number +/reviewer:download-issues --pr 123 + +# Without PR number (auto-detects latest open PR) +/reviewer:download-issues +``` + +## How Claude Code Executes This + +When you run `/reviewer:download-issues --pr 123` or `/reviewer:download-issues`, Claude Code will: + +1. Find the installed `pr-reviewer` skill location +2. Execute the download script from the skill directory +3. Save output to **your current working directory** (the repo you're working on) + +## Command + +```bash +# Execute from skill location, save to current working directory +CWD=$(pwd) node ~/.claude/plugins/marketplaces/claude-craftkit/plugins/reviewer/skills/pull-request-skill/run.js download + +# Or without PR number (auto-detect latest) +CWD=$(pwd) node ~/.claude/plugins/marketplaces/claude-craftkit/plugins/reviewer/skills/pull-request-skill/run.js download +``` + +**Note:** The exact path may vary depending on where Claude Code installs plugins. Claude will handle finding the correct path automatically. + +## Prerequisites + +1. **GitHub Personal Access Token** + + The skill needs a `.env` file in its installation directory with your GitHub token: + + ```bash + # Location: ~/.claude/plugins/marketplaces/claude-craftkit/plugins/reviewer/skills/pull-request-skill/.env + GITHUB_TOKEN=ghp_your_personal_access_token_here + OUTPUT_DIR=./.reviews + LOG_LEVEL=info + PR_REVIEW_TZ=America/Sao_Paulo + ``` + + Generate token at: https://github.com/settings/tokens + Required scopes: `repo` (full repository access) + +2. **Dependencies** + + Dependencies will be auto-installed on first run. To install manually: + + ```bash + cd ~/.claude/plugins/marketplaces/claude-craftkit/plugins/reviewer/skills/pull-request-skill + bun install + ``` + +## Output Structure + +The command creates a directory structure in **your repo's** `.reviews/reviews-pr-/`: + +``` +your-repo/ +└── .reviews/ # Created in YOUR working directory + └── reviews-pr-123/ + ├── summary.md # 📊 Overview with statistics + ├── pr-review-combined.log # 📋 Full execution logs + ├── pr-review-error.log # ⚠️ Error logs only + ├── issues/ # 🔧 Resolvable issues (threads) + │ ├── issue_001_critical_unresolved.md + │ ├── issue_002_major_unresolved.md + │ └── issue_003_trivial_resolved.md + └── comments/ # 💬 General comments + ├── comment_001.md + └── comment_002.md +``` + +## Issue Severity Levels + +Issues are automatically categorized by severity: + +- **🔴 Critical**: Serious problems requiring immediate attention +- **🟠 Major**: Important issues affecting functionality +- **🔵 Trivial**: Minor issues and style improvements + +## Next Steps + +After downloading reviews: + +1. **Review the summary**: Check `.reviews/reviews-pr-/summary.md` +2. **Start fixing issues**: Use the `/fix` command +3. **Track progress**: Issues are marked as resolved/unresolved + +## Environment Variables + +- `GITHUB_TOKEN` (required): GitHub Personal Access Token +- `OUTPUT_DIR` (optional): Output directory relative to working dir (default: `./.reviews`) +- `CWD` (optional): Override working directory (default: current directory) +- `LOG_LEVEL` (optional): Logging level - `error`, `warn`, `info`, `debug` (default: `info`) +- `PR_REVIEW_TZ` (optional): Timezone for dates (default: system timezone) + +## Examples + +```bash +# Download PR #123 - saves to current directory's .reviews/ +/reviewer:download-issues --pr 123 + +# Download latest open PR (auto-detects) +/reviewer:download-issues + +# With debug logging +LOG_LEVEL=debug /reviewer:download-issues --pr 123 + +# Save to custom directory +OUTPUT_DIR=./my-reviews /reviewer:download-issues --pr 123 +``` + +## Troubleshooting + +**"GITHUB_TOKEN is not set"** +- Create `.env` file in the skill's installation directory +- Path: `~/.claude/plugins/marketplaces/claude-craftkit/plugins/reviewer/skills/pull-request-skill/.env` + +**"No CodeRabbit AI comments found"** +- CodeRabbit hasn't reviewed the PR yet +- Check if PR has comments from `@coderabbitai[bot]` + +**"Repository information could not be parsed"** +- Verify you're in a git repository +- Check git remote: `git remote -v` +- Remote must be: `https://github.com/owner/repo.git` + +## See Also + +- `/reviewer:fix-issues` - Fix issues from a downloaded PR review +- `/reviewer:pr-status` - Check status of PR reviews diff --git a/commands/fix-issues.md b/commands/fix-issues.md new file mode 100644 index 0000000..1bfd6a5 --- /dev/null +++ b/commands/fix-issues.md @@ -0,0 +1,107 @@ +--- +description: Fix issues for a given PR +--- + +--type +--pr +--from + +## Fix PR Review Issues + +This command helps you systematically fix issues from a downloaded CodeRabbit AI review. + +**Works from any directory** - reads issues from `.reviews/` in your current working directory. + +## Helper Commands + +Before starting work on fixing issues, use the `read-pr-issues.sh` script to review what needs to be addressed: + +```bash +# Read all issues for a PR (from your working directory's .reviews/) +~/.claude/plugins/marketplaces/claude-craftkit/plugins/reviewer/skills/pull-request-skill/read-pr-issues.sh --pr --type issue --all + +# Read a specific range of issues +~/.claude/plugins/marketplaces/claude-craftkit/plugins/reviewer/skills/pull-request-skill/read-pr-issues.sh --pr --type issue --from --to 10 + +# Read critical issues only +~/.claude/plugins/marketplaces/claude-craftkit/plugins/reviewer/skills/pull-request-skill/read-pr-issues.sh --pr --type critical --all + +# Read major issues only +~/.claude/plugins/marketplaces/claude-craftkit/plugins/reviewer/skills/pull-request-skill/read-pr-issues.sh --pr --type major --all + +# Read trivial issues only +~/.claude/plugins/marketplaces/claude-craftkit/plugins/reviewer/skills/pull-request-skill/read-pr-issues.sh --pr --type trivial --all +``` + +**Note:** The exact path may vary. Claude Code will find the skill automatically. + +This script displays issues in a clean, readable format with: + +- Issue numbers and titles +- File locations +- Current status (resolved/unresolved) +- Issue descriptions +- Thread IDs for GitHub reference + +## Critical Requirements + + +- **YOU NEED** to fix the from in the `.reviews/reviews-pr-`, and only finish when ALL THESE ISSUES are addressed; +- This should be fixed in THE BEST WAY possible, not using workarounds; +- **YOU MUST** follow project standards and rules from `.cursor/rules` or `.claude/CLAUDE.md`, and ensure all parameters are addressed; +- If, in the end, you don't have all issues addressed, your work will be **INVALIDATED**; +- After making all the changes, you need to update the progress in the `summary.md` file and all the related issue files. +- **MUST DO:** After resolving every issue run `~/.claude/plugins/marketplaces/claude-craftkit/plugins/reviewer/skills/pull-request-skill/resolve-pr-issues.sh --pr-dir .reviews/reviews-pr- --from --to ` so the script calls `gh` to close the review threads and refreshes the summary. + + +## Workflow + +1. **Read issues** to understand what needs to be fixed +2. **Fix the code** following best practices and project standards +3. **Mark as resolved** using the resolve script +4. **Commit changes** with descriptive message + +## After Finishing + + + +- **MUST COMMIT:** After fixing ALL issues in this batch and ensuring `make lint && make test` pass (or equivalent), + commit the changes with a descriptive message that references the PR and fixed issues. + Example: `git commit -am "fix(repo): resolve PR # issues [batch -]"` + Note: Commit locally only - do NOT push. Multiple batches will be committed separately. + + +## Examples + +```bash +# Fix critical issues starting from issue 1 +/fix --pr 123 --type critical --from 1 + +# Fix major issues from 5 to 10 +/fix --pr 123 --type major --from 5 + +# Fix all trivial issues +/fix --pr 123 --type trivial --from 1 +``` + +## Resolving Issues + +After fixing issues, mark them as resolved: + +```bash +# Resolve issues 1-10 +~/.claude/plugins/marketplaces/claude-craftkit/plugins/reviewer/skills/pull-request-skill/resolve-pr-issues.sh --pr-dir .reviews/reviews-pr-123 --from 1 --to 10 + +# Resolve all unresolved issues +~/.claude/plugins/marketplaces/claude-craftkit/plugins/reviewer/skills/pull-request-skill/resolve-pr-issues.sh --pr-dir .reviews/reviews-pr-123 --all +``` + +This will: +- Mark threads as resolved in GitHub +- Rename files from `*_unresolved.md` to `*_resolved.md` +- Update the summary + +## See Also + +- `/reviewer:download-issues` - Download PR reviews +- `/reviewer:pr-status` - Check status of PR reviews diff --git a/commands/pr-status.md b/commands/pr-status.md new file mode 100644 index 0000000..6e1c62f --- /dev/null +++ b/commands/pr-status.md @@ -0,0 +1,82 @@ +--- +description: Check status of PR review issues +--- + +--pr + +## Check PR Review Status + +This command displays a summary of the current status of issues for a Pull Request review. + +## Usage + +```bash +# View summary file for a PR +cat .reviews/reviews-pr-/summary.md + +# Count unresolved issues +find .reviews/reviews-pr-/issues -name "*_unresolved.md" | wc -l + +# Count resolved issues +find .reviews/reviews-pr-/issues -name "*_resolved.md" | wc -l + +# List unresolved critical issues +ls .reviews/reviews-pr-/issues/*_critical_unresolved.md + +# List unresolved major issues +ls .reviews/reviews-pr-/issues/*_major_unresolved.md + +# List unresolved trivial issues +ls .reviews/reviews-pr-/issues/*_trivial_unresolved.md +``` + +## Summary File + +The `summary.md` file contains: + +- **Total Issues**: Breakdown by severity (Critical/Major/Trivial) +- **Resolution Status**: Resolved vs Unresolved counts +- **Issue Links**: Direct links to each issue file +- **File Locations**: Code locations for each issue + +## Progress Tracking + +The summary is automatically updated when you: + +1. Download PR reviews (`/reviewer:download-issues`) +2. Resolve issues using the resolve script + +## Example Output + +```markdown +# PR Review #123 - CodeRabbit AI Export + +## Summary + +- **Issues (resolvable review comments):** 15 + - 🔴 Critical: 3 + - 🟠 Major: 7 + - 🔵 Trivial: 5 +- **Comments (simple, not resolvable):** 8 + - **Resolved issues:** 10 ✓ + - **Unresolved issues:** 5 + +**Generated on:** 2025-01-15 14:30:00 America/Sao_Paulo +``` + +## Quick Status Check + +Create an alias for quick status checks: + +```bash +# Add to your .bashrc or .zshrc +alias pr-status='f() { cat .reviews/reviews-pr-$1/summary.md | head -20; }; f' + +# Usage +pr-status 123 +``` + +## See Also + +- `/reviewer:download-issues` - Download PR reviews +- `/reviewer:fix-issues` - Fix issues from a PR review diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..a0c4b33 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,85 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:marcioaltoe/claude-craftkit:plugins/reviewer", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "4c24d16c9543015a9e837650b08304b0685bf7ab", + "treeHash": "c5192abb7e28d6c97deae56dbbae4ead8e357d4962bf3b0d306b953cf1b9329f", + "generatedAt": "2025-11-28T10:27:01.481271Z", + "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": "reviewer", + "description": "Claude Code Skill for exporting and fixing CodeRabbit AI review comments from GitHub Pull Requests. Auto-downloads PR reviews, organizes them by severity, and provides commands to systematically resolve issues.", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "0f8511cd5b9b8fbac0d122299225d432929a02588c1b7231a4efc6daefe00555" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "4387bb4e6f0718e9c9db4f86d2efd919798d63664c4c4bbfd9e25eb0973409c5" + }, + { + "path": "commands/fix-issues.md", + "sha256": "c34a0a6e766a1fe375784ba7f0dbdfb414a36597ba084ff95119f1372eb1ea05" + }, + { + "path": "commands/pr-status.md", + "sha256": "eb48150ed0f3172bb0eb4a019a99087e8315fb48c29df9c64ea3b0c335e9ffce" + }, + { + "path": "commands/download-issues.md", + "sha256": "319c9982d009b82dfb876f4a22d0c0645b143b20b575cc20fdc04efd11054d65" + }, + { + "path": "skills/pull-request-skill/read-pr-issues.sh", + "sha256": "624fd73901635617b4f3724f81c126ba213a7c68b69a4fb7e1bf441fc819877f" + }, + { + "path": "skills/pull-request-skill/run.js", + "sha256": "5fbac52066b70ee15443a12a5d251605a1471588c01894e7cf9b20b607abaec8" + }, + { + "path": "skills/pull-request-skill/.gitignore", + "sha256": "11e7f15002a201677c5c6da04175302ea9cbdca8bcecfef9d1bc845dbbd82c48" + }, + { + "path": "skills/pull-request-skill/package.json", + "sha256": "27a8e36f3e3056eaaaeac1cbb34d645180e7421d4e69680083fd57c8a3608905" + }, + { + "path": "skills/pull-request-skill/SKILL.md", + "sha256": "58753a849a5c5f733b00f269c0b3ed7608885b0dddaf336380d3729f6e0752a0" + }, + { + "path": "skills/pull-request-skill/pr-review.ts", + "sha256": "193a4c98b12562f93dceec222082983f17d7f39f996b632b382fc316ecf460d8" + }, + { + "path": "skills/pull-request-skill/resolve-pr-issues.sh", + "sha256": "4f5a6a28e5d8e77e0fbda3dd0049bf8841044ba5b247a7f842224a22e0293a4b" + }, + { + "path": "skills/pull-request-skill/.env.example", + "sha256": "33338c740294b1b3fb317919185ab8b2649f57dc18928c4f969c1772a7e6f0b0" + } + ], + "dirSha256": "c5192abb7e28d6c97deae56dbbae4ead8e357d4962bf3b0d306b953cf1b9329f" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/pull-request-skill/.env.example b/skills/pull-request-skill/.env.example new file mode 100644 index 0000000..d001cea --- /dev/null +++ b/skills/pull-request-skill/.env.example @@ -0,0 +1,19 @@ +# GitHub Personal Access Token (OBRIGATÓRIO) +# Create a token at: https://github.com/settings/tokens +# Required scopes: 'repo' (full control of private repositories) +GITHUB_TOKEN=ghp_your_personal_access_token_here + +# Diretório de saída (OPCIONAL) +# Onde os arquivos serão salvos (relativo à raiz do projeto) +# Padrão: ./.reviews +OUTPUT_DIR=./.reviews + +# Timezone para formatação de datas (OPCIONAL) +# Padrão: timezone do sistema local +# Exemplos: America/Sao_Paulo, Europe/London, UTC +# PR_REVIEW_TZ=America/Sao_Paulo + +# Nível de logging (OPCIONAL) +# Padrão: info +# Opções: error, warn, info, debug +# LOG_LEVEL=debug diff --git a/skills/pull-request-skill/.gitignore b/skills/pull-request-skill/.gitignore new file mode 100644 index 0000000..e67fb40 --- /dev/null +++ b/skills/pull-request-skill/.gitignore @@ -0,0 +1,19 @@ +# Environment files +.env +.env.local + +# Dependencies +node_modules/ +bun.lockb + +# Logs +*.log +pr-review-combined.log +pr-review-error.log + +# Temporary files +.temp-execution-*.js +*.tmp + +# Backup files +*.bak diff --git a/skills/pull-request-skill/SKILL.md b/skills/pull-request-skill/SKILL.md new file mode 100644 index 0000000..1b3b628 --- /dev/null +++ b/skills/pull-request-skill/SKILL.md @@ -0,0 +1,359 @@ +--- +name: pull-request-skill +description: Pull Request review manager for CodeRabbit AI. **ALWAYS use when user needs to work with PR reviews, fix CodeRabbit issues, or check review status.** Downloads, organizes, and helps resolve review comments systematically. Examples - "download PR reviews", "fix CodeRabbit issues for PR 123", "check review status", "organize review comments by severity". +--- + +You are an expert Pull Request Review Manager specializing in working with CodeRabbit AI review comments from GitHub Pull Requests. + +## When to Engage + +You should proactively assist when: + +- User mentions working with CodeRabbit reviews +- User wants to download PR review comments +- User needs to fix issues from a PR review +- User asks about PR review status +- User mentions CodeRabbit, PR reviews, or review comments +- User wants to organize or prioritize review feedback +- User needs to resolve review threads + +**Trigger Keywords**: coderabbit, pr review, pull request review, review comments, fix issues, download reviews, pr status + +## Your Role + +As a Pull Request Review Manager, you: + +1. **Download** - Fetch CodeRabbit AI review comments from GitHub PRs +2. **Organize** - Categorize issues by severity (critical, major, trivial) +3. **Track** - Monitor issue resolution status +4. **Guide** - Help users systematically resolve review feedback +5. **Report** - Provide clear summaries of review status + +## Available Commands + +The reviewer plugin provides three slash commands: + +1. `/reviewer:download-issues --pr ` - Download CodeRabbit reviews +2. `/reviewer:fix-issues --pr ` - Fix issues from a PR review +3. `/reviewer:pr-status --pr ` - Check review status + +**Note**: PR number is optional - if not provided, the latest open PR is used. + +## Workflow + +### 1. Download Reviews + +```bash +# Download reviews for PR #123 +/reviewer:download-issues --pr 123 + +# Download latest open PR +/reviewer:download-issues +``` + +**What happens:** + +- Fetches all CodeRabbit AI comments from the PR +- Organizes them into issues (review threads) and comments +- Categorizes by severity: 🔴 Critical, 🟠 Major, 🔵 Trivial +- Saves to `.reviews/reviews-pr-/` in the working directory +- Generates a summary report + +**Output Structure:** + +``` +.reviews/reviews-pr-123/ +├── summary.md # 📊 Overview with statistics +├── pr-review-combined.log # 📋 Full execution logs +├── pr-review-error.log # ⚠️ Error logs only +├── issues/ # 🔧 Resolvable issues (threads) +│ ├── issue_001_critical_unresolved.md +│ ├── issue_002_major_unresolved.md +│ └── issue_003_trivial_resolved.md +└── comments/ # 💬 General comments + ├── comment_001.md + └── comment_002.md +``` + +### 2. Review Summary + +After downloading, **always** read and show the user: + +```bash +# Read the summary +cat .reviews/reviews-pr-/summary.md +``` + +The summary includes: + +- Total issues by severity +- Resolved vs unresolved count +- Issue list with file paths and descriptions +- Quick overview for prioritization + +### 3. Fix Issues + +Help users systematically resolve issues: + +```bash +# Fix issues from PR #123 +/reviewer:fix-issues --pr 123 +``` + +**Your role when fixing:** + +1. Read the issue file to understand the problem +2. Locate the relevant code file +3. Analyze the CodeRabbit suggestion +4. Implement the fix following best practices +5. Mark the issue as resolved +6. Move to the next issue + +**Priority order:** + +1. 🔴 Critical issues first +2. 🟠 Major issues next +3. 🔵 Trivial issues last + +### 4. Check Status + +Monitor progress on review resolution: + +```bash +# Check status of PR #123 +/reviewer:pr-status --pr 123 +``` + +Shows: + +- Total issues vs resolved +- Remaining critical/major/trivial issues +- Progress percentage +- Next recommended actions + +## Prerequisites + +### GitHub Token Setup + +The skill requires a `.env` file in its installation directory: + +```bash +# Location: ~/.claude/plugins/marketplaces/claude-craftkit/plugins/reviewer/skills/pull-request-skill/.env +GITHUB_TOKEN=ghp_your_personal_access_token_here +OUTPUT_DIR=./.reviews +LOG_LEVEL=info +PR_REVIEW_TZ=America/Sao_Paulo +``` + +**If the token is not set:** + +1. Guide user to create `.env` file in skill directory +2. Help them generate token at: https://github.com/settings/tokens +3. Required scopes: `repo` (full repository access) + +### Dependencies + +Dependencies are auto-installed on first run. The skill uses: + +- `@octokit/rest` - GitHub API client +- `@octokit/graphql` - GraphQL API +- `winston` - Logging +- `zod` - Validation +- `dotenv` - Environment variables + +## Issue Severity Levels + +Issues are automatically categorized: + +- **🔴 Critical**: Security issues, bugs, breaking changes + + - **Action**: Fix immediately + - **Examples**: Memory leaks, security vulnerabilities, data corruption + +- **🟠 Major**: Important issues affecting functionality + + - **Action**: Fix before merging + - **Examples**: Logic errors, performance issues, incorrect behavior + +- **🔵 Trivial**: Minor issues and style improvements + - **Action**: Fix when convenient + - **Examples**: Code style, formatting, minor optimizations + +## Best Practices + +### When Downloading Reviews + +1. **Always show summary first** + + ```bash + /reviewer:download-issues --pr 123 + # Then immediately: + cat .reviews/reviews-pr-123/summary.md + ``` + +2. **Verify repository context** + + - Ensure you're in the correct repository + - Check git remote: `git remote -v` + +3. **Handle errors gracefully** + - Check for `.env` file if token error + - Verify PR exists and has CodeRabbit comments + - Check logs if issues occur: `pr-review-error.log` + +### When Fixing Issues + +1. **Work systematically** + + - Start with critical issues + - Fix one issue at a time + - Test after each fix + +2. **Read the full issue** + + - Understand CodeRabbit's reasoning + - Check the suggested code change + - Consider the context and impact + +3. **Follow project standards** + + - Use project's code style + - Run linters/formatters after changes + - Ensure tests pass + +4. **Mark issues as resolved** + - Rename file: `issue_001_critical_unresolved.md` → `issue_001_critical_resolved.md` + - Or move to `resolved/` subfolder + +### When Reporting Status + +1. **Provide clear summary** + + - Total issues vs resolved + - Breakdown by severity + - Estimated remaining work + +2. **Recommend next actions** + - Prioritize critical issues + - Suggest grouping similar issues + - Identify quick wins + +## Environment Variables + +- `GITHUB_TOKEN` (required): GitHub Personal Access Token +- `OUTPUT_DIR` (optional): Output directory relative to working dir (default: `./.reviews`) +- `CWD` (optional): Override working directory (default: current directory) +- `LOG_LEVEL` (optional): Logging level - `error`, `warn`, `info`, `debug` (default: `info`) +- `PR_REVIEW_TZ` (optional): Timezone for dates (default: system timezone) + +## Common Scenarios + +### Scenario 1: User wants to work on PR feedback + +**User**: "I got CodeRabbit feedback on my PR, help me fix it" + +**Your actions**: + +1. Ask for PR number (or auto-detect) +2. Run `/reviewer:download-issues --pr ` +3. Show the summary +4. Ask which severity level to start with +5. Begin fixing issues systematically + +### Scenario 2: User wants to see what needs fixing + +**User**: "What issues are left on PR 123?" + +**Your actions**: + +1. Run `/reviewer:pr-status --pr 123` +2. Show breakdown by severity +3. Recommend starting with critical issues +4. Offer to begin fixing + +### Scenario 3: User completed some fixes + +**User**: "I fixed the critical issues, what's next?" + +**Your actions**: + +1. Run `/reviewer:pr-status --pr 123` +2. Verify critical issues are resolved +3. Show remaining major issues +4. Offer to continue with major issues + +## Troubleshooting + +### "GITHUB_TOKEN is not set" + +**Solution**: + +1. Create `.env` file: `~/.claude/plugins/marketplaces/claude-craftkit/plugins/reviewer/skills/pull-request-skill/.env` +2. Add: `GITHUB_TOKEN=ghp_...` +3. Generate token at: https://github.com/settings/tokens + +### "No CodeRabbit AI comments found" + +**Causes**: + +- CodeRabbit hasn't reviewed the PR yet +- PR doesn't have comments from `@coderabbitai[bot]` +- Wrong PR number + +**Solution**: Verify PR has CodeRabbit comments on GitHub + +### "Repository information could not be parsed" + +**Causes**: + +- Not in a git repository +- No remote configured +- Remote URL format incorrect + +**Solution**: + +1. Check: `git remote -v` +2. Remote must be: `https://github.com/owner/repo.git` + +### "Dependencies not found" + +**Solution**: + +```bash +cd ~/.claude/plugins/marketplaces/claude-craftkit/plugins/reviewer/skills/pull-request-skill +bun install +``` + +## Integration with Development Workflow + +This skill works best when integrated with other tools: + +1. **After PR creation** → Download reviews +2. **While fixing** → Use `/quality:check` to verify changes +3. **After fixes** → Use `/git:commit` for conventional commits +4. **Before merge** → Use `/reviewer:pr-status` to verify all issues resolved + +## Remember + +- **Always download first** - Get fresh reviews before working +- **Prioritize by severity** - Critical → Major → Trivial +- **One issue at a time** - Focus on quality over speed +- **Test after changes** - Ensure fixes don't break anything +- **Track progress** - Use status command to monitor completion +- **Communicate clearly** - Show summaries and next steps to users + +## Success Criteria + +A successful review resolution workflow: + +✅ **Downloads** reviews successfully with clear summary +✅ **Prioritizes** issues by severity (critical first) +✅ **Fixes** issues systematically with proper testing +✅ **Tracks** progress with status updates +✅ **Completes** all critical and major issues before merge +✅ **Maintains** code quality and project standards +✅ **Communicates** progress clearly to the user + +--- + +**You are the Pull Request Review Manager. When users need to work with CodeRabbit reviews, use this skill to guide them through the process systematically.** diff --git a/skills/pull-request-skill/package.json b/skills/pull-request-skill/package.json new file mode 100644 index 0000000..34c49e3 --- /dev/null +++ b/skills/pull-request-skill/package.json @@ -0,0 +1,35 @@ +{ + "name": "pull-request-skill", + "version": "1.0.0", + "description": "Pull Request review exporter and issue resolver for CodeRabbit AI comments", + "author": "marcio", + "main": "run.js", + "type": "module", + "scripts": { + "setup": "bun install", + "download": "bun run pr-review.ts", + "download-pr": "bun run pr-review.ts" + }, + "keywords": [ + "pull-request", + "code-review", + "coderabbit", + "github", + "claude-skill" + ], + "dependencies": { + "@octokit/graphql": "^9.0.2", + "@octokit/plugin-retry": "^8.0.2", + "@octokit/plugin-throttling": "^11.0.2", + "@octokit/rest": "^22.0.0", + "@octokit/types": "^15.0.1", + "@types/node": "^24.9.1", + "dotenv": "^17.2.3", + "winston": "^3.18.3", + "zod": "^4.1.12" + }, + "engines": { + "bun": ">=1.0.0" + }, + "license": "MIT" +} diff --git a/skills/pull-request-skill/pr-review.ts b/skills/pull-request-skill/pr-review.ts new file mode 100644 index 0000000..4b8ce1f --- /dev/null +++ b/skills/pull-request-skill/pr-review.ts @@ -0,0 +1,1101 @@ +#!/usr/bin/env bun + +/** + * PR Review Exporter (improved) + * + * Enhanced version with: + * - Robust error handling and retry logic + * - Rate limiting and throttling + * - Structured logging with Winston + * - Input validation with Zod + * - Optimized API calls + * - Improved markdown processing + * - Better performance and reliability + * - Environment file support (.env) + * - Auto-detection of latest open PR + * - Automatic directory creation + * + * Usage: + * # Option 1: Environment variable + * GITHUB_TOKEN=ghp_... bun pr-review.ts [PR_NUMBER] + * + * # Option 2: .env file (recommended) + * echo "GITHUB_TOKEN=ghp_..." > scripts/.env + * bun pr-review.ts [PR_NUMBER] + * + * # PR_NUMBER is optional - if not provided, uses latest open PR + */ + +import { execSync } from 'node:child_process' +import { promises as fs } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { graphql } from '@octokit/graphql' +import { retry } from '@octokit/plugin-retry' +import { throttling } from '@octokit/plugin-throttling' +import { Octokit } from '@octokit/rest' +import type { Endpoints } from '@octokit/types' +import { config as dotenvConfig } from 'dotenv' +import * as winston from 'winston' +import { z } from 'zod' + +// ---------- Environment Configuration ---------- +// Load environment variables from .env file in the scripts directory +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) +// Prefer scripts/.env (as documented); fallback to scripts/review/.env without overriding existing vars +dotenvConfig({ path: join(__dirname, '..', '.env') }) +dotenvConfig({ path: join(__dirname, '.env') }) + +// Working directory - use CWD environment variable if provided, otherwise use process.cwd() +const WORKING_DIR = process.env.CWD || process.cwd() + +// ---------- Configuration & Validation ---------- + +// Input validation schemas +const PRNumberSchema = z.number().int().positive() +const GitHubTokenSchema = z.string().min(1) +const RepoInfoSchema = z.object({ + owner: z.string().min(1), + repo: z.string().min(1), +}) + +// Logger setup (initially only console) +const logger = winston.createLogger({ + level: process.env.LOG_LEVEL || 'info', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.errors({ stack: true }), + winston.format.json(), + ), + defaultMeta: { service: 'pr-review-exporter' }, + transports: [ + new winston.transports.Console({ + format: winston.format.combine(winston.format.colorize(), winston.format.simple()), + }), + ], +}) + +// Enhanced Octokit with retry and throttling +const EnhancedOctokit = Octokit.plugin(retry, throttling) + +// Type for the enhanced Octokit instance +type EnhancedOctokitInstance = InstanceType + +// ---------- Types ---------- +interface BaseUser { + login: string +} + +interface Comment { + body: string + user: BaseUser + created_at: string + + // Present only for review (inline) comments: + path?: string + line?: number + + // Present only for review (inline) comments from REST: + id?: number // REST numeric id + node_id?: string // REST relay/global ID (matches GraphQL id) +} + +interface ReviewComment extends Comment { + path: string + line: number + id: number + node_id: string +} + +interface IssueComment extends Comment { + // General PR comments; no path/line/id resolution +} + +interface SimpleReviewComment { + // Pull Request Review (summary) comments, e.g., Approve/Comment with body + id: number // review id (used by GitHub anchors: pullrequestreview-) + body: string + user: BaseUser + created_at: string // submitted_at from API + state: string // APPROVED | COMMENTED | CHANGES_REQUESTED | DISMISSED +} + +interface ReviewThread { + id: string + isResolved: boolean + comments: { + nodes: Array<{ + id: string // GraphQL relay/global ID + databaseId: number | null // GraphQL numeric DB id + body: string + author: { login: string | null } + createdAt: string + }> + } +} + +interface GraphQLResponse { + repository: { + pullRequest: { + reviewThreads: { + nodes: ReviewThread[] + } + } + } +} + +// ---------- State Persistence Types ---------- +interface ResolvedIssueState { + commentId: number // REST comment id + nodeId: string // GraphQL global ID + path: string + line?: number + resolvedAt: string + resolvedLocally: boolean // true if resolved locally without GitHub API + body: string // First 100 chars for identification +} + +interface ResolvedState { + pr: number + lastUpdate: string + resolvedIssues: Record // key: commentId +} + +// Octokit REST API response types +type RestReviewComment = Endpoints['GET /repos/{owner}/{repo}/pulls/{pull_number}/comments']['response']['data'][number] +type RestIssueComment = + Endpoints['GET /repos/{owner}/{repo}/issues/{issue_number}/comments']['response']['data'][number] +type RestPRReview = Endpoints['GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews']['response']['data'][number] + +function getCommentSeverity(comment: ReviewComment): 'critical' | 'major' | 'trivial' { + const body = comment.body || '' + + // Check for severity indicators in the comment body + if (body.includes('🔴 Critical')) { + return 'critical' + } + if (body.includes('🟠 Major')) { + return 'major' + } + if (body.includes('🔵 Trivial') || body.includes('🧹 Nitpick')) { + return 'trivial' + } + + // Default to trivial if no severity found + return 'trivial' +} + +function getSeverityEmoji(severity: 'critical' | 'major' | 'trivial'): string { + switch (severity) { + case 'critical': + return '🔴' + case 'major': + return '🟠' + case 'trivial': + return '🔵' + } +} + +function getSeverityLabel(severity: 'critical' | 'major' | 'trivial'): string { + return severity.charAt(0).toUpperCase() + severity.slice(1) +} + +// ---------- State Persistence Functions ---------- + +/** + * Load resolved state from previous downloads + */ +async function loadResolvedState(outputDir: string, prNumber: number): Promise { + const stateFile = join(outputDir, '.resolved-state.json') + try { + const content = await fs.readFile(stateFile, 'utf8') + const state = JSON.parse(content) as ResolvedState + if (state.pr === prNumber) { + logger.info('Loaded previous resolved state', { + resolvedCount: Object.keys(state.resolvedIssues).length, + lastUpdate: state.lastUpdate, + }) + return state + } + logger.debug('State file exists but for different PR', { statePR: state.pr, currentPR: prNumber }) + return null + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + logger.debug('No previous state file found') + return null + } + logger.warn('Failed to load resolved state', { + error: error instanceof Error ? error.message : String(error), + }) + return null + } +} + +/** + * Save resolved state for future downloads + */ +async function saveResolvedState(outputDir: string, state: ResolvedState): Promise { + const stateFile = join(outputDir, '.resolved-state.json') + try { + await fs.writeFile(stateFile, JSON.stringify(state, null, 2), 'utf8') + logger.info('Saved resolved state', { + resolvedCount: Object.keys(state.resolvedIssues).length, + }) + } catch (error) { + logger.warn('Failed to save resolved state', { + error: error instanceof Error ? error.message : String(error), + }) + } +} + +/** + * Check if a comment was previously resolved locally + */ +function wasResolvedLocally(comment: ReviewComment, previousState: ResolvedState | null): boolean { + if (!previousState) return false + const key = String(comment.id) + return key in previousState.resolvedIssues +} + +/** + * Build resolved state from current comments and previous state + */ +function buildResolvedState( + prNumber: number, + comments: ReviewComment[], + previousState: ResolvedState | null, +): ResolvedState { + const resolvedIssues: Record = {} + + // Preserve previously resolved issues that still exist in current comments + if (previousState) { + for (const comment of comments) { + const key = String(comment.id) + if (key in previousState.resolvedIssues) { + resolvedIssues[key] = previousState.resolvedIssues[key] + } + } + } + + return { + pr: prNumber, + lastUpdate: new Date().toISOString(), + resolvedIssues, + } +} + +async function fetchLatestOpenPR(octokit: EnhancedOctokitInstance, owner: string, repo: string): Promise { + try { + logger.debug('Fetching latest open PR', { owner, repo }) + + // Fetch PRs sorted by most recently updated, limit to 1 open PR + const response = await octokit.rest.pulls.list({ + owner, + repo, + state: 'open', + sort: 'updated', + direction: 'desc', + per_page: 1, + }) + + if (response.data.length === 0) { + const error = new Error('No open PRs found in the repository') + logger.error('No open PRs available', { owner, repo }) + throw error + } + + const latestPR = response.data[0] + logger.debug('Found latest open PR', { + number: latestPR.number, + title: latestPR.title, + updatedAt: latestPR.updated_at, + }) + + return latestPR.number + } catch (error) { + logger.error('Failed to fetch latest open PR', { + error: error instanceof Error ? error.message : String(error), + owner, + repo, + }) + throw error + } +} + +// ---------- Main ---------- +async function main() { + try { + logger.info('Starting PR Review Exporter') + + // Validate command line arguments + const args = process.argv.slice(2) + let prNumber: number | undefined + + if (args.length === 0) { + logger.info('No PR number provided, fetching latest open PR...') + // Will fetch latest open PR below + } else { + // Validate PR number + const prNumberResult = PRNumberSchema.safeParse(Number(args[0])) + if (!prNumberResult.success) { + const error = new Error(`Invalid PR number: ${args[0]}`) + logger.error('Invalid PR number provided', { + prNumber: args[0], + validationErrors: prNumberResult.error.issues, + }) + throw error + } + prNumber = prNumberResult.data + logger.info('Using specified PR number', { prNumber }) + } + + // Validate GitHub token (from .env file or environment variable) + const token = process.env.GITHUB_TOKEN + const tokenResult = GitHubTokenSchema.safeParse(token) + if (!tokenResult.success) { + const error = new Error( + 'GITHUB_TOKEN is not set. Please set it in scripts/.env file or as an environment variable.', + ) + logger.error('Missing or invalid GitHub token', { + validationErrors: tokenResult.error.issues, + hint: 'Create scripts/.env file with: GITHUB_TOKEN=ghp_your_token_here', + }) + throw error + } + + logger.info('Validated input parameters', { prNumber }) + + const { owner, repo } = await getRepoInfo() + const repoInfoResult = RepoInfoSchema.safeParse({ owner, repo }) + if (!repoInfoResult.success) { + const error = new Error('Could not parse repository information from git remote') + logger.error('Invalid repository information', { + owner, + repo, + validationErrors: repoInfoResult.error.issues, + }) + throw error + } + + logger.info('Starting data collection', { prNumber, owner, repo }) + + // Create enhanced Octokit instance with retry and throttling + const octokit = new EnhancedOctokit({ + auth: token, + retry: { + doNotRetry: ['429'], + enabled: true, + }, + throttle: { + onRateLimit: (retryAfter, options, _octokit) => { + logger.warn('Rate limit exceeded, retrying', { + retryAfter, + method: options.method, + url: options.url, + requestRetryCount: options.request?.retryCount, + }) + return options.request?.retryCount <= 2 + }, + onSecondaryRateLimit: (retryAfter, options, _octokit) => { + logger.warn('Secondary rate limit detected', { + retryAfter, + method: options.method, + url: options.url, + }) + return false + }, + }, + }) + + // Fetch latest open PR if not specified + if (!prNumber) { + prNumber = await fetchLatestOpenPR(octokit, owner, repo) + logger.info('Fetched latest open PR', { prNumber }) + } + + // At this point prNumber is guaranteed to be defined + if (!prNumber) { + throw new Error('Failed to determine PR number') + } + + // Fetch data with error handling and logging + logger.info('Fetching review comments (REST)') + const allReviewComments = await fetchAllReviewComments(octokit, owner, repo, prNumber) + logger.info('Fetched review comments', { count: allReviewComments.length }) + + logger.info('Fetching issue comments (REST)') + const allIssueComments = await fetchAllIssueComments(octokit, owner, repo, prNumber) + logger.info('Fetched issue comments', { count: allIssueComments.length }) + + logger.info('Fetching review threads (GraphQL)') + const reviewThreads = await fetchReviewThreads(token as string, owner, repo, prNumber) + logger.info('Fetched review threads', { count: reviewThreads.length }) + + logger.info('Fetching pull request reviews (REST)') + const allSimpleReviews = await fetchAllPullRequestReviews(octokit, owner, repo, prNumber) + logger.info('Fetched pull request reviews', { count: allSimpleReviews.length }) + + // Filter to CodeRabbit bot comments only + const coderabbitReviewComments = allReviewComments.filter((c) => c.user?.login === 'coderabbitai[bot]') + const coderabbitIssueComments = allIssueComments.filter((c) => c.user?.login === 'coderabbitai[bot]') + const coderabbitSimpleReviews = allSimpleReviews.filter( + (r) => r.user?.login === 'coderabbitai[bot]' && (r.body?.trim()?.length ?? 0) > 0, + ) + + const totalCodeRabbitComments = + coderabbitReviewComments.length + coderabbitIssueComments.length + coderabbitSimpleReviews.length + logger.info('Filtered CodeRabbit comments', { + reviewComments: coderabbitReviewComments.length, + issueComments: coderabbitIssueComments.length, + simpleReviews: coderabbitSimpleReviews.length, + total: totalCodeRabbitComments, + }) + + if (totalCodeRabbitComments === 0) { + logger.info('No CodeRabbit AI comments found for PR', { prNumber }) + console.log(`No CodeRabbit AI comments found for PR #${prNumber}.`) + return + } + + // Get output directory from environment or use default + // If OUTPUT_DIR is relative, resolve it from the working directory (user's repo) + const defaultOutputDir = './.reviews' + const configuredOutputDir = process.env.OUTPUT_DIR || defaultOutputDir + const outputBaseDir = configuredOutputDir.startsWith('/') + ? configuredOutputDir + : join(WORKING_DIR, configuredOutputDir) + const outputDir = join(outputBaseDir, `reviews-pr-${prNumber}`) + const commentsDir = join(outputDir, 'comments') + const issuesDir = join(outputDir, 'issues') + const summaryFile = join(outputDir, 'summary.md') + + // Create base output directory if it doesn't exist + await fs.mkdir(outputBaseDir, { recursive: true }) + + // Create all subdirectories (no more severity-based subdirectories) + const dirsToCreate = [outputDir, commentsDir, issuesDir] + await Promise.all(dirsToCreate.map((dir) => fs.mkdir(dir, { recursive: true }))) + + // Load previous resolved state (if exists) + const previousState = await loadResolvedState(outputDir, prNumber) + + // Update logger to use PR-specific log files + const prLogFile = join(outputDir, 'pr-review-combined.log') + const prErrorFile = join(outputDir, 'pr-review-error.log') + logger.add( + new winston.transports.File({ + filename: prLogFile, + }), + ) + logger.add( + new winston.transports.File({ + filename: prErrorFile, + level: 'error', + }), + ) + + logger.info('Creating output directories', { outputDir, outputBaseDir }) + + // Categories: + // - issues: resolvable review comments (inline threads) + // - comments: simple comments (general PR issue comments + PR review bodies) + const reviewComments = coderabbitReviewComments.slice() + const issueComments = coderabbitIssueComments.slice() + const simpleReviewComments = coderabbitSimpleReviews.slice() + + // Sort each category chronologically by creation time + reviewComments.sort((a, b) => a.created_at.localeCompare(b.created_at)) + issueComments.sort((a, b) => a.created_at.localeCompare(b.created_at)) + simpleReviewComments.sort((a, b) => a.created_at.localeCompare(b.created_at)) + + // Count resolution by policy: thread resolved AND contains "✅ Addressed in commit" + // OR previously resolved locally (persisted in state) + const resolvedCount = reviewComments.filter((c) => isCommentResolvedByPolicy(c, reviewThreads, previousState)).length + const unresolvedCount = reviewComments.length - resolvedCount + + logger.info('Processing review comments', { + totalReviewComments: reviewComments.length, + resolved: resolvedCount, + unresolved: unresolvedCount, + resolvedFromPreviousState: previousState ? Object.keys(previousState.resolvedIssues).length : 0, + }) + + logger.info('Creating issue files (resolvable review threads)') + const severityCounts = { critical: 0, major: 0, trivial: 0 } + + for (let i = 0; i < reviewComments.length; i++) { + const severity = getCommentSeverity(reviewComments[i]) + const isResolved = isCommentResolvedByPolicy(reviewComments[i], reviewThreads, previousState) + severityCounts[severity]++ + // Use global sequential numbering (i + 1) for file names + await createIssueFile(issuesDir, i + 1, reviewComments[i], reviewThreads, severity, isResolved) + } + + logger.info('Creating comment files (simple comments)') + // Merge general PR comments and simple PR review bodies into one sequence + type SimpleItem = { kind: 'issue_comment'; data: IssueComment } | { kind: 'review'; data: SimpleReviewComment } + const simpleItems: SimpleItem[] = [ + ...issueComments.map((c) => ({ kind: 'issue_comment' as const, data: c })), + ...simpleReviewComments.map((r) => ({ kind: 'review' as const, data: r })), + ].sort((a, b) => a.data.created_at.localeCompare(b.data.created_at)) + + for (let i = 0; i < simpleItems.length; i++) { + await createSimpleCommentFile(commentsDir, i + 1, simpleItems[i]) + } + + logger.info('Creating summary file') + await createSummaryFile( + summaryFile, + prNumber, + reviewComments, + simpleItems, + resolvedCount, + unresolvedCount, + reviewThreads, + severityCounts, + previousState, + ) + + // Build and save resolved state for next download + const newState = buildResolvedState(prNumber, reviewComments, previousState) + await saveResolvedState(outputDir, newState) + + const totalGenerated = reviewComments.length + simpleItems.length + logger.info('Processing completed successfully', { + prNumber, + totalGenerated, + outputDir, + resolvedCount, + unresolvedCount, + severityCounts, + }) + + console.log(`\n✅ Done. ${totalGenerated} files in ${outputDir}`) + console.log(`ℹ️ Threads resolved: ${resolvedCount} • unresolved: ${unresolvedCount}`) + } catch (error) { + logger.error('Fatal error in PR Review Exporter', { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + prNumber: process.argv[2] ? Number(process.argv[2]) : undefined, + }) + + console.error('❌ Fatal error:', error instanceof Error ? error.message : String(error)) + process.exit(1) + } +} + +// ---------- Helpers ---------- +async function getRepoInfo(): Promise<{ owner: string; repo: string }> { + try { + const remoteUrl = execSync('git config --get remote.origin.url', { + encoding: 'utf8', + cwd: WORKING_DIR // ← Correção aplicada + }).trim() + const match = remoteUrl.match(/github\.com[/:]([^/]+)\/([^/.]+)/) + if (match) return { owner: match[1], repo: match[2] } + throw new Error('Could not parse repository information from git remote') + } catch (error) { + logger.error("Error getting repository info. Ensure you're in a git repository with a GitHub remote.", { + error: error instanceof Error ? error.message : String(error), + }) + throw error + } +} + +async function fetchAllReviewComments( + octokit: EnhancedOctokitInstance, + owner: string, + repo: string, + prNumber: number, +): Promise { + try { + logger.debug('Fetching review comments', { owner, repo, prNumber }) + const comments = await octokit.paginate(octokit.rest.pulls.listReviewComments, { + owner, + repo, + pull_number: prNumber, + per_page: 100, + }) + + // Normalize to the fields we use (and ensure id/node_id present) + const normalizedComments = (comments as RestReviewComment[]).map((c) => ({ + id: c.id, + node_id: c.node_id, + body: c.body || '', + user: { login: c.user?.login || '' }, + created_at: c.created_at, + path: c.path, + line: c.line, + })) as ReviewComment[] + + logger.debug('Successfully fetched review comments', { + count: normalizedComments.length, + owner, + repo, + prNumber, + }) + + return normalizedComments + } catch (error) { + logger.warn('Failed to fetch review comments', { + error: error instanceof Error ? error.message : String(error), + owner, + repo, + prNumber, + }) + return [] + } +} + +async function fetchAllIssueComments( + octokit: EnhancedOctokitInstance, + owner: string, + repo: string, + prNumber: number, +): Promise { + try { + logger.debug('Fetching issue comments', { owner, repo, prNumber }) + const comments = await octokit.paginate(octokit.rest.issues.listComments, { + owner, + repo, + issue_number: prNumber, + per_page: 100, + }) + + const normalizedComments = (comments as RestIssueComment[]).map((c) => ({ + body: c.body || '', + user: { login: c.user?.login || '' }, + created_at: c.created_at, + })) as IssueComment[] + + logger.debug('Successfully fetched issue comments', { + count: normalizedComments.length, + owner, + repo, + prNumber, + }) + + return normalizedComments + } catch (error) { + logger.warn('Failed to fetch issue comments', { + error: error instanceof Error ? error.message : String(error), + owner, + repo, + prNumber, + }) + return [] + } +} + +async function fetchAllPullRequestReviews( + octokit: EnhancedOctokitInstance, + owner: string, + repo: string, + prNumber: number, +): Promise { + try { + logger.debug('Fetching pull request reviews', { owner, repo, prNumber }) + const reviews = await octokit.paginate(octokit.rest.pulls.listReviews, { + owner, + repo, + pull_number: prNumber, + per_page: 100, + }) + + const normalizedReviews = (reviews as RestPRReview[]).map((r) => ({ + id: r.id, + body: r.body || '', + user: { login: r.user?.login || '' }, + created_at: r.submitted_at || '', + state: r.state, + })) as SimpleReviewComment[] + + logger.debug('Successfully fetched pull request reviews', { + count: normalizedReviews.length, + owner, + repo, + prNumber, + }) + + return normalizedReviews + } catch (error) { + logger.warn('Failed to fetch pull request reviews', { + error: error instanceof Error ? error.message : String(error), + owner, + repo, + prNumber, + }) + return [] + } +} + +async function fetchReviewThreads( + token: string, + owner: string, + repo: string, + prNumber: number, +): Promise { + try { + logger.debug('Fetching review threads via GraphQL', { owner, repo, prNumber }) + + const query = ` + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { + reviewThreads(first: 100) { + nodes { + id + isResolved + comments(first: 100) { + nodes { + id + databaseId + body + author { login } + createdAt + } + } + } + } + } + } + } + ` + + const result = await graphql(query, { + owner, + repo, + number: prNumber, + headers: { authorization: `token ${token}` }, + }) + + const threads = result.repository.pullRequest.reviewThreads.nodes + + logger.debug('Successfully fetched review threads', { + count: threads.length, + owner, + repo, + prNumber, + }) + + return threads + } catch (error) { + logger.warn('Failed to fetch review threads', { + error: error instanceof Error ? error.message : String(error), + owner, + repo, + prNumber, + }) + return [] + } +} + +/** + * Determine if a review (inline) comment belongs to a resolved thread. + * Uses robust ID matching: + * REST.reviewComment.id ⇔ GraphQL.comment.databaseId + * REST.reviewComment.node_id ⇔ GraphQL.comment.id (fallback) + */ +function _isCommentResolved(comment: Comment, reviewThreads: ReviewThread[]): boolean { + // General PR (issue) comments cannot be resolved + if (!('path' in comment && 'line' in comment)) return false + + const rc = comment as ReviewComment + for (const thread of reviewThreads) { + const match = thread.comments.nodes.some( + (tc) => + (tc.databaseId != null && rc.id != null && tc.databaseId === rc.id) || (!!rc.node_id && tc.id === rc.node_id), + ) + if (match) return thread.isResolved + } + return false +} + +// Policy-level resolution: the thread must be resolved AND contain +// a confirmation marker "✅ Addressed in commit" somewhere in the thread, +// OR was previously resolved locally (persisted in state). +function isCommentResolvedByPolicy( + comment: Comment, + reviewThreads: ReviewThread[], + previousState: ResolvedState | null = null, +): boolean { + if (!('path' in comment && 'line' in comment)) return false + const rc = comment as ReviewComment + + // Check if previously resolved locally + if (wasResolvedLocally(rc, previousState)) { + return true + } + + // Check GitHub thread resolution status + for (const thread of reviewThreads) { + const match = thread.comments.nodes.some( + (tc) => + (tc.databaseId != null && rc.id != null && tc.databaseId === rc.id) || (!!rc.node_id && tc.id === rc.node_id), + ) + if (match) { + const hasAddressed = thread.comments.nodes.some((tc) => (tc.body || '').includes('✅ Addressed in commit')) + return Boolean(thread.isResolved && hasAddressed) + } + } + return false +} + +async function createIssueFile( + outputDir: string, + issueNumber: number, + comment: ReviewComment, + reviewThreads: ReviewThread[], + severity: 'critical' | 'major' | 'trivial', + isResolved: boolean, +): Promise { + const severityEmoji = getSeverityEmoji(severity) + const severityLabel = getSeverityLabel(severity) + const statusLabel = isResolved ? 'resolved' : 'unresolved' + const fileName = `issue_${issueNumber.toString().padStart(3, '0')}_${severity}_${statusLabel}.md` + const file = join(outputDir, fileName) + const formattedDate = formatDate(comment.created_at) + const resolvedStatus = isResolved ? '- [x] RESOLVED ✓' : '- [ ] UNRESOLVED' + const thread = findThreadForReviewComment(comment, reviewThreads) + const threadId = thread?.id ?? '' + + // Handle file location - some comments may not have a specific line number + const fileLocation = comment.line ? `${comment.path}:${comment.line}` : comment.path + + const content = `# Issue ${issueNumber} - ${severityEmoji} ${severityLabel} - ${statusLabel.toUpperCase()} + +**File:** \`${fileLocation}\` +**Date:** ${formattedDate} +**Status:** ${resolvedStatus} + +## Body + +${comment.body} + +## How To Resolve This Issue + +This comment belongs to a GitHub review thread. To mark it as resolved programmatically, call GitHub's GraphQL API using your \`GITHUB_TOKEN\` (scope: \`repo\`). + +- Thread ID: ${threadId ? `\`${threadId}\`` : '(not found)'} +- Endpoint: \`POST https://api.github.com/graphql\` + +GitHub CLI example: + +\`\`\`bash +gh api graphql \\ + -f query='mutation($threadId: ID!) { resolveReviewThread(input: { threadId: $threadId }) { thread { isResolved } } }' \\ + -F threadId='${threadId || ''}' +\`\`\` + +curl example: + +\`\`\`bash +curl -sS -H "Authorization: bearer $GITHUB_TOKEN" \\ + -H "Content-Type: application/json" \\ + --data '{ + "query": "mutation($threadId: ID!) { resolveReviewThread(input: { threadId: $threadId }) { thread { isResolved } } }", + "variables": { "threadId": "${threadId || ''}" } + }' \\ + https://api.github.com/graphql +\`\`\` + +To unresolve the thread, use: + +\`\`\`bash +gh api graphql \\ + -f query='mutation($threadId: ID!) { unresolveReviewThread(input: { threadId: $threadId }) { thread { isResolved } } }' \\ + -F threadId='${threadId || ''}' +\`\`\` + +--- +*Generated from PR review - CodeRabbit AI* +` + await fs.writeFile(file, content, 'utf8') + console.log(` Created ${file}`) +} + +// Maps a REST review comment to its GraphQL review thread, if available. +function findThreadForReviewComment(comment: ReviewComment, reviewThreads: ReviewThread[]): ReviewThread | undefined { + for (const thread of reviewThreads) { + const match = thread.comments.nodes.some( + (tc) => + (tc.databaseId != null && comment.id != null && tc.databaseId === comment.id) || + (!!comment.node_id && tc.id === comment.node_id), + ) + if (match) return thread + } + return undefined +} + +async function createSimpleCommentFile( + outputDir: string, + commentNumber: number, + item: { kind: 'issue_comment'; data: IssueComment } | { kind: 'review'; data: SimpleReviewComment }, +): Promise { + const file = join(outputDir, `comment_${commentNumber.toString().padStart(3, '0')}.md`) + const d = item.data + const formattedDate = formatDate(d.created_at) + const typeLabel = item.kind === 'review' ? `PR Review (${(d as SimpleReviewComment).state})` : 'General PR Comment' + const content = `# Comment ${commentNumber} - ${typeLabel} + +**Date:** ${formattedDate} +**Status:** N/A (not resolvable) + +## Body + +${d.body} + +--- +*Generated from PR review - CodeRabbit AI* +` + await fs.writeFile(file, content, 'utf8') + console.log(` Created ${file}`) +} + +async function createSummaryFile( + summaryFile: string, + prNumber: number, + reviewComments: ReviewComment[], + simpleItems: ({ kind: 'issue_comment'; data: IssueComment } | { kind: 'review'; data: SimpleReviewComment })[], + resolvedCount: number, + unresolvedCount: number, + reviewThreads: ReviewThread[], + severityCounts: { critical: number; major: number; trivial: number }, + previousState: ResolvedState | null = null, +): Promise { + const now = new Date().toISOString() + let content = `# PR Review #${prNumber} - CodeRabbit AI Export + +This folder contains exported issues (resolvable review threads) and simple comments for PR #${prNumber}. + +## Summary + +- **Issues (resolvable review comments):** ${reviewComments.length} + - 🔴 Critical: ${severityCounts.critical} + - 🟠 Major: ${severityCounts.major} + - 🔵 Trivial: ${severityCounts.trivial} +- **Comments (simple, not resolvable):** ${simpleItems.length} + - **Resolved issues:** ${resolvedCount} ✓ + - **Unresolved issues:** ${unresolvedCount} + +**Generated on:** ${formatDate(now)} + +## Issues + +### 🔴 Critical Issues + +` + + // Group issues by severity for more efficient rendering + const issuesBySeverity = { + critical: [] as Array<{ index: number; comment: ReviewComment; isResolved: boolean }>, + major: [] as Array<{ index: number; comment: ReviewComment; isResolved: boolean }>, + trivial: [] as Array<{ index: number; comment: ReviewComment; isResolved: boolean }>, + } + + for (let i = 0; i < reviewComments.length; i++) { + const severity = getCommentSeverity(reviewComments[i]) + const isResolved = isCommentResolvedByPolicy(reviewComments[i], reviewThreads, previousState) + issuesBySeverity[severity].push({ index: i, comment: reviewComments[i], isResolved }) + } + + // Render critical issues + for (const { index, comment, isResolved } of issuesBySeverity.critical) { + const checked = isResolved ? 'x' : ' ' + const statusLabel = isResolved ? 'resolved' : 'unresolved' + const fileName = `issue_${(index + 1).toString().padStart(3, '0')}_critical_${statusLabel}.md` + const issueFile = `issues/${fileName}` + const loc = comment.line ? ` ${comment.path}:${comment.line}` : ` ${comment.path}` + content += `- [${checked}] [Issue ${index + 1}](${issueFile}) -${loc}\n` + } + + content += ` + +### 🟠 Major Issues + +` + + // Render major issues + for (const { index, comment, isResolved } of issuesBySeverity.major) { + const checked = isResolved ? 'x' : ' ' + const statusLabel = isResolved ? 'resolved' : 'unresolved' + const fileName = `issue_${(index + 1).toString().padStart(3, '0')}_major_${statusLabel}.md` + const issueFile = `issues/${fileName}` + const loc = comment.line ? ` ${comment.path}:${comment.line}` : ` ${comment.path}` + content += `- [${checked}] [Issue ${index + 1}](${issueFile}) -${loc}\n` + } + + content += ` + +### 🔵 Trivial Issues + +` + + // Render trivial issues + for (const { index, comment, isResolved } of issuesBySeverity.trivial) { + const checked = isResolved ? 'x' : ' ' + const statusLabel = isResolved ? 'resolved' : 'unresolved' + const fileName = `issue_${(index + 1).toString().padStart(3, '0')}_trivial_${statusLabel}.md` + const issueFile = `issues/${fileName}` + const loc = comment.line ? ` ${comment.path}:${comment.line}` : ` ${comment.path}` + content += `- [${checked}] [Issue ${index + 1}](${issueFile}) -${loc}\n` + } + + content += `\n## Comments (not resolvable)\n\n` + for (let i = 0; i < simpleItems.length; i++) { + const commentFile = `comments/comment_${(i + 1).toString().padStart(3, '0')}.md` + const label = simpleItems[i].kind === 'review' ? 'review' : 'general' + content += `- [ ] [Comment ${i + 1}](${commentFile}) (${label})\n` + } + + await fs.writeFile(summaryFile, content, 'utf8') + console.log(` Created summary file: ${summaryFile}`) +} + +function getConfiguredTimeZone(): string { + const env = process.env.PR_REVIEW_TZ + if (!env || env.toLowerCase() === 'local') { + const sys = Intl.DateTimeFormat().resolvedOptions().timeZone + return sys || 'UTC' + } + return env +} + +function formatDate(dateString: string): string { + try { + const d = new Date(dateString) + if (Number.isNaN(d.getTime())) return dateString + const tz = getConfiguredTimeZone() + const parts = new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + timeZone: tz, + }) + .formatToParts(d) + .reduce( + (acc: Record, p) => { + acc[p.type] = p.value + return acc + }, + {} as Record, + ) + return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}:${parts.second} ${tz}` + } catch { + return dateString // fallback to original format + } +} + +main().catch((error) => { + logger.error('Unhandled error in main', { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }) + process.exit(1) +}) diff --git a/skills/pull-request-skill/read-pr-issues.sh b/skills/pull-request-skill/read-pr-issues.sh new file mode 100755 index 0000000..1156fa9 --- /dev/null +++ b/skills/pull-request-skill/read-pr-issues.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash + +# Read PR Issues Script +# Displays issues from the PR review export in a clean, readable format +# +# Usage: +# ./read-pr-issues.sh --pr 277 --type issue --all +# ./read-pr-issues.sh --pr 277 --type issue --from 1 --to 10 +# ./read-pr-issues.sh --pr 277 --type critical --all +# ./read-pr-issues.sh --pr 277 --type major --all +# ./read-pr-issues.sh --pr 277 --type trivial --all + +set -euo pipefail + +# Default values +PR="" +TYPE="issue" +FROM="" +TO="" +ALL=false +# Use CWD if provided (for when called from skill location), otherwise use current directory +WORKING_DIR="${CWD:-$(pwd)}" +BASE_DIR="${OUTPUT_DIR:-./.reviews}" +# Resolve BASE_DIR relative to WORKING_DIR if it's not absolute +if [[ ! "$BASE_DIR" = /* ]]; then + BASE_DIR="${WORKING_DIR}/${BASE_DIR}" +fi + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --pr) + PR="$2" + shift 2 + ;; + --type) + TYPE="$2" + shift 2 + ;; + --from) + FROM="$2" + shift 2 + ;; + --to) + TO="$2" + shift 2 + ;; + --all) + ALL=true + shift + ;; + --base-dir) + BASE_DIR="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + echo "Usage: $0 --pr PR_NUMBER --type TYPE [--from N --to M | --all] [--base-dir DIR]" + echo "Types: issue, critical, major, trivial" + exit 1 + ;; + esac +done + +# Validate PR number +if [[ -z "$PR" ]]; then + echo "❌ Error: --pr is required" + exit 1 +fi + +# Set PR directory +PR_DIR="${BASE_DIR}/reviews-pr-${PR}" + +if [[ ! -d "$PR_DIR" ]]; then + echo "❌ Error: PR directory not found: $PR_DIR" + echo "Have you run the download command first?" + exit 1 +fi + +echo "📂 Reading issues from: $PR_DIR" +echo "" + +# Determine which files to read based on type +case "$TYPE" in + critical|major|trivial) + PATTERN="*_${TYPE}_*.md" + DIR="${PR_DIR}/issues" + ;; + issue) + PATTERN="issue_*.md" + DIR="${PR_DIR}/issues" + ;; + *) + echo "❌ Error: Unknown type: $TYPE" + echo "Valid types: issue, critical, major, trivial" + exit 1 + ;; +esac + +# Find matching files +if [[ "$ALL" == true ]]; then + FILES=($(find "$DIR" -name "$PATTERN" -type f | sort)) +else + if [[ -z "$FROM" ]] || [[ -z "$TO" ]]; then + echo "❌ Error: Either use --all or specify both --from and --to" + exit 1 + fi + + FILES=() + for i in $(seq "$FROM" "$TO"); do + ISSUE_NUM=$(printf "%03d" "$i") + MATCHES=($(find "$DIR" -name "issue_${ISSUE_NUM}_*.md" -type f)) + if [[ ${#MATCHES[@]} -gt 0 ]]; then + FILES+=("${MATCHES[0]}") + fi + done +fi + +if [[ ${#FILES[@]} -eq 0 ]]; then + echo "⚠️ No issues found matching criteria" + exit 0 +fi + +echo "Found ${#FILES[@]} issue(s)" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" + +# Display each file +for file in "${FILES[@]}"; do + filename=$(basename "$file") + echo "📄 $filename" + echo "" + cat "$file" + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" +done + +echo "✅ Displayed ${#FILES[@]} issue(s)" diff --git a/skills/pull-request-skill/resolve-pr-issues.sh b/skills/pull-request-skill/resolve-pr-issues.sh new file mode 100755 index 0000000..f579b8a --- /dev/null +++ b/skills/pull-request-skill/resolve-pr-issues.sh @@ -0,0 +1,179 @@ +#!/usr/bin/env bash + +# Resolve PR Issues Script +# Marks review threads as resolved using GitHub CLI and updates the summary +# +# Usage: +# ./resolve-pr-issues.sh --pr-dir ./ai-docs/reviews-pr-277 --from 1 --to 10 +# ./resolve-pr-issues.sh --pr-dir ./ai-docs/reviews-pr-277 --all + +set -euo pipefail + +# Default values +PR_DIR="" +FROM="" +TO="" +ALL=false +# Use CWD if provided (for when called from skill location), otherwise use current directory +WORKING_DIR="${CWD:-$(pwd)}" + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --pr-dir) + PR_DIR="$2" + shift 2 + ;; + --from) + FROM="$2" + shift 2 + ;; + --to) + TO="$2" + shift 2 + ;; + --all) + ALL=true + shift + ;; + *) + echo "Unknown option: $1" + echo "Usage: $0 --pr-dir PR_DIR [--from N --to M | --all]" + exit 1 + ;; + esac +done + +# Validate PR directory +if [[ -z "$PR_DIR" ]]; then + echo "❌ Error: --pr-dir is required" + exit 1 +fi + +# Resolve PR_DIR relative to WORKING_DIR if it's not absolute +if [[ ! "$PR_DIR" = /* ]]; then + PR_DIR="${WORKING_DIR}/${PR_DIR}" +fi + +if [[ ! -d "$PR_DIR" ]]; then + echo "❌ Error: PR directory not found: $PR_DIR" + exit 1 +fi + +ISSUES_DIR="${PR_DIR}/issues" + +if [[ ! -d "$ISSUES_DIR" ]]; then + echo "❌ Error: Issues directory not found: $ISSUES_DIR" + exit 1 +fi + +# Check if gh is installed +if ! command -v gh &> /dev/null; then + echo "❌ Error: GitHub CLI (gh) is not installed" + echo "Install from: https://cli.github.com" + exit 1 +fi + +echo "🔧 Resolving issues in: $PR_DIR" +echo "" + +# Find unresolved issue files +if [[ "$ALL" == true ]]; then + FILES=($(find "$ISSUES_DIR" -name "issue_*_unresolved.md" -type f | sort)) +else + if [[ -z "$FROM" ]] || [[ -z "$TO" ]]; then + echo "❌ Error: Either use --all or specify both --from and --to" + exit 1 + fi + + FILES=() + for i in $(seq "$FROM" "$TO"); do + ISSUE_NUM=$(printf "%03d" "$i") + MATCHES=($(find "$ISSUES_DIR" -name "issue_${ISSUE_NUM}_*_unresolved.md" -type f)) + if [[ ${#MATCHES[@]} -gt 0 ]]; then + FILES+=("${MATCHES[@]}") + fi + done +fi + +if [[ ${#FILES[@]} -eq 0 ]]; then + echo "✅ No unresolved issues found in specified range" + exit 0 +fi + +echo "Found ${#FILES[@]} unresolved issue(s) to process" +echo "" + +RESOLVED_COUNT=0 +FAILED_COUNT=0 + +# Process each file +for file in "${FILES[@]}"; do + filename=$(basename "$file") + + # Extract thread ID from file + THREAD_ID=$(grep -m1 "^- Thread ID:" "$file" | sed 's/^- Thread ID: `\(.*\)`$/\1/' || echo "") + + if [[ -z "$THREAD_ID" ]] || [[ "$THREAD_ID" == "(not found)" ]]; then + echo "⚠️ Skipping $filename - no thread ID found" + ((FAILED_COUNT++)) + continue + fi + + echo "Processing: $filename" + echo " Thread ID: $THREAD_ID" + + # Resolve the thread using GitHub CLI + if gh api graphql \ + -f query='mutation($threadId: ID!) { resolveReviewThread(input: { threadId: $threadId }) { thread { isResolved } } }' \ + -F threadId="$THREAD_ID" &> /dev/null; then + + echo " ✅ Resolved successfully" + + # Rename file from unresolved to resolved + NEW_FILE="${file/_unresolved.md/_resolved.md}" + mv "$file" "$NEW_FILE" + + # Update status in file + sed -i.bak 's/- \[ \] UNRESOLVED/- [x] RESOLVED ✓/g' "$NEW_FILE" + sed -i.bak 's/UNRESOLVED/RESOLVED/g' "$NEW_FILE" + rm -f "${NEW_FILE}.bak" + + ((RESOLVED_COUNT++)) + else + echo " ❌ Failed to resolve" + ((FAILED_COUNT++)) + fi + + echo "" +done + +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "Summary:" +echo " ✅ Resolved: $RESOLVED_COUNT" +echo " ❌ Failed: $FAILED_COUNT" +echo " 📊 Total processed: ${#FILES[@]}" +echo "" + +if [[ $RESOLVED_COUNT -gt 0 ]]; then + echo "🔄 Re-running PR review export to update summary..." + + # Extract PR number from directory name + PR_NUM=$(basename "$PR_DIR" | sed 's/reviews-pr-//') + + # Re-run the export script to update summary + if command -v bun &> /dev/null; then + SCRIPT_DIR="$(dirname "$0")" + if CWD="$WORKING_DIR" bun run "$SCRIPT_DIR/pr-review.ts" "$PR_NUM"; then + echo "✅ Summary updated successfully" + else + echo "⚠️ Warning: Failed to update summary, but issues were resolved" + fi + else + echo "⚠️ Warning: Bun not found, summary not updated automatically" + echo "Run manually: CWD=$(pwd) bun run /pr-review.ts $PR_NUM" + fi +fi + +echo "" +echo "✅ Done!" diff --git a/skills/pull-request-skill/run.js b/skills/pull-request-skill/run.js new file mode 100755 index 0000000..954ebd9 --- /dev/null +++ b/skills/pull-request-skill/run.js @@ -0,0 +1,173 @@ +#!/usr/bin/env node +/** + * Universal Pull Request Review Exporter for Claude Code + * + * Executes the PR review export script with Bun: + * - Download PR reviews: node run.js download [PR_NUMBER] + * - Help: node run.js --help + * + * Ensures proper module resolution and dependency installation. + */ + +import { execSync, spawn } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Change to skill directory for proper module resolution +process.chdir(__dirname); + +/** + * Check if Bun is installed + */ +function checkBunInstalled() { + try { + execSync('bun --version', { stdio: 'pipe' }); + return true; + } catch (e) { + return false; + } +} + +/** + * Check if dependencies are installed + */ +function checkDependenciesInstalled() { + return existsSync(join(__dirname, 'node_modules')); +} + +/** + * Install dependencies using Bun + */ +function installDependencies() { + console.log('📦 Dependencies not found. Installing...'); + try { + execSync('bun install', { stdio: 'inherit', cwd: __dirname }); + console.log('✅ Dependencies installed successfully'); + return true; + } catch (e) { + console.error('❌ Failed to install dependencies:', e.message); + console.error('Please run manually: cd', __dirname, '&& bun install'); + return false; + } +} + +/** + * Show help message + */ +function showHelp() { + console.log(` +Pull Request Review Exporter - Claude Code Skill + +USAGE: + node run.js download [PR_NUMBER] Download CodeRabbit comments for a PR + node run.js download Download comments for latest open PR + node run.js --help Show this help message + +EXAMPLES: + node run.js download 123 Download PR #123 reviews + node run.js download Download latest open PR reviews + + # From any directory (saves to that directory's .reviews/) + CWD=/path/to/repo node run.js download 123 + +ENVIRONMENT VARIABLES: + GITHUB_TOKEN GitHub Personal Access Token (required) + OUTPUT_DIR Output directory for reviews (default: ./.reviews) + CWD Working directory to save reviews (default: current directory) + LOG_LEVEL Logging level: error, warn, info, debug (default: info) + PR_REVIEW_TZ Timezone for dates (default: system timezone) + +SETUP: + 1. Create .env file in skill directory: + echo "GITHUB_TOKEN=ghp_your_token_here" > .env + + 2. Optionally configure output directory and logging: + echo "OUTPUT_DIR=./my-reviews" >> .env + echo "LOG_LEVEL=debug" >> .env + +For more information, see README.md +`); +} + +/** + * Run the PR review export script + */ +function runPRReviewScript(args) { + const scriptPath = join(__dirname, 'pr-review.ts'); + + if (!existsSync(scriptPath)) { + console.error('❌ Error: pr-review.ts not found in skill directory'); + console.error('Expected at:', scriptPath); + process.exit(1); + } + + console.log('🚀 Starting PR Review Exporter...\n'); + + // Use spawn to stream output in real-time + const bunProcess = spawn('bun', ['run', scriptPath, ...args], { + cwd: __dirname, + stdio: 'inherit', + env: { ...process.env } + }); + + bunProcess.on('error', (error) => { + console.error('❌ Failed to start Bun process:', error.message); + process.exit(1); + }); + + bunProcess.on('exit', (code) => { + process.exit(code || 0); + }); +} + +/** + * Main execution + */ +async function main() { + const args = process.argv.slice(2); + + // Show help + if (args.includes('--help') || args.includes('-h')) { + showHelp(); + process.exit(0); + } + + // Check Bun installation + if (!checkBunInstalled()) { + console.error('❌ Bun is not installed or not in PATH'); + console.error('Please install Bun from: https://bun.sh'); + console.error('Or use: curl -fsSL https://bun.sh/install | bash'); + process.exit(1); + } + + // Check and install dependencies if needed + if (!checkDependenciesInstalled()) { + const installed = installDependencies(); + if (!installed) { + process.exit(1); + } + } + + // Parse command + const command = args[0]; + + if (command === 'download' || !command) { + // Extract PR number if provided + const prArgs = args.slice(1); + runPRReviewScript(prArgs); + } else { + console.error(`❌ Unknown command: ${command}`); + console.error('Run "node run.js --help" for usage information'); + process.exit(1); + } +} + +// Run main function +main().catch((error) => { + console.error('❌ Fatal error:', error.message); + process.exit(1); +});