diff --git a/.changeset/clear-boats-rhyme.md b/.changeset/clear-boats-rhyme.md new file mode 100644 index 000000000..b8c67fc3a --- /dev/null +++ b/.changeset/clear-boats-rhyme.md @@ -0,0 +1,5 @@ +--- +"@nanocollective/nanocoder": minor +--- + +Added support for a .nanocoderignore file, letting you hide tracked-but-unwanted files (lockfiles, .env, generated fixtures) from the AI even when .gitignore doesn't cover them. Thanks to @A-S-Manoj. Closes #755. diff --git a/docs/features/file-explorer.md b/docs/features/file-explorer.md index 02f2e7acb..f8b80157a 100644 --- a/docs/features/file-explorer.md +++ b/docs/features/file-explorer.md @@ -78,3 +78,5 @@ Sometimes you want to browse your project and pick files visually rather than ty ``` The explorer respects your `.gitignore`, so you won't see `node_modules`, `dist`, or other ignored directories. + +It also respects an optional `.nanocoderignore`. Use this for files that are tracked in git (and so aren't covered by `.gitignore`) but that you don't want the AI to read (large lockfiles like `package-lock.json`, generated fixtures, or sensitive files like `.env`). Patterns in `.nanocoderignore` use the same syntax as `.gitignore`. diff --git a/source/utils/gitignore-loader.spec.ts b/source/utils/gitignore-loader.spec.ts index 6c0e2d478..4197bda1a 100644 --- a/source/utils/gitignore-loader.spec.ts +++ b/source/utils/gitignore-loader.spec.ts @@ -84,6 +84,65 @@ test.serial('loadGitignore works without .gitignore file', async t => { } }); +test.serial('loadGitignore loads .nanocoderignore patterns', async t => { + const testDir = join(process.cwd(), 'test-nanocoderignore-temp'); + + try { + mkdirSync(testDir, {recursive: true}); + writeFileSync(join(testDir, '.nanocoderignore'), '*.secret\ndata/\n'); + + const ig = loadGitignore(testDir); + + t.true(ig.ignores('api.secret'), 'Should ignore .secret files'); + t.true(ig.ignores('data/dump.csv'), 'Should ignore data/ directory'); + t.false(ig.ignores('file.ts'), 'Should not ignore unrelated files'); + } finally { + rmSync(testDir, {recursive: true, force: true}); + } +}); + +test.serial( + 'loadGitignore merges .gitignore and .nanocoderignore patterns', + async t => { + const testDir = join(process.cwd(), 'test-both-ignores-temp'); + + try { + mkdirSync(testDir, {recursive: true}); + writeFileSync(join(testDir, '.gitignore'), '*.log\n'); + writeFileSync(join(testDir, '.nanocoderignore'), '.env\npackage-lock.json\n'); + + const ig = loadGitignore(testDir); + + t.true(ig.ignores('file.log'), 'Should ignore .gitignore patterns'); + t.true(ig.ignores('.env'), 'Should ignore .nanocoderignore patterns'); + t.true( + ig.ignores('package-lock.json'), + 'Should ignore committed files listed in .nanocoderignore', + ); + t.false(ig.ignores('file.ts'), 'Should not ignore unrelated files'); + } finally { + rmSync(testDir, {recursive: true, force: true}); + } + }, +); + +test.serial('loadGitignore works without .nanocoderignore file', async t => { + const testDir = join(process.cwd(), 'test-no-nanocoderignore-temp'); + + try { + mkdirSync(testDir, {recursive: true}); + // No .nanocoderignore file + + const ig = loadGitignore(testDir); + + // Should still have default ignores and not throw + t.true(ig.ignores('node_modules/file.js')); + t.false(ig.ignores('src/file.ts')); + } finally { + rmSync(testDir, {recursive: true, force: true}); + } +}); + test('loadGitignore ignores all language-specific directories', t => { const ig = loadGitignore(process.cwd()); diff --git a/source/utils/gitignore-loader.ts b/source/utils/gitignore-loader.ts index 40bfe9e24..866aa2cb1 100644 --- a/source/utils/gitignore-loader.ts +++ b/source/utils/gitignore-loader.ts @@ -38,15 +38,22 @@ const DEFAULT_IGNORE_DIRS = [ ]; /** - * Load and parse .gitignore file, returns an ignore instance. + * Load and parse .gitignore and .nanocoderignore files, returns an ignore instance. * Always includes default ignore patterns for common directories. * - * @param cwd - The current working directory to load .gitignore from + * .nanocoderignore is an additional, nanocoder-specific ignore file. It is useful + * for hiding files from the AI (to save tokens / avoid context bloat) even when + * those files are tracked in git and therefore not covered by .gitignore + * (e.g. package-lock.json, large fixtures, or sensitive files like .env that are + * intentionally committed). + * + * @param cwd - The current working directory to load .gitignore / .nanocoderignore from * @returns An ignore instance configured with patterns */ export function loadGitignore(cwd: string): ReturnType { const ig = ignore(); const gitignorePath = join(cwd, '.gitignore'); + const nanocoderignorePath = join(cwd, '.nanocoderignore'); // Always ignore common directories ig.add(DEFAULT_IGNORE_DIRS); @@ -62,6 +69,18 @@ export function loadGitignore(cwd: string): ReturnType { } } + // Load .nanocoderignore if it exists. Patterns are additive on top of + // .gitignore and the default ignores, not a replacement for them. + if (existsSync(nanocoderignorePath)) { + try { + const nanocoderignoreContent = readFileSync(nanocoderignorePath, 'utf-8'); + ig.add(nanocoderignoreContent); + } catch { + // Silently fail if we can't read .nanocoderignore + // The gitignore + hardcoded ignores above will still apply + } + } + return ig; }