From 2edc1572176047891c89cab7cebce1641530ffc8 Mon Sep 17 00:00:00 2001 From: nyatinte Date: Thu, 31 Jul 2025 00:03:49 +0900 Subject: [PATCH] fix: remove recursive option and always scan from HOME directory (#69) - Remove recursive option from ScanOptions type - Change default scan path to HOME directory instead of cwd - Always scan deeply with maxDepth: 20 in fast-scanner - Add intelligent filtering when scanning from HOME: - Prioritize likely project directories by name patterns - Check for marker files (.git, package.json, etc.) - Exclude system/cache/media directories - Update all scanner functions to use HOME as default - Fix tests to work with new scanning behavior This ensures CLAUDE.md files are discovered even when ccexp is run from subdirectories, addressing the core issue in #69. --- src/_types.ts | 3 +- src/claude-md-scanner.ts | 391 ++++++++++++++++++--------- src/fast-scanner.ts | 64 +---- src/hooks/useFileNavigation.test.tsx | 40 +-- src/hooks/useFileNavigation.tsx | 6 +- src/settings-json-scanner.ts | 28 +- src/slash-command-scanner.ts | 23 +- src/subagent-scanner.ts | 2 - 8 files changed, 294 insertions(+), 263 deletions(-) diff --git a/src/_types.ts b/src/_types.ts index e0653cd..17ba323 100644 --- a/src/_types.ts +++ b/src/_types.ts @@ -58,8 +58,7 @@ export type SubAgentInfo = { // Scan options export type ScanOptions = { - readonly path?: string | undefined; - readonly recursive?: boolean | undefined; + readonly path?: string | undefined; // default: HOME directory readonly type?: ClaudeFileType | undefined; readonly includeHidden?: boolean | undefined; }; diff --git a/src/claude-md-scanner.ts b/src/claude-md-scanner.ts index f5fb23b..e5b8cf4 100644 --- a/src/claude-md-scanner.ts +++ b/src/claude-md-scanner.ts @@ -19,128 +19,75 @@ import { findClaudeFiles } from './fast-scanner.ts'; export const scanClaudeFiles = async ( options: ScanOptions = {}, ): Promise => { + const { homedir } = await import('node:os'); + const homeDir = homedir(); + const { - path = process.cwd(), - recursive = true, + path = homeDir, // Default to HOME directory includeHidden = false, } = options; try { - // Scan specified path - const files = await findClaudeFiles({ - path, - recursive, - includeHidden, - }); + const files: string[] = []; + + // If scanning from HOME directory (default), use intelligent filtering + if (path === homeDir) { + // 1. Scan ~/.claude directory + const globalClaudeDir = join(homeDir, '.claude'); + if (existsSync(globalClaudeDir)) { + const globalFiles = await findClaudeFiles({ + path: globalClaudeDir, + includeHidden, + }); + files.push(...globalFiles); + } - // Also scan global Claude directory if scanning recursively - if (recursive) { - const { homedir } = await import('node:os'); - const homeDir = homedir(); - - // Only scan if current path is not already the home directory or .claude directory - if (path !== homeDir && path !== join(homeDir, '.claude')) { - // 1. Scan ~/.claude directory recursively - const globalClaudeDir = join(homeDir, '.claude'); - if (existsSync(globalClaudeDir)) { - const globalFiles = await findClaudeFiles({ - path: globalClaudeDir, - recursive: true, - includeHidden, - }); - files.push(...globalFiles); - } + // 2. Check for CLAUDE.md directly in home directory + const homeClaudeFile = join(homeDir, 'CLAUDE.md'); + if (existsSync(homeClaudeFile)) { + files.push(homeClaudeFile); + } - // 2. Check for CLAUDE.md directly in home directory - const homeClaudeFile = join(homeDir, 'CLAUDE.md'); - if (existsSync(homeClaudeFile)) { - files.push(homeClaudeFile); - } + // 3. Scan all directories under HOME with intelligent filtering + try { + const homeContents = await readdir(homeDir, { withFileTypes: true }); + + for (const entry of homeContents) { + if (!entry.isDirectory() || shouldExcludeDirectory(entry.name)) + continue; + + const dirPath = join(homeDir, entry.name); + + // Prioritize likely project directories + if (await isLikelyProjectDirectory(dirPath, entry.name)) { + const projectFiles = await findClaudeFiles({ + path: dirPath, + includeHidden: false, + }); + files.push(...projectFiles); + } else { + // For other directories, just check the first level + const claudeMdPath = join(dirPath, 'CLAUDE.md'); + if (existsSync(claudeMdPath)) { + files.push(claudeMdPath); + } - // 3. Scan first-level subdirectories in home for CLAUDE.md files - // This finds project-level CLAUDE.md files without deep recursion - try { - const homeContents = await readdir(homeDir, { withFileTypes: true }); - const directoriesToSkip = new Set([ - '.cache', - '.npm', - '.yarn', - '.pnpm', - 'node_modules', - '.git', - '.svn', - '.hg', - 'Library', - 'Applications', - '.Trash', - '.local', - '.config', - '.vscode', - '.idea', - ]); - - // Common project directories that should be scanned deeper - const projectDirectories = new Set([ - 'my_programs', - 'projects', - 'dev', - 'development', - 'workspace', - 'work', - 'code', - 'repos', - 'git', - 'Documents', - 'Desktop', - 'src', - 'source', - ]); - - for (const entry of homeContents) { - if ( - entry.isDirectory() && - !directoriesToSkip.has(entry.name) && - !entry.name.startsWith('.') - ) { - const dirPath = join(homeDir, entry.name); - - // Check for CLAUDE.md in this directory - const claudeMdPath = join(dirPath, 'CLAUDE.md'); - if (existsSync(claudeMdPath)) { - files.push(claudeMdPath); - } - - // Also check for CLAUDE.local.md - const claudeLocalPath = join(dirPath, 'CLAUDE.local.md'); - if (existsSync(claudeLocalPath)) { - files.push(claudeLocalPath); - } - - // For project directories, scan recursively but with constraints - if (projectDirectories.has(entry.name)) { - try { - // Use findClaudeFiles to recursively scan project directories - const projectFiles = await findClaudeFiles({ - path: dirPath, - recursive: true, - includeHidden: false, - }); - files.push(...projectFiles); - } catch (error) { - // If we can't scan the directory, just skip it - console.warn( - `Failed to scan ${entry.name} subdirectories:`, - error, - ); - } - } + const claudeLocalPath = join(dirPath, 'CLAUDE.local.md'); + if (existsSync(claudeLocalPath)) { + files.push(claudeLocalPath); } } - } catch (error) { - // If we can't read home directory, just continue - console.warn('Failed to scan home subdirectories:', error); } + } catch (error) { + console.warn('Failed to scan home subdirectories:', error); } + } else { + // Scan from a specific path (not HOME) + const scanFiles = await findClaudeFiles({ + path, + includeHidden, + }); + files.push(...scanFiles); } // Remove duplicates based on file path using es-toolkit @@ -237,6 +184,106 @@ class ClaudeMdScanner extends BaseFileScanner { const scanner = new ClaudeMdScanner(); const processClaudeFile = (filePath: string) => scanner.processFile(filePath); +// Helper function to determine if a directory is likely a project directory +async function isLikelyProjectDirectory( + dirPath: string, + dirName: string, +): Promise { + // 1. Pattern-based detection + const namePatterns = [ + /^(dev|develop|development)$/i, + /^(proj|project|projects)$/i, + /^(work|workspace|code|src|source)$/i, + /^(repo|repos|repositories)$/i, + /^(git|github|gitlab)$/i, + /^my_programs$/i, // Keep specific known directories + ]; + + if (namePatterns.some((pattern) => pattern.test(dirName))) { + return true; + } + + // 2. Marker file detection + const markers = [ + '.git', + 'package.json', + 'Cargo.toml', + 'go.mod', + 'requirements.txt', + '.claude', + 'CLAUDE.md', + 'pyproject.toml', + 'composer.json', + 'Gemfile', + 'pom.xml', + 'build.gradle', + 'CMakeLists.txt', + ]; + + for (const marker of markers) { + if (existsSync(join(dirPath, marker))) { + return true; + } + } + + return false; +} + +// Comprehensive exclusion patterns +function shouldExcludeDirectory(name: string): boolean { + // Start with dot check + if (name.startsWith('.')) { + // Allow .claude directory specifically + if (name === '.claude') return false; + return true; + } + + const excludePatterns = [ + // System/Cache + /^\.cache$/, + /^\.npm$/, + /^\.yarn$/, + /^\.pnpm$/, + /^node_modules$/, + /^\.git$/, + /^\.svn$/, + /^\.hg$/, + + // OS-specific + /^Library$/, + /^Applications$/, + /^\.Trash$/, // macOS + /^AppData$/, + /^LocalAppData$/, // Windows + + // Large media directories + /^Downloads$/, + /^Pictures$/, + /^Movies$/, + /^Music$/, + /^Videos$/, + + // Hidden config (already caught by dot check but explicit for clarity) + /^\.local$/, + /^\.config$/, + /^\.vscode$/, + /^\.idea$/, + + // Build/temp directories + /^dist$/, + /^build$/, + /^out$/, + /^tmp$/, + /^temp$/, + + // Package managers + /^vendor$/, + /^bower_components$/, + ]; + + return excludePatterns.some((pattern) => pattern.test(name)); +} + // Removed unused function _findGlobalClaudeFiles // InSource tests @@ -251,32 +298,26 @@ if (import.meta.vitest != null) { describe('getSearchPatterns', () => { test('should return all patterns when no type specified', () => { - const patterns = getSearchPatterns(undefined, true); + const patterns = getSearchPatterns(undefined); expect(patterns).toContain('**/CLAUDE.md'); expect(patterns).toContain('**/CLAUDE.local.md'); expect(patterns).toContain('**/.claude/commands/**/*.md'); }); test('should return specific pattern for project-memory type', () => { - const patterns = getSearchPatterns('project-memory', true); + const patterns = getSearchPatterns('project-memory'); expect(patterns).toContain('**/CLAUDE.md'); expect(patterns).not.toContain('**/CLAUDE.local.md'); }); - test('should respect recursive option', () => { - const patterns = getSearchPatterns('project-memory', false); - expect(patterns).toContain('CLAUDE.md'); - expect(patterns).not.toContain('**/CLAUDE.md'); - }); - test('should return user slash commands pattern for personal-command type', () => { - const patterns = getSearchPatterns('personal-command', true); + const patterns = getSearchPatterns('personal-command'); expect(patterns).toHaveLength(1); expect(patterns[0]).toContain('.claude/commands/'); }); test('should include personal-command pattern when no type specified', () => { - const patterns = getSearchPatterns(undefined, true); + const patterns = getSearchPatterns(undefined); expect( patterns.some( (p) => p.includes('.claude/commands/') && p.includes('/'), @@ -295,7 +336,6 @@ if (import.meta.vitest != null) { const result = await scanClaudeFiles({ path: fixture.getPath('test-scan'), - recursive: false, // Don't scan recursively to avoid scanning home directory }); expect(Array.isArray(result)).toBe(true); @@ -313,7 +353,6 @@ if (import.meta.vitest != null) { async (f) => { const result = await scanClaudeFiles({ path: f.getPath('empty-dir'), - recursive: false, }); expect(result).toEqual([]); return f; @@ -344,7 +383,6 @@ if (import.meta.vitest != null) { const result = await scanClaudeFiles({ path: fixture.getPath('sort-test'), - recursive: false, }); // Local file should come first (newer) @@ -391,7 +429,6 @@ if (import.meta.vitest != null) { const result = await scanClaudeFiles({ path: fixture.getPath('my-app'), - recursive: false, // Don't scan recursively to avoid home directory scan }); expect(result.length).toBe(2); // CLAUDE.md and CLAUDE.local.md @@ -415,14 +452,12 @@ if (import.meta.vitest != null) { // Without includeHidden const withoutHidden = await scanClaudeFiles({ path: fixture.path, - recursive: false, // Don't scan recursively to avoid scanning home directory includeHidden: false, }); // With includeHidden const withHidden = await scanClaudeFiles({ path: fixture.path, - recursive: false, // Don't scan recursively to avoid scanning home directory includeHidden: true, }); @@ -430,4 +465,108 @@ if (import.meta.vitest != null) { expect(withHidden.length).toBe(2); }, 10000); // Add timeout for slower operations }); + + describe('scanClaudeFiles - subdirectory scanning', () => { + test('should find project CLAUDE.md files when run from subdirectory', async () => { + // Create a project structure with CLAUDE.md files + await using fixture = await createComplexProjectFixture(); + + // Simulate scanning from a project directory + const result = await scanClaudeFiles({ + path: fixture.getPath('my-app'), // Run from project root + }); + + // Should find CLAUDE.md files from parent directories + const projectFiles = result.filter( + (f) => f.type === 'project-memory' || f.type === 'project-memory-local', + ); + + // Should find at least the my-app/CLAUDE.md and my-app/CLAUDE.local.md + expect(projectFiles.length).toBeGreaterThanOrEqual(2); + }); + + test('should find files when scanning a project directory structure', async () => { + const { createFixture } = await import('fs-fixture'); + await using fixture = await createFixture({ + 'CLAUDE.md': DEFAULT_CLAUDE_MD, // Root level file + my_programs: { + project1: { + 'CLAUDE.md': DEFAULT_CLAUDE_MD, + '.git': {}, // Marker file + }, + }, + development: { + project2: { + 'CLAUDE.md': DEFAULT_CLAUDE_MD, + 'package.json': '{}', + }, + }, + Downloads: { + 'CLAUDE.md': DEFAULT_CLAUDE_MD, // Normal directory + }, + '.cache': { + 'CLAUDE.md': DEFAULT_CLAUDE_MD, // Hidden directory + }, + 'random-folder': { + 'CLAUDE.md': DEFAULT_CLAUDE_MD, // Should be found + }, + }); + + // Scan from the fixture path + const result = await scanClaudeFiles({ + path: fixture.path, + includeHidden: false, // Don't include hidden directories + }); + + // Non-recursive scan finds files at root and in first-level directories + // (excluding hidden directories like .cache) + expect(result.length).toBeGreaterThan(0); + const paths = result.map((f) => f.path); + + // Should find the root CLAUDE.md + expect( + paths.some((p) => { + const parts = p.split('/'); + return ( + parts[parts.length - 1] === 'CLAUDE.md' && + parts[parts.length - 2] !== undefined + ); + }), + ).toBe(true); + + // Should find files in first-level directories (except hidden) + expect(paths.some((p) => p.includes('Downloads/CLAUDE.md'))).toBe(true); + expect(paths.some((p) => p.includes('random-folder/CLAUDE.md'))).toBe( + true, + ); + + // Should NOT find files in hidden directories + expect(paths.some((p) => p.includes('.cache/CLAUDE.md'))).toBe(false); + + // Now it SHOULD find files in nested directories (always scans deeply) + expect(paths.some((p) => p.includes('project1/CLAUDE.md'))).toBe(true); + + // Now scan specific directories + const myProgramsResult = await scanClaudeFiles({ + path: join(fixture.path, 'my_programs'), + }); + + const devResult = await scanClaudeFiles({ + path: join(fixture.path, 'development'), + }); + + // Should find files in subdirectories + expect(myProgramsResult.length).toBeGreaterThan(0); + expect(devResult.length).toBeGreaterThan(0); + + // Verify the paths + const myProgramsPaths = myProgramsResult.map((f) => f.path); + expect( + myProgramsPaths.some((p) => p.includes('project1/CLAUDE.md')), + ).toBe(true); + + const devPaths = devResult.map((f) => f.path); + expect(devPaths.some((p) => p.includes('project2/CLAUDE.md'))).toBe(true); + }); + }); } diff --git a/src/fast-scanner.ts b/src/fast-scanner.ts index d46e8c5..e097290 100644 --- a/src/fast-scanner.ts +++ b/src/fast-scanner.ts @@ -7,7 +7,6 @@ import { DEFAULT_EXCLUSIONS } from './scan-exclusions.ts'; type CrawlerOptions = { readonly includeHidden: boolean; - readonly recursive: boolean; readonly maxDepth: number; }; @@ -37,9 +36,7 @@ const createBaseCrawler = (options: CrawlerOptions): fdir => { return false; }); - return options.recursive - ? crawler.withMaxDepth(options.maxDepth) - : crawler.withMaxDepth(options.maxDepth); + return crawler.withMaxDepth(options.maxDepth); }; /** @@ -50,16 +47,11 @@ const createBaseCrawler = (options: CrawlerOptions): fdir => { export const findClaudeFiles = async ( options: ScanOptions = {}, ): Promise => { - const { - path = process.cwd(), - recursive = true, - includeHidden = false, - } = options; + const { path = homedir(), includeHidden = false } = options; const crawler = createBaseCrawler({ includeHidden, - recursive, - maxDepth: recursive ? 20 : 1, + maxDepth: 20, // Always scan deeply }).filter((filePath) => { const fileName = basename(filePath); return CLAUDE_FILE_REGEX.test(fileName); @@ -84,16 +76,11 @@ export const findClaudeFiles = async ( export const findSlashCommands = async ( options: ScanOptions = {}, ): Promise => { - const { - path = process.cwd(), - recursive = true, - includeHidden = false, - } = options; + const { path = homedir(), includeHidden = false } = options; const crawler = createBaseCrawler({ includeHidden, - recursive, - maxDepth: recursive ? 20 : 3, + maxDepth: 20, }).filter((filePath) => { return ( (filePath.includes('/.claude/commands/') || @@ -121,18 +108,13 @@ export const findSlashCommands = async ( export const findSubAgents = async ( options: ScanOptions = {}, ): Promise => { - const { - path = process.cwd(), - recursive = true, - includeHidden = false, - } = options; + const { path = homedir(), includeHidden = false } = options; const results: string[] = []; const projectCrawler = createBaseCrawler({ includeHidden, - recursive, - maxDepth: recursive ? 20 : 4, + maxDepth: 20, }).filter((filePath) => { return filePath.includes('/.claude/agents/') && filePath.endsWith('.md'); }); @@ -167,16 +149,11 @@ export const findSubAgents = async ( export const findSettingsJson = async ( options: ScanOptions = {}, ): Promise => { - const { - path = process.cwd(), - recursive = true, - includeHidden = false, - } = options; + const { path = homedir(), includeHidden = false } = options; const crawler = createBaseCrawler({ includeHidden, - recursive, - maxDepth: recursive ? 20 : 4, + maxDepth: 20, }).filter((filePath) => { // Look for settings.json or settings.local.json in .claude directories const fileName = basename(filePath); @@ -249,7 +226,6 @@ if (import.meta.vitest != null) { const files = await findClaudeFiles({ path: fixture.getPath('scanner-test'), - recursive: false, }); expect(Array.isArray(files)).toBe(true); @@ -262,7 +238,7 @@ if (import.meta.vitest != null) { ).toBe(true); }); - test('should respect recursive option with nested structure', async () => { + test('should find files in nested structure', async () => { await using _fixture = await withTempFixture( { project: { @@ -275,18 +251,12 @@ if (import.meta.vitest != null) { }, }, async (f) => { - const nonRecursive = await findClaudeFiles({ - path: f.getPath('project'), - recursive: false, - }); - - const recursive = await findClaudeFiles({ + const files = await findClaudeFiles({ path: f.getPath('project'), - recursive: true, }); - expect(nonRecursive.length).toBe(1); - expect(recursive.length).toBe(2); + // Always scans deeply now + expect(files.length).toBe(2); return f; }, ); @@ -297,7 +267,6 @@ if (import.meta.vitest != null) { const commands = await findSlashCommands({ path: fixture.getPath('my-app'), - recursive: true, }); expect(Array.isArray(commands)).toBe(true); @@ -314,7 +283,6 @@ if (import.meta.vitest != null) { test('should handle non-existent paths gracefully', async () => { const files = await findClaudeFiles({ path: '/non/existent/path', - recursive: false, }); expect(Array.isArray(files)).toBe(true); @@ -337,7 +305,6 @@ if (import.meta.vitest != null) { async (f) => { const settings = await findSettingsJson({ path: f.path, - recursive: true, }); expect(Array.isArray(settings)).toBe(true); @@ -371,7 +338,6 @@ if (import.meta.vitest != null) { async (f) => { const files = await findClaudeFiles({ path: f.getPath('test-project'), - recursive: true, }); expect(files.length).toBe(1); @@ -401,7 +367,6 @@ if (import.meta.vitest != null) { const start = Date.now(); const files = await findClaudeFiles({ path: f.path, - recursive: true, }); const duration = Date.now() - start; @@ -431,14 +396,12 @@ if (import.meta.vitest != null) { // Without includeHidden const withoutHidden = await findClaudeFiles({ path: f.path, - recursive: true, includeHidden: false, }); // With includeHidden const withHidden = await findClaudeFiles({ path: f.path, - recursive: true, includeHidden: true, }); @@ -449,7 +412,6 @@ if (import.meta.vitest != null) { // Check slash commands - .claude should be included even without includeHidden const commands = await findSlashCommands({ path: f.path, - recursive: true, includeHidden: false, }); diff --git a/src/hooks/useFileNavigation.test.tsx b/src/hooks/useFileNavigation.test.tsx index 0774aee..0c5a59c 100644 --- a/src/hooks/useFileNavigation.test.tsx +++ b/src/hooks/useFileNavigation.test.tsx @@ -26,7 +26,7 @@ function TestComponent({ onFilesLoaded?: (files: ClaudeFileInfo[]) => void; }) { const { files, selectedFile, isLoading, error } = useFileNavigation( - { recursive: false }, + {}, scanner, ); @@ -80,20 +80,17 @@ if (import.meta.vitest) { scanClaudeFiles({ ...options, path: fixture.getPath('test-project'), - recursive: false, }), scanSlashCommands: (options) => scanSlashCommands({ ...options, path: fixture.getPath('test-project'), - recursive: false, }), scanSubAgents: async () => [], // No subagents in test scanSettingsJson: (options) => scanSettingsJson({ ...options, path: fixture.getPath('test-project'), - recursive: false, }), }; @@ -175,20 +172,17 @@ if (import.meta.vitest) { scanClaudeFiles({ ...options, path: fixture.getPath('accessible'), - recursive: false, }), scanSlashCommands: (options) => scanSlashCommands({ ...options, path: fixture.getPath('accessible'), - recursive: false, }), scanSubAgents: async () => [], // No subagents in test scanSettingsJson: (options) => scanSettingsJson({ ...options, path: fixture.getPath('accessible'), - recursive: false, }), }; @@ -224,20 +218,17 @@ if (import.meta.vitest) { scanClaudeFiles({ ...options, path: f.getPath('empty-project'), - recursive: false, }), scanSlashCommands: (options) => scanSlashCommands({ ...options, path: f.getPath('empty-project'), - recursive: false, }), scanSubAgents: async () => [], // No subagents in test scanSettingsJson: (options) => scanSettingsJson({ ...options, path: f.getPath('empty-project'), - recursive: false, }), }; @@ -274,20 +265,17 @@ if (import.meta.vitest) { scanClaudeFiles({ ...options, path: fixture.getPath('my-app'), - recursive: false, }), scanSlashCommands: (options) => scanSlashCommands({ ...options, path: fixture.getPath('my-app'), - recursive: false, }), scanSubAgents: async () => [], // No subagents in test scanSettingsJson: (options) => scanSettingsJson({ ...options, path: fixture.getPath('my-app'), - recursive: false, }), }; @@ -329,14 +317,12 @@ if (import.meta.vitest) { const localFiles = await scanClaudeFiles({ ...options, path: fixture.getPath('project'), - recursive: false, }); // Scan global files const globalFiles = await scanClaudeFiles({ ...options, path: fixture.getPath('.claude'), - recursive: false, }); // Combine results @@ -346,14 +332,12 @@ if (import.meta.vitest) { scanSlashCommands({ ...options, path: fixture.getPath('project'), - recursive: false, }), scanSubAgents: async () => [], // No subagents in test scanSettingsJson: (options) => scanSettingsJson({ ...options, path: fixture.getPath('project'), - recursive: false, }), }; @@ -388,20 +372,17 @@ if (import.meta.vitest) { scanClaudeFiles({ ...options, path: fixture.getPath('slash-project'), - recursive: false, }), scanSlashCommands: (options) => scanSlashCommands({ ...options, path: fixture.getPath('slash-project'), - recursive: false, }), scanSubAgents: async () => [], // No subagents in test scanSettingsJson: (options) => scanSettingsJson({ ...options, path: fixture.getPath('slash-project'), - recursive: false, }), }; @@ -435,20 +416,17 @@ if (import.meta.vitest) { scanClaudeFiles({ ...options, path: fixture.getPath('mixed-project'), - recursive: false, }), scanSlashCommands: (options) => scanSlashCommands({ ...options, path: fixture.getPath('mixed-project'), - recursive: false, }), scanSubAgents: async () => [], // No subagents in test scanSettingsJson: (options) => scanSettingsJson({ ...options, path: fixture.getPath('mixed-project'), - recursive: false, }), }; @@ -480,20 +458,17 @@ if (import.meta.vitest) { scanClaudeFiles({ ...options, path: fixture.getPath('update-test'), - recursive: false, }), scanSlashCommands: (options) => scanSlashCommands({ ...options, path: fixture.getPath('update-test'), - recursive: false, }), scanSubAgents: async () => [], // No subagents in test scanSettingsJson: (options) => scanSettingsJson({ ...options, path: fixture.getPath('update-test'), - recursive: false, }), }; @@ -536,20 +511,17 @@ if (import.meta.vitest) { scanClaudeFiles({ ...options, path: fixture.getPath('nested-project'), - recursive: true, // This test specifically tests recursive scanning }), scanSlashCommands: (options) => scanSlashCommands({ ...options, path: fixture.getPath('nested-project'), - recursive: true, // This test specifically tests recursive scanning }), scanSubAgents: async () => [], // No subagents in test scanSettingsJson: (options) => scanSettingsJson({ ...options, path: fixture.getPath('nested-project'), - recursive: true, // This test specifically tests recursive scanning }), }; @@ -628,10 +600,7 @@ if (import.meta.vitest) { // Create a test component that captures fileGroups function TestEmptyGroupsComponent({ scanner }: { scanner: FileScanner }) { - const { fileGroups, isLoading, error } = useFileNavigation( - { recursive: false }, - scanner, - ); + const { fileGroups, isLoading, error } = useFileNavigation({}, scanner); React.useEffect(() => { if (!isLoading) { @@ -857,10 +826,7 @@ if (import.meta.vitest) { // Create a test component that captures fileGroups function TestGroupOrderComponent({ scanner }: { scanner: FileScanner }) { - const { fileGroups, isLoading, error } = useFileNavigation( - { recursive: false }, - scanner, - ); + const { fileGroups, isLoading, error } = useFileNavigation({}, scanner); React.useEffect(() => { if (!isLoading) { diff --git a/src/hooks/useFileNavigation.tsx b/src/hooks/useFileNavigation.tsx index 929af71..ebec376 100644 --- a/src/hooks/useFileNavigation.tsx +++ b/src/hooks/useFileNavigation.tsx @@ -66,11 +66,11 @@ export function useFileNavigation( const [error, setError] = useState(); // Destructure object dependencies - const { path, recursive = true } = options; + const { path } = options; useEffect(() => { // Execute file scan - const scanOptions = { recursive, path }; + const scanOptions = { path }; Promise.all([ scanner.scanClaudeFiles(scanOptions), scanner.scanSlashCommands(scanOptions), @@ -166,7 +166,7 @@ export function useFileNavigation( setError(err.message || 'Failed to scan files'); setIsLoading(false); }); - }, [path, recursive, scanner]); + }, [path, scanner]); const selectFile = useCallback((file: NavigationFile): void => { setSelectedFile(file); diff --git a/src/settings-json-scanner.ts b/src/settings-json-scanner.ts index f797cdc..83ff8cf 100644 --- a/src/settings-json-scanner.ts +++ b/src/settings-json-scanner.ts @@ -45,36 +45,15 @@ class SettingsJsonScanner extends BaseFileScanner { export const scanSettingsJson = async ( options: ScanOptions = {}, ): Promise => { - const { - path = process.cwd(), - recursive = true, - includeHidden = false, - } = options; + const { homedir } = await import('node:os'); + const { path = homedir(), includeHidden = false } = options; - // Scan specified path + // Scan from the specified path const paths = await findSettingsJson({ path, - recursive, includeHidden, }); - // Also scan global Claude directory if scanning recursively - if (recursive) { - const { homedir } = await import('node:os'); - const { join } = await import('node:path'); - const globalClaudePath = join(homedir(), '.claude'); - - // Only scan global .claude directory if it's different from the current path - if (globalClaudePath !== path && !path.startsWith(globalClaudePath)) { - const globalFiles = await findSettingsJson({ - path: globalClaudePath, - recursive: false, - includeHidden, - }); - paths.push(...globalFiles); - } - } - // Remove duplicates based on file path const uniquePaths: string[] = Array.from(new Set(paths)); @@ -184,7 +163,6 @@ if (import.meta.vitest != null) { try { const results = await scanSettingsJson({ path: fixture.path, - recursive: false, // Don't scan home directory in tests }); expect(results).toHaveLength(3); // 2 settings.json + 1 settings.local.json diff --git a/src/slash-command-scanner.ts b/src/slash-command-scanner.ts index 390d617..85ede37 100644 --- a/src/slash-command-scanner.ts +++ b/src/slash-command-scanner.ts @@ -14,13 +14,9 @@ import { findSlashCommands } from './fast-scanner.ts'; export const scanSlashCommands = async ( options: ScanOptions = {}, ): Promise => { - const { - path = process.cwd(), - recursive = true, - includeHidden = false, - } = options; + const { path = homedir(), includeHidden = false } = options; - const _patterns = getSlashCommandPatterns(recursive); + const _patterns = getSlashCommandPatterns(); const searchPaths = [path, join(homedir(), '.claude', 'commands')]; try { @@ -34,7 +30,6 @@ export const scanSlashCommands = async ( // Use fast scanner for better performance and security const files = await findSlashCommands({ path: searchPath, - recursive, includeHidden, }); @@ -62,8 +57,8 @@ export const scanSlashCommands = async ( } }; -const getSlashCommandPatterns = (recursive = true): string[] => { - const prefix = recursive ? '**/' : ''; +const getSlashCommandPatterns = (): string[] => { + const prefix = '**/'; return [ `${prefix}.claude/commands/**/*.md`, `${prefix}commands/**/*.md`, // Alternative location @@ -172,17 +167,11 @@ if (import.meta.vitest != null) { const { describe, test, expect } = import.meta.vitest; describe('getSlashCommandPatterns', () => { - test('should return command patterns with recursion', () => { - const patterns = getSlashCommandPatterns(true); + test('should return command patterns', () => { + const patterns = getSlashCommandPatterns(); expect(patterns).toContain('**/.claude/commands/**/*.md'); expect(patterns).toContain('**/commands/**/*.md'); }); - - test('should return patterns without recursion', () => { - const patterns = getSlashCommandPatterns(false); - expect(patterns).toContain('.claude/commands/**/*.md'); - expect(patterns).toContain('commands/**/*.md'); - }); }); describe('getRelativeCommandPath', () => { diff --git a/src/subagent-scanner.ts b/src/subagent-scanner.ts index 28ae2e4..75714df 100644 --- a/src/subagent-scanner.ts +++ b/src/subagent-scanner.ts @@ -93,7 +93,6 @@ This is the system prompt for the test agent.`; async (f) => { const results = await scanSubAgents({ path: f.path, - recursive: true, }); // Find test agent by name and scope (project scope means it's from our test directory) @@ -130,7 +129,6 @@ System prompt content.`; async (f) => { const results = await scanSubAgents({ path: f.path, - recursive: true, }); // Find broken agent by name and scope