@@ -90,6 +90,45 @@ export const parseGitHubUrl = (url: string): GitHubRepo | null => {
9090 } ;
9191} ;
9292
93+ const getRepositoryFromRemoteUrl = async (
94+ directoryPath : string ,
95+ ) : Promise < string > => {
96+ const remoteUrl = await getRemoteUrl ( directoryPath ) ;
97+ if ( ! remoteUrl ) {
98+ throw new Error ( "No remote URL found" ) ;
99+ }
100+
101+ // Parse repo from URL (handles both HTTPS and SSH formats)
102+ const repoMatch = remoteUrl . match (
103+ / g i t h u b \. c o m [: / ] ( [ ^ / ] + \/ [ ^ / ] + ?) (?: \. g i t ) ? $ / ,
104+ ) ;
105+ if ( ! repoMatch ) {
106+ throw new Error ( `Cannot parse repository from URL: ${ remoteUrl } ` ) ;
107+ }
108+
109+ return repoMatch [ 1 ] ;
110+ } ;
111+
112+ const validatePullRequestNumber = ( prNumber : number ) : void => {
113+ if (
114+ typeof prNumber !== "number" ||
115+ ! Number . isInteger ( prNumber ) ||
116+ prNumber < 1
117+ ) {
118+ throw new Error ( `Invalid pull request number: ${ prNumber } ` ) ;
119+ }
120+ } ;
121+
122+ const validateCommentId = ( commentId : number ) : void => {
123+ if (
124+ typeof commentId !== "number" ||
125+ ! Number . isInteger ( commentId ) ||
126+ commentId < 1
127+ ) {
128+ throw new Error ( `Invalid comment ID: ${ commentId } ` ) ;
129+ }
130+ } ;
131+
93132export const isGitRepository = async (
94133 directoryPath : string ,
95134) : Promise < boolean > => {
@@ -517,6 +556,104 @@ export const detectSSHError = (output: string): string | undefined => {
517556 return `SSH test failed: ${ output . substring ( 0 , 200 ) } ` ;
518557} ;
519558
559+ const getAllPullRequestComments = async (
560+ directoryPath : string ,
561+ prNumber : number ,
562+ ) : Promise < any > => {
563+ validatePullRequestNumber ( prNumber ) ;
564+
565+ try {
566+ const { stdout } = await execAsync (
567+ `gh pr view ${ prNumber } --json comments` ,
568+ { cwd : directoryPath } ,
569+ ) ;
570+ return JSON . parse ( stdout ) ;
571+ } catch ( error ) {
572+ throw new Error ( `Failed to fetch PR comments: ${ error } ` ) ;
573+ }
574+ } ;
575+
576+ const getPullRequestReviewComments = async (
577+ directoryPath : string ,
578+ prNumber : number ,
579+ ) : Promise < any > => {
580+ validatePullRequestNumber ( prNumber ) ;
581+
582+ try {
583+ const repo = await getRepositoryFromRemoteUrl ( directoryPath ) ;
584+
585+ // TODO: Paginate if many comments
586+ const { stdout } = await execAsync (
587+ `gh api repos/${ repo } /pulls/${ prNumber } /comments` ,
588+ { cwd : directoryPath } ,
589+ ) ;
590+ return JSON . parse ( stdout ) ;
591+ } catch ( error ) {
592+ throw new Error ( `Failed to fetch PR review comments: ${ error } ` ) ;
593+ }
594+ } ;
595+
596+ interface AddPullRequestCommentOptions {
597+ body : string ;
598+ commitId : string ;
599+ path : string ;
600+ line : number ;
601+ side ?: "LEFT" | "RIGHT" ;
602+ }
603+
604+ const addPullRequestComment = async (
605+ directoryPath : string ,
606+ prNumber : number ,
607+ options : AddPullRequestCommentOptions ,
608+ ) : Promise < any > => {
609+ validatePullRequestNumber ( prNumber ) ;
610+
611+ // Validate required options
612+ if ( ! options . body || ! options . commitId || ! options . path ) {
613+ throw new Error ( "body, commitId, and path are required" ) ;
614+ }
615+
616+ if ( typeof options . line !== "number" || options . line < 1 ) {
617+ throw new Error ( "line must be a positive number" ) ;
618+ }
619+
620+ try {
621+ const repo = await getRepositoryFromRemoteUrl ( directoryPath ) ;
622+ const side = options . side || "RIGHT" ;
623+
624+ const { stdout } = await execAsync (
625+ `gh api repos/${ repo } /pulls/${ prNumber } /comments ` +
626+ `-f body="${ options . body . replace ( / " / g, '\\"' ) } " ` +
627+ `-f commit_id="${ options . commitId } " ` +
628+ `-f path="${ options . path } " ` +
629+ `-f line=${ options . line } ` +
630+ `-f side="${ side } "` ,
631+ { cwd : directoryPath } ,
632+ ) ;
633+ return JSON . parse ( stdout ) ;
634+ } catch ( error ) {
635+ throw new Error ( `Failed to add PR comment: ${ error } ` ) ;
636+ }
637+ } ;
638+
639+ const deletePullRequestComment = async (
640+ directoryPath : string ,
641+ commentId : number ,
642+ ) : Promise < void > => {
643+ validateCommentId ( commentId ) ;
644+
645+ try {
646+ const repo = await getRepositoryFromRemoteUrl ( directoryPath ) ;
647+
648+ await execAsync (
649+ `gh api repos/${ repo } /pulls/comments/${ commentId } -X DELETE` ,
650+ { cwd : directoryPath } ,
651+ ) ;
652+ } catch ( error ) {
653+ throw new Error ( `Failed to delete PR comment: ${ error } ` ) ;
654+ }
655+ } ;
656+
520657export function registerGitIpc (
521658 getMainWindow : ( ) => BrowserWindow | null ,
522659) : void {
@@ -796,4 +933,49 @@ export function registerGitIpc(
796933 return discardFileChanges ( directoryPath , filePath , fileStatus ) ;
797934 } ,
798935 ) ;
936+
937+ ipcMain . handle (
938+ "get-pr-comments" ,
939+ async (
940+ _event : IpcMainInvokeEvent ,
941+ directoryPath : string ,
942+ prNumber : number ,
943+ ) : Promise < any > => {
944+ return getAllPullRequestComments ( directoryPath , prNumber ) ;
945+ } ,
946+ ) ;
947+
948+ ipcMain . handle (
949+ "get-pr-review-comments" ,
950+ async (
951+ _event : IpcMainInvokeEvent ,
952+ directoryPath : string ,
953+ prNumber : number ,
954+ ) : Promise < any > => {
955+ return getPullRequestReviewComments ( directoryPath , prNumber ) ;
956+ } ,
957+ ) ;
958+
959+ ipcMain . handle (
960+ "add-pr-comment" ,
961+ async (
962+ _event : IpcMainInvokeEvent ,
963+ directoryPath : string ,
964+ prNumber : number ,
965+ options : AddPullRequestCommentOptions ,
966+ ) : Promise < any > => {
967+ return addPullRequestComment ( directoryPath , prNumber , options ) ;
968+ } ,
969+ ) ;
970+
971+ ipcMain . handle (
972+ "delete-pr-comment" ,
973+ async (
974+ _event : IpcMainInvokeEvent ,
975+ directoryPath : string ,
976+ commentId : number ,
977+ ) : Promise < void > => {
978+ return deletePullRequestComment ( directoryPath , commentId ) ;
979+ } ,
980+ ) ;
799981}
0 commit comments