diff --git a/packages/coding-agent/src/dap/config.ts b/packages/coding-agent/src/dap/config.ts index fb4e61d5f..c09375346 100644 --- a/packages/coding-agent/src/dap/config.ts +++ b/packages/coding-agent/src/dap/config.ts @@ -1,10 +1,10 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; -import { isRecord, logger, pathIsWithin } from "@oh-my-pi/pi-utils"; +import { isRecord, logger } from "@oh-my-pi/pi-utils"; import { YAML } from "bun"; import { getConfigDirPaths } from "../config"; -import { type ClaudePluginRoot, getPreloadedPluginRoots } from "../discovery/helpers"; +import { getPreloadedPluginRoots } from "../discovery/helpers"; import { hasRootMarkers, resolveCommand } from "../lsp/config"; import DEFAULTS from "./defaults.json" with { type: "json" }; import type { DapAdapterConfig, DapResolvedAdapter } from "./types"; @@ -121,44 +121,6 @@ function fileConfigSource(filePath: string): ConfigSource { }; } -function readMarketplaceDapConfig(root: ClaudePluginRoot): NormalizedConfig | null { - const catalogPaths = [ - path.resolve(root.path, "..", "..", ".omp-plugin", "marketplace.json"), - path.resolve(root.path, "..", "..", "marketplace.json"), - path.resolve(root.path, "..", "..", ".claude-plugin", "marketplace.json"), - ]; - - for (const catalogPath of catalogPaths) { - try { - const catalog = JSON.parse(fs.readFileSync(catalogPath, "utf-8")) as unknown; - if (!isRecord(catalog) || !Array.isArray(catalog.plugins)) continue; - - for (const plugin of catalog.plugins) { - if (!isRecord(plugin) || plugin.name !== root.plugin) continue; - - const dapAdapters = plugin.dapAdapters; - if (typeof dapAdapters === "string") { - const configPath = path.resolve(root.path, dapAdapters); - if (!pathIsWithin(root.path, configPath)) return null; - return readConfigFile(configPath); - } - if (isRecord(dapAdapters)) { - return normalizeConfig({ adapters: dapAdapters }); - } - return null; - } - } catch {} - } - - return null; -} - -function marketplaceConfigSource(root: ClaudePluginRoot): ConfigSource { - return { - read: () => readMarketplaceDapConfig(root), - }; -} - function getConfigSources(cwd: string): ConfigSource[] { const filenames = ["dap.json", ".dap.json", "dap.yaml", ".dap.yaml", "dap.yml", ".dap.yml"]; const sources: ConfigSource[] = []; @@ -186,7 +148,6 @@ function getConfigSources(cwd: string): ConfigSource[] { for (const filename of filenames) { sources.push(fileConfigSource(path.join(root.path, filename))); } - sources.push(marketplaceConfigSource(root)); } for (const filename of filenames) { diff --git a/packages/coding-agent/src/extensibility/plugins/marketplace/manager.ts b/packages/coding-agent/src/extensibility/plugins/marketplace/manager.ts index 049583cbf..34b602cc7 100644 --- a/packages/coding-agent/src/extensibility/plugins/marketplace/manager.ts +++ b/packages/coding-agent/src/extensibility/plugins/marketplace/manager.ts @@ -291,6 +291,7 @@ export class MarketplaceManager { version = await this.#resolvePluginVersion(pluginEntry, sourcePath); cachePath = await cachePlugin(sourcePath, this.#opts.pluginsCacheDir, marketplace, name, version); await this.#writeEmbeddedLspConfig(pluginEntry, cachePath); + await this.#writeEmbeddedDapConfig(pluginEntry, cachePath); } finally { // Clean up temp clone dirs created by resolvePluginSource; leave user-supplied local dirs alone if (tempCloneRoot) { @@ -361,6 +362,24 @@ export class MarketplaceManager { await Bun.write(targetPath, `${JSON.stringify({ servers: lspServers }, null, 2)}\n`); } + async #writeEmbeddedDapConfig(entry: MarketplacePluginEntry, cachePath: string): Promise { + const dapAdapters = entry.dapAdapters; + if (!dapAdapters) return; + + const targetPath = path.join(cachePath, ".dap.json"); + if (typeof dapAdapters === "string") { + const sourcePath = path.resolve(cachePath, dapAdapters); + if (!pathIsWithin(cachePath, sourcePath)) { + throw new Error(`Plugin "${entry.name}" dapAdapters path escapes the plugin directory`); + } + const content = await Bun.file(sourcePath).text(); + await Bun.write(targetPath, content); + return; + } + + await Bun.write(targetPath, `${JSON.stringify({ adapters: dapAdapters }, null, 2)}\n`); + } + /** * Resolve plugin version from multiple sources: * 1. Catalog entry version (if set) diff --git a/packages/coding-agent/src/extensibility/plugins/marketplace/types.ts b/packages/coding-agent/src/extensibility/plugins/marketplace/types.ts index acce656fe..fd101ffd7 100644 --- a/packages/coding-agent/src/extensibility/plugins/marketplace/types.ts +++ b/packages/coding-agent/src/extensibility/plugins/marketplace/types.ts @@ -92,6 +92,7 @@ export interface MarketplacePluginEntry { hooks?: string | Record; mcpServers?: string | Record; lspServers?: string | Record; + dapAdapters?: string | Record; } // ── Plugin source variants ─────────────────────────────────────────── diff --git a/packages/coding-agent/test/debug/dap-config.test.ts b/packages/coding-agent/test/debug/dap-config.test.ts index bb7557bee..333ed9806 100644 --- a/packages/coding-agent/test/debug/dap-config.test.ts +++ b/packages/coding-agent/test/debug/dap-config.test.ts @@ -3,7 +3,7 @@ import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; import { getAdapterConfigs, resolveAdapter, selectLaunchAdapter } from "../../src/dap/config"; -import { clearClaudePluginRootsCache, injectPluginDirRoots } from "../../src/discovery/helpers"; +import { injectPluginDirRoots } from "../../src/discovery/helpers"; const tempDirs: string[] = []; const ORIGINAL_OMP_PLUGIN_DIR = process.env.OMP_PLUGIN_DIR; @@ -28,7 +28,6 @@ afterEach(async () => { process.env.OMP_MARKETPLACE_DIR = ORIGINAL_OMP_MARKETPLACE_DIR; } await injectPluginDirRoots(os.homedir(), []); - clearClaudePluginRootsCache(); await Promise.all(tempDirs.splice(0).map(dir => fs.rm(dir, { recursive: true, force: true }))); }); @@ -122,11 +121,9 @@ describe("DAP adapter configuration", () => { expect(selected?.launchDefaults).toEqual({ request: "launch", projectRoot: "." }); }); - it("loads plugin DAP adapters from OMP marketplace catalogs", async () => { - const cwd = await makeTempDir("omp-dap-config-marketplace-"); - const marketplaceRoot = path.join(cwd, "marketplaces", "test-market"); - const pluginRoot = path.join(marketplaceRoot, "plugins", "acme-debug"); - await fs.mkdir(path.join(marketplaceRoot, ".omp-plugin"), { recursive: true }); + it("loads plugin DAP adapters from plugin config files", async () => { + const cwd = await makeTempDir("omp-dap-config-plugin-"); + const pluginRoot = path.join(cwd, "plugins", "acme-debug"); await fs.mkdir(path.join(pluginRoot, ".claude-plugin"), { recursive: true }); await fs.writeFile(path.join(cwd, "app.rb"), "puts 'hi'\n"); await fs.writeFile( @@ -134,19 +131,14 @@ describe("DAP adapter configuration", () => { JSON.stringify({ name: "acme-debug" }), ); await fs.writeFile( - path.join(marketplaceRoot, ".omp-plugin", "marketplace.json"), + path.join(pluginRoot, ".dap.json"), JSON.stringify({ - plugins: [ - { - name: "acme-debug", - dapAdapters: { - "acme-ruby": { - command: "ruby-debug-adapter", - fileTypes: [".rb"], - }, - }, + adapters: { + "acme-ruby": { + command: "ruby-debug-adapter", + fileTypes: [".rb"], }, - ], + }, }), ); process.env.OMP_PLUGIN_DIR = path.join(cwd, "plugins"); diff --git a/packages/coding-agent/test/marketplace/manager.test.ts b/packages/coding-agent/test/marketplace/manager.test.ts index 2d5c43e97..c9c978d6c 100644 --- a/packages/coding-agent/test/marketplace/manager.test.ts +++ b/packages/coding-agent/test/marketplace/manager.test.ts @@ -229,6 +229,51 @@ describe("MarketplaceManager", () => { }); }); + it("installPlugin embeds config-only marketplace DAP metadata", async () => { + const marketplaceDir = path.join(ctx.tmpDir, "config-only-dap-marketplace"); + const pluginDir = path.join(marketplaceDir, "plugins", "ruby-dap"); + await fs.promises.mkdir(pluginDir, { recursive: true }); + await Bun.write(path.join(pluginDir, "README.md"), "config-only Ruby DAP plugin\n"); + await fs.promises.mkdir(path.join(marketplaceDir, ".claude-plugin"), { recursive: true }); + await Bun.write( + path.join(marketplaceDir, ".claude-plugin", "marketplace.json"), + `${JSON.stringify( + { + name: "config-only-dap-marketplace", + owner: { name: "Test Author" }, + plugins: [ + { + name: "ruby-dap", + source: "./plugins/ruby-dap", + version: "1.0.0", + dapAdapters: { + "ruby-debug": { + command: "ruby-debug-adapter", + fileTypes: [".rb"], + }, + }, + }, + ], + }, + null, + 2, + )}\n`, + ); + + await ctx.manager.addMarketplace(marketplaceDir); + const instEntry = await ctx.manager.installPlugin("ruby-dap", "config-only-dap-marketplace"); + + const dapConfig = await Bun.file(path.join(instEntry.installPath, ".dap.json")).json(); + expect(dapConfig).toEqual({ + adapters: { + "ruby-debug": { + command: "ruby-debug-adapter", + fileTypes: [".rb"], + }, + }, + }); + }); + it("installPlugin with scope:project → persisted in project registry, isolated from user", async () => { await ctx.manager.addMarketplace(FIXTURE_DIR); const instEntry = await ctx.manager.installPlugin("hello-plugin", "test-marketplace", {