add opencode review plugin (port from pi-coding-agent extension)

This commit is contained in:
2026-03-31 21:19:48 +00:00
parent 86afae7d6c
commit 66ff22f9e6
3 changed files with 457 additions and 0 deletions

237
.opencode/plugins/review.ts Normal file
View File

@@ -0,0 +1,237 @@
import { type Plugin, tool } from "@opencode-ai/plugin"
function normalizeRemoteUrl(value: string): string {
return value
.trim()
.replace(/^git@github\.com:/, "https://github.com/")
.replace(/^ssh:\/\/git@github\.com\//, "https://github.com/")
.replace(/\.git$/, "")
.toLowerCase()
}
function sanitizeRemoteName(value: string): string {
const sanitized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "")
return sanitized || "gh-pr"
}
export const ReviewPlugin: Plugin = async ({ $ }) => {
return {
tool: {
review_materialize_pr: tool({
description:
"Materialize a GitHub pull request locally using jj for code review. " +
"Fetches the PR branch, creates a new jj change on top of it, and returns " +
"metadata needed for the review. Handles cross-repository (forked) PRs. " +
"Call this before reviewing a PR to set up the local state.",
args: {
prNumber: tool.schema
.number()
.describe("The PR number to materialize (e.g. 123)"),
},
async execute(args, context) {
const prNumber = args.prNumber
// Check for pending working-copy changes
const statusResult =
await $`jj diff --summary 2>/dev/null`.nothrow().quiet()
if (
statusResult.exitCode === 0 &&
statusResult.stdout.toString().trim().length > 0
) {
return JSON.stringify({
success: false,
error:
"Cannot materialize PR: you have local jj changes. Please snapshot or discard them first.",
})
}
// Save current position for later restoration
const currentChangeResult =
await $`jj log -r @ --no-graph -T 'change_id.shortest(8)'`
.nothrow()
.quiet()
const savedChangeId = currentChangeResult.stdout.toString().trim()
// Get PR info from GitHub CLI
const prInfoResult =
await $`gh pr view ${prNumber} --json baseRefName,title,headRefName,isCrossRepository,headRepository,headRepositoryOwner`
.nothrow()
.quiet()
if (prInfoResult.exitCode !== 0) {
return JSON.stringify({
success: false,
error: `Could not find PR #${prNumber}. Make sure gh is authenticated and the PR exists.`,
})
}
let prInfo: {
baseRefName: string
title: string
headRefName: string
isCrossRepository: boolean
headRepository?: { name: string; url: string }
headRepositoryOwner?: { login: string }
}
try {
prInfo = JSON.parse(prInfoResult.stdout.toString())
} catch {
return JSON.stringify({
success: false,
error: "Failed to parse PR info from gh CLI",
})
}
// Determine the remote to use
const remotesResult =
await $`jj git remote list`.nothrow().quiet()
const remotes = remotesResult.stdout
.toString()
.trim()
.split("\n")
.filter(Boolean)
.map((line: string) => {
const [name, ...urlParts] = line.split(/\s+/)
return { name, url: urlParts.join(" ") }
})
.filter(
(r: { name: string; url: string }) => r.name && r.url,
)
const defaultRemote =
remotes.find(
(r: { name: string; url: string }) =>
r.name === "origin",
) ?? remotes[0]
if (!defaultRemote) {
return JSON.stringify({
success: false,
error: "No jj remotes are configured for this repository",
})
}
let remoteName = defaultRemote.name
let addedTemporaryRemote = false
if (prInfo.isCrossRepository) {
const repoSlug =
prInfo.headRepositoryOwner?.login &&
prInfo.headRepository?.name
? `${prInfo.headRepositoryOwner.login}/${prInfo.headRepository.name}`.toLowerCase()
: undefined
const forkUrl = prInfo.headRepository?.url
// Check if we already have a remote for this fork
const existingRemote = remotes.find(
(r: { name: string; url: string }) => {
if (
forkUrl &&
normalizeRemoteUrl(r.url) ===
normalizeRemoteUrl(forkUrl)
) {
return true
}
return repoSlug
? normalizeRemoteUrl(r.url).includes(
`github.com/${repoSlug}`,
)
: false
},
)
if (existingRemote) {
remoteName = existingRemote.name
} else if (forkUrl) {
const remoteBaseName = sanitizeRemoteName(
`gh-pr-${prInfo.headRepositoryOwner?.login ?? "remote"}-${prInfo.headRepository?.name ?? prNumber}`,
)
const existingNames = new Set(
remotes.map(
(r: { name: string; url: string }) =>
r.name,
),
)
remoteName = remoteBaseName
let suffix = 2
while (existingNames.has(remoteName)) {
remoteName = `${remoteBaseName}-${suffix}`
suffix += 1
}
const addResult =
await $`jj git remote add ${remoteName} ${forkUrl}`
.nothrow()
.quiet()
if (addResult.exitCode !== 0) {
return JSON.stringify({
success: false,
error:
addResult.stderr.toString() ||
"Failed to add PR remote",
})
}
addedTemporaryRemote = true
} else {
return JSON.stringify({
success: false,
error: "PR head repository URL is unavailable",
})
}
}
// Fetch the PR branch
const fetchResult =
await $`jj git fetch --remote ${remoteName} --branch ${prInfo.headRefName}`
.nothrow()
.quiet()
if (fetchResult.exitCode !== 0) {
if (addedTemporaryRemote) {
await $`jj git remote remove ${remoteName}`
.nothrow()
.quiet()
}
return JSON.stringify({
success: false,
error:
fetchResult.stderr.toString() ||
"Failed to fetch PR branch",
})
}
// Create a new change on top of the PR branch
const bookmarkRevset = `remote_bookmarks(exact:"${prInfo.headRefName}", exact:"${remoteName}")`
const editResult =
await $`jj new ${bookmarkRevset}`.nothrow().quiet()
if (editResult.exitCode !== 0) {
if (addedTemporaryRemote) {
await $`jj git remote remove ${remoteName}`
.nothrow()
.quiet()
}
return JSON.stringify({
success: false,
error:
editResult.stderr.toString() ||
"Failed to create change on PR branch",
})
}
// Clean up temporary remote
if (addedTemporaryRemote) {
await $`jj git remote remove ${remoteName}`
.nothrow()
.quiet()
}
return JSON.stringify({
success: true,
prNumber,
title: prInfo.title,
baseBookmark: prInfo.baseRefName,
headBookmark: prInfo.headRefName,
remote: remoteName,
savedChangeId,
})
},
}),
},
}
}