diff --git a/packages/core/src/extension/marketplace.test.ts b/packages/core/src/extension/marketplace.test.ts index 45762fb0b77..dfe0bf9b70f 100644 --- a/packages/core/src/extension/marketplace.test.ts +++ b/packages/core/src/extension/marketplace.test.ts @@ -10,6 +10,7 @@ import { loadMarketplaceConfigFromSource, } from './marketplace.js'; import * as fs from 'node:fs/promises'; +import * as http from 'node:http'; import * as https from 'node:https'; // Mock dependencies @@ -27,6 +28,10 @@ vi.mock('node:https', () => ({ get: vi.fn(), })); +vi.mock('node:http', () => ({ + get: vi.fn(), +})); + vi.mock('./github.js', () => ({ parseGitHubRepoForReleases: vi.fn((url: string) => { const match = url.match(/github\.com\/([^/]+)\/([^/]+)/); @@ -51,6 +56,17 @@ describe('parseInstallSource', () => { } return { on: vi.fn() } as never; }); + vi.mocked(http.get).mockImplementation((_url, _options, callback) => { + const mockRes = { + statusCode: 404, + resume: vi.fn(), + on: vi.fn(), + }; + if (typeof callback === 'function') { + callback(mockRes as never); + } + return { on: vi.fn(), setTimeout: vi.fn(), destroy: vi.fn() } as never; + }); }); afterEach(() => { @@ -343,6 +359,45 @@ describe('parseInstallSource', () => { }); describe('loadMarketplaceConfigFromSource', () => { + it('fetches direct HTTP marketplace JSON with the HTTP client', async () => { + vi.mocked(fs.stat).mockRejectedValueOnce(new Error('ENOENT')); + const cfg = { + name: 'http-marketplace', + owner: { name: 'Owner' }, + plugins: [{ name: 'p1' }], + }; + vi.mocked(http.get).mockImplementation((_url, _options, callback) => { + const mockRes = { + statusCode: 200, + resume: vi.fn(), + on: vi.fn((event, handler) => { + if (event === 'data') { + handler(Buffer.from(JSON.stringify(cfg))); + } + if (event === 'end') { + handler(); + } + }), + }; + if (typeof callback === 'function') { + callback(mockRes as never); + } + return { on: vi.fn(), setTimeout: vi.fn(), destroy: vi.fn() } as never; + }); + + const result = await loadMarketplaceConfigFromSource( + 'http://example.com/marketplace.json', + ); + + expect(result).toEqual(cfg); + expect(http.get).toHaveBeenCalledWith( + 'http://example.com/marketplace.json', + { headers: { 'User-Agent': 'qwen-code' } }, + expect.any(Function), + ); + expect(https.get).not.toHaveBeenCalled(); + }); + it('resolves a marketplace from a git@ SSH source', async () => { vi.mocked(fs.stat).mockRejectedValueOnce(new Error('ENOENT')); const cfg = { diff --git a/packages/core/src/extension/marketplace.ts b/packages/core/src/extension/marketplace.ts index b2958151165..8f87f69878d 100644 --- a/packages/core/src/extension/marketplace.ts +++ b/packages/core/src/extension/marketplace.ts @@ -9,6 +9,7 @@ import type { ExtensionInstallMetadata } from '../config/config.js'; import type { ClaudeMarketplaceConfig } from './claude-converter.js'; import * as fs from 'node:fs'; import * as path from 'node:path'; +import * as http from 'node:http'; import * as https from 'node:https'; import { stat } from 'node:fs/promises'; import { parseGitHubRepoForReleases } from './github.js'; @@ -140,6 +141,9 @@ function fetchUrl( url: string, headers: Record, ): Promise { + const protocol = new URL(url).protocol; + const client = protocol === 'http:' ? http : https; + return new Promise((resolve) => { let settled = false; const done = (value: string | null) => { @@ -155,7 +159,7 @@ function fetchUrl( req.destroy(); done(null); }, MARKETPLACE_FETCH_TIMEOUT_MS); - const req = https.get(url, { headers }, (res) => { + const req = client.get(url, { headers }, (res) => { if (res.statusCode !== 200) { res.resume(); // drain so the socket can be freed done(null); @@ -254,7 +258,7 @@ async function readLocalMarketplaceConfig( * - Local directory containing `.claude-plugin/marketplace.json` * - Local path directly to a `marketplace.json` file * - `owner/repo`, `https://github.com/owner/repo`, `git@github.com:owner/repo.git` - * - Arbitrary `https://host/.../marketplace.json` returning the JSON document + * - Arbitrary `http(s)://host/.../marketplace.json` returning the JSON document * * Returns `null` when no marketplace config can be resolved. */