@@ -27,7 +27,8 @@ import { Provider } from "../../provider/provider"
2727import { Bus } from "../../bus"
2828import { MessageV2 } from "../../session/message-v2"
2929import { SessionPrompt } from "@/session/prompt"
30- import { $ } from "bun"
30+ import { Process } from "@/util/process"
31+ import { git } from "@/util/git"
3132
3233type 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@v 6 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 }
0 commit comments