refactor(cli): convert pr command to effectCmd (#25465)
This commit is contained in:
@@ -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,125 +14,91 @@ 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 localBranchName = `pr/${prNumber}`
|
||||||
|
UI.println(`Fetching and checking out PR #${prNumber}...`)
|
||||||
|
|
||||||
|
const checkout = yield* Effect.promise(() =>
|
||||||
|
Process.run(["gh", "pr", "checkout", `${prNumber}`, "--branch", localBranchName, "--force"], { nothrow: true }),
|
||||||
|
)
|
||||||
|
if (checkout.code !== 0) {
|
||||||
|
return yield* fail(`Failed to checkout PR #${prNumber}. Make sure you have gh CLI installed and authenticated.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const prInfoResult = yield* Effect.promise(() =>
|
||||||
|
Process.text(
|
||||||
|
["gh", "pr", "view", `${prNumber}`, "--json", "headRepository,headRepositoryOwner,isCrossRepository,headRefName,body"],
|
||||||
|
{ nothrow: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
let sessionId: string | undefined
|
||||||
|
|
||||||
|
if (prInfoResult.code === 0 && prInfoResult.text.trim()) {
|
||||||
|
const prInfo = JSON.parse(prInfoResult.text)
|
||||||
|
|
||||||
|
if (prInfo?.isCrossRepository && prInfo.headRepository && prInfo.headRepositoryOwner) {
|
||||||
|
const forkOwner = prInfo.headRepositoryOwner.login
|
||||||
|
const forkName = prInfo.headRepository.name
|
||||||
|
const remoteName = forkOwner
|
||||||
|
|
||||||
|
const remotes = (yield* git.run(["remote"], { cwd: worktree })).text().trim()
|
||||||
|
if (!remotes.split("\n").includes(remoteName)) {
|
||||||
|
yield* git.run(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], { cwd: worktree })
|
||||||
|
UI.println(`Added fork remote: ${remoteName}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const prNumber = args.number
|
yield* git.run(
|
||||||
const localBranchName = `pr/${prNumber}`
|
["branch", `--set-upstream-to=${remoteName}/${prInfo.headRefName}`, localBranchName],
|
||||||
UI.println(`Fetching and checking out PR #${prNumber}...`)
|
{ cwd: worktree },
|
||||||
|
|
||||||
// Use gh pr checkout with custom branch name
|
|
||||||
const result = await Process.run(
|
|
||||||
["gh", "pr", "checkout", `${prNumber}`, "--branch", localBranchName, "--force"],
|
|
||||||
{
|
|
||||||
nothrow: true,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (result.code !== 0) {
|
if (prInfo?.body) {
|
||||||
UI.error(`Failed to checkout PR #${prNumber}. Make sure you have gh CLI installed and authenticated.`)
|
const sessionMatch = prInfo.body.match(/https:\/\/opncd\.ai\/s\/([a-zA-Z0-9_-]+)/)
|
||||||
process.exit(1)
|
if (sessionMatch) {
|
||||||
}
|
const sessionUrl = sessionMatch[0]
|
||||||
|
UI.println(`Found opencode session: ${sessionUrl}`)
|
||||||
|
UI.println(`Importing session...`)
|
||||||
|
|
||||||
// Fetch PR info for fork handling and session link detection
|
const importResult = yield* Effect.promise(() => Process.text(["opencode", "import", sessionUrl], { nothrow: true }))
|
||||||
const prInfoResult = await Process.text(
|
if (importResult.code === 0) {
|
||||||
[
|
const sessionIdMatch = importResult.text.trim().match(/Imported session: ([a-zA-Z0-9_-]+)/)
|
||||||
"gh",
|
if (sessionIdMatch) {
|
||||||
"pr",
|
sessionId = sessionIdMatch[1]
|
||||||
"view",
|
UI.println(`Session imported: ${sessionId}`)
|
||||||
`${prNumber}`,
|
|
||||||
"--json",
|
|
||||||
"headRepository,headRepositoryOwner,isCrossRepository,headRefName,body",
|
|
||||||
],
|
|
||||||
{ nothrow: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
let sessionId: string | undefined
|
|
||||||
|
|
||||||
if (prInfoResult.code === 0) {
|
|
||||||
const prInfoText = prInfoResult.text
|
|
||||||
if (prInfoText.trim()) {
|
|
||||||
const prInfo = JSON.parse(prInfoText)
|
|
||||||
|
|
||||||
// Handle fork PRs
|
|
||||||
if (prInfo && prInfo.isCrossRepository && prInfo.headRepository && prInfo.headRepositoryOwner) {
|
|
||||||
const forkOwner = prInfo.headRepositoryOwner.login
|
|
||||||
const forkName = prInfo.headRepository.name
|
|
||||||
const remoteName = forkOwner
|
|
||||||
|
|
||||||
// Check if remote already exists
|
|
||||||
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)) {
|
|
||||||
await AppRuntime.runPromise(
|
|
||||||
Git.Service.use((git) =>
|
|
||||||
git.run(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
|
|
||||||
cwd: Instance.worktree,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
UI.println(`Added fork remote: ${remoteName}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set upstream to the fork so pushes go there
|
|
||||||
const headRefName = prInfo.headRefName
|
|
||||||
await AppRuntime.runPromise(
|
|
||||||
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 && prInfo.body) {
|
|
||||||
const sessionMatch = prInfo.body.match(/https:\/\/opncd\.ai\/s\/([a-zA-Z0-9_-]+)/)
|
|
||||||
if (sessionMatch) {
|
|
||||||
const sessionUrl = sessionMatch[0]
|
|
||||||
UI.println(`Found opencode session: ${sessionUrl}`)
|
|
||||||
UI.println(`Importing session...`)
|
|
||||||
|
|
||||||
const importResult = await Process.text(["opencode", "import", sessionUrl], {
|
|
||||||
nothrow: true,
|
|
||||||
})
|
|
||||||
if (importResult.code === 0) {
|
|
||||||
const importOutput = importResult.text.trim()
|
|
||||||
// Extract session ID from the output (format: "Imported session: <session-id>")
|
|
||||||
const sessionIdMatch = importOutput.match(/Imported session: ([a-zA-Z0-9_-]+)/)
|
|
||||||
if (sessionIdMatch) {
|
|
||||||
sessionId = sessionIdMatch[1]
|
|
||||||
UI.println(`Session imported: ${sessionId}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
UI.println(`Successfully checked out PR #${prNumber} as branch '${localBranchName}'`)
|
UI.println(`Successfully checked out PR #${prNumber} as branch '${localBranchName}'`)
|
||||||
UI.println()
|
UI.println()
|
||||||
UI.println("Starting opencode...")
|
UI.println("Starting opencode...")
|
||||||
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(() =>
|
||||||
stdin: "inherit",
|
Process.spawn(["opencode", ...opencodeArgs], {
|
||||||
stdout: "inherit",
|
stdin: "inherit",
|
||||||
stderr: "inherit",
|
stdout: "inherit",
|
||||||
cwd: process.cwd(),
|
stderr: "inherit",
|
||||||
})
|
cwd: process.cwd(),
|
||||||
const code = await opencodeProcess.exited
|
}).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}`))
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user