@@ -8,6 +8,20 @@ import { arePathsEqual } from "../../utils/path"
88import { getBinPath } from "../../services/ripgrep"
99import { DIRS_TO_IGNORE } from "./constants"
1010
11+ /**
12+ * Context object for directory scanning operations
13+ */
14+ interface ScanContext {
15+ /** Whether this is the explicitly targeted directory */
16+ isTargetDir : boolean
17+ /** Whether we're inside an explicitly targeted hidden directory */
18+ insideExplicitHiddenTarget : boolean
19+ /** The base path for the scan operation */
20+ basePath : string
21+ /** The ignore instance for gitignore handling */
22+ ignoreInstance : ReturnType < typeof ignore >
23+ }
24+
1125/**
1226 * List files in a directory, with optional recursive traversal
1327 *
@@ -70,7 +84,13 @@ async function getFirstLevelDirectories(dirPath: string, ignoreInstance: ReturnT
7084 for ( const entry of entries ) {
7185 if ( entry . isDirectory ( ) && ! entry . isSymbolicLink ( ) ) {
7286 const fullDirPath = path . join ( absolutePath , entry . name )
73- if ( shouldIncludeDirectory ( entry . name , fullDirPath , dirPath , ignoreInstance ) ) {
87+ const context : ScanContext = {
88+ isTargetDir : false ,
89+ insideExplicitHiddenTarget : false ,
90+ basePath : dirPath ,
91+ ignoreInstance,
92+ }
93+ if ( shouldIncludeDirectory ( entry . name , fullDirPath , context ) ) {
7494 const formattedPath = fullDirPath . endsWith ( "/" ) ? fullDirPath : `${ fullDirPath } /`
7595 directories . push ( formattedPath )
7696 }
@@ -179,10 +199,14 @@ async function listFilesWithRipgrep(
179199 recursive : boolean ,
180200 limit : number ,
181201) : Promise < string [ ] > {
182- const absolutePath = path . resolve ( dirPath )
183- const rgArgs = buildRipgrepArgs ( absolutePath , recursive )
202+ const rgArgs = buildRipgrepArgs ( dirPath , recursive )
203+
204+ const relativePaths = await execRipgrep ( rgPath , rgArgs , limit )
184205
185- return execRipgrep ( rgPath , rgArgs , limit )
206+ // Convert relative paths from ripgrep to absolute paths
207+ // Resolve dirPath once here for the mapping operation
208+ const absolutePath = path . resolve ( dirPath )
209+ return relativePaths . map ( ( relativePath ) => path . resolve ( absolutePath , relativePath ) )
186210}
187211
188212/**
@@ -209,8 +233,11 @@ function buildRecursiveArgs(dirPath: string): string[] {
209233 // (ripgrep does this automatically)
210234
211235 // Check if we're explicitly targeting a hidden directory
212- // We need to check all parts of the path, not just the basename
213- const pathParts = dirPath . split ( path . sep ) . filter ( ( part ) => part !== "" )
236+ // Normalize the path first to handle edge cases
237+ const normalizedPath = path . normalize ( dirPath )
238+ // Split by separator and filter out empty parts
239+ // This handles cases like trailing slashes, multiple separators, etc.
240+ const pathParts = normalizedPath . split ( path . sep ) . filter ( ( part ) => part . length > 0 )
214241 const isTargetingHiddenDir = pathParts . some ( ( part ) => part . startsWith ( "." ) )
215242
216243 // Get the target directory name to check if it's in the ignore list
@@ -310,7 +337,7 @@ async function createIgnoreInstance(dirPath: string): Promise<ReturnType<typeof
310337 ignoreInstance . add ( content )
311338 } catch ( err ) {
312339 // Continue if we can't read a .gitignore file
313- console . warn ( `Error reading .gitignore at ${ gitignoreFile } : ${ err } ` )
340+ console . warn ( `Could not read .gitignore at ${ gitignoreFile } : ${ err } ` )
314341 }
315342 }
316343
@@ -361,11 +388,20 @@ async function listFilteredDirectories(
361388 const absolutePath = path . resolve ( dirPath )
362389 const directories : string [ ] = [ ]
363390
364- async function scanDirectory (
365- currentPath : string ,
366- isTargetDir : boolean = false ,
367- insideExplicitHiddenTarget : boolean = false ,
368- ) : Promise < void > {
391+ // For environment details generation, we don't want to treat the root as a "target"
392+ // if we're doing a general recursive scan, as this would include hidden directories
393+ // Only treat as target if we're explicitly scanning a single hidden directory
394+ const isExplicitHiddenTarget = path . basename ( absolutePath ) . startsWith ( "." )
395+
396+ // Create initial context for the scan
397+ const initialContext : ScanContext = {
398+ isTargetDir : isExplicitHiddenTarget ,
399+ insideExplicitHiddenTarget : isExplicitHiddenTarget ,
400+ basePath : dirPath ,
401+ ignoreInstance,
402+ }
403+
404+ async function scanDirectory ( currentPath : string , context : ScanContext ) : Promise < void > {
369405 try {
370406 // List all entries in the current directory
371407 const entries = await fs . promises . readdir ( currentPath , { withFileTypes : true } )
@@ -376,9 +412,15 @@ async function listFilteredDirectories(
376412 const dirName = entry . name
377413 const fullDirPath = path . join ( currentPath , dirName )
378414
379- // Check if this directory should be included
415+ // Create context for subdirectory checks
380416 // Subdirectories found during scanning are never target directories themselves
381- if ( shouldIncludeDirectory ( dirName , fullDirPath , dirPath , ignoreInstance , false , insideExplicitHiddenTarget ) ) {
417+ const subdirContext : ScanContext = {
418+ ...context ,
419+ isTargetDir : false ,
420+ }
421+
422+ // Check if this directory should be included
423+ if ( shouldIncludeDirectory ( dirName , fullDirPath , subdirContext ) ) {
382424 // Add the directory to our results (with trailing slash)
383425 // fullDirPath is already absolute since it's built with path.join from absolutePath
384426 const formattedPath = fullDirPath . endsWith ( "/" ) ? fullDirPath : `${ fullDirPath } /`
@@ -393,105 +435,132 @@ async function listFilteredDirectories(
393435 // Use the same logic as shouldIncludeDirectory for recursion decisions
394436 // When inside an explicitly targeted hidden directory, only block critical directories
395437 let shouldRecurseIntoDir = true
396- if ( insideExplicitHiddenTarget ) {
438+ if ( context . insideExplicitHiddenTarget ) {
397439 // Only apply the most critical ignore patterns when inside explicit hidden target
398- const criticalIgnorePatterns = [ "node_modules" , ".git" , "__pycache__" , "venv" , "env" ]
399- shouldRecurseIntoDir = ! criticalIgnorePatterns . includes ( dirName )
440+ shouldRecurseIntoDir = ! CRITICAL_IGNORE_PATTERNS . has ( dirName )
400441 } else {
401442 shouldRecurseIntoDir = ! isDirectoryExplicitlyIgnored ( dirName )
402443 }
403444
404445 const shouldRecurse =
405446 recursive &&
406447 shouldRecurseIntoDir &&
407- ! ( isHiddenDir && DIRS_TO_IGNORE . includes ( ".*" ) && ! isTargetDir && ! insideExplicitHiddenTarget )
448+ ! (
449+ isHiddenDir &&
450+ DIRS_TO_IGNORE . includes ( ".*" ) &&
451+ ! context . isTargetDir &&
452+ ! context . insideExplicitHiddenTarget
453+ )
408454 if ( shouldRecurse ) {
409455 // If we're entering a hidden directory that's the target, or we're already inside one,
410456 // mark that we're inside an explicitly targeted hidden directory
411- const newInsideExplicitHiddenTarget = insideExplicitHiddenTarget || ( isHiddenDir && isTargetDir )
412- await scanDirectory ( fullDirPath , false , newInsideExplicitHiddenTarget )
457+ const newInsideExplicitHiddenTarget =
458+ context . insideExplicitHiddenTarget || ( isHiddenDir && context . isTargetDir )
459+ const newContext : ScanContext = {
460+ ...context ,
461+ isTargetDir : false ,
462+ insideExplicitHiddenTarget : newInsideExplicitHiddenTarget ,
463+ }
464+ await scanDirectory ( fullDirPath , newContext )
413465 }
414466 }
415467 }
416468 } catch ( err ) {
417- // Silently continue if we can't read a directory
469+ // Continue if we can't read a directory
418470 console . warn ( `Could not read directory ${ currentPath } : ${ err } ` )
419471 }
420472 }
421473
422474 // Start scanning from the root directory
423- // For environment details generation, we don't want to treat the root as a "target"
424- // if we're doing a general recursive scan, as this would include hidden directories
425- // Only treat as target if we're explicitly scanning a single hidden directory
426- const isExplicitHiddenTarget = path . basename ( absolutePath ) . startsWith ( "." )
427- await scanDirectory ( absolutePath , isExplicitHiddenTarget , isExplicitHiddenTarget )
475+ await scanDirectory ( absolutePath , initialContext )
428476
429477 return directories
430478}
431479
432480/**
433- * Determine if a directory should be included in results based on filters
481+ * Critical directories that should always be ignored, even inside explicitly targeted hidden directories
482+ */
483+ const CRITICAL_IGNORE_PATTERNS = new Set ( [ "node_modules" , ".git" , "__pycache__" , "venv" , "env" ] )
484+
485+ /**
486+ * Check if a directory matches any of the given patterns
434487 */
435- function shouldIncludeDirectory (
436- dirName : string ,
488+ function matchesIgnorePattern ( dirName : string , patterns : string [ ] ) : boolean {
489+ for ( const pattern of patterns ) {
490+ if ( pattern === dirName || ( pattern . includes ( "/" ) && pattern . split ( "/" ) [ 0 ] === dirName ) ) {
491+ return true
492+ }
493+ }
494+ return false
495+ }
496+
497+ /**
498+ * Check if a directory is ignored by gitignore
499+ */
500+ function isIgnoredByGitignore (
437501 fullDirPath : string ,
438502 basePath : string ,
439503 ignoreInstance : ReturnType < typeof ignore > ,
440- isTargetDir : boolean = false ,
441- insideExplicitHiddenTarget : boolean = false ,
442504) : boolean {
443- // If this is the explicitly targeted directory, allow it even if it's hidden
444- // This preserves the ability to explicitly target hidden directories like .roo-memory
445- if ( isTargetDir ) {
446- // Only apply non-hidden-directory ignore rules to target directories
447- const nonHiddenIgnorePatterns = DIRS_TO_IGNORE . filter ( ( pattern ) => pattern !== ".*" )
448- for ( const pattern of nonHiddenIgnorePatterns ) {
449- if ( pattern === dirName || ( pattern . includes ( "/" ) && pattern . split ( "/" ) [ 0 ] === dirName ) ) {
450- return false
451- }
452- }
453- return true
454- }
505+ const relativePath = path . relative ( basePath , fullDirPath )
506+ const normalizedPath = relativePath . replace ( / \\ / g, "/" )
507+ return ignoreInstance . ignores ( normalizedPath ) || ignoreInstance . ignores ( normalizedPath + "/" )
508+ }
455509
456- // If we're inside an explicitly targeted hidden directory, allow subdirectories
457- // even if they would normally be filtered out by the ".*" pattern or other ignore rules
458- if ( insideExplicitHiddenTarget ) {
459- // Only apply the most critical ignore patterns when inside explicit hidden target
460- // Allow temp, rules, etc. but still block node_modules, .git, etc.
461- const criticalIgnorePatterns = [ "node_modules" , ".git" , "__pycache__" , "venv" , "env" ]
462- for ( const pattern of criticalIgnorePatterns ) {
463- if ( pattern === dirName || ( pattern . includes ( "/" ) && pattern . split ( "/" ) [ 0 ] === dirName ) ) {
464- return false
465- }
466- }
467- // Check against gitignore patterns using the ignore library
468- const relativePath = path . relative ( basePath , fullDirPath )
469- const normalizedPath = relativePath . replace ( / \\ / g, "/" )
470- if ( ignoreInstance . ignores ( normalizedPath ) || ignoreInstance . ignores ( normalizedPath + "/" ) ) {
471- return false
472- }
473- return true
510+ /**
511+ * Check if a target directory should be included
512+ */
513+ function shouldIncludeTargetDirectory ( dirName : string ) : boolean {
514+ // Only apply non-hidden-directory ignore rules to target directories
515+ const nonHiddenIgnorePatterns = DIRS_TO_IGNORE . filter ( ( pattern ) => pattern !== ".*" )
516+ return ! matchesIgnorePattern ( dirName , nonHiddenIgnorePatterns )
517+ }
518+
519+ /**
520+ * Check if a directory inside an explicitly targeted hidden directory should be included
521+ */
522+ function shouldIncludeInsideHiddenTarget ( dirName : string , fullDirPath : string , context : ScanContext ) : boolean {
523+ // Only apply the most critical ignore patterns when inside explicit hidden target
524+ if ( CRITICAL_IGNORE_PATTERNS . has ( dirName ) ) {
525+ return false
474526 }
475527
476- // Check against explicit ignore patterns (excluding the ".*" pattern for now)
528+ // Check against gitignore patterns
529+ return ! isIgnoredByGitignore ( fullDirPath , context . basePath , context . ignoreInstance )
530+ }
531+
532+ /**
533+ * Check if a regular directory should be included
534+ */
535+ function shouldIncludeRegularDirectory ( dirName : string , fullDirPath : string , context : ScanContext ) : boolean {
536+ // Check against explicit ignore patterns (excluding the ".*" pattern)
477537 const nonHiddenIgnorePatterns = DIRS_TO_IGNORE . filter ( ( pattern ) => pattern !== ".*" )
478- for ( const pattern of nonHiddenIgnorePatterns ) {
479- if ( pattern === dirName || ( pattern . includes ( "/" ) && pattern . split ( "/" ) [ 0 ] === dirName ) ) {
480- return false
481- }
538+ if ( matchesIgnorePattern ( dirName , nonHiddenIgnorePatterns ) ) {
539+ return false
482540 }
483541
484- // Check against gitignore patterns using the ignore library
485- // Calculate relative path from the base directory
486- const relativePath = path . relative ( basePath , fullDirPath )
487- const normalizedPath = relativePath . replace ( / \\ / g, "/" )
542+ // Check against gitignore patterns
543+ return ! isIgnoredByGitignore ( fullDirPath , context . basePath , context . ignoreInstance )
544+ }
488545
489- // Check if the directory is ignored by .gitignore
490- if ( ignoreInstance . ignores ( normalizedPath ) || ignoreInstance . ignores ( normalizedPath + "/" ) ) {
491- return false
546+ /**
547+ * Determine if a directory should be included in results based on filters
548+ */
549+ function shouldIncludeDirectory ( dirName : string , fullDirPath : string , context : ScanContext ) : boolean {
550+ // If this is the explicitly targeted directory, allow it even if it's hidden
551+ // This preserves the ability to explicitly target hidden directories like .roo-memory
552+ if ( context . isTargetDir ) {
553+ return shouldIncludeTargetDirectory ( dirName )
554+ }
555+
556+ // If we're inside an explicitly targeted hidden directory, allow subdirectories
557+ // even if they would normally be filtered out by the ".*" pattern or other ignore rules
558+ if ( context . insideExplicitHiddenTarget ) {
559+ return shouldIncludeInsideHiddenTarget ( dirName , fullDirPath , context )
492560 }
493561
494- return true
562+ // Regular directory inclusion logic
563+ return shouldIncludeRegularDirectory ( dirName , fullDirPath , context )
495564}
496565
497566/**
@@ -619,9 +688,8 @@ async function execRipgrep(rgPath: string, args: string[], limit: number): Promi
619688 // Process each complete line
620689 for ( const line of lines ) {
621690 if ( line . trim ( ) && results . length < limit ) {
622- // Convert relative path from ripgrep to absolute path
623- const absolutePath = path . resolve ( searchDir , line )
624- results . push ( absolutePath )
691+ // Keep the relative path as returned by ripgrep
692+ results . push ( line )
625693 } else if ( results . length >= limit ) {
626694 break
627695 }
0 commit comments