Skip to content

Commit 373fe44

Browse files
committed
feat: support dot-folder scanning via .codesightignore negation (#41)
Scanner previously skipped any entry whose name begins with `.` (except .env/.env.example/.env.local), making it impossible to scan projects that keep code under folders like `.source/` or `.config/`. Add gitignore-style negation: lines prefixed with `!` in .codesightignore explicitly re-include an entry that would otherwise be skipped, and negations override positive ignore patterns for the same name. Example .codesightignore: .* !.source !.config - Skips most dot-folders (default behavior preserved) - Walks into .source/ and .config/ - Negation wins when same name appears in both lists Closes #41
1 parent 254710e commit 373fe44

3 files changed

Lines changed: 105 additions & 9 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codesight",
3-
"version": "1.13.1",
3+
"version": "1.14.0",
44
"description": "See your codebase clearly. Universal AI context generator that maps routes, schema, components, dependencies, and more for Claude Code, Cursor, Copilot, Codex, and any AI coding tool.",
55
"main": "dist/index.js",
66
"bin": {

src/scanner.ts

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -126,19 +126,41 @@ export async function collectFiles(
126126
): Promise<string[]> {
127127
const files: string[] = [];
128128

129-
// Build a set of exact dir names to skip (simple patterns like "data", "fixtures")
130-
// Also support simple glob-style with trailing /* or /**
131-
const extraIgnore = new Set(
132-
ignorePatterns.map((p) => p.replace(/\/\*\*?$/, "").replace(/^\//, ""))
133-
);
129+
// Split positive ignores from gitignore-style negations (`!pattern`). A
130+
// negation explicitly re-includes an entry that would otherwise be skipped
131+
// — including dot-folders, which are skipped by default.
132+
const positivePatterns: string[] = [];
133+
const negationPatterns: string[] = [];
134+
for (const raw of ignorePatterns) {
135+
if (raw.startsWith("!")) {
136+
negationPatterns.push(raw.slice(1));
137+
} else {
138+
positivePatterns.push(raw);
139+
}
140+
}
141+
142+
const normalize = (p: string) => p.replace(/\/\*\*?$/, "").replace(/^\//, "");
143+
const extraIgnore = new Set(positivePatterns.map(normalize));
144+
const negationNames = new Set(negationPatterns.map(normalize));
145+
146+
function isExplicitlyIncluded(name: string, fullPath: string): boolean {
147+
if (negationNames.has(name)) return true;
148+
const rel = fullPath.replace(root, "").replace(/^[/\\]/, "");
149+
for (const pattern of negationPatterns) {
150+
const clean = normalize(pattern);
151+
if (rel === clean || rel.startsWith(clean + "/") || rel.startsWith(clean + "\\")) return true;
152+
}
153+
return false;
154+
}
134155

135156
function shouldIgnoreDir(name: string, fullPath: string): boolean {
157+
if (isExplicitlyIncluded(name, fullPath)) return false;
136158
if (IGNORE_DIRS.has(name)) return true;
137159
if (extraIgnore.has(name)) return true;
138160
// Check if any pattern matches a path segment
139161
const rel = fullPath.replace(root, "").replace(/^[/\\]/, "");
140-
for (const pattern of ignorePatterns) {
141-
const clean = pattern.replace(/\/\*\*?$/, "").replace(/^\//, "");
162+
for (const pattern of positivePatterns) {
163+
const clean = normalize(pattern);
142164
if (rel === clean || rel.startsWith(clean + "/") || rel.startsWith(clean + "\\")) return true;
143165
}
144166
return false;
@@ -153,8 +175,14 @@ export async function collectFiles(
153175
return;
154176
}
155177
for (const entry of entries) {
156-
if (entry.name.startsWith(".") && entry.name !== ".env" && entry.name !== ".env.example" && entry.name !== ".env.local") continue;
157178
const fullPath = join(dir, entry.name);
179+
if (
180+
entry.name.startsWith(".") &&
181+
entry.name !== ".env" &&
182+
entry.name !== ".env.example" &&
183+
entry.name !== ".env.local" &&
184+
!isExplicitlyIncluded(entry.name, fullPath)
185+
) continue;
158186
if (entry.isDirectory()) {
159187
if (shouldIgnoreDir(entry.name, fullPath)) continue;
160188
await walk(fullPath, depth + 1);

tests/detectors.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,3 +1300,71 @@ end sub
13001300
);
13011301
});
13021302
});
1303+
1304+
// =================== DOT-FOLDER INCLUSION TESTS (issue #41) ===================
1305+
1306+
describe("Dot-folder inclusion via .codesightignore negation", async () => {
1307+
const mods = await loadModules();
1308+
1309+
it("skips dot-folders by default", async () => {
1310+
const dir = await writeFixture("dotfolder-default-skip", {
1311+
"package.json": JSON.stringify({ name: "test" }),
1312+
"src/index.ts": "export const a = 1;",
1313+
".source/lib.ts": "export const fromDot = 1;",
1314+
});
1315+
const files = await mods.collectFiles(dir);
1316+
assert.ok(
1317+
files.some((f: string) => f.endsWith("src/index.ts")),
1318+
"expected src/index.ts in default scan"
1319+
);
1320+
assert.ok(
1321+
!files.some((f: string) => f.includes(".source/")),
1322+
`did not expect .source/* in default scan, got: ${files.join(", ")}`
1323+
);
1324+
});
1325+
1326+
it("includes a dot-folder when explicitly unignored via !.name", async () => {
1327+
const dir = await writeFixture("dotfolder-negate-include", {
1328+
"package.json": JSON.stringify({ name: "test" }),
1329+
"src/index.ts": "export const a = 1;",
1330+
".source/lib.ts": "export const fromDot = 1;",
1331+
".config/skip.ts": "export const skipped = 1;",
1332+
});
1333+
// .codesightignore unignores .source but leaves .config alone
1334+
const files = await mods.collectFiles(dir, 10, ["!.source"]);
1335+
assert.ok(
1336+
files.some((f: string) => f.endsWith(".source/lib.ts")),
1337+
`expected .source/lib.ts in scan, got: ${files.join(", ")}`
1338+
);
1339+
assert.ok(
1340+
!files.some((f: string) => f.includes(".config/")),
1341+
`did not expect .config/* in scan, got: ${files.join(", ")}`
1342+
);
1343+
});
1344+
1345+
it("supports multiple negated dot-folders", async () => {
1346+
const dir = await writeFixture("dotfolder-multi-negate", {
1347+
"package.json": JSON.stringify({ name: "test" }),
1348+
".source/a.ts": "export const a = 1;",
1349+
".config/b.ts": "export const b = 1;",
1350+
".hidden/c.ts": "export const c = 1;",
1351+
});
1352+
const files = await mods.collectFiles(dir, 10, ["!.source", "!.config"]);
1353+
assert.ok(files.some((f: string) => f.endsWith(".source/a.ts")), `expected .source/a.ts`);
1354+
assert.ok(files.some((f: string) => f.endsWith(".config/b.ts")), `expected .config/b.ts`);
1355+
assert.ok(!files.some((f: string) => f.includes(".hidden/")), `did not expect .hidden/*`);
1356+
});
1357+
1358+
it("negation overrides positive ignore for same name", async () => {
1359+
const dir = await writeFixture("dotfolder-negate-overrides", {
1360+
"package.json": JSON.stringify({ name: "test" }),
1361+
".source/lib.ts": "export const x = 1;",
1362+
});
1363+
// Even if user lists .source as a positive ignore, the negation wins.
1364+
const files = await mods.collectFiles(dir, 10, [".source", "!.source"]);
1365+
assert.ok(
1366+
files.some((f: string) => f.endsWith(".source/lib.ts")),
1367+
`expected .source/lib.ts (negation should override), got: ${files.join(", ")}`
1368+
);
1369+
});
1370+
});

0 commit comments

Comments
 (0)