mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 16:04:03 +00:00
fix(coding-agent): respect .gitignore/.ignore/.fdignore when scanning package resources
fixes #1072
This commit is contained in:
parent
678e3e28bd
commit
098f396cf3
5 changed files with 140 additions and 5 deletions
|
|
@ -23,6 +23,7 @@ read README.md, then ask which module(s) to work on. Based on the answer, read t
|
|||
- After code changes (not documentation changes): `npm run check` (get full output, no tail). Fix all errors, warnings, and infos before committing.
|
||||
- NEVER run: `npm run dev`, `npm run build`, `npm test`
|
||||
- Only run specific tests if user instructs: `npm test -- test/specific.test.ts`
|
||||
- When writing tests, run them, identify issues in either the test or implementation, and iterate until fixed.
|
||||
- NEVER commit unless user asks
|
||||
|
||||
## GitHub Issues
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
### Fixed
|
||||
|
||||
- Read tool now handles macOS filenames with curly quotes (U+2019) and NFD Unicode normalization ([#1078](https://github.com/badlogic/pi-mono/issues/1078))
|
||||
- Respect .gitignore, .ignore, and .fdignore files when scanning package resources for skills, prompts, themes, and extensions ([#1072](https://github.com/badlogic/pi-mono/issues/1072))
|
||||
|
||||
## [0.50.3] - 2026-01-29
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@
|
|||
"diff": "^8.0.2",
|
||||
"file-type": "^21.1.1",
|
||||
"glob": "^11.0.3",
|
||||
"ignore": "^7.0.5",
|
||||
"marked": "^15.0.12",
|
||||
"minimatch": "^10.1.1",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import { spawn, spawnSync } from "node:child_process";
|
|||
import { createHash } from "node:crypto";
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
import { basename, dirname, join, relative, resolve } from "node:path";
|
||||
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
||||
import ignore from "ignore";
|
||||
import { minimatch } from "minimatch";
|
||||
import { CONFIG_DIR_NAME } from "../config.js";
|
||||
import { looksLikeGitUrl } from "../utils/git.js";
|
||||
|
|
@ -115,6 +116,57 @@ const FILE_PATTERNS: Record<ResourceType, RegExp> = {
|
|||
themes: /\.json$/,
|
||||
};
|
||||
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
|
||||
function isPattern(s: string): boolean {
|
||||
return s.startsWith("!") || s.startsWith("+") || s.startsWith("-") || s.includes("*") || s.includes("?");
|
||||
}
|
||||
|
|
@ -132,10 +184,20 @@ function splitPatterns(entries: string[]): { plain: string[]; patterns: string[]
|
|||
return { plain, patterns };
|
||||
}
|
||||
|
||||
function collectFiles(dir: string, filePattern: RegExp, skipNodeModules = true): string[] {
|
||||
function collectFiles(
|
||||
dir: string,
|
||||
filePattern: RegExp,
|
||||
skipNodeModules = true,
|
||||
ignoreMatcher?: IgnoreMatcher,
|
||||
rootDir?: string,
|
||||
): string[] {
|
||||
const files: string[] = [];
|
||||
if (!existsSync(dir)) return files;
|
||||
|
||||
const root = rootDir ?? dir;
|
||||
const ig = ignoreMatcher ?? ignore();
|
||||
addIgnoreRules(ig, dir, root);
|
||||
|
||||
try {
|
||||
const entries = readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
|
|
@ -156,8 +218,12 @@ function collectFiles(dir: string, filePattern: RegExp, skipNodeModules = true):
|
|||
}
|
||||
}
|
||||
|
||||
const relPath = toPosixPath(relative(root, fullPath));
|
||||
const ignorePath = isDir ? `${relPath}/` : relPath;
|
||||
if (ig.ignores(ignorePath)) continue;
|
||||
|
||||
if (isDir) {
|
||||
files.push(...collectFiles(fullPath, filePattern, skipNodeModules));
|
||||
files.push(...collectFiles(fullPath, filePattern, skipNodeModules, ig, root));
|
||||
} else if (isFile && filePattern.test(entry.name)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
|
|
@ -169,10 +235,19 @@ function collectFiles(dir: string, filePattern: RegExp, skipNodeModules = true):
|
|||
return files;
|
||||
}
|
||||
|
||||
function collectSkillEntries(dir: string, includeRootFiles = true): string[] {
|
||||
function collectSkillEntries(
|
||||
dir: string,
|
||||
includeRootFiles = true,
|
||||
ignoreMatcher?: IgnoreMatcher,
|
||||
rootDir?: string,
|
||||
): string[] {
|
||||
const entries: string[] = [];
|
||||
if (!existsSync(dir)) return entries;
|
||||
|
||||
const root = rootDir ?? dir;
|
||||
const ig = ignoreMatcher ?? ignore();
|
||||
addIgnoreRules(ig, dir, root);
|
||||
|
||||
try {
|
||||
const dirEntries = readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of dirEntries) {
|
||||
|
|
@ -193,8 +268,12 @@ function collectSkillEntries(dir: string, includeRootFiles = true): string[] {
|
|||
}
|
||||
}
|
||||
|
||||
const relPath = toPosixPath(relative(root, fullPath));
|
||||
const ignorePath = isDir ? `${relPath}/` : relPath;
|
||||
if (ig.ignores(ignorePath)) continue;
|
||||
|
||||
if (isDir) {
|
||||
entries.push(...collectSkillEntries(fullPath, false));
|
||||
entries.push(...collectSkillEntries(fullPath, false, ig, root));
|
||||
} else if (isFile) {
|
||||
const isRootMd = includeRootFiles && entry.name.endsWith(".md");
|
||||
const isSkillMd = !includeRootFiles && entry.name === "SKILL.md";
|
||||
|
|
@ -218,6 +297,9 @@ function collectAutoPromptEntries(dir: string): string[] {
|
|||
const entries: string[] = [];
|
||||
if (!existsSync(dir)) return entries;
|
||||
|
||||
const ig = ignore();
|
||||
addIgnoreRules(ig, dir, dir);
|
||||
|
||||
try {
|
||||
const dirEntries = readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of dirEntries) {
|
||||
|
|
@ -234,6 +316,9 @@ function collectAutoPromptEntries(dir: string): string[] {
|
|||
}
|
||||
}
|
||||
|
||||
const relPath = toPosixPath(relative(dir, fullPath));
|
||||
if (ig.ignores(relPath)) continue;
|
||||
|
||||
if (isFile && entry.name.endsWith(".md")) {
|
||||
entries.push(fullPath);
|
||||
}
|
||||
|
|
@ -249,6 +334,9 @@ function collectAutoThemeEntries(dir: string): string[] {
|
|||
const entries: string[] = [];
|
||||
if (!existsSync(dir)) return entries;
|
||||
|
||||
const ig = ignore();
|
||||
addIgnoreRules(ig, dir, dir);
|
||||
|
||||
try {
|
||||
const dirEntries = readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of dirEntries) {
|
||||
|
|
@ -265,6 +353,9 @@ function collectAutoThemeEntries(dir: string): string[] {
|
|||
}
|
||||
}
|
||||
|
||||
const relPath = toPosixPath(relative(dir, fullPath));
|
||||
if (ig.ignores(relPath)) continue;
|
||||
|
||||
if (isFile && entry.name.endsWith(".json")) {
|
||||
entries.push(fullPath);
|
||||
}
|
||||
|
|
@ -320,6 +411,9 @@ function collectAutoExtensionEntries(dir: string): string[] {
|
|||
const entries: string[] = [];
|
||||
if (!existsSync(dir)) return entries;
|
||||
|
||||
const ig = ignore();
|
||||
addIgnoreRules(ig, dir, dir);
|
||||
|
||||
try {
|
||||
const dirEntries = readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of dirEntries) {
|
||||
|
|
@ -340,6 +434,10 @@ function collectAutoExtensionEntries(dir: string): string[] {
|
|||
}
|
||||
}
|
||||
|
||||
const relPath = toPosixPath(relative(dir, fullPath));
|
||||
const ignorePath = isDir ? `${relPath}/` : relPath;
|
||||
if (ig.ignores(ignorePath)) continue;
|
||||
|
||||
if (isFile && (entry.name.endsWith(".ts") || entry.name.endsWith(".js"))) {
|
||||
entries.push(fullPath);
|
||||
} else if (isDir) {
|
||||
|
|
|
|||
|
|
@ -113,6 +113,40 @@ Content`,
|
|||
});
|
||||
});
|
||||
|
||||
describe("ignore files", () => {
|
||||
it("should respect .gitignore in skill directories", async () => {
|
||||
const skillsDir = join(agentDir, "skills");
|
||||
mkdirSync(skillsDir, { recursive: true });
|
||||
writeFileSync(join(skillsDir, ".gitignore"), "venv\n__pycache__\n");
|
||||
|
||||
const goodSkillDir = join(skillsDir, "good-skill");
|
||||
mkdirSync(goodSkillDir, { recursive: true });
|
||||
writeFileSync(join(goodSkillDir, "SKILL.md"), "---\nname: good-skill\ndescription: Good\n---\nContent");
|
||||
|
||||
const ignoredSkillDir = join(skillsDir, "venv", "bad-skill");
|
||||
mkdirSync(ignoredSkillDir, { recursive: true });
|
||||
writeFileSync(join(ignoredSkillDir, "SKILL.md"), "---\nname: bad-skill\ndescription: Bad\n---\nContent");
|
||||
|
||||
settingsManager.setSkillPaths(["skills"]);
|
||||
|
||||
const result = await packageManager.resolve();
|
||||
expect(result.skills.some((r) => r.path.includes("good-skill") && r.enabled)).toBe(true);
|
||||
expect(result.skills.some((r) => r.path.includes("venv") && r.enabled)).toBe(false);
|
||||
});
|
||||
|
||||
it("should not apply parent .gitignore to .pi auto-discovery", async () => {
|
||||
writeFileSync(join(tempDir, ".gitignore"), ".pi\n");
|
||||
|
||||
const skillDir = join(tempDir, ".pi", "skills", "auto-skill");
|
||||
mkdirSync(skillDir, { recursive: true });
|
||||
const skillPath = join(skillDir, "SKILL.md");
|
||||
writeFileSync(skillPath, "---\nname: auto-skill\ndescription: Auto\n---\nContent");
|
||||
|
||||
const result = await packageManager.resolve();
|
||||
expect(result.skills.some((r) => r.path === skillPath && r.enabled)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveExtensionSources", () => {
|
||||
it("should resolve local paths", async () => {
|
||||
const extPath = join(tempDir, "ext.ts");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue