@@ -27,8 +27,9 @@ import { Provider } from "../../provider/provider"
2727import { Bus } from "../../bus"
2828import { MessageV2 } from "../../session/message-v2"
2929import { SessionPrompt } from "@/session/prompt"
30- import { $ } from "bun"
3130import { setTimeout as sleep } from "node:timers/promises"
31+ import { Process } from "@/util/process"
32+ import { git } from "@/util/git"
3233
3334type GitHubAuthor = {
3435 login : string
@@ -255,7 +256,7 @@ export const GithubInstallCommand = cmd({
255256 }
256257
257258 // Get repo info
258- const info = ( await $ ` git remote get-url origin` . quiet ( ) . nothrow ( ) . text ( ) ) . trim ( )
259+ const info = ( await git ( [ " remote" , " get-url" , " origin" ] , { cwd : Instance . worktree } ) ) . text ( ) . trim ( )
259260 const parsed = parseGitHubRemote ( info )
260261 if ( ! parsed ) {
261262 prompts . log . error ( `Could not find git repository. Please run this command from a git repository.` )
@@ -493,6 +494,26 @@ export const GithubRunCommand = cmd({
493494 ? "pr_review"
494495 : "issue"
495496 : undefined
497+ const gitText = async ( args : string [ ] ) => {
498+ const result = await git ( args , { cwd : Instance . worktree } )
499+ if ( result . exitCode !== 0 ) {
500+ throw new Process . RunFailedError ( [ "git" , ...args ] , result . exitCode , result . stdout , result . stderr )
501+ }
502+ return result . text ( ) . trim ( )
503+ }
504+ const gitRun = async ( args : string [ ] ) => {
505+ const result = await git ( args , { cwd : Instance . worktree } )
506+ if ( result . exitCode !== 0 ) {
507+ throw new Process . RunFailedError ( [ "git" , ...args ] , result . exitCode , result . stdout , result . stderr )
508+ }
509+ return result
510+ }
511+ const gitStatus = ( args : string [ ] ) => git ( args , { cwd : Instance . worktree } )
512+ const commitChanges = async ( summary : string , actor ?: string ) => {
513+ const args = [ "commit" , "-m" , summary ]
514+ if ( actor ) args . push ( "-m" , `Co-authored-by: ${ actor } <${ actor } @users.noreply.github.com>` )
515+ await gitRun ( args )
516+ }
496517
497518 try {
498519 if ( useGithubToken ) {
@@ -553,7 +574,7 @@ export const GithubRunCommand = cmd({
553574 }
554575 const branchPrefix = isWorkflowDispatchEvent ? "dispatch" : "schedule"
555576 const branch = await checkoutNewBranch ( branchPrefix )
556- const head = ( await $ `git rev-parse HEAD` ) . stdout . toString ( ) . trim ( )
577+ const head = await gitText ( [ " rev-parse" , " HEAD" ] )
557578 const response = await chat ( userPrompt , promptFiles )
558579 const { dirty, uncommittedChanges, switched } = await branchIsDirty ( head , branch )
559580 if ( switched ) {
@@ -587,7 +608,7 @@ export const GithubRunCommand = cmd({
587608 // Local PR
588609 if ( prData . headRepository . nameWithOwner === prData . baseRepository . nameWithOwner ) {
589610 await checkoutLocalBranch ( prData )
590- const head = ( await $ `git rev-parse HEAD` ) . stdout . toString ( ) . trim ( )
611+ const head = await gitText ( [ " rev-parse" , " HEAD" ] )
591612 const dataPrompt = buildPromptDataForPR ( prData )
592613 const response = await chat ( `${ userPrompt } \n\n${ dataPrompt } ` , promptFiles )
593614 const { dirty, uncommittedChanges, switched } = await branchIsDirty ( head , prData . headRefName )
@@ -605,7 +626,7 @@ export const GithubRunCommand = cmd({
605626 // Fork PR
606627 else {
607628 const forkBranch = await checkoutForkBranch ( prData )
608- const head = ( await $ `git rev-parse HEAD` ) . stdout . toString ( ) . trim ( )
629+ const head = await gitText ( [ " rev-parse" , " HEAD" ] )
609630 const dataPrompt = buildPromptDataForPR ( prData )
610631 const response = await chat ( `${ userPrompt } \n\n${ dataPrompt } ` , promptFiles )
611632 const { dirty, uncommittedChanges, switched } = await branchIsDirty ( head , forkBranch )
@@ -624,7 +645,7 @@ export const GithubRunCommand = cmd({
624645 // Issue
625646 else {
626647 const branch = await checkoutNewBranch ( "issue" )
627- const head = ( await $ `git rev-parse HEAD` ) . stdout . toString ( ) . trim ( )
648+ const head = await gitText ( [ " rev-parse" , " HEAD" ] )
628649 const issueData = await fetchIssue ( )
629650 const dataPrompt = buildPromptDataForIssue ( issueData )
630651 const response = await chat ( `${ userPrompt } \n\n${ dataPrompt } ` , promptFiles )
@@ -658,7 +679,7 @@ export const GithubRunCommand = cmd({
658679 exitCode = 1
659680 console . error ( e instanceof Error ? e . message : String ( e ) )
660681 let msg = e
661- if ( e instanceof $ . ShellError ) {
682+ if ( e instanceof Process . RunFailedError ) {
662683 msg = e . stderr . toString ( )
663684 } else if ( e instanceof Error ) {
664685 msg = e . message
@@ -1049,29 +1070,29 @@ export const GithubRunCommand = cmd({
10491070 const config = "http.https://github.com/.extraheader"
10501071 // actions/checkout@v 6 no longer stores credentials in .git/config,
10511072 // so this may not exist - use nothrow() to handle gracefully
1052- const ret = await $ `git config --local --get ${ config } ` . nothrow ( )
1073+ const ret = await gitStatus ( [ " config" , " --local" , " --get" , config ] )
10531074 if ( ret . exitCode === 0 ) {
10541075 gitConfig = ret . stdout . toString ( ) . trim ( )
1055- await $ `git config --local --unset-all ${ config } `
1076+ await gitRun ( [ " config" , " --local" , " --unset-all" , config ] )
10561077 }
10571078
10581079 const newCredentials = Buffer . from ( `x-access-token:${ appToken } ` , "utf8" ) . toString ( "base64" )
10591080
1060- await $ `git config --local ${ config } " AUTHORIZATION: basic ${ newCredentials } "`
1061- await $ `git config --global user.name " ${ AGENT_USERNAME } "`
1062- await $ `git config --global user.email " ${ AGENT_USERNAME } @users.noreply.github.com"`
1081+ await gitRun ( [ " config" , " --local" , config , ` AUTHORIZATION: basic ${ newCredentials } ` ] )
1082+ await gitRun ( [ " config" , " --global" , " user.name" , AGENT_USERNAME ] )
1083+ await gitRun ( [ " config" , " --global" , " user.email" , ` ${ AGENT_USERNAME } @users.noreply.github.com` ] )
10631084 }
10641085
10651086 async function restoreGitConfig ( ) {
10661087 if ( gitConfig === undefined ) return
10671088 const config = "http.https://github.com/.extraheader"
1068- await $ `git config --local ${ config } " ${ gitConfig } "`
1089+ await gitRun ( [ " config" , " --local" , config , gitConfig ] )
10691090 }
10701091
10711092 async function checkoutNewBranch ( type : "issue" | "schedule" | "dispatch" ) {
10721093 console . log ( "Checking out new branch..." )
10731094 const branch = generateBranchName ( type )
1074- await $ `git checkout -b ${ branch } `
1095+ await gitRun ( [ " checkout" , "-b" , branch ] )
10751096 return branch
10761097 }
10771098
@@ -1081,8 +1102,8 @@ export const GithubRunCommand = cmd({
10811102 const branch = pr . headRefName
10821103 const depth = Math . max ( pr . commits . totalCount , 20 )
10831104
1084- await $ `git fetch origin --depth=${ depth } ${ branch } `
1085- await $ `git checkout ${ branch } `
1105+ await gitRun ( [ " fetch" , " origin" , ` --depth=${ depth } ` , branch ] )
1106+ await gitRun ( [ " checkout" , branch ] )
10861107 }
10871108
10881109 async function checkoutForkBranch ( pr : GitHubPullRequest ) {
@@ -1092,9 +1113,9 @@ export const GithubRunCommand = cmd({
10921113 const localBranch = generateBranchName ( "pr" )
10931114 const depth = Math . max ( pr . commits . totalCount , 20 )
10941115
1095- await $ `git remote add fork https://github.com/${ pr . headRepository . nameWithOwner } .git`
1096- await $ `git fetch fork --depth=${ depth } ${ remoteBranch } `
1097- await $ `git checkout -b ${ localBranch } fork/${ remoteBranch } `
1116+ await gitRun ( [ " remote" , " add" , " fork" , ` https://github.com/${ pr . headRepository . nameWithOwner } .git`] )
1117+ await gitRun ( [ " fetch" , " fork" , ` --depth=${ depth } ` , remoteBranch ] )
1118+ await gitRun ( [ " checkout" , "-b" , localBranch , ` fork/${ remoteBranch } `] )
10981119 return localBranch
10991120 }
11001121
@@ -1115,28 +1136,23 @@ export const GithubRunCommand = cmd({
11151136 async function pushToNewBranch ( summary : string , branch : string , commit : boolean , isSchedule : boolean ) {
11161137 console . log ( "Pushing to new branch..." )
11171138 if ( commit ) {
1118- await $ `git add .`
1139+ await gitRun ( [ " add" , "." ] )
11191140 if ( isSchedule ) {
1120- // No co-author for scheduled events - the schedule is operating as the repo
1121- await $ `git commit -m "${ summary } "`
1141+ await commitChanges ( summary )
11221142 } else {
1123- await $ `git commit -m "${ summary }
1124-
1125- Co-authored-by: ${ actor } <${ actor } @users.noreply.github.com>"`
1143+ await commitChanges ( summary , actor )
11261144 }
11271145 }
1128- await $ `git push -u origin ${ branch } `
1146+ await gitRun ( [ " push" , "-u" , " origin" , branch ] )
11291147 }
11301148
11311149 async function pushToLocalBranch ( summary : string , commit : boolean ) {
11321150 console . log ( "Pushing to local branch..." )
11331151 if ( commit ) {
1134- await $ `git add .`
1135- await $ `git commit -m "${ summary }
1136-
1137- Co-authored-by: ${ actor } <${ actor } @users.noreply.github.com>"`
1152+ await gitRun ( [ "add" , "." ] )
1153+ await commitChanges ( summary , actor )
11381154 }
1139- await $ `git push`
1155+ await gitRun ( [ " push" ] )
11401156 }
11411157
11421158 async function pushToForkBranch ( summary : string , pr : GitHubPullRequest , commit : boolean ) {
@@ -1145,30 +1161,28 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
11451161 const remoteBranch = pr . headRefName
11461162
11471163 if ( commit ) {
1148- await $ `git add .`
1149- await $ `git commit -m "${ summary }
1150-
1151- Co-authored-by: ${ actor } <${ actor } @users.noreply.github.com>"`
1164+ await gitRun ( [ "add" , "." ] )
1165+ await commitChanges ( summary , actor )
11521166 }
1153- await $ `git push fork HEAD:${ remoteBranch } `
1167+ await gitRun ( [ " push" , " fork" , ` HEAD:${ remoteBranch } `] )
11541168 }
11551169
11561170 async function branchIsDirty ( originalHead : string , expectedBranch : string ) {
11571171 console . log ( "Checking if branch is dirty..." )
11581172 // Detect if the agent switched branches during chat (e.g. created
11591173 // its own branch, committed, and possibly pushed/created a PR).
1160- const current = ( await $ `git rev-parse --abbrev-ref HEAD` ) . stdout . toString ( ) . trim ( )
1174+ const current = await gitText ( [ " rev-parse" , " --abbrev-ref" , " HEAD" ] )
11611175 if ( current !== expectedBranch ) {
11621176 console . log ( `Branch changed during chat: expected ${ expectedBranch } , now on ${ current } ` )
11631177 return { dirty : true , uncommittedChanges : false , switched : true }
11641178 }
11651179
1166- const ret = await $ `git status --porcelain`
1180+ const ret = await gitStatus ( [ " status" , " --porcelain" ] )
11671181 const status = ret . stdout . toString ( ) . trim ( )
11681182 if ( status . length > 0 ) {
11691183 return { dirty : true , uncommittedChanges : true , switched : false }
11701184 }
1171- const head = ( await $ `git rev-parse HEAD` ) . stdout . toString ( ) . trim ( )
1185+ const head = await gitText ( [ " rev-parse" , " HEAD" ] )
11721186 return {
11731187 dirty : head !== originalHead ,
11741188 uncommittedChanges : false ,
@@ -1180,11 +1194,11 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
11801194 // Falls back to fetching from origin when local refs are missing
11811195 // (common in shallow clones from actions/checkout).
11821196 async function hasNewCommits ( base : string , head : string ) {
1183- const result = await $ `git rev-list --count ${ base } ..${ head } `. nothrow ( )
1197+ const result = await gitStatus ( [ " rev-list" , " --count" , ` ${ base } ..${ head } `] )
11841198 if ( result . exitCode !== 0 ) {
11851199 console . log ( `rev-list failed, fetching origin/${ base } ...` )
1186- await $ `git fetch origin ${ base } --depth=1` . nothrow ( )
1187- const retry = await $ `git rev-list --count origin/${ base } ..${ head } `. nothrow ( )
1200+ await gitStatus ( [ " fetch" , " origin" , base , " --depth=1" ] )
1201+ const retry = await gitStatus ( [ " rev-list" , " --count" , ` origin/${ base } ..${ head } `] )
11881202 if ( retry . exitCode !== 0 ) return true // assume dirty if we can't tell
11891203 return parseInt ( retry . stdout . toString ( ) . trim ( ) ) > 0
11901204 }
0 commit comments