refactor(cli): convert pr command to effectCmd (#25465)

This commit is contained in:
Kit Langton
2026-05-02 17:01:46 -04:00
committed by GitHub
parent 43e20874f4
commit 8396d6b016
+39 -73
View File
@@ -1,11 +1,11 @@
import { Effect } from "effect"
import { UI } from "../ui" import { UI } from "../ui"
import { cmd } from "./cmd" import { effectCmd, fail } from "../effect-cmd"
import { AppRuntime } from "@/effect/app-runtime"
import { Git } from "@/git" import { Git } from "@/git"
import { Instance } from "@/project/instance" import { InstanceRef } from "@/effect/instance-ref"
import { Process } from "@/util/process" import { Process } from "@/util/process"
export const PrCommand = cmd({ export const PrCommand = effectCmd({
command: "pr <number>", command: "pr <number>",
describe: "fetch and checkout a GitHub PR branch, then run opencode", describe: "fetch and checkout a GitHub PR branch, then run opencode",
builder: (yargs) => builder: (yargs) =>
@@ -14,100 +14,66 @@ export const PrCommand = cmd({
describe: "PR number to checkout", describe: "PR number to checkout",
demandOption: true, demandOption: true,
}), }),
async handler(args) { handler: Effect.fn("Cli.pr")(function* (args) {
await Instance.provide({ const ctx = yield* InstanceRef
directory: process.cwd(), if (!ctx) return yield* fail("Could not load instance context")
async fn() { if (ctx.project.vcs !== "git") {
const project = Instance.project return yield* fail("Could not find git repository. Please run this command from a git repository.")
if (project.vcs !== "git") {
UI.error("Could not find git repository. Please run this command from a git repository.")
process.exit(1)
} }
const git = yield* Git.Service
const worktree = ctx.worktree
const prNumber = args.number const prNumber = args.number
const localBranchName = `pr/${prNumber}` const localBranchName = `pr/${prNumber}`
UI.println(`Fetching and checking out PR #${prNumber}...`) UI.println(`Fetching and checking out PR #${prNumber}...`)
// Use gh pr checkout with custom branch name const checkout = yield* Effect.promise(() =>
const result = await Process.run( Process.run(["gh", "pr", "checkout", `${prNumber}`, "--branch", localBranchName, "--force"], { nothrow: true }),
["gh", "pr", "checkout", `${prNumber}`, "--branch", localBranchName, "--force"],
{
nothrow: true,
},
) )
if (checkout.code !== 0) {
if (result.code !== 0) { return yield* fail(`Failed to checkout PR #${prNumber}. Make sure you have gh CLI installed and authenticated.`)
UI.error(`Failed to checkout PR #${prNumber}. Make sure you have gh CLI installed and authenticated.`)
process.exit(1)
} }
// Fetch PR info for fork handling and session link detection const prInfoResult = yield* Effect.promise(() =>
const prInfoResult = await Process.text( Process.text(
[ ["gh", "pr", "view", `${prNumber}`, "--json", "headRepository,headRepositoryOwner,isCrossRepository,headRefName,body"],
"gh",
"pr",
"view",
`${prNumber}`,
"--json",
"headRepository,headRepositoryOwner,isCrossRepository,headRefName,body",
],
{ nothrow: true }, { nothrow: true },
),
) )
let sessionId: string | undefined let sessionId: string | undefined
if (prInfoResult.code === 0) { if (prInfoResult.code === 0 && prInfoResult.text.trim()) {
const prInfoText = prInfoResult.text const prInfo = JSON.parse(prInfoResult.text)
if (prInfoText.trim()) {
const prInfo = JSON.parse(prInfoText)
// Handle fork PRs if (prInfo?.isCrossRepository && prInfo.headRepository && prInfo.headRepositoryOwner) {
if (prInfo && prInfo.isCrossRepository && prInfo.headRepository && prInfo.headRepositoryOwner) {
const forkOwner = prInfo.headRepositoryOwner.login const forkOwner = prInfo.headRepositoryOwner.login
const forkName = prInfo.headRepository.name const forkName = prInfo.headRepository.name
const remoteName = forkOwner const remoteName = forkOwner
// Check if remote already exists const remotes = (yield* git.run(["remote"], { cwd: worktree })).text().trim()
const remotes = await AppRuntime.runPromise(
Git.Service.use((git) => git.run(["remote"], { cwd: Instance.worktree })),
).then((x) => x.text().trim())
if (!remotes.split("\n").includes(remoteName)) { if (!remotes.split("\n").includes(remoteName)) {
await AppRuntime.runPromise( yield* git.run(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], { cwd: worktree })
Git.Service.use((git) =>
git.run(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
cwd: Instance.worktree,
}),
),
)
UI.println(`Added fork remote: ${remoteName}`) UI.println(`Added fork remote: ${remoteName}`)
} }
// Set upstream to the fork so pushes go there yield* git.run(
const headRefName = prInfo.headRefName ["branch", `--set-upstream-to=${remoteName}/${prInfo.headRefName}`, localBranchName],
await AppRuntime.runPromise( { cwd: worktree },
Git.Service.use((git) =>
git.run(["branch", `--set-upstream-to=${remoteName}/${headRefName}`, localBranchName], {
cwd: Instance.worktree,
}),
),
) )
} }
// Check for opencode session link in PR body if (prInfo?.body) {
if (prInfo && prInfo.body) {
const sessionMatch = prInfo.body.match(/https:\/\/opncd\.ai\/s\/([a-zA-Z0-9_-]+)/) const sessionMatch = prInfo.body.match(/https:\/\/opncd\.ai\/s\/([a-zA-Z0-9_-]+)/)
if (sessionMatch) { if (sessionMatch) {
const sessionUrl = sessionMatch[0] const sessionUrl = sessionMatch[0]
UI.println(`Found opencode session: ${sessionUrl}`) UI.println(`Found opencode session: ${sessionUrl}`)
UI.println(`Importing session...`) UI.println(`Importing session...`)
const importResult = await Process.text(["opencode", "import", sessionUrl], { const importResult = yield* Effect.promise(() => Process.text(["opencode", "import", sessionUrl], { nothrow: true }))
nothrow: true,
})
if (importResult.code === 0) { if (importResult.code === 0) {
const importOutput = importResult.text.trim() const sessionIdMatch = importResult.text.trim().match(/Imported session: ([a-zA-Z0-9_-]+)/)
// Extract session ID from the output (format: "Imported session: <session-id>")
const sessionIdMatch = importOutput.match(/Imported session: ([a-zA-Z0-9_-]+)/)
if (sessionIdMatch) { if (sessionIdMatch) {
sessionId = sessionIdMatch[1] sessionId = sessionIdMatch[1]
UI.println(`Session imported: ${sessionId}`) UI.println(`Session imported: ${sessionId}`)
@@ -116,7 +82,6 @@ export const PrCommand = cmd({
} }
} }
} }
}
UI.println(`Successfully checked out PR #${prNumber} as branch '${localBranchName}'`) UI.println(`Successfully checked out PR #${prNumber} as branch '${localBranchName}'`)
UI.println() UI.println()
@@ -124,15 +89,16 @@ export const PrCommand = cmd({
UI.println() UI.println()
const opencodeArgs = sessionId ? ["-s", sessionId] : [] const opencodeArgs = sessionId ? ["-s", sessionId] : []
const opencodeProcess = Process.spawn(["opencode", ...opencodeArgs], { const code = yield* Effect.promise(() =>
Process.spawn(["opencode", ...opencodeArgs], {
stdin: "inherit", stdin: "inherit",
stdout: "inherit", stdout: "inherit",
stderr: "inherit", stderr: "inherit",
cwd: process.cwd(), cwd: process.cwd(),
}) }).exited,
const code = await opencodeProcess.exited )
if (code !== 0) throw new Error(`opencode exited with code ${code}`) // Match legacy throw semantics — propagate as a defect so the top-level
}, // index.ts catch handles it identically (exit 1, "Unexpected error" banner).
}) if (code !== 0) return yield* Effect.die(new Error(`opencode exited with code ${code}`))
}, }),
}) })