@@ -60,6 +60,10 @@ PACKAGE INFORMATION:
60
60
compare ⚖️ Compare two versions of a package
61
61
search 🔍 Search for packages in the registry
62
62
63
+ BRANCH MANAGEMENT:
64
+ cleanup 🧹 Clean up stale buddy-bot branches
65
+ list-branches 📋 List all buddy-bot branches and their status
66
+
63
67
CONFIGURATION & SETUP:
64
68
open-settings 🔧 Open GitHub repository and organization settings pages
65
69
@@ -69,6 +73,8 @@ Examples:
69
73
buddy-bot scan --verbose # Scan for updates (npm + Composer)
70
74
buddy-bot rebase 17 # Rebase PR #17
71
75
buddy-bot update-check # Auto-rebase checked PRs
76
+ buddy-bot cleanup # Clean up stale branches
77
+ buddy-bot list-branches # List all buddy-bot branches
72
78
buddy-bot info laravel/framework # Get Composer package info
73
79
buddy-bot info react # Get npm package info
74
80
buddy-bot versions react --latest 5 # Show recent versions
@@ -1052,6 +1058,31 @@ This helps maintain the intended behavior of dynamic version indicators while pr
1052
1058
if ( closedCount === 0 && rebasedCount === 0 ) {
1053
1059
logger . info ( '✅ No PRs need attention' )
1054
1060
}
1061
+
1062
+ // Automatic cleanup of stale branches after processing PRs
1063
+ if ( ! options . dryRun ) {
1064
+ logger . info ( '\n🧹 Checking for stale branches to clean up...' )
1065
+ try {
1066
+ const cleanupResult = await gitProvider . cleanupStaleBranches ( 7 , false ) // Clean branches older than 7 days
1067
+
1068
+ if ( cleanupResult . deleted . length > 0 ) {
1069
+ logger . success ( `🧹 Automatically cleaned up ${ cleanupResult . deleted . length } stale branch(es)` )
1070
+ if ( options . verbose ) {
1071
+ cleanupResult . deleted . forEach ( branch => logger . info ( ` ✅ Deleted: ${ branch } ` ) )
1072
+ }
1073
+ }
1074
+
1075
+ if ( cleanupResult . failed . length > 0 ) {
1076
+ logger . warn ( `⚠️ Failed to clean up ${ cleanupResult . failed . length } branch(es)` )
1077
+ if ( options . verbose ) {
1078
+ cleanupResult . failed . forEach ( branch => logger . warn ( ` ❌ Failed: ${ branch } ` ) )
1079
+ }
1080
+ }
1081
+ }
1082
+ catch ( cleanupError ) {
1083
+ logger . warn ( '⚠️ Branch cleanup failed:' , cleanupError )
1084
+ }
1085
+ }
1055
1086
}
1056
1087
catch ( error ) {
1057
1088
logger . error ( 'update-check failed:' , error )
@@ -1756,6 +1787,229 @@ cli
1756
1787
}
1757
1788
} )
1758
1789
1790
+ cli
1791
+ . command ( 'cleanup' , 'Clean up stale buddy-bot branches that don\'t have associated open PRs' )
1792
+ . option ( '--verbose, -v' , 'Enable verbose logging' )
1793
+ . option ( '--dry-run' , 'Show what would be deleted without actually deleting' )
1794
+ . option ( '--days <number>' , 'Delete branches older than N days (default: 7)' , { default : '7' } )
1795
+ . option ( '--force' , 'Force cleanup without confirmation prompt' )
1796
+ . example ( 'buddy-bot cleanup' )
1797
+ . example ( 'buddy-bot cleanup --dry-run' )
1798
+ . example ( 'buddy-bot cleanup --days 14' )
1799
+ . example ( 'buddy-bot cleanup --force' )
1800
+ . action ( async ( options : CLIOptions & { dryRun ?: boolean , days ?: string , force ?: boolean } ) => {
1801
+ const logger = options . verbose ? Logger . verbose ( ) : Logger . quiet ( )
1802
+
1803
+ try {
1804
+ logger . info ( '🧹 Starting buddy-bot branch cleanup...' )
1805
+
1806
+ // Check if repository is configured
1807
+ if ( ! config . repository ) {
1808
+ logger . error ( '❌ Repository configuration required for branch cleanup' )
1809
+ logger . info ( 'Configure repository.provider, repository.owner, repository.name in buddy-bot.config.ts' )
1810
+ process . exit ( 1 )
1811
+ }
1812
+
1813
+ // Get GitHub token from environment
1814
+ const token = process . env . BUDDY_BOT_TOKEN || process . env . GITHUB_TOKEN
1815
+ if ( ! token ) {
1816
+ logger . error ( '❌ GITHUB_TOKEN or BUDDY_BOT_TOKEN environment variable required for branch operations' )
1817
+ process . exit ( 1 )
1818
+ }
1819
+
1820
+ const { GitHubProvider } = await import ( '../src/git/github-provider' )
1821
+ const hasWorkflowPermissions = ! ! process . env . BUDDY_BOT_TOKEN
1822
+ const gitProvider = new GitHubProvider (
1823
+ token ,
1824
+ config . repository . owner ,
1825
+ config . repository . name ,
1826
+ hasWorkflowPermissions ,
1827
+ )
1828
+
1829
+ const days = Number . parseInt ( options . days || '7' , 10 )
1830
+ if ( Number . isNaN ( days ) || days < 1 ) {
1831
+ logger . error ( '❌ Invalid days value. Must be a positive number.' )
1832
+ process . exit ( 1 )
1833
+ }
1834
+
1835
+ logger . info ( `🔍 Looking for buddy-bot branches older than ${ days } days...` )
1836
+
1837
+ // Get all orphaned branches
1838
+ const orphanedBranches = await gitProvider . getOrphanedBuddyBotBranches ( )
1839
+ const cutoffDate = new Date ( )
1840
+ cutoffDate . setDate ( cutoffDate . getDate ( ) - days )
1841
+ const staleBranches = orphanedBranches . filter ( branch => branch . lastCommitDate < cutoffDate )
1842
+
1843
+ if ( staleBranches . length === 0 ) {
1844
+ logger . success ( '✅ No stale branches found!' )
1845
+ logger . info ( `📊 Total buddy-bot branches: ${ orphanedBranches . length } ` )
1846
+ logger . info ( `📊 Stale branches (>${ days } days): 0` )
1847
+ return
1848
+ }
1849
+
1850
+ logger . info ( `📊 Found ${ staleBranches . length } stale branches to clean up:` )
1851
+ staleBranches . forEach ( branch => {
1852
+ const daysOld = Math . floor ( ( Date . now ( ) - branch . lastCommitDate . getTime ( ) ) / ( 1000 * 60 * 60 * 24 ) )
1853
+ logger . info ( ` - ${ branch . name } (${ daysOld } days old)` )
1854
+ } )
1855
+
1856
+ if ( options . dryRun ) {
1857
+ logger . info ( '\n🔍 [DRY RUN] These branches would be deleted' )
1858
+ logger . info ( '💡 Run without --dry-run to actually delete them' )
1859
+ return
1860
+ }
1861
+
1862
+ // Confirmation prompt (unless --force is used)
1863
+ if ( ! options . force ) {
1864
+ const response = await prompts ( {
1865
+ type : 'confirm' ,
1866
+ name : 'confirmed' ,
1867
+ message : `Are you sure you want to delete ${ staleBranches . length } stale branches?` ,
1868
+ initial : false ,
1869
+ } )
1870
+
1871
+ if ( ! response . confirmed ) {
1872
+ logger . info ( '❌ Cleanup cancelled' )
1873
+ return
1874
+ }
1875
+ }
1876
+
1877
+ // Perform cleanup
1878
+ const result = await gitProvider . cleanupStaleBranches ( days , false )
1879
+
1880
+ if ( result . deleted . length > 0 ) {
1881
+ logger . success ( `✅ Successfully deleted ${ result . deleted . length } stale branches` )
1882
+ }
1883
+
1884
+ if ( result . failed . length > 0 ) {
1885
+ logger . warn ( `⚠️ Failed to delete ${ result . failed . length } branches` )
1886
+ result . failed . forEach ( branch => logger . warn ( ` - ${ branch } ` ) )
1887
+ }
1888
+
1889
+ logger . info ( `\n📊 Cleanup Summary:` )
1890
+ logger . info ( ` ✅ Deleted: ${ result . deleted . length } ` )
1891
+ logger . info ( ` ❌ Failed: ${ result . failed . length } ` )
1892
+ logger . info ( ` 📊 Total processed: ${ staleBranches . length } ` )
1893
+ }
1894
+ catch ( error ) {
1895
+ logger . error ( 'Branch cleanup failed:' , error )
1896
+ process . exit ( 1 )
1897
+ }
1898
+ } )
1899
+
1900
+ cli
1901
+ . command ( 'list-branches' , 'List all buddy-bot branches and their status' )
1902
+ . option ( '--verbose, -v' , 'Enable verbose logging' )
1903
+ . option ( '--orphaned-only' , 'Show only branches without associated open PRs' )
1904
+ . option ( '--stale-only' , 'Show only stale branches (older than 7 days)' )
1905
+ . option ( '--days <number>' , 'Define stale threshold in days (default: 7)' , { default : '7' } )
1906
+ . example ( 'buddy-bot list-branches' )
1907
+ . example ( 'buddy-bot list-branches --orphaned-only' )
1908
+ . example ( 'buddy-bot list-branches --stale-only --days 14' )
1909
+ . action ( async ( options : CLIOptions & { orphanedOnly ?: boolean , staleOnly ?: boolean , days ?: string } ) => {
1910
+ const logger = options . verbose ? Logger . verbose ( ) : Logger . quiet ( )
1911
+
1912
+ try {
1913
+ logger . info ( '📋 Listing buddy-bot branches...' )
1914
+
1915
+ // Check if repository is configured
1916
+ if ( ! config . repository ) {
1917
+ logger . error ( '❌ Repository configuration required for branch listing' )
1918
+ logger . info ( 'Configure repository.provider, repository.owner, repository.name in buddy-bot.config.ts' )
1919
+ process . exit ( 1 )
1920
+ }
1921
+
1922
+ // Get GitHub token from environment
1923
+ const token = process . env . BUDDY_BOT_TOKEN || process . env . GITHUB_TOKEN
1924
+ if ( ! token ) {
1925
+ logger . error ( '❌ GITHUB_TOKEN or BUDDY_BOT_TOKEN environment variable required for branch operations' )
1926
+ process . exit ( 1 )
1927
+ }
1928
+
1929
+ const { GitHubProvider } = await import ( '../src/git/github-provider' )
1930
+ const hasWorkflowPermissions = ! ! process . env . BUDDY_BOT_TOKEN
1931
+ const gitProvider = new GitHubProvider (
1932
+ token ,
1933
+ config . repository . owner ,
1934
+ config . repository . name ,
1935
+ hasWorkflowPermissions ,
1936
+ )
1937
+
1938
+ const days = Number . parseInt ( options . days || '7' , 10 )
1939
+ const cutoffDate = new Date ( )
1940
+ cutoffDate . setDate ( cutoffDate . getDate ( ) - days )
1941
+
1942
+ // Get all branches and PRs
1943
+ const [ allBuddyBranches , openPRs ] = await Promise . all ( [
1944
+ gitProvider . getBuddyBotBranches ( ) ,
1945
+ gitProvider . getPullRequests ( 'open' ) ,
1946
+ ] )
1947
+
1948
+ const prBranches = new Set ( openPRs . map ( pr => pr . head ) )
1949
+
1950
+ // Filter branches based on options
1951
+ let branches = allBuddyBranches
1952
+ if ( options . orphanedOnly ) {
1953
+ branches = branches . filter ( branch => ! prBranches . has ( branch . name ) )
1954
+ }
1955
+ if ( options . staleOnly ) {
1956
+ branches = branches . filter ( branch => branch . lastCommitDate < cutoffDate && ! prBranches . has ( branch . name ) )
1957
+ }
1958
+
1959
+ if ( branches . length === 0 ) {
1960
+ if ( options . orphanedOnly && options . staleOnly ) {
1961
+ logger . success ( '✅ No stale orphaned branches found!' )
1962
+ }
1963
+ else if ( options . orphanedOnly ) {
1964
+ logger . success ( '✅ No orphaned branches found!' )
1965
+ }
1966
+ else if ( options . staleOnly ) {
1967
+ logger . success ( '✅ No stale branches found!' )
1968
+ }
1969
+ else {
1970
+ logger . info ( '📋 No buddy-bot branches found' )
1971
+ }
1972
+ return
1973
+ }
1974
+
1975
+ console . log ( `\n📊 Found ${ branches . length } buddy-bot branch${ branches . length !== 1 ? 'es' : '' } :\n` )
1976
+
1977
+ // Sort by last commit date (newest first)
1978
+ branches . sort ( ( a , b ) => b . lastCommitDate . getTime ( ) - a . lastCommitDate . getTime ( ) )
1979
+
1980
+ branches . forEach ( branch => {
1981
+ const hasOpenPR = prBranches . has ( branch . name )
1982
+ const daysOld = Math . floor ( ( Date . now ( ) - branch . lastCommitDate . getTime ( ) ) / ( 1000 * 60 * 60 * 24 ) )
1983
+ const isStale = branch . lastCommitDate < cutoffDate
1984
+
1985
+ const status = hasOpenPR ? '🔴 Open PR' : ( isStale ? '🟡 Stale' : '🟢 Recent' )
1986
+ const shortSha = branch . sha . substring ( 0 , 7 )
1987
+
1988
+ console . log ( `${ status } ${ branch . name } ` )
1989
+ console . log ( ` 📅 ${ daysOld } days old | 📝 ${ shortSha } | 🗓️ ${ branch . lastCommitDate . toISOString ( ) . split ( 'T' ) [ 0 ] } ` )
1990
+ console . log ( )
1991
+ } )
1992
+
1993
+ // Summary
1994
+ const orphanedCount = branches . filter ( branch => ! prBranches . has ( branch . name ) ) . length
1995
+ const staleCount = branches . filter ( branch => branch . lastCommitDate < cutoffDate && ! prBranches . has ( branch . name ) ) . length
1996
+
1997
+ console . log ( '📊 Summary:' )
1998
+ console . log ( ` 📋 Total buddy-bot branches: ${ allBuddyBranches . length } ` )
1999
+ console . log ( ` 🔴 With open PRs: ${ allBuddyBranches . length - orphanedCount } ` )
2000
+ console . log ( ` 🟡 Orphaned: ${ orphanedCount } ` )
2001
+ console . log ( ` 🗑️ Stale (>${ days } days): ${ staleCount } ` )
2002
+
2003
+ if ( staleCount > 0 ) {
2004
+ console . log ( `\n💡 Run 'buddy-bot cleanup' to clean up stale branches` )
2005
+ }
2006
+ }
2007
+ catch ( error ) {
2008
+ logger . error ( 'Branch listing failed:' , error )
2009
+ process . exit ( 1 )
2010
+ }
2011
+ } )
2012
+
1759
2013
cli
1760
2014
. command ( 'open-settings' , 'Open GitHub repository and organization settings pages' )
1761
2015
. option ( '--verbose, -v' , 'Enable verbose logging' )
0 commit comments