@@ -139,6 +139,13 @@ export async function globalInit(params: GlobalInitParams): Promise<GlobalInitRe
139139 const globalDb = openGlobalDatabase ( ) ;
140140
141141 try {
142+ // --------------------------------------------------------
143+ // Deduplicate: Remove parent projects that contain sub-projects
144+ // e.g., AudioGrabber/ contains AudioGrabber/AudioGrabber/ and AudioGrabber/AudioGrabber2/
145+ // → keep only the sub-projects, skip the parent
146+ // --------------------------------------------------------
147+ const deduplicatedProjects = deduplicateProjects ( scanResult . projects ) ;
148+
142149 // Get existing projects for comparison
143150 const existingProjects = new Map (
144151 globalDb . getProjects ( ) . map ( p => [ p . path . replace ( / \\ / g, '/' ) , p ] )
@@ -147,8 +154,8 @@ export async function globalInit(params: GlobalInitParams): Promise<GlobalInitRe
147154 let newCount = 0 ;
148155 let updatedCount = 0 ;
149156
150- // Register each found project
151- for ( const project of scanResult . projects ) {
157+ // Register each found project (deduplicated)
158+ for ( const project of deduplicatedProjects ) {
152159 const normalizedPath = project . path . replace ( / \\ / g, '/' ) ;
153160 const stats = readProjectStats ( project . path ) ;
154161 if ( ! stats ) continue ;
@@ -185,9 +192,27 @@ export async function globalInit(params: GlobalInitParams): Promise<GlobalInitRe
185192 }
186193 }
187194
188- // Collect paths of indexed projects for exclusion
195+ // Remove parent-duplicate projects already in the global DB
196+ // (from previous runs before deduplication was added)
197+ const allRegistered = globalDb . getProjects ( ) ;
198+ const allPaths = allRegistered . map ( p => ( {
199+ id : p . id ,
200+ path : p . path . replace ( / \\ / g, '/' ) . replace ( / \/ + $ / , '' ) + '/' ,
201+ } ) ) ;
202+ for ( const project of allPaths ) {
203+ const isParent = allPaths . some ( other =>
204+ other . id !== project . id &&
205+ other . path . startsWith ( project . path )
206+ ) ;
207+ if ( isParent ) {
208+ globalDb . unregisterProject ( project . path . replace ( / \/ + $ / , '' ) ) ;
209+ removedCount ++ ;
210+ }
211+ }
212+
213+ // Collect paths of indexed projects for exclusion (use deduplicated list)
189214 const indexedPaths = new Set (
190- scanResult . projects . map ( p => p . path . replace ( / \\ / g, '/' ) )
215+ deduplicatedProjects . map ( p => p . path . replace ( / \\ / g, '/' ) )
191216 ) ;
192217
193218 // Find projects without .aidex/ index
@@ -290,8 +315,8 @@ export async function globalInit(params: GlobalInitParams): Promise<GlobalInitRe
290315 success : true ,
291316 searchPath,
292317 registered : params . indexUnindexed
293- ? scanResult . projects . length + ( indexedResults ?. filter ( r => r . success ) . length ?? 0 )
294- : scanResult . projects . length ,
318+ ? deduplicatedProjects . length + ( indexedResults ?. filter ( r => r . success ) . length ?? 0 )
319+ : deduplicatedProjects . length ,
295320 newProjects : newCount ,
296321 updatedProjects : updatedCount ,
297322 removedProjects : removedCount ,
@@ -477,3 +502,36 @@ function findUnindexedProjects(searchPath: string, maxDepth: number, indexedPath
477502 walk ( searchPath , 0 ) ;
478503 return projects ;
479504}
505+
506+ /**
507+ * Deduplicate projects: If project A is a parent directory of project B,
508+ * keep only B (the more specific sub-project).
509+ *
510+ * Example: Given paths [AudioGrabber/, AudioGrabber/AudioGrabber/, AudioGrabber/AudioGrabber2/]
511+ * → Remove AudioGrabber/ because it contains sub-projects.
512+ * → Keep AudioGrabber/AudioGrabber/ and AudioGrabber/AudioGrabber2/.
513+ */
514+ function deduplicateProjects < T extends { path : string ; name : string } > ( projects : T [ ] ) : T [ ] {
515+ // Normalize all paths
516+ const normalized = projects . map ( p => ( {
517+ ...p ,
518+ _normPath : p . path . replace ( / \\ / g, '/' ) . replace ( / \/ + $ / , '' ) + '/' ,
519+ } ) ) ;
520+
521+ // Sort by path length descending (deepest first)
522+ normalized . sort ( ( a , b ) => b . _normPath . length - a . _normPath . length ) ;
523+
524+ // A project is a "parent" if any other project's path starts with it
525+ const result : typeof projects = [ ] ;
526+ for ( const project of normalized ) {
527+ const isParent = normalized . some ( other =>
528+ other !== project &&
529+ other . _normPath . startsWith ( project . _normPath )
530+ ) ;
531+ if ( ! isParent ) {
532+ result . push ( project ) ;
533+ }
534+ }
535+
536+ return result ;
537+ }
0 commit comments