diff --git a/packages/coding-agent/src/extensibility/plugins/marketplace/fetcher.ts b/packages/coding-agent/src/extensibility/plugins/marketplace/fetcher.ts index 28d342fcec3..6a610734344 100644 --- a/packages/coding-agent/src/extensibility/plugins/marketplace/fetcher.ts +++ b/packages/coding-agent/src/extensibility/plugins/marketplace/fetcher.ts @@ -196,19 +196,20 @@ export function parseMarketplaceCatalog(content: string, filePath: string): Mark * Catalog paths tried in priority order: omp-namespaced override first, then * the Claude Code-compatible fallback so existing marketplaces keep loading. */ -const CATALOG_RELATIVE_PATHS: readonly string[] = [ - path.join(".omp-plugin", "marketplace.json"), - path.join(".claude-plugin", "marketplace.json"), -]; +const CATALOG_RELATIVE_PATHS: readonly string[] = [".omp-plugin/marketplace.json", ".claude-plugin/marketplace.json"]; -async function readMarketplaceCatalog(root: string): Promise<{ catalogPath: string; content: string }> { +async function readMarketplaceCatalog( + root: string, + options: { relativeDisplayPaths?: boolean } = {}, +): Promise<{ catalogPath: string; displayPath: string; content: string }> { const tried: string[] = []; for (const rel of CATALOG_RELATIVE_PATHS) { - const catalogPath = path.join(root, rel); - tried.push(catalogPath); + const catalogPath = path.join(root, ...rel.split("/")); + const displayPath = options.relativeDisplayPaths ? rel : catalogPath; + tried.push(displayPath); try { const content = await Bun.file(catalogPath).text(); - return { catalogPath, content }; + return { catalogPath, displayPath, content }; } catch (err) { if (isEnoent(err)) continue; throw err; @@ -252,11 +253,11 @@ export async function fetchMarketplace(source: string, cacheDir: string): Promis if (type === "github") { const url = `https://github.com/${source}.git`; - return cloneAndReadCatalog(url, cacheDir); + return cloneAndReadCatalog(url, source, cacheDir); } if (type === "git") { - return cloneAndReadCatalog(source, cacheDir); + return cloneAndReadCatalog(source, source, cacheDir); } // type === "url" @@ -284,7 +285,7 @@ export async function fetchMarketplace(source: string, cacheDir: string): Promis * responsible for promoting the clone to its final cache location via * `promoteCloneToCache` after any duplicate/drift checks pass. */ -async function cloneAndReadCatalog(url: string, cacheDir: string): Promise { +async function cloneAndReadCatalog(url: string, source: string, cacheDir: string): Promise { const tmpDir = path.join(cacheDir, `.tmp-clone-${Date.now()}`); await fs.mkdir(cacheDir, { recursive: true }); @@ -292,12 +293,12 @@ async function cloneAndReadCatalog(url: string, cacheDir: string): Promise {}); - throw new Error(`Cloned repository ${url}: ${(err as Error).message}`, { cause: err }); + throw new Error(`Cloned repository ${url}: ${(err as Error).message} (source: ${source})`, { cause: err }); } } diff --git a/packages/coding-agent/test/marketplace/fetcher.test.ts b/packages/coding-agent/test/marketplace/fetcher.test.ts index e72c84f1e41..5e73e051761 100644 --- a/packages/coding-agent/test/marketplace/fetcher.test.ts +++ b/packages/coding-agent/test/marketplace/fetcher.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; @@ -7,6 +7,7 @@ import { fetchMarketplace, parseMarketplaceCatalog, } from "@oh-my-pi/pi-coding-agent/extensibility/plugins/marketplace"; +import * as git from "@oh-my-pi/pi-coding-agent/utils/git"; import { removeSyncWithRetries } from "@oh-my-pi/pi-utils"; // Fixture lives at test/marketplace/fixtures/valid-marketplace/ @@ -231,6 +232,24 @@ describe("fetchMarketplace", () => { ); }); + it("hides temp clone paths in cloned catalog validation errors", async () => { + const cloneSpy = spyOn(git, "clone").mockImplementation(async (_url, targetDir) => { + fs.mkdirSync(path.join(targetDir, ".claude-plugin"), { recursive: true }); + fs.writeFileSync( + path.join(targetDir, ".claude-plugin", "marketplace.json"), + JSON.stringify({ name: "broken-marketplace", plugins: [] }), + ); + }); + + try { + await expect(fetchMarketplace("kubeshark/kubeshark", tmpDir)).rejects.toThrow( + 'Cloned repository https://github.com/kubeshark/kubeshark.git: Missing or invalid field "owner" in catalog: .claude-plugin/marketplace.json (source: kubeshark/kubeshark)', + ); + } finally { + cloneSpy.mockRestore(); + } + }); + // Network-dependent tests — skip in CI / offline environments. // These verify real git clone and HTTP fetch error handling. it.skip("github source throws on nonexistent repo", async () => {