fix(debug): embed marketplace dap metadata on install

This commit is contained in:
DanZAIO
2026-06-19 20:13:00 -03:00
parent dd7dcccc45
commit 9682186ff7
5 changed files with 77 additions and 59 deletions
+2 -41
View File
@@ -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) {
@@ -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<void> {
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)
@@ -92,6 +92,7 @@ export interface MarketplacePluginEntry {
hooks?: string | Record<string, unknown>;
mcpServers?: string | Record<string, unknown>;
lspServers?: string | Record<string, unknown>;
dapAdapters?: string | Record<string, unknown>;
}
// ── Plugin source variants ───────────────────────────────────────────
@@ -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");
@@ -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", {