@@ -118,48 +118,216 @@ const execCommand = (command, options = {}) => {
118118} ;
119119
120120/**
121- * Get current git branch name
122- * @returns {string|null } Branch name or null
121+ * Execute gh CLI command and return result
122+ * @param {string[] } args - Command arguments
123+ * @returns {{success: boolean, stdout: string, stderr: string} }
123124 */
124- const getCurrentBranch = ( ) => {
125- return execCommand ( "git rev-parse --abbrev-ref HEAD" ) ;
125+ const ghCommand = ( args ) => {
126+ const result = spawnSync ( "gh" , args , {
127+ encoding : "utf8" ,
128+ stdio : [ "pipe" , "pipe" , "pipe" ] ,
129+ } ) ;
130+
131+ return {
132+ success : result . status === 0 ,
133+ stdout : ( result . stdout || "" ) . trim ( ) ,
134+ stderr : ( result . stderr || "" ) . trim ( ) ,
135+ } ;
126136} ;
127137
128138/**
129- * Get repository owner/name from git remote
139+ * Parse a GitHub repository from a remote URL
140+ * @param {string } remoteUrl - Remote URL
130141 * @returns {string|null } Repository in owner/repo format or null
131142 */
132- const getRepoFromRemote = ( ) => {
133- const remoteUrl = execCommand ( "git remote get-url origin" ) ;
134- if ( ! remoteUrl ) return null ;
143+ const parseGitHubRepoFromRemoteUrl = ( remoteUrl ) => {
144+ if ( ! remoteUrl ) {
145+ return null ;
146+ }
135147
136- // Handle SSH format: git@github .com:owner/repo.git
137148 const sshMatch = remoteUrl . match ( / g i t @ g i t h u b \. c o m : ( [ ^ / ] + \/ [ ^ . ] + ) (?: \. g i t ) ? $ / ) ;
138- if ( sshMatch ) return sshMatch [ 1 ] ;
149+ if ( sshMatch ) {
150+ return sshMatch [ 1 ] ;
151+ }
139152
140- // Handle HTTPS format: https://github.com/owner/repo.git
141153 const httpsMatch = remoteUrl . match ( / h t t p s : \/ \/ g i t h u b \. c o m \/ ( [ ^ / ] + \/ [ ^ . ] + ) (?: \. g i t ) ? $ / ) ;
142- if ( httpsMatch ) return httpsMatch [ 1 ] ;
154+ if ( httpsMatch ) {
155+ return httpsMatch [ 1 ] ;
156+ }
143157
144158 return null ;
145159} ;
146160
161+ const rankRemoteName = ( remoteName ) => {
162+ if ( remoteName === "upstream" ) {
163+ return 0 ;
164+ }
165+ if ( remoteName === "origin" ) {
166+ return 1 ;
167+ }
168+ return 2 ;
169+ } ;
170+
171+ /**
172+ * Get current git branch name
173+ * @returns {string|null } Branch name or null
174+ */
175+ const getCurrentBranch = ( ) => {
176+ return execCommand ( "git rev-parse --abbrev-ref HEAD" ) ;
177+ } ;
178+
179+ /**
180+ * Get HEAD commit sha
181+ * @returns {string|null } Commit sha or null
182+ */
183+ const getHeadCommitSha = ( ) => {
184+ return execCommand ( "git rev-parse HEAD" ) ;
185+ } ;
186+
187+ /**
188+ * Get repository candidates from git remotes
189+ * @param {string|null } explicitRepo - Explicit repository override
190+ * @param {boolean } verbose - Whether to log verbosely
191+ * @returns {string[] } Candidate repositories in owner/repo format
192+ */
193+ const getRepoCandidates = ( explicitRepo , verbose ) => {
194+ if ( explicitRepo ) {
195+ return [ explicitRepo ] ;
196+ }
197+
198+ const remoteOutput = execCommand ( "git remote -v" ) ;
199+ if ( ! remoteOutput ) {
200+ return [ ] ;
201+ }
202+
203+ const remotes = [ ] ;
204+ const seenRepos = new Set ( ) ;
205+
206+ for ( const line of remoteOutput . split ( "\n" ) ) {
207+ const match = line . match ( / ^ ( \S + ) \s + ( \S + ) \s + \( ( f e t c h | p u s h ) \) $ / ) ;
208+ if ( ! match || match [ 3 ] !== "fetch" ) {
209+ continue ;
210+ }
211+
212+ const [ , remoteName , remoteUrl ] = match ;
213+ const repo = parseGitHubRepoFromRemoteUrl ( remoteUrl ) ;
214+ if ( ! repo || seenRepos . has ( repo ) ) {
215+ continue ;
216+ }
217+
218+ remotes . push ( { remoteName, repo } ) ;
219+ seenRepos . add ( repo ) ;
220+ }
221+
222+ remotes . sort ( ( left , right ) => {
223+ const rankDiff = rankRemoteName ( left . remoteName ) - rankRemoteName ( right . remoteName ) ;
224+ return rankDiff !== 0 ? rankDiff : left . remoteName . localeCompare ( right . remoteName ) ;
225+ } ) ;
226+
227+ const repos = remotes . map ( ( { repo } ) => repo ) ;
228+ if ( repos . length > 0 ) {
229+ log ( verbose , `Repository candidates: ${ repos . join ( ", " ) } ` ) ;
230+ }
231+ return repos ;
232+ } ;
233+
147234/**
148235 * Get PR number from current branch
149236 * @param {string } repo - Repository in owner/repo format
150237 * @param {string } branch - Branch name
151238 * @returns {number|null } PR number or null
152239 */
153240const getPrNumberFromBranch = ( repo , branch ) => {
154- const result = execCommand (
155- `gh pr list --repo ${ repo } --head ${ branch } --json number --jq '.[0].number'`
156- ) ;
157- if ( result && ! isNaN ( parseInt ( result , 10 ) ) ) {
158- return parseInt ( result , 10 ) ;
241+ const result = ghCommand ( [
242+ "pr" ,
243+ "list" ,
244+ "--repo" ,
245+ repo ,
246+ "--head" ,
247+ branch ,
248+ "--json" ,
249+ "number" ,
250+ "--jq" ,
251+ ".[0].number" ,
252+ ] ) ;
253+
254+ if ( result . success && result . stdout && ! isNaN ( parseInt ( result . stdout , 10 ) ) ) {
255+ return parseInt ( result . stdout , 10 ) ;
159256 }
160257 return null ;
161258} ;
162259
260+ /**
261+ * Check whether a PR exists in a repository
262+ * @param {string } repo - Repository in owner/repo format
263+ * @param {number } prNumber - PR number
264+ * @returns {boolean } Whether the PR exists
265+ */
266+ const prExists = ( repo , prNumber ) => {
267+ const result = ghCommand ( [
268+ "pr" ,
269+ "view" ,
270+ prNumber . toString ( ) ,
271+ "--repo" ,
272+ repo ,
273+ "--json" ,
274+ "number" ,
275+ "--jq" ,
276+ ".number" ,
277+ ] ) ;
278+
279+ return result . success && result . stdout === prNumber . toString ( ) ;
280+ } ;
281+
282+ /**
283+ * Extract a PR number from a docker-git workspace branch
284+ * @param {string } branch - Branch name
285+ * @returns {number|null } PR number or null
286+ */
287+ const getPrNumberFromWorkspaceBranch = ( branch ) => {
288+ const match = branch . match ( / ^ p r - r e f s - p u l l - ( [ 0 - 9 ] + ) - h e a d $ / ) ;
289+ if ( ! match ) {
290+ return null ;
291+ }
292+
293+ const prNumber = parseInt ( match [ 1 ] , 10 ) ;
294+ return Number . isNaN ( prNumber ) ? null : prNumber ;
295+ } ;
296+
297+ /**
298+ * Find an open PR for the current branch across repo candidates
299+ * @param {string[] } repos - Candidate repositories
300+ * @param {string } branch - Branch name
301+ * @param {boolean } verbose - Whether to log verbosely
302+ * @returns {{repo: string, prNumber: number} | null } PR context or null
303+ */
304+ const findPrContext = ( repos , branch , verbose ) => {
305+ for ( const repo of repos ) {
306+ log ( verbose , `Checking open PR in ${ repo } for branch ${ branch } ` ) ;
307+ const prNumber = getPrNumberFromBranch ( repo , branch ) ;
308+ if ( prNumber !== null ) {
309+ return { repo, prNumber } ;
310+ }
311+ }
312+
313+ const workspacePrNumber = getPrNumberFromWorkspaceBranch ( branch ) ;
314+ if ( workspacePrNumber === null ) {
315+ return null ;
316+ }
317+
318+ for ( const repo of repos ) {
319+ log (
320+ verbose ,
321+ `Checking workspace PR #${ workspacePrNumber } in ${ repo } for branch ${ branch } `
322+ ) ;
323+ if ( prExists ( repo , workspacePrNumber ) ) {
324+ return { repo, prNumber : workspacePrNumber } ;
325+ }
326+ }
327+
328+ return null ;
329+ } ;
330+
163331/**
164332 * Find session directories to backup
165333 * @param {string|null } explicitPath - Explicit session directory path
@@ -322,13 +490,15 @@ const createGist = (files, description, dryRun, verbose) => {
322490 * @param {boolean } verbose - Whether to log verbosely
323491 * @returns {boolean } Whether the comment was posted successfully
324492 */
325- const postPrComment = ( repo , prNumber , gistUrl , dryRun , verbose ) => {
493+ const postPrComment = ( repo , prNumber , gistUrl , commitSha , dryRun , verbose ) => {
326494 const timestamp = new Date ( ) . toISOString ( ) ;
495+ const commitLine = commitSha ? `**Commit:** \`${ commitSha } \`\n\n` : "" ;
496+ const commitMarker = commitSha ? `\n<!-- docker-git-session-backup:${ commitSha } -->` : "" ;
327497 const comment = `## AI Session Backup
328498
329499A snapshot of the AI agent session has been saved to a private gist:
330500
331- **Gist URL:** ${ gistUrl }
501+ ${ commitLine } **Gist URL:** ${ gistUrl }
332502
333503To resume this session, you can use:
334504\`\`\`bash
@@ -345,7 +515,7 @@ gemini --resume <session-id>
345515For extracting session dialogs, see: https://github.com/ProverCoderAI/context-doc
346516
347517---
348- *Backup created at: ${ timestamp } *` ;
518+ *Backup created at: ${ timestamp } *${ commitMarker } ` ;
349519
350520 if ( dryRun ) {
351521 console . log ( `[dry-run] Would post comment to PR #${ prNumber } in ${ repo } :` ) ;
@@ -389,11 +559,12 @@ const main = () => {
389559 log ( verbose , "Starting session backup..." ) ;
390560
391561 // Get repository info
392- const repo = args . repo || getRepoFromRemote ( ) ;
393- if ( ! repo ) {
562+ const repoCandidates = getRepoCandidates ( args . repo , verbose ) ;
563+ if ( repoCandidates . length === 0 ) {
394564 console . error ( "[session-backup] Could not determine repository. Use --repo option." ) ;
395565 process . exit ( 1 ) ;
396566 }
567+ const repo = repoCandidates [ 0 ] ;
397568 log ( verbose , `Repository: ${ repo } ` ) ;
398569
399570 // Get current branch
@@ -405,15 +576,17 @@ const main = () => {
405576 log ( verbose , `Branch: ${ branch } ` ) ;
406577
407578 // Get PR number
408- let prNumber = args . prNumber ;
409- if ( ! prNumber && args . postComment ) {
410- prNumber = getPrNumberFromBranch ( repo , branch ) ;
411- if ( ! prNumber ) {
412- log ( verbose , "No PR found for current branch, skipping comment" ) ;
413- }
579+ let prContext = null ;
580+ if ( args . prNumber !== null ) {
581+ prContext = { repo, prNumber : args . prNumber } ;
582+ } else if ( args . postComment ) {
583+ prContext = findPrContext ( repoCandidates , branch , verbose ) ;
414584 }
415- if ( prNumber ) {
416- log ( verbose , `PR number: ${ prNumber } ` ) ;
585+
586+ if ( prContext !== null ) {
587+ log ( verbose , `PR number: ${ prContext . prNumber } (${ prContext . repo } )` ) ;
588+ } else if ( args . postComment ) {
589+ log ( verbose , "No PR found for current branch, skipping comment" ) ;
417590 }
418591
419592 // Find session directories
@@ -438,7 +611,17 @@ const main = () => {
438611 log ( verbose , `Total files to backup: ${ allFiles . length } ` ) ;
439612
440613 // Create gist
441- const description = `AI Session Backup - ${ repo } - ${ branch } - ${ new Date ( ) . toISOString ( ) } ` ;
614+ const commitSha = getHeadCommitSha ( ) ;
615+ const descriptionParts = [
616+ "AI Session Backup" ,
617+ prContext !== null ? prContext . repo : repo ,
618+ branch ,
619+ ] ;
620+ if ( commitSha ) {
621+ descriptionParts . push ( commitSha . slice ( 0 , 12 ) ) ;
622+ }
623+ descriptionParts . push ( new Date ( ) . toISOString ( ) ) ;
624+ const description = descriptionParts . join ( " - " ) ;
442625 const gistUrl = createGist ( allFiles , description , args . dryRun , verbose ) ;
443626
444627 if ( ! gistUrl ) {
@@ -449,8 +632,15 @@ const main = () => {
449632 console . log ( `[session-backup] Created gist: ${ gistUrl } ` ) ;
450633
451634 // Post PR comment
452- if ( args . postComment && prNumber ) {
453- postPrComment ( repo , prNumber , gistUrl , args . dryRun , verbose ) ;
635+ if ( args . postComment && prContext !== null ) {
636+ postPrComment (
637+ prContext . repo ,
638+ prContext . prNumber ,
639+ gistUrl ,
640+ commitSha ,
641+ args . dryRun ,
642+ verbose
643+ ) ;
454644 }
455645
456646 console . log ( "[session-backup] Session backup complete" ) ;
0 commit comments