diff --git a/.agents/rules/base.md b/.agents/rules/base.md index 2279a52b6..161ba35b4 100644 --- a/.agents/rules/base.md +++ b/.agents/rules/base.md @@ -74,6 +74,11 @@ repomix/ - Description should be clear and concise in present tense - Description must start with a capital letter +### Commit Body Guidelines +- Include context about what led to this commit +- Describe the conversation or problem that motivated the change +- This helps preserve the decision-making process for future reference + ## Pull Request Guidelines - All pull requests must follow the template: ```md diff --git a/.gitignore b/.gitignore index 240f0a933..e11026aee 100644 --- a/.gitignore +++ b/.gitignore @@ -39,9 +39,14 @@ repomix-output.md # aider .aider* -# private files +# repomix runner +.repomix/ + +# Agent .cursor/rules/ -.github/todo.md .mcp.json +.agents/local/ + +# private files .private/ -.agents/local/ \ No newline at end of file +.env diff --git a/src/cli/actions/defaultAction.ts b/src/cli/actions/defaultAction.ts index afbda104e..3d19ea907 100644 --- a/src/cli/actions/defaultAction.ts +++ b/src/cli/actions/defaultAction.ts @@ -81,19 +81,15 @@ export const handleStdinProcessing = async ( spinner.update('Packing files...'); - // Create a custom pack variant that uses the stdin file paths directly + // Use pack with predefined files from stdin packResult = await pack( [cwd], config, (message) => { spinner.update(message); }, - { - searchFiles: async () => ({ - filePaths: stdinResult.filePaths.map((filePath) => path.relative(cwd, filePath)), - emptyDirPaths: stdinResult.emptyDirPaths, - }), - }, + {}, + stdinResult.filePaths, ); } catch (error) { spinner.fail('Error reading from stdin or during packing'); diff --git a/src/core/file/fileSearch.ts b/src/core/file/fileSearch.ts index 5e6cd7bec..974822291 100644 --- a/src/core/file/fileSearch.ts +++ b/src/core/file/fileSearch.ts @@ -92,7 +92,11 @@ export const normalizeGlobPattern = (pattern: string): string => { }; // Get all file paths considering the config -export const searchFiles = async (rootDir: string, config: RepomixConfigMerged): Promise => { +export const searchFiles = async ( + rootDir: string, + config: RepomixConfigMerged, + explicitFiles?: string[], +): Promise => { // Check if the path exists and get its type let pathStats: Stats; try { @@ -140,9 +144,6 @@ export const searchFiles = async (rootDir: string, config: RepomixConfigMerged): ); } - const includePatterns = - config.include.length > 0 ? config.include.map((pattern) => escapeGlobPattern(pattern)) : ['**/*']; - try { const [ignorePatterns, ignoreFilePatterns] = await Promise.all([ getIgnorePatterns(rootDir, config), @@ -152,7 +153,6 @@ export const searchFiles = async (rootDir: string, config: RepomixConfigMerged): // Normalize ignore patterns to handle trailing slashes consistently const normalizedIgnorePatterns = ignorePatterns.map(normalizeGlobPattern); - logger.trace('Include patterns:', includePatterns); logger.trace('Ignore patterns:', normalizedIgnorePatterns); logger.trace('Ignore file patterns:', ignoreFilePatterns); @@ -171,6 +171,26 @@ export const searchFiles = async (rootDir: string, config: RepomixConfigMerged): } } + // Start with configured include patterns + let includePatterns = config.include.map((pattern) => escapeGlobPattern(pattern)); + + // If explicit files are provided, add them to include patterns + if (explicitFiles) { + const relativePaths = explicitFiles.map((filePath) => { + const relativePath = path.relative(rootDir, filePath); + // Escape the path to handle special characters + return escapeGlobPattern(relativePath); + }); + includePatterns = [...includePatterns, ...relativePaths]; + } + + // If no include patterns at all, default to all files + if (includePatterns.length === 0) { + includePatterns = ['**/*']; + } + + logger.trace('Include patterns with explicit files:', includePatterns); + const filePaths = await globby(includePatterns, { cwd: rootDir, ignore: [...adjustedIgnorePatterns], diff --git a/src/core/packager.ts b/src/core/packager.ts index c259175ec..a229c4635 100644 --- a/src/core/packager.ts +++ b/src/core/packager.ts @@ -44,6 +44,7 @@ export const pack = async ( config: RepomixConfigMerged, progressCallback: RepomixProgressCallback = () => {}, overrideDeps: Partial = {}, + explicitFiles?: string[], ): Promise => { const deps = { ...defaultDeps, @@ -54,7 +55,7 @@ export const pack = async ( const filePathsByDir = await Promise.all( rootDirs.map(async (rootDir) => ({ rootDir, - filePaths: (await deps.searchFiles(rootDir, config)).filePaths, + filePaths: (await deps.searchFiles(rootDir, config, explicitFiles)).filePaths, })), ); diff --git a/tests/cli/actions/defaultAction.test.ts b/tests/cli/actions/defaultAction.test.ts index fdb9937d7..741400eea 100644 --- a/tests/cli/actions/defaultAction.test.ts +++ b/tests/cli/actions/defaultAction.test.ts @@ -1,5 +1,6 @@ import path from 'node:path'; import process from 'node:process'; +import { globby } from 'globby'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { buildCliConfig, @@ -14,6 +15,7 @@ import * as packageJsonParser from '../../../src/core/file/packageJsonParse.js'; import * as packager from '../../../src/core/packager.js'; import type { PackResult } from '../../../src/core/packager.js'; +vi.mock('globby'); vi.mock('../../../src/core/packager'); vi.mock('../../../src/config/configLoad'); vi.mock('../../../src/core/file/packageJsonParse'); @@ -27,6 +29,8 @@ describe('defaultAction', () => { vi.resetAllMocks(); vi.mocked(packageJsonParser.getVersion).mockResolvedValue('1.0.0'); vi.mocked(configLoader.loadFileConfig).mockResolvedValue({}); + // Default globby mock + vi.mocked(globby).mockResolvedValue([]); vi.mocked(configLoader.mergeConfigs).mockReturnValue({ cwd: process.cwd(), input: { @@ -684,24 +688,15 @@ describe('defaultAction', () => { vi.mocked(fileStdin.readFilePathsFromStdin).mockResolvedValue(stdinResult); - await handleStdinProcessing(['.'], testCwd, mockConfig, mockCliOptions); + // Mock globby to return the expected filtered files (sorted by sortPaths) + vi.mocked(globby).mockResolvedValue([path.join('subdir', 'file2.txt'), 'file1.txt']); - expect(packager.pack).toHaveBeenCalledWith([testCwd], mockConfig, expect.any(Function), { - searchFiles: expect.any(Function), - }); + await handleStdinProcessing(['.'], testCwd, mockConfig, mockCliOptions); - // Test the searchFiles function - const packCall = vi.mocked(packager.pack).mock.calls[0]; - const searchFiles = packCall[3]?.searchFiles; - expect(searchFiles).toBeDefined(); - - if (searchFiles) { - const searchResult = await searchFiles(testCwd, mockConfig); - expect(searchResult).toEqual({ - filePaths: ['file1.txt', path.join('subdir', 'file2.txt')], - emptyDirPaths: [path.resolve(testCwd, 'emptydir')], - }); - } + expect(packager.pack).toHaveBeenCalledWith([testCwd], mockConfig, expect.any(Function), {}, [ + path.join(testCwd, 'file1.txt'), + path.join(testCwd, 'subdir', 'file2.txt'), + ]); }); it('should propagate errors from readFilePathsFromStdin', async () => { diff --git a/tests/core/file/fileSearch.test.ts b/tests/core/file/fileSearch.test.ts index 3b3e5b4c3..de6dad94e 100644 --- a/tests/core/file/fileSearch.test.ts +++ b/tests/core/file/fileSearch.test.ts @@ -562,5 +562,52 @@ node_modules expect(result.filePaths).toEqual(['test.js']); expect(result.emptyDirPaths).toEqual([]); }); + + test('should filter explicit files based on include and ignore patterns', async () => { + const mockConfig = createMockConfig({ + include: ['**/*.ts'], + ignore: { + useGitignore: false, + useDefaultPatterns: false, + customPatterns: ['**/*.test.ts'], + }, + }); + + const explicitFiles = [ + '/test/src/file1.ts', + '/test/src/file1.test.ts', + '/test/src/file2.js', + '/test/src/file3.ts', + ]; + + // Mock globby to return the expected filtered files + vi.mocked(globby).mockResolvedValue(['src/file1.ts', 'src/file3.ts']); + + const result = await searchFiles('/test', mockConfig, explicitFiles); + + expect(result.filePaths).toEqual(['src/file1.ts', 'src/file3.ts']); + expect(result.emptyDirPaths).toEqual([]); + }); + + test('should handle explicit files with ignore patterns only', async () => { + const mockConfig = createMockConfig({ + include: [], + ignore: { + useGitignore: false, + useDefaultPatterns: false, + customPatterns: ['tests/**'], + }, + }); + + const explicitFiles = ['/test/src/main.ts', '/test/tests/unit.test.ts', '/test/lib/utils.ts']; + + // Mock globby to return the expected filtered files + vi.mocked(globby).mockResolvedValue(['src/main.ts', 'lib/utils.ts']); + + const result = await searchFiles('/test', mockConfig, explicitFiles); + + expect(result.filePaths).toEqual(['lib/utils.ts', 'src/main.ts']); + expect(result.emptyDirPaths).toEqual([]); + }); }); }); diff --git a/tests/core/packager.test.ts b/tests/core/packager.test.ts index d0ea82626..5e154ecb1 100644 --- a/tests/core/packager.test.ts +++ b/tests/core/packager.test.ts @@ -71,7 +71,7 @@ describe('packager', () => { const progressCallback = vi.fn(); const result = await pack(['root'], mockConfig, progressCallback, mockDeps); - expect(mockDeps.searchFiles).toHaveBeenCalledWith('root', mockConfig); + expect(mockDeps.searchFiles).toHaveBeenCalledWith('root', mockConfig, undefined); expect(mockDeps.collectFiles).toHaveBeenCalledWith(mockFilePaths, 'root', mockConfig, progressCallback); expect(mockDeps.validateFileSafety).toHaveBeenCalled(); expect(mockDeps.processFiles).toHaveBeenCalled();