fix(coding-agent): respect ignore files in skill loader

This commit is contained in:
Mario Zechner 2026-02-05 20:24:15 +01:00
parent 91e09765e7
commit f89b49baeb
3 changed files with 86 additions and 3 deletions

View file

@ -1,6 +1,7 @@
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "fs";
import ignore from "ignore";
import { homedir } from "os";
import { basename, dirname, isAbsolute, join, resolve, sep } from "path";
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "path";
import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
import { parseFrontmatter } from "../utils/frontmatter.js";
import type { ResourceDiagnostic } from "./diagnostics.js";
@ -11,6 +12,57 @@ const MAX_NAME_LENGTH = 64;
/** Max description length per spec */
const MAX_DESCRIPTION_LENGTH = 1024;
const IGNORE_FILE_NAMES = [".gitignore", ".ignore", ".fdignore"];
type IgnoreMatcher = ReturnType<typeof ignore>;
function toPosixPath(p: string): string {
return p.split(sep).join("/");
}
function prefixIgnorePattern(line: string, prefix: string): string | null {
const trimmed = line.trim();
if (!trimmed) return null;
if (trimmed.startsWith("#") && !trimmed.startsWith("\\#")) return null;
let pattern = line;
let negated = false;
if (pattern.startsWith("!")) {
negated = true;
pattern = pattern.slice(1);
} else if (pattern.startsWith("\\!")) {
pattern = pattern.slice(1);
}
if (pattern.startsWith("/")) {
pattern = pattern.slice(1);
}
const prefixed = prefix ? `${prefix}${pattern}` : pattern;
return negated ? `!${prefixed}` : prefixed;
}
function addIgnoreRules(ig: IgnoreMatcher, dir: string, rootDir: string): void {
const relativeDir = relative(rootDir, dir);
const prefix = relativeDir ? `${toPosixPath(relativeDir)}/` : "";
for (const filename of IGNORE_FILE_NAMES) {
const ignorePath = join(dir, filename);
if (!existsSync(ignorePath)) continue;
try {
const content = readFileSync(ignorePath, "utf-8");
const patterns = content
.split(/\r?\n/)
.map((line) => prefixIgnorePattern(line, prefix))
.filter((line): line is string => Boolean(line));
if (patterns.length > 0) {
ig.add(patterns);
}
} catch {}
}
}
export interface SkillFrontmatter {
name?: string;
description?: string;
@ -96,7 +148,13 @@ export function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkills
return loadSkillsFromDirInternal(dir, source, true);
}
function loadSkillsFromDirInternal(dir: string, source: string, includeRootFiles: boolean): LoadSkillsResult {
function loadSkillsFromDirInternal(
dir: string,
source: string,
includeRootFiles: boolean,
ignoreMatcher?: IgnoreMatcher,
rootDir?: string,
): LoadSkillsResult {
const skills: Skill[] = [];
const diagnostics: ResourceDiagnostic[] = [];
@ -104,6 +162,10 @@ function loadSkillsFromDirInternal(dir: string, source: string, includeRootFiles
return { skills, diagnostics };
}
const root = rootDir ?? dir;
const ig = ignoreMatcher ?? ignore();
addIgnoreRules(ig, dir, root);
try {
const entries = readdirSync(dir, { withFileTypes: true });
@ -133,8 +195,14 @@ function loadSkillsFromDirInternal(dir: string, source: string, includeRootFiles
}
}
const relPath = toPosixPath(relative(root, fullPath));
const ignorePath = isDirectory ? `${relPath}/` : relPath;
if (ig.ignores(ignorePath)) {
continue;
}
if (isDirectory) {
const subResult = loadSkillsFromDirInternal(fullPath, source, false);
const subResult = loadSkillsFromDirInternal(fullPath, source, false, ig, root);
skills.push(...subResult.skills);
diagnostics.push(...subResult.diagnostics);
continue;