From f28075c0274de23475be95a434ce753e28a54adc Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Sat, 20 Jun 2026 17:08:00 +0800 Subject: [PATCH 1/2] fix(extension): accept uppercase URL schemes in Claude plugin sources resolvePluginSource compared a string plugin source against 'http://' and 'https://' case-sensitively, so a marketplace.json source such as 'HTTPS://github.com/owner/repo' fell through to local-path handling and failed with "Plugin source not found". Lowercase the source before the scheme check, matching #5426 / #5429 / #5439. --- .../src/extension/claude-converter.test.ts | 57 ++++++++++++++++++- .../core/src/extension/claude-converter.ts | 8 ++- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/packages/core/src/extension/claude-converter.test.ts b/packages/core/src/extension/claude-converter.test.ts index 517504fea77..6bfd46dc103 100644 --- a/packages/core/src/extension/claude-converter.test.ts +++ b/packages/core/src/extension/claude-converter.test.ts @@ -19,7 +19,7 @@ import { type ClaudeMarketplacePluginConfig, type ClaudeMarketplaceConfig, } from './claude-converter.js'; -import { cloneFromGit } from './github.js'; +import { cloneFromGit, downloadFromGitHubRelease } from './github.js'; import { HookType } from '../hooks/types.js'; import { performVariableReplacement } from './variables.js'; @@ -1356,3 +1356,58 @@ describe('convertClaudePluginPackage — git-subdir source', () => { fs.rmSync(secretDir, { recursive: true, force: true }); }); }); + +describe('convertClaudePluginPackage — string URL source', () => { + let extDir: string; + + beforeEach(() => { + extDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-url-')); + vi.mocked(downloadFromGitHubRelease).mockReset(); + vi.mocked(cloneFromGit).mockReset(); + }); + + afterEach(() => { + if (fs.existsSync(extDir)) { + fs.rmSync(extDir, { recursive: true, force: true }); + } + }); + + const writeMarketplace = (source: string) => { + const mp = path.join(extDir, '.claude-plugin'); + fs.mkdirSync(mp, { recursive: true }); + fs.writeFileSync( + path.join(mp, 'marketplace.json'), + JSON.stringify({ + name: 'm', + owner: { name: 'o', email: 'e' }, + plugins: [{ name: 'p', version: '1.0.0', source }], + }), + 'utf-8', + ); + }; + + it('treats an uppercase HTTPS:// source as a URL download, not a local path', async () => { + // The scheme check was case-sensitive, so an uppercase URL fell through to + // local-path handling and failed with "Plugin source not found". + vi.mocked(downloadFromGitHubRelease).mockImplementation( + async (_meta, dir) => { + fs.mkdirSync(path.join(dir as string, '.claude-plugin'), { + recursive: true, + }); + fs.writeFileSync( + path.join(dir as string, '.claude-plugin', 'plugin.json'), + JSON.stringify({ name: 'p', version: '1.0.0' }), + 'utf-8', + ); + }, + ); + + writeMarketplace('HTTPS://github.com/owner/repo'); + + const result = await convertClaudePluginPackage(extDir, 'p'); + expect(result.config.name).toBe('p'); + expect(vi.mocked(downloadFromGitHubRelease)).toHaveBeenCalled(); + + fs.rmSync(result.convertedDir, { recursive: true, force: true }); + }); +}); diff --git a/packages/core/src/extension/claude-converter.ts b/packages/core/src/extension/claude-converter.ts index 1a5cd096772..3ed29922397 100644 --- a/packages/core/src/extension/claude-converter.ts +++ b/packages/core/src/extension/claude-converter.ts @@ -1002,8 +1002,12 @@ async function resolvePluginSource( // Handle string source (relative path or URL) if (typeof source === 'string') { - // Check if it's a URL - if (source.startsWith('http://') || source.startsWith('https://')) { + // Check if it's a URL (scheme is case-insensitive, e.g. HTTPS://) + const lowerSource = source.toLowerCase(); + if ( + lowerSource.startsWith('http://') || + lowerSource.startsWith('https://') + ) { // Download from URL const installMetadata: ExtensionInstallMetadata = { source, From 8fc886ea425e0eda98d653f57ac415b5d7916963 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Sat, 20 Jun 2026 19:00:32 +0800 Subject: [PATCH 2/2] test: return a GitHubDownloadResult from the download mock The mockImplementation returned void, which tsc --build rejected (TS2345) even though vitest passed. Return a GitHubDownloadResult so the build is clean. --- packages/core/src/extension/claude-converter.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/extension/claude-converter.test.ts b/packages/core/src/extension/claude-converter.test.ts index 6bfd46dc103..d0e23391cf1 100644 --- a/packages/core/src/extension/claude-converter.test.ts +++ b/packages/core/src/extension/claude-converter.test.ts @@ -1399,6 +1399,7 @@ describe('convertClaudePluginPackage — string URL source', () => { JSON.stringify({ name: 'p', version: '1.0.0' }), 'utf-8', ); + return { tagName: 'v1.0.0', type: 'github-release' }; }, );