fix(discovery): isolated plugin root caches by home

Included the resolved OMP plugin registry path in cache identity and covered shared Claude config directories across isolated SDK homes.
This commit is contained in:
roboomp
2026-08-13 15:44:02 +00:00
parent 3629464cf9
commit 406306f972
2 changed files with 39 additions and 3 deletions
@@ -906,17 +906,18 @@ export function registerPluginCacheInvalidator(invalidator: () => void): void {
* List all installed Claude Code plugin roots from its active plugin cache and
* ~/.omp/plugins/installed_plugins.json, plus the nearest project registry when present.
*
* Results are cached per Claude config directory, project registry, and canonical active project.
* Results are cached per Claude and OMP config directories, project registry, and canonical active project.
*/
export async function listClaudePluginRoots(
home: string,
cwd?: string,
): Promise<{ roots: ClaudePluginRoot[]; warnings: string[] }> {
const claudeConfigDir = resolveClaudePaths(home).configDir;
const ompRegistryPath = path.join(getPluginsDir(home), "installed_plugins.json");
const resolvedProjectPath = cwd ? await resolveActiveProjectRegistryPath(cwd) : null;
const projectRoot = resolvedProjectPath ? path.dirname(path.dirname(path.dirname(resolvedProjectPath))) : cwd;
const activeClaudeProjectPath = projectRoot ? await canonicalClaudeProjectPath(projectRoot) : null;
const cacheKey = `${claudeConfigDir}:${resolvedProjectPath ?? ""}:${activeClaudeProjectPath ?? ""}`;
const cacheKey = `${claudeConfigDir}:${ompRegistryPath}:${resolvedProjectPath ?? ""}:${activeClaudeProjectPath ?? ""}`;
const cached = pluginRootsCache.get(cacheKey);
if (cached) return cached;
@@ -983,7 +984,7 @@ export async function listClaudePluginRoots(
// In production `home` is `os.homedir()`, so `getPluginsDir(home)` resolves to the
// same XDG-aware path the marketplace writer uses (reads and writes always agree).
// Tests pass a temp dir, which short-circuits the resolver for deterministic isolation.
const ompRegistryPath = path.join(getPluginsDir(home), "installed_plugins.json");
// Computed before the cache lookup because isolated SDK homes select distinct OMP registries.
const ompContent = await readFile(ompRegistryPath);
if (ompContent) {
const ompRegistry = parseClaudePluginsRegistry(ompContent);
@@ -393,6 +393,41 @@ describe("listClaudePluginRoots", () => {
expect(result3.roots).toHaveLength(2);
});
test("isolates cached OMP plugin roots by home when Claude config is shared", async () => {
const sharedClaudeConfig = path.join(tempDir, "shared-claude");
const firstHome = path.join(tempDir, "first-home");
const secondHome = path.join(tempDir, "second-home");
process.env.CLAUDE_CONFIG_DIR = sharedClaudeConfig;
for (const [home, pluginId] of [
[firstHome, "first@market"],
[secondHome, "second@market"],
] as const) {
const pluginsDir = path.join(home, ".omp", "plugins");
await fs.mkdir(pluginsDir, { recursive: true });
await fs.writeFile(
path.join(pluginsDir, "installed_plugins.json"),
JSON.stringify({
version: 2,
plugins: {
[pluginId]: [
{
scope: "user",
installPath: `/path/to/${pluginId.split("@")[0]}`,
version: "1.0.0",
},
],
},
}),
);
}
const first = await listClaudePluginRoots(firstHome);
const second = await listClaudePluginRoots(secondHome);
expect(first.roots.map(root => root.id)).toEqual(["first@market"]);
expect(second.roots.map(root => root.id)).toEqual(["second@market"]);
});
test("defaults scope to user when not specified", async () => {
const pluginsDir = path.join(tempDir, ".claude", "plugins");
await fs.mkdir(pluginsDir, { recursive: true });