Skip to content

Commit 7073a46

Browse files
Apply PR #16286: refactor(opencode): replace Bun shell in core flows
2 parents eb6cebc + add16af commit 7073a46

15 files changed

Lines changed: 612 additions & 346 deletions

File tree

packages/opencode/src/cli/cmd/github.ts

Lines changed: 56 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ import { Provider } from "../../provider/provider"
2727
import { Bus } from "../../bus"
2828
import { MessageV2 } from "../../session/message-v2"
2929
import { SessionPrompt } from "@/session/prompt"
30-
import { $ } from "bun"
30+
import { Process } from "@/util/process"
31+
import { git } from "@/util/git"
3132

3233
type GitHubAuthor = {
3334
login: string
@@ -254,7 +255,7 @@ export const GithubInstallCommand = cmd({
254255
}
255256

256257
// Get repo info
257-
const info = (await $`git remote get-url origin`.quiet().nothrow().text()).trim()
258+
const info = (await git(["remote", "get-url", "origin"], { cwd: Instance.worktree })).text().trim()
258259
const parsed = parseGitHubRemote(info)
259260
if (!parsed) {
260261
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
@@ -492,6 +493,26 @@ export const GithubRunCommand = cmd({
492493
? "pr_review"
493494
: "issue"
494495
: undefined
496+
const gitText = async (args: string[]) => {
497+
const result = await git(args, { cwd: Instance.worktree })
498+
if (result.exitCode !== 0) {
499+
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
500+
}
501+
return result.text().trim()
502+
}
503+
const gitRun = async (args: string[]) => {
504+
const result = await git(args, { cwd: Instance.worktree })
505+
if (result.exitCode !== 0) {
506+
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
507+
}
508+
return result
509+
}
510+
const gitStatus = (args: string[]) => git(args, { cwd: Instance.worktree })
511+
const commitChanges = async (summary: string, actor?: string) => {
512+
const args = ["commit", "-m", summary]
513+
if (actor) args.push("-m", `Co-authored-by: ${actor} <${actor}@users.noreply.github.com>`)
514+
await gitRun(args)
515+
}
495516

496517
try {
497518
if (useGithubToken) {
@@ -552,7 +573,7 @@ export const GithubRunCommand = cmd({
552573
}
553574
const branchPrefix = isWorkflowDispatchEvent ? "dispatch" : "schedule"
554575
const branch = await checkoutNewBranch(branchPrefix)
555-
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
576+
const head = await gitText(["rev-parse", "HEAD"])
556577
const response = await chat(userPrompt, promptFiles)
557578
const { dirty, uncommittedChanges, switched } = await branchIsDirty(head, branch)
558579
if (switched) {
@@ -586,7 +607,7 @@ export const GithubRunCommand = cmd({
586607
// Local PR
587608
if (prData.headRepository.nameWithOwner === prData.baseRepository.nameWithOwner) {
588609
await checkoutLocalBranch(prData)
589-
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
610+
const head = await gitText(["rev-parse", "HEAD"])
590611
const dataPrompt = buildPromptDataForPR(prData)
591612
const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
592613
const { dirty, uncommittedChanges, switched } = await branchIsDirty(head, prData.headRefName)
@@ -604,7 +625,7 @@ export const GithubRunCommand = cmd({
604625
// Fork PR
605626
else {
606627
const forkBranch = await checkoutForkBranch(prData)
607-
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
628+
const head = await gitText(["rev-parse", "HEAD"])
608629
const dataPrompt = buildPromptDataForPR(prData)
609630
const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
610631
const { dirty, uncommittedChanges, switched } = await branchIsDirty(head, forkBranch)
@@ -623,7 +644,7 @@ export const GithubRunCommand = cmd({
623644
// Issue
624645
else {
625646
const branch = await checkoutNewBranch("issue")
626-
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
647+
const head = await gitText(["rev-parse", "HEAD"])
627648
const issueData = await fetchIssue()
628649
const dataPrompt = buildPromptDataForIssue(issueData)
629650
const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
@@ -657,7 +678,7 @@ export const GithubRunCommand = cmd({
657678
exitCode = 1
658679
console.error(e instanceof Error ? e.message : String(e))
659680
let msg = e
660-
if (e instanceof $.ShellError) {
681+
if (e instanceof Process.RunFailedError) {
661682
msg = e.stderr.toString()
662683
} else if (e instanceof Error) {
663684
msg = e.message
@@ -1048,29 +1069,29 @@ export const GithubRunCommand = cmd({
10481069
const config = "http.https://github.com/.extraheader"
10491070
// actions/checkout@v6 no longer stores credentials in .git/config,
10501071
// so this may not exist - use nothrow() to handle gracefully
1051-
const ret = await $`git config --local --get ${config}`.nothrow()
1072+
const ret = await gitStatus(["config", "--local", "--get", config])
10521073
if (ret.exitCode === 0) {
10531074
gitConfig = ret.stdout.toString().trim()
1054-
await $`git config --local --unset-all ${config}`
1075+
await gitRun(["config", "--local", "--unset-all", config])
10551076
}
10561077

10571078
const newCredentials = Buffer.from(`x-access-token:${appToken}`, "utf8").toString("base64")
10581079

1059-
await $`git config --local ${config} "AUTHORIZATION: basic ${newCredentials}"`
1060-
await $`git config --global user.name "${AGENT_USERNAME}"`
1061-
await $`git config --global user.email "${AGENT_USERNAME}@users.noreply.github.com"`
1080+
await gitRun(["config", "--local", config, `AUTHORIZATION: basic ${newCredentials}`])
1081+
await gitRun(["config", "--global", "user.name", AGENT_USERNAME])
1082+
await gitRun(["config", "--global", "user.email", `${AGENT_USERNAME}@users.noreply.github.com`])
10621083
}
10631084

10641085
async function restoreGitConfig() {
10651086
if (gitConfig === undefined) return
10661087
const config = "http.https://github.com/.extraheader"
1067-
await $`git config --local ${config} "${gitConfig}"`
1088+
await gitRun(["config", "--local", config, gitConfig])
10681089
}
10691090

10701091
async function checkoutNewBranch(type: "issue" | "schedule" | "dispatch") {
10711092
console.log("Checking out new branch...")
10721093
const branch = generateBranchName(type)
1073-
await $`git checkout -b ${branch}`
1094+
await gitRun(["checkout", "-b", branch])
10741095
return branch
10751096
}
10761097

@@ -1080,8 +1101,8 @@ export const GithubRunCommand = cmd({
10801101
const branch = pr.headRefName
10811102
const depth = Math.max(pr.commits.totalCount, 20)
10821103

1083-
await $`git fetch origin --depth=${depth} ${branch}`
1084-
await $`git checkout ${branch}`
1104+
await gitRun(["fetch", "origin", `--depth=${depth}`, branch])
1105+
await gitRun(["checkout", branch])
10851106
}
10861107

10871108
async function checkoutForkBranch(pr: GitHubPullRequest) {
@@ -1091,9 +1112,9 @@ export const GithubRunCommand = cmd({
10911112
const localBranch = generateBranchName("pr")
10921113
const depth = Math.max(pr.commits.totalCount, 20)
10931114

1094-
await $`git remote add fork https://github.com/${pr.headRepository.nameWithOwner}.git`
1095-
await $`git fetch fork --depth=${depth} ${remoteBranch}`
1096-
await $`git checkout -b ${localBranch} fork/${remoteBranch}`
1115+
await gitRun(["remote", "add", "fork", `https://github.com/${pr.headRepository.nameWithOwner}.git`])
1116+
await gitRun(["fetch", "fork", `--depth=${depth}`, remoteBranch])
1117+
await gitRun(["checkout", "-b", localBranch, `fork/${remoteBranch}`])
10971118
return localBranch
10981119
}
10991120

@@ -1114,28 +1135,23 @@ export const GithubRunCommand = cmd({
11141135
async function pushToNewBranch(summary: string, branch: string, commit: boolean, isSchedule: boolean) {
11151136
console.log("Pushing to new branch...")
11161137
if (commit) {
1117-
await $`git add .`
1138+
await gitRun(["add", "."])
11181139
if (isSchedule) {
1119-
// No co-author for scheduled events - the schedule is operating as the repo
1120-
await $`git commit -m "${summary}"`
1140+
await commitChanges(summary)
11211141
} else {
1122-
await $`git commit -m "${summary}
1123-
1124-
Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
1142+
await commitChanges(summary, actor)
11251143
}
11261144
}
1127-
await $`git push -u origin ${branch}`
1145+
await gitRun(["push", "-u", "origin", branch])
11281146
}
11291147

11301148
async function pushToLocalBranch(summary: string, commit: boolean) {
11311149
console.log("Pushing to local branch...")
11321150
if (commit) {
1133-
await $`git add .`
1134-
await $`git commit -m "${summary}
1135-
1136-
Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
1151+
await gitRun(["add", "."])
1152+
await commitChanges(summary, actor)
11371153
}
1138-
await $`git push`
1154+
await gitRun(["push"])
11391155
}
11401156

11411157
async function pushToForkBranch(summary: string, pr: GitHubPullRequest, commit: boolean) {
@@ -1144,30 +1160,28 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
11441160
const remoteBranch = pr.headRefName
11451161

11461162
if (commit) {
1147-
await $`git add .`
1148-
await $`git commit -m "${summary}
1149-
1150-
Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
1163+
await gitRun(["add", "."])
1164+
await commitChanges(summary, actor)
11511165
}
1152-
await $`git push fork HEAD:${remoteBranch}`
1166+
await gitRun(["push", "fork", `HEAD:${remoteBranch}`])
11531167
}
11541168

11551169
async function branchIsDirty(originalHead: string, expectedBranch: string) {
11561170
console.log("Checking if branch is dirty...")
11571171
// Detect if the agent switched branches during chat (e.g. created
11581172
// its own branch, committed, and possibly pushed/created a PR).
1159-
const current = (await $`git rev-parse --abbrev-ref HEAD`).stdout.toString().trim()
1173+
const current = await gitText(["rev-parse", "--abbrev-ref", "HEAD"])
11601174
if (current !== expectedBranch) {
11611175
console.log(`Branch changed during chat: expected ${expectedBranch}, now on ${current}`)
11621176
return { dirty: true, uncommittedChanges: false, switched: true }
11631177
}
11641178

1165-
const ret = await $`git status --porcelain`
1179+
const ret = await gitStatus(["status", "--porcelain"])
11661180
const status = ret.stdout.toString().trim()
11671181
if (status.length > 0) {
11681182
return { dirty: true, uncommittedChanges: true, switched: false }
11691183
}
1170-
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
1184+
const head = await gitText(["rev-parse", "HEAD"])
11711185
return {
11721186
dirty: head !== originalHead,
11731187
uncommittedChanges: false,
@@ -1179,11 +1193,11 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
11791193
// Falls back to fetching from origin when local refs are missing
11801194
// (common in shallow clones from actions/checkout).
11811195
async function hasNewCommits(base: string, head: string) {
1182-
const result = await $`git rev-list --count ${base}..${head}`.nothrow()
1196+
const result = await gitStatus(["rev-list", "--count", `${base}..${head}`])
11831197
if (result.exitCode !== 0) {
11841198
console.log(`rev-list failed, fetching origin/${base}...`)
1185-
await $`git fetch origin ${base} --depth=1`.nothrow()
1186-
const retry = await $`git rev-list --count origin/${base}..${head}`.nothrow()
1199+
await gitStatus(["fetch", "origin", base, "--depth=1"])
1200+
const retry = await gitStatus(["rev-list", "--count", `origin/${base}..${head}`])
11871201
if (retry.exitCode !== 0) return true // assume dirty if we can't tell
11881202
return parseInt(retry.stdout.toString().trim()) > 0
11891203
}

packages/opencode/src/cli/cmd/pr.ts

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { UI } from "../ui"
22
import { cmd } from "./cmd"
33
import { Instance } from "@/project/instance"
4-
import { $ } from "bun"
4+
import { Process } from "@/util/process"
5+
import { git } from "@/util/git"
56

67
export const PrCommand = cmd({
78
command: "pr <number>",
@@ -27,21 +28,35 @@ export const PrCommand = cmd({
2728
UI.println(`Fetching and checking out PR #${prNumber}...`)
2829

2930
// Use gh pr checkout with custom branch name
30-
const result = await $`gh pr checkout ${prNumber} --branch ${localBranchName} --force`.nothrow()
31+
const result = await Process.run(
32+
["gh", "pr", "checkout", `${prNumber}`, "--branch", localBranchName, "--force"],
33+
{
34+
nothrow: true,
35+
},
36+
)
3137

32-
if (result.exitCode !== 0) {
38+
if (result.code !== 0) {
3339
UI.error(`Failed to checkout PR #${prNumber}. Make sure you have gh CLI installed and authenticated.`)
3440
process.exit(1)
3541
}
3642

3743
// Fetch PR info for fork handling and session link detection
38-
const prInfoResult =
39-
await $`gh pr view ${prNumber} --json headRepository,headRepositoryOwner,isCrossRepository,headRefName,body`.nothrow()
44+
const prInfoResult = await Process.text(
45+
[
46+
"gh",
47+
"pr",
48+
"view",
49+
`${prNumber}`,
50+
"--json",
51+
"headRepository,headRepositoryOwner,isCrossRepository,headRefName,body",
52+
],
53+
{ nothrow: true },
54+
)
4055

4156
let sessionId: string | undefined
4257

43-
if (prInfoResult.exitCode === 0) {
44-
const prInfoText = prInfoResult.text()
58+
if (prInfoResult.code === 0) {
59+
const prInfoText = prInfoResult.text
4560
if (prInfoText.trim()) {
4661
const prInfo = JSON.parse(prInfoText)
4762

@@ -52,15 +67,19 @@ export const PrCommand = cmd({
5267
const remoteName = forkOwner
5368

5469
// Check if remote already exists
55-
const remotes = (await $`git remote`.nothrow().text()).trim()
70+
const remotes = (await git(["remote"], { cwd: Instance.worktree })).text().trim()
5671
if (!remotes.split("\n").includes(remoteName)) {
57-
await $`git remote add ${remoteName} https://github.com/${forkOwner}/${forkName}.git`.nothrow()
72+
await git(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
73+
cwd: Instance.worktree,
74+
})
5875
UI.println(`Added fork remote: ${remoteName}`)
5976
}
6077

6178
// Set upstream to the fork so pushes go there
6279
const headRefName = prInfo.headRefName
63-
await $`git branch --set-upstream-to=${remoteName}/${headRefName} ${localBranchName}`.nothrow()
80+
await git(["branch", `--set-upstream-to=${remoteName}/${headRefName}`, localBranchName], {
81+
cwd: Instance.worktree,
82+
})
6483
}
6584

6685
// Check for opencode session link in PR body
@@ -71,9 +90,11 @@ export const PrCommand = cmd({
7190
UI.println(`Found opencode session: ${sessionUrl}`)
7291
UI.println(`Importing session...`)
7392

74-
const importResult = await $`opencode import ${sessionUrl}`.nothrow()
75-
if (importResult.exitCode === 0) {
76-
const importOutput = importResult.text().trim()
93+
const importResult = await Process.text(["opencode", "import", sessionUrl], {
94+
nothrow: true,
95+
})
96+
if (importResult.code === 0) {
97+
const importOutput = importResult.text.trim()
7798
// Extract session ID from the output (format: "Imported session: <session-id>")
7899
const sessionIdMatch = importOutput.match(/Imported session: ([a-zA-Z0-9_-]+)/)
79100
if (sessionIdMatch) {

0 commit comments

Comments
 (0)