fix(debug): embed marketplace dap metadata on install
This commit is contained in:
@@ -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", {
|
||||
|
||||
Reference in New Issue
Block a user