@@ -13,6 +13,7 @@ import {FETCH_ISSUE_TIMELINE_PAGE, FETCH_ISSUES_FOR_SYNC, FETCH_SINGLE_ISSUE} fr
1313import { GET_ISSUE_ID , UPDATE_ISSUE } from '../queries/mutations.mjs' ;
1414import contentPath from '../shared/contentPath.mjs' ;
1515import { createContentIndexEntry , updateContentIndex } from '../shared/contentIndex.mjs' ;
16+ import pruneEmptyDirs from '../shared/pruneEmptyDirs.mjs' ;
1617
1718const issueSyncConfig = aiConfig . issueSync ;
1819const lineBreaksRegex = / [ \r \n ] + / g;
@@ -608,6 +609,7 @@ class IssueSyncer extends Base {
608609 } ;
609610
610611 const indexMutations = { upsert : [ ] , remove : [ ] } ;
612+ let shouldPruneEmptyDirs = false ;
611613
612614 const planBuckets = this . #planBuckets( metadata , allIssues ) ;
613615
@@ -650,6 +652,7 @@ class IssueSyncer extends Base {
650652 try {
651653 const oldPath = this . #resolvePath( oldPathRelative ) ;
652654 await fs . unlink ( oldPath ) ;
655+ shouldPruneEmptyDirs = true ;
653656 logger . debug ( `🗑️ Removed dropped issue #${ issueNumber } : ${ oldPath } ` ) ;
654657 } catch ( e ) { /* File might not exist */ }
655658 }
@@ -690,10 +693,11 @@ class IssueSyncer extends Base {
690693 stats . pulled . moved ++ ;
691694 try {
692695 await fs . rename ( oldAbsolutePath , targetPath ) ;
696+ shouldPruneEmptyDirs = true ;
693697 logger . debug ( `📦 Moved #${ issueNumber } : ${ oldAbsolutePath } → ${ targetPath } ` ) ;
694698 } catch ( e ) {
695699 logger . warn ( `Could not rename #${ issueNumber } , falling back to write. Error: ${ e . message } ` ) ;
696- await fs . unlink ( oldAbsolutePath ) . catch ( ( ) => { } ) ;
700+ await fs . unlink ( oldAbsolutePath ) . then ( ( ) => { shouldPruneEmptyDirs = true ; } ) . catch ( ( ) => { } ) ;
697701 }
698702 } else {
699703 stats . pulled . updated ++ ;
@@ -789,6 +793,11 @@ class IssueSyncer extends Base {
789793 stats . pulled . issues . push ( ...refetchStats . refetched . issues ) ;
790794 }
791795
796+ await pruneEmptyDirs ( issueSyncConfig . issuesDir ) ;
797+ if ( shouldPruneEmptyDirs ) {
798+ await pruneEmptyDirs ( path . join ( issueSyncConfig . archiveRoot , 'issues' ) ) ;
799+ }
800+
792801 try {
793802 await updateContentIndex ( issueSyncConfig , indexMutations ) ;
794803 } catch ( e ) {
@@ -1023,6 +1032,7 @@ class IssueSyncer extends Base {
10231032 logger . info ( '🔄 Reconciling closed issue locations...' ) ;
10241033
10251034 const stats = { count : 0 , issues : [ ] } ;
1035+ let shouldPruneEmptyDirs = false ;
10261036
10271037 // Ensure releases are loaded
10281038 if ( ! ReleaseNotesSyncer . sortedReleases || ReleaseNotesSyncer . sortedReleases . length === 0 ) {
@@ -1078,6 +1088,7 @@ class IssueSyncer extends Base {
10781088
10791089 // Move the file
10801090 await fs . rename ( currentAbsolutePath , correctPath ) ;
1091+ shouldPruneEmptyDirs = true ;
10811092
10821093 // Update metadata with relative path
10831094 metadata . issues [ issueNumber ] . path = this . #relativePath( correctPath ) ;
@@ -1092,6 +1103,8 @@ class IssueSyncer extends Base {
10921103 }
10931104 }
10941105
1106+ await pruneEmptyDirs ( issueSyncConfig . issuesDir ) ;
1107+
10951108 if ( stats . count > 0 ) {
10961109 logger . info ( `📦 Archived ${ stats . count } closed issue(s)` ) ;
10971110 } else {
@@ -1195,37 +1208,13 @@ class IssueSyncer extends Base {
11951208 await updateContentIndex ( issueSyncConfig , { upsert : upserts } ) ;
11961209
11971210 // Prune chunk/version directories emptied by the relocation.
1198- await this . # pruneEmptyDirs( path . join ( issueSyncConfig . archiveRoot , 'issues' ) ) ;
1199- await this . # pruneEmptyDirs( issueSyncConfig . issuesDir ) ;
1211+ await pruneEmptyDirs ( path . join ( issueSyncConfig . archiveRoot , 'issues' ) ) ;
1212+ await pruneEmptyDirs ( issueSyncConfig . issuesDir ) ;
12001213
12011214 logger . info ( `[REBUCKET] moved ${ moves . length } issue(s), ${ unchanged } unchanged. Distribution: ${ JSON . stringify ( byVersion ) } ` ) ;
12021215 return summary ;
12031216 }
12041217
1205- /**
1206- * Recursively removes now-empty directories under `root` (after a re-bucket relocation leaves
1207- * source chunk/version folders empty). Children are pruned before parents; `root` is never removed.
1208- * @param {string } root Absolute directory to prune within.
1209- * @private
1210- */
1211- async #pruneEmptyDirs( root ) {
1212- let entries ;
1213- try {
1214- entries = await fs . readdir ( root , { withFileTypes : true } ) ;
1215- } catch {
1216- return ; // root absent — nothing to prune
1217- }
1218-
1219- for ( const entry of entries ) {
1220- if ( ! entry . isDirectory ( ) ) continue ;
1221- const child = path . join ( root , entry . name ) ;
1222- await this . #pruneEmptyDirs( child ) ;
1223- try {
1224- if ( ( await fs . readdir ( child ) ) . length === 0 ) await fs . rmdir ( child ) ;
1225- } catch { /* race / already removed */ }
1226- }
1227- }
1228-
12291218 /**
12301219 * Recursively scans the configured issue directory to find all local .md issue files.
12311220 * This operation is intentionally limited to the active issues directory as a performance
0 commit comments