From a52635fb2be7b551d13115dad7eb85721a12e3e7 Mon Sep 17 00:00:00 2001 From: JoAdo07 Date: Sun, 18 Oct 2026 22:17:57 +0200 Subject: [PATCH 1/2] fix(validate-plugins): skip remote URL sources instead of failing validate-plugins.mjs rejected any marketplace entry whose source is a remote URL, even though schemas/marketplace.schema.json:68 explicitly allows 'a remote URL'. resolve(root, entry.source) turns a URL into a bogus local path that never exists, so a schema-valid marketplace with remote plugins failed validation with a misleading 'does not exist' error. Detect remote sources before resolving and skip them, so spec and tooling agree. Local paths are unaffected. Test Plan: ran scripts/validate-plugins.mjs against the repo's marketplace.json (all local sources) -> passes. Temporarily added a remote-URL entry (source: https://github.com/user/remote-test-plugin) to marketplace.json -> validator passes with my fix and would have failed with 'does not exist' without it; reverted the marketplace.json change so the diff is scripts-only. Closes #640. --- scripts/validate-plugins.mjs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/validate-plugins.mjs b/scripts/validate-plugins.mjs index fda6c1361..adf0f507c 100644 --- a/scripts/validate-plugins.mjs +++ b/scripts/validate-plugins.mjs @@ -50,6 +50,12 @@ if (!validateMarketplace(marketplace)) { // 2. Validate each plugin for (const entry of marketplace.plugins ?? []) { + // Remote URLs are a valid `source` per the marketplace schema + // (schemas/marketplace.schema.json:68), but `resolve()` cannot turn one into + // a local directory, so skip them here instead of failing with a misleading + // "does not exist" error. + if (/^[a-z][a-z0-9+.-]*:\/\//i.test(entry.source)) continue; + const pluginDir = resolve(root, entry.source); const pluginJsonPath = resolve(pluginDir, ".cursor-plugin/plugin.json"); @@ -98,6 +104,10 @@ const claudeMarketplace = existsSync(claudeMarketplacePath) : { plugins: [] }; for (const entry of claudeMarketplace.plugins ?? []) { + // Remote URLs are a valid `source` per the marketplace schema, but `resolve()` + // cannot turn one into a local directory — skip them here. + if (/^[a-z][a-z0-9+.-]*:\/\//i.test(entry.source)) continue; + const pluginDir = resolve(root, entry.source); const pluginJsonPath = resolve(pluginDir, ".claude-plugin/plugin.json"); From 4a7a54ba62f9a5c9b397bff39b8c5079ebc7223a Mon Sep 17 00:00:00 2001 From: JoAdo07 Date: Tue, 20 Oct 2026 18:10:14 +0200 Subject: [PATCH 2/2] fix(validate-plugins): scope remote skip to http(s) and log it Address review feedback on #714: only treat http(s) URLs as remote sources, print an INFO line when a source is skipped so the run stays visible, and move the shared check into one helper used by both the Cursor and Claude marketplace loops. The two call sites are not duplicates: the validator walks .cursor-plugin/marketplace.json and .claude-plugin/marketplace.json separately, and both need the same remote-source handling. --- scripts/validate-plugins.mjs | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/scripts/validate-plugins.mjs b/scripts/validate-plugins.mjs index adf0f507c..eafc9c39a 100644 --- a/scripts/validate-plugins.mjs +++ b/scripts/validate-plugins.mjs @@ -31,6 +31,21 @@ function fail(message) { errors++; } +// The schema allows `source` to be a remote URL. Nothing here fetches it, so +// report the skip instead of resolving it as a path (which turns "https://x" +// into "/https:/x" and reports a missing directory). +function isRemoteSource(source) { + return typeof source === "string" && /^https?:\/\//i.test(source); +} + +function skipRemoteSource(entry) { + if (!isRemoteSource(entry.source)) return false; + console.log( + `INFO: Plugin "${entry.name}": remote source "${entry.source}" is not validated locally` + ); + return true; +} + // 1. Validate marketplace.json const marketplacePath = resolve(root, ".cursor-plugin/marketplace.json"); @@ -50,11 +65,7 @@ if (!validateMarketplace(marketplace)) { // 2. Validate each plugin for (const entry of marketplace.plugins ?? []) { - // Remote URLs are a valid `source` per the marketplace schema - // (schemas/marketplace.schema.json:68), but `resolve()` cannot turn one into - // a local directory, so skip them here instead of failing with a misleading - // "does not exist" error. - if (/^[a-z][a-z0-9+.-]*:\/\//i.test(entry.source)) continue; + if (skipRemoteSource(entry)) continue; const pluginDir = resolve(root, entry.source); const pluginJsonPath = resolve(pluginDir, ".cursor-plugin/plugin.json"); @@ -104,9 +115,7 @@ const claudeMarketplace = existsSync(claudeMarketplacePath) : { plugins: [] }; for (const entry of claudeMarketplace.plugins ?? []) { - // Remote URLs are a valid `source` per the marketplace schema, but `resolve()` - // cannot turn one into a local directory — skip them here. - if (/^[a-z][a-z0-9+.-]*:\/\//i.test(entry.source)) continue; + if (skipRemoteSource(entry)) continue; const pluginDir = resolve(root, entry.source); const pluginJsonPath = resolve(pluginDir, ".claude-plugin/plugin.json"); @@ -136,6 +145,7 @@ for (const entry of [ ...(marketplace.plugins ?? []), ...(claudeMarketplace.plugins ?? []), ]) { + if (isRemoteSource(entry.source)) continue; sources.set(resolve(root, entry.source), entry.name); }