use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use serde_json::Value;
use crate::path_compat::dedup_key;
pub(super) fn admits(path: &Path) -> bool {
Active::default().admits(path)
}
pub(super) fn retain_active(paths: &mut Vec<PathBuf>) {
let mut active = Active::default();
paths.retain(|path| active.admits(path));
}
#[derive(Default)]
struct Active(HashMap<PathBuf, Option<HashSet<PathBuf>>>);
impl Active {
fn admits(&mut self, path: &Path) -> bool {
let Some((claude_dir, root)) = install_root_of(path) else {
return true;
};
match self
.0
.entry(claude_dir.clone())
.or_insert_with(|| active_roots(&claude_dir))
{
Some(roots) => roots.contains(&dedup_key(&root)),
None => true,
}
}
}
fn install_root_of(path: &Path) -> Option<(PathBuf, PathBuf)> {
let components: Vec<_> = path.components().collect();
let cache = components
.windows(2)
.position(|w| w[0].as_os_str() == "plugins" && w[1].as_os_str() == "cache")?;
if components.len() <= cache + 5 {
return None;
}
let claude_dir: PathBuf = components[..cache].iter().collect();
let root: PathBuf = components[..cache + 5].iter().collect();
Some((claude_dir, root))
}
fn active_roots(claude_dir: &Path) -> Option<HashSet<PathBuf>> {
let raw =
std::fs::read_to_string(claude_dir.join("plugins").join("installed_plugins.json")).ok()?;
let registry: Value = serde_json::from_str(&raw).ok()?;
Some(
registry
.get("plugins")?
.as_object()?
.values()
.flat_map(|installs| installs.as_array().into_iter().flatten())
.filter_map(|install| install.get("installPath").and_then(Value::as_str))
.map(|root| dedup_key(Path::new(root)))
.collect(),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn write(path: &Path, body: &str) {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, body).unwrap();
}
fn claude_dir() -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
let cache = dir.path().join("plugins/cache/official");
for version in ["new", "old"] {
write(
&cache.join("context7").join(version).join(".mcp.json"),
"{}",
);
}
write(&cache.join("posthog/v1/.mcp.json"), "{}");
write(&cache.join("codex/v1/.mcp.json"), "{}");
let root = |p: &str| cache.join(p).display().to_string();
write(
&dir.path().join("plugins/installed_plugins.json"),
&serde_json::json!({"version": 2, "plugins": {
"context7@official": [{"scope": "user", "installPath": root("context7/new")}],
"posthog@official": [{"scope": "user", "installPath": root("posthog/v1")}],
"codex@official": [{"scope": "local", "installPath": root("codex/v1"),
"projectPath": "/work"}],
}})
.to_string(),
);
write(
&dir.path().join("settings.json"),
r#"{"enabledPlugins": {"context7@official": true, "posthog@official": false}}"#,
);
dir
}
#[test]
fn only_the_installed_version_of_each_plugin_is_reported() {
let dir = claude_dir();
let cache = dir.path().join("plugins/cache/official");
let mut paths = vec![
cache.join("context7/new/.mcp.json"),
cache.join("context7/old/.mcp.json"),
cache.join("posthog/v1/.mcp.json"),
cache.join("codex/v1/.mcp.json"),
];
retain_active(&mut paths);
assert_eq!(
paths,
vec![
cache.join("context7/new/.mcp.json"),
cache.join("posthog/v1/.mcp.json"),
cache.join("codex/v1/.mcp.json"),
],
"a disabled plugin and a project install are still plugins that can run"
);
assert!(!admits(&cache.join("context7/old/.mcp.json")));
}
#[test]
fn a_path_outside_a_plugin_version_dir_is_not_filtered() {
let dir = claude_dir();
assert!(admits(&dir.path().join("settings.json")));
assert!(admits(&dir.path().join("plugins/cache/official/context7")));
}
#[test]
fn an_unreadable_registry_filters_nothing() {
let dir = claude_dir();
std::fs::write(
dir.path().join("plugins/installed_plugins.json"),
"not json",
)
.unwrap();
assert!(admits(
&dir.path()
.join("plugins/cache/official/context7/old/.mcp.json")
));
}
}