diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index 8580e1f3..80ca94b1 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -797,10 +797,10 @@ export class DefaultPackageManager implements PackageManager { // No filter: load everything based on manifest or directory structure const manifest = this.readPiManifest(packageRoot); if (manifest) { - this.addManifestEntries(manifest.extensions, packageRoot, accumulator.extensions); - this.addManifestEntries(manifest.skills, packageRoot, accumulator.skills); - this.addManifestEntries(manifest.prompts, packageRoot, accumulator.prompts); - this.addManifestEntries(manifest.themes, packageRoot, accumulator.themes); + this.addManifestEntries(manifest.extensions, packageRoot, "extensions", accumulator.extensions); + this.addManifestEntries(manifest.skills, packageRoot, "skills", accumulator.skills); + this.addManifestEntries(manifest.prompts, packageRoot, "prompts", accumulator.prompts); + this.addManifestEntries(manifest.themes, packageRoot, "themes", accumulator.themes); return true; } @@ -833,7 +833,7 @@ export class DefaultPackageManager implements PackageManager { private collectDefaultExtensions(packageRoot: string, accumulator: ResourceAccumulator): void { const manifest = this.readPiManifest(packageRoot); if (manifest?.extensions) { - this.addManifestEntries(manifest.extensions, packageRoot, accumulator.extensions); + this.addManifestEntries(manifest.extensions, packageRoot, "extensions", accumulator.extensions); return; } const extensionsDir = join(packageRoot, "extensions"); @@ -845,7 +845,7 @@ export class DefaultPackageManager implements PackageManager { private collectDefaultSkills(packageRoot: string, accumulator: ResourceAccumulator): void { const manifest = this.readPiManifest(packageRoot); if (manifest?.skills) { - this.addManifestEntries(manifest.skills, packageRoot, accumulator.skills); + this.addManifestEntries(manifest.skills, packageRoot, "skills", accumulator.skills); return; } const skillsDir = join(packageRoot, "skills"); @@ -857,7 +857,7 @@ export class DefaultPackageManager implements PackageManager { private collectDefaultPrompts(packageRoot: string, accumulator: ResourceAccumulator): void { const manifest = this.readPiManifest(packageRoot); if (manifest?.prompts) { - this.addManifestEntries(manifest.prompts, packageRoot, accumulator.prompts); + this.addManifestEntries(manifest.prompts, packageRoot, "prompts", accumulator.prompts); return; } const promptsDir = join(packageRoot, "prompts"); @@ -869,7 +869,7 @@ export class DefaultPackageManager implements PackageManager { private collectDefaultThemes(packageRoot: string, accumulator: ResourceAccumulator): void { const manifest = this.readPiManifest(packageRoot); if (manifest?.themes) { - this.addManifestEntries(manifest.themes, packageRoot, accumulator.themes); + this.addManifestEntries(manifest.themes, packageRoot, "themes", accumulator.themes); return; } const themesDir = join(packageRoot, "themes"); @@ -972,12 +972,60 @@ export class DefaultPackageManager implements PackageManager { } } - private addManifestEntries(entries: string[] | undefined, root: string, target: Set): void { + private addManifestEntries( + entries: string[] | undefined, + root: string, + resourceType: ResourceType, + target: Set, + ): void { if (!entries) return; - for (const entry of entries) { - const resolved = resolve(root, entry); - this.addPath(target, resolved); + + if (!hasPatterns(entries)) { + // No patterns - resolve directly + for (const entry of entries) { + const resolved = resolve(root, entry); + this.addPath(target, resolved); + } + return; } + + // Has patterns - enumerate and filter + const allFiles = this.collectAllManifestFiles(entries, root, resourceType); + const patterns = entries.filter(isPattern); + const filtered = applyPatterns(allFiles, patterns, root); + for (const f of filtered) { + this.addPath(target, f); + } + } + + /** + * Collect files from manifest entries for pattern matching. + * Plain paths are resolved and enumerated; pattern strings are skipped (used for filtering). + */ + private collectAllManifestFiles(entries: string[], root: string, resourceType: ResourceType): string[] { + const files: string[] = []; + for (const entry of entries) { + if (isPattern(entry)) continue; + + const resolved = resolve(root, entry); + if (!existsSync(resolved)) continue; + + try { + const stats = statSync(resolved); + if (stats.isFile()) { + files.push(resolved); + } else if (stats.isDirectory()) { + if (resourceType === "skills") { + files.push(...collectSkillEntries(resolved)); + } else { + files.push(...collectFiles(resolved, FILE_PATTERNS[resourceType])); + } + } + } catch { + // Ignore errors + } + } + return files; } private addPath(set: Set, value: string): void { diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index 324a290b..a5b6f560 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -232,6 +232,58 @@ Content`, }); }); + describe("pattern filtering in pi manifest", () => { + it("should support glob patterns in manifest extensions", async () => { + const pkgDir = join(tempDir, "manifest-pkg"); + mkdirSync(join(pkgDir, "extensions"), { recursive: true }); + mkdirSync(join(pkgDir, "node_modules/dep/extensions"), { recursive: true }); + writeFileSync(join(pkgDir, "extensions", "local.ts"), "export default function() {}"); + writeFileSync(join(pkgDir, "node_modules/dep/extensions", "remote.ts"), "export default function() {}"); + writeFileSync(join(pkgDir, "node_modules/dep/extensions", "skip.ts"), "export default function() {}"); + writeFileSync( + join(pkgDir, "package.json"), + JSON.stringify({ + name: "manifest-pkg", + pi: { + extensions: ["extensions", "node_modules/dep/extensions", "!**/skip.ts"], + }, + }), + ); + + const result = await packageManager.resolveExtensionSources([pkgDir]); + expect(result.extensions.some((p) => p.endsWith("local.ts"))).toBe(true); + expect(result.extensions.some((p) => p.endsWith("remote.ts"))).toBe(true); + expect(result.extensions.some((p) => p.endsWith("skip.ts"))).toBe(false); + }); + + it("should support glob patterns in manifest skills", async () => { + const pkgDir = join(tempDir, "skill-manifest-pkg"); + mkdirSync(join(pkgDir, "skills/good-skill"), { recursive: true }); + mkdirSync(join(pkgDir, "skills/bad-skill"), { recursive: true }); + writeFileSync( + join(pkgDir, "skills/good-skill", "SKILL.md"), + "---\nname: good-skill\ndescription: Good\n---\nContent", + ); + writeFileSync( + join(pkgDir, "skills/bad-skill", "SKILL.md"), + "---\nname: bad-skill\ndescription: Bad\n---\nContent", + ); + writeFileSync( + join(pkgDir, "package.json"), + JSON.stringify({ + name: "skill-manifest-pkg", + pi: { + skills: ["skills", "!**/bad-skill"], + }, + }), + ); + + const result = await packageManager.resolveExtensionSources([pkgDir]); + expect(result.skills.some((p) => p.includes("good-skill"))).toBe(true); + expect(result.skills.some((p) => p.includes("bad-skill"))).toBe(false); + }); + }); + describe("pattern filtering in package filters", () => { it("should exclude extensions from package with ! pattern", async () => { const pkgDir = join(tempDir, "pattern-pkg");