From 7e0a5c900935a2d6183031e7345e12439ed5c994 Mon Sep 17 00:00:00 2001 From: Dean Rie Date: Fri, 23 Oct 2026 17:38:36 +0300 Subject: [PATCH] fix(github-plugin): treat a sub-plugin dir as a literal path, not a regex parseGitHubPlugin built the per-directory matchers with new RegExp(`^${prefix}rules/...`) where prefix is a sub-plugin dir taken from marketplace.json plugins[].source or from the tree. Any regex metacharacter in the dir name changed the pattern; a name like plugins/c++ made RegExp throw "Nothing to repeat", a plain SyntaxError that escaped parseGitHubPlugin instead of a GitHubParseError. A dot in a dir name matched any character, and a leading ./ in source only worked because . matched the literal dot. pathUnder() escapes the prefix before building the RegExp, and normalizeSubPluginDir() strips a leading ./ so that case keeps working on purpose rather than by accident. --- apps/cursor/src/lib/github-plugin/parse.ts | 29 ++++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/apps/cursor/src/lib/github-plugin/parse.ts b/apps/cursor/src/lib/github-plugin/parse.ts index 6fb62149..860b8551 100644 --- a/apps/cursor/src/lib/github-plugin/parse.ts +++ b/apps/cursor/src/lib/github-plugin/parse.ts @@ -132,6 +132,24 @@ export function githubAuthHeaders(): Record { const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); +/** + * Normalize a sub-plugin dir to `a/b/` form: no leading `./` or `/`, exactly + * one trailing `/`. Marketplace manifests commonly write `./plugins/foo`. + */ +function normalizeSubPluginDir(source: string): string { + const trimmed = source.replace(/^(?:\.\/)+/, "").replace(/^\/+|\/+$/g, ""); + return trimmed ? `${trimmed}/` : ""; +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +/** Match tree paths under `prefix` (a literal dir) followed by `pattern` (a regex). */ +function pathUnder(prefix: string, pattern: string): RegExp { + return new RegExp(`^${escapeRegExp(prefix)}${pattern}`); +} + /** * Fetch with bounded retry on GitHub rate-limit (429 / 403 + retry-after). * @@ -344,7 +362,8 @@ export async function parseGitHubPlugin( if (entry && typeof entry === "object") { const source = (entry as Record).source; if (typeof source === "string" && source && !source.includes("..")) { - subPluginDirs.add(`${source.replace(/^\/+|\/+$/g, "")}/`); + const dir = normalizeSubPluginDir(source); + if (dir) subPluginDirs.add(dir); } } } @@ -389,7 +408,7 @@ export async function parseGitHubPlugin( const ruleFiles = tree.filter( (f) => f.type === "blob" && - new RegExp(`^${prefix}rules/.*\\.mdc$`).test(f.path), + pathUnder(prefix, "rules/.*\\.mdc$").test(f.path), ); for (const rf of ruleFiles) { const raw = await fetchGitHubFile(owner, repo, rf.path); @@ -452,7 +471,7 @@ export async function parseGitHubPlugin( const agentFiles = tree.filter( (f) => f.type === "blob" && - new RegExp(`^${prefix}agents/.*\\.md$`).test(f.path), + pathUnder(prefix, "agents/.*\\.md$").test(f.path), ); for (const af of agentFiles) { const raw = await fetchGitHubFile(owner, repo, af.path); @@ -473,7 +492,7 @@ export async function parseGitHubPlugin( const skillFiles = tree.filter( (f) => f.type === "blob" && - new RegExp(`^${prefix}skills/[^/]+/SKILL\\.md$`).test(f.path), + pathUnder(prefix, "skills/[^/]+/SKILL\\.md$").test(f.path), ); for (const sf of skillFiles) { const raw = await fetchGitHubFile(owner, repo, sf.path); @@ -495,7 +514,7 @@ export async function parseGitHubPlugin( const commandFiles = tree.filter( (f) => f.type === "blob" && - new RegExp(`^${prefix}commands/.*\\.md$`).test(f.path), + pathUnder(prefix, "commands/.*\\.md$").test(f.path), ); for (const cf of commandFiles) { const raw = await fetchGitHubFile(owner, repo, cf.path);