Sweep every first-party crate source (1956 .rs files) to the project comment guidelines: delete redundant restatements, decorative banners, change narration, and end-of-line comments; keep and tighten the crucial ones (invariants, bug rationale, SAFETY blocks, ported-source attribution). No functional code changed. Every edit is proven comment-only against the prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a separate doctest-fence check. Where removing a comment made rustfmt or clippy want to re-lay-out adjacent code, the minimal triggering comment is restored so code tokens stay byte-identical. Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy --workspace --all-targets (0 warnings). Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for these guidelines (flags banners, end-of-line comments, change narration, and commented-out code).
498 lines
17 KiB
Rust
498 lines
17 KiB
Rust
//! Marketplace plugin discovery.
|
|
//!
|
|
//! Sources:
|
|
//! - `extraKnownMarketplaces` in `.claude/settings.json` (project-level)
|
|
//! - `~/.claude/plugins/known_marketplaces.json` (user-level registry)
|
|
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
#[derive(Debug)]
|
|
pub struct ResolvedMarketplace {
|
|
pub name: String,
|
|
pub path: PathBuf,
|
|
pub plugin_dirs: Vec<PathBuf>,
|
|
}
|
|
|
|
/// Resolve marketplaces and their enabled plugins from `extraKnownMarketplaces`
|
|
/// and `enabledPlugins` in `.claude/settings.json`. Local directory sources only.
|
|
pub fn resolve(git_root: &Path) -> Vec<ResolvedMarketplace> {
|
|
let settings_path = git_root.join(".claude").join("settings.json");
|
|
let json: serde_json::Value = match std::fs::read_to_string(&settings_path) {
|
|
Ok(c) => match serde_json::from_str(&c) {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "malformed .claude/settings.json");
|
|
return vec![];
|
|
}
|
|
},
|
|
Err(_) => return vec![],
|
|
};
|
|
|
|
let enabled = enabled_plugin_names(&json);
|
|
if enabled.is_empty() {
|
|
return vec![];
|
|
}
|
|
|
|
let Some(marketplaces) = json
|
|
.get("extraKnownMarketplaces")
|
|
.and_then(|v| v.as_object())
|
|
else {
|
|
return vec![];
|
|
};
|
|
|
|
let mut result = Vec::new();
|
|
for (name, config) in marketplaces {
|
|
let Some(rel_path) = config
|
|
.get("source")
|
|
.and_then(|s| s.get("path"))
|
|
.and_then(|p| p.as_str())
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
let marketplace_path = git_root.join(rel_path);
|
|
let plugins_dir = marketplace_path.join("plugins");
|
|
let Ok(entries) = std::fs::read_dir(&plugins_dir) else {
|
|
continue;
|
|
};
|
|
|
|
let mut plugin_dirs = Vec::new();
|
|
for entry in entries.flatten() {
|
|
let path = entry.path();
|
|
if !path.is_dir() {
|
|
continue;
|
|
}
|
|
let plugin_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
|
if enabled.contains(plugin_name) {
|
|
tracing::info!(marketplace = %name, plugin = plugin_name, "marketplace plugin");
|
|
plugin_dirs.push(path);
|
|
}
|
|
}
|
|
|
|
result.push(ResolvedMarketplace {
|
|
name: name.clone(),
|
|
path: marketplace_path,
|
|
plugin_dirs,
|
|
});
|
|
}
|
|
result
|
|
}
|
|
|
|
/// Enabled plugin names from `enabledPlugins` (`"name@marketplace"` keys).
|
|
fn enabled_plugin_names(json: &serde_json::Value) -> HashSet<String> {
|
|
json.get("enabledPlugins")
|
|
.and_then(|v| v.as_object())
|
|
.map(|obj| {
|
|
obj.iter()
|
|
.filter(|(_, v)| v.as_bool().unwrap_or(false))
|
|
.filter_map(|(k, _)| k.split('@').next().map(String::from))
|
|
.collect()
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Parse `enabledPlugins` from a settings JSON value into enabled/disabled lists.
|
|
///
|
|
/// The `enabledPlugins` object has keys like `"name@marketplace"` with boolean values.
|
|
/// Keys with `true` are returned in the first vec (enabled), `false` in the second (disabled).
|
|
/// The `@marketplace` suffix is stripped — only the plugin name is returned.
|
|
pub fn parse_enabled_disabled_plugins(json: &serde_json::Value) -> (Vec<String>, Vec<String>) {
|
|
let Some(obj) = json.get("enabledPlugins").and_then(|v| v.as_object()) else {
|
|
return (vec![], vec![]);
|
|
};
|
|
// Deduplicate by plugin name: the same name may appear under different
|
|
// marketplace keys (e.g. "foo@market1": true, "foo@market2": false).
|
|
// If any entry for a name is `false`, the plugin is disabled (safe default).
|
|
let mut state: HashMap<String, bool> = HashMap::new();
|
|
for (key, val) in obj {
|
|
let name = key.split('@').next().unwrap_or(key).to_string();
|
|
if name.is_empty() {
|
|
continue;
|
|
}
|
|
let Some(value) = val.as_bool() else {
|
|
continue;
|
|
};
|
|
let entry = state.entry(name).or_insert(value);
|
|
// disabled (false) wins on conflict
|
|
if !value {
|
|
*entry = false;
|
|
}
|
|
}
|
|
let mut enabled = Vec::new();
|
|
let mut disabled = Vec::new();
|
|
for (name, is_enabled) in state {
|
|
if is_enabled {
|
|
enabled.push(name);
|
|
} else {
|
|
disabled.push(name);
|
|
}
|
|
}
|
|
(enabled, disabled)
|
|
}
|
|
|
|
/// Load and parse `enabledPlugins` from a `.claude/settings.json` file path.
|
|
///
|
|
/// Returns `(enabled, disabled)` plugin name lists.
|
|
/// Returns empty vecs if the file is missing or malformed.
|
|
pub fn load_enabled_disabled_plugins(path: &Path) -> (Vec<String>, Vec<String>) {
|
|
let content = match std::fs::read_to_string(path) {
|
|
Ok(c) => c,
|
|
Err(_) => return (vec![], vec![]),
|
|
};
|
|
let json: serde_json::Value = match serde_json::from_str(&content) {
|
|
Ok(v) => v,
|
|
Err(_) => return (vec![], vec![]),
|
|
};
|
|
parse_enabled_disabled_plugins(&json)
|
|
}
|
|
|
|
// Compat known_marketplaces.json
|
|
|
|
/// Entry in `~/.claude/plugins/known_marketplaces.json`.
|
|
#[derive(serde::Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct KnownMarketplaceEntry {
|
|
install_location: PathBuf,
|
|
}
|
|
|
|
/// Resolve user-level marketplaces from `known_marketplaces.json`.
|
|
///
|
|
/// Returns marketplace entries with their local `installLocation` paths.
|
|
/// Plugin dirs are filtered to names present in user-level
|
|
/// `~/.claude/settings{.local}.json` `enabledPlugins` with any value (a
|
|
/// `false` entry is an installed-but-disabled plugin whose state we
|
|
/// mirror), so never-installed catalog plugins are not discovered.
|
|
pub fn resolve_known_marketplaces() -> Vec<ResolvedMarketplace> {
|
|
let Some(home) = dirs::home_dir() else {
|
|
return vec![];
|
|
};
|
|
resolve_known_marketplaces_in(&home.join(".claude"))
|
|
}
|
|
|
|
/// Like [`resolve_known_marketplaces`] but reads from an explicit `~/.claude`
|
|
/// root, so tests stay isolated from the developer's real home dir.
|
|
pub fn resolve_known_marketplaces_in(claude_dir: &Path) -> Vec<ResolvedMarketplace> {
|
|
let json_path = claude_dir.join("plugins").join("known_marketplaces.json");
|
|
let content = match std::fs::read_to_string(&json_path) {
|
|
Ok(c) => c,
|
|
Err(_) => return vec![],
|
|
};
|
|
let registry: HashMap<String, KnownMarketplaceEntry> = match serde_json::from_str(&content) {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "failed to parse known_marketplaces.json");
|
|
return vec![];
|
|
}
|
|
};
|
|
|
|
let installed = installed_plugin_keys(claude_dir);
|
|
|
|
registry
|
|
.into_iter()
|
|
.filter_map(|(name, entry)| {
|
|
let path = entry.install_location;
|
|
if !path.is_dir() {
|
|
return None;
|
|
}
|
|
// Collect plugin subdirectories from plugins/ and external_plugins/
|
|
let mut plugin_dirs = Vec::new();
|
|
for subdir in &["plugins", "external_plugins"] {
|
|
let dir = path.join(subdir);
|
|
if let Ok(entries) = std::fs::read_dir(&dir) {
|
|
for entry in entries.flatten() {
|
|
let p = entry.path();
|
|
if !p.is_dir() {
|
|
continue;
|
|
}
|
|
let plugin_name = p.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
|
let is_installed = match installed.get(plugin_name) {
|
|
Some(None) => true,
|
|
Some(Some(marketplaces)) => marketplaces.contains(name.as_str()),
|
|
None => false,
|
|
};
|
|
if is_installed {
|
|
plugin_dirs.push(p);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Some(ResolvedMarketplace {
|
|
name,
|
|
path,
|
|
plugin_dirs,
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// `enabledPlugins` keys from `<claude_dir>/settings.local.json` and
|
|
/// `<claude_dir>/settings.json`, keyed by plugin name. `None` = a bare key
|
|
/// (matches any marketplace); `Some(set)` = only those marketplaces.
|
|
/// Entries with any boolean value count; non-boolean values are skipped.
|
|
fn installed_plugin_keys(claude_dir: &Path) -> HashMap<String, Option<HashSet<String>>> {
|
|
let mut keys: HashMap<String, Option<HashSet<String>>> = HashMap::new();
|
|
for settings_name in ["settings.local.json", "settings.json"] {
|
|
let path = claude_dir.join(settings_name);
|
|
let Ok(content) = std::fs::read_to_string(&path) else {
|
|
continue;
|
|
};
|
|
let json: serde_json::Value = match serde_json::from_str(&content) {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
tracing::warn!(path = %path.display(), error = %e, "malformed settings.json");
|
|
continue;
|
|
}
|
|
};
|
|
let Some(obj) = json.get("enabledPlugins").and_then(|v| v.as_object()) else {
|
|
continue;
|
|
};
|
|
for (key, value) in obj {
|
|
if !value.is_boolean() {
|
|
continue;
|
|
}
|
|
let mut parts = key.splitn(2, '@');
|
|
let Some(plugin_name) = parts.next().filter(|n| !n.is_empty()) else {
|
|
continue;
|
|
};
|
|
match parts.next() {
|
|
Some(marketplace) => {
|
|
if let Some(marketplaces) = keys
|
|
.entry(plugin_name.to_string())
|
|
.or_insert_with(|| Some(HashSet::new()))
|
|
{
|
|
marketplaces.insert(marketplace.to_string());
|
|
}
|
|
}
|
|
None => {
|
|
keys.insert(plugin_name.to_string(), None);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
keys
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parse_enabled_disabled_both() {
|
|
let json = serde_json::json!({
|
|
"enabledPlugins": {
|
|
"alpha@marketplace": true,
|
|
"beta@marketplace": false,
|
|
"gamma@other": true
|
|
}
|
|
});
|
|
let (enabled, disabled) = parse_enabled_disabled_plugins(&json);
|
|
assert!(enabled.contains(&"alpha".to_string()));
|
|
assert!(enabled.contains(&"gamma".to_string()));
|
|
assert_eq!(enabled.len(), 2);
|
|
assert_eq!(disabled.len(), 1);
|
|
assert!(disabled.contains(&"beta".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_enabled_disabled_empty() {
|
|
let json = serde_json::json!({});
|
|
let (enabled, disabled) = parse_enabled_disabled_plugins(&json);
|
|
assert!(enabled.is_empty());
|
|
assert!(disabled.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_enabled_disabled_no_at_sign() {
|
|
let json = serde_json::json!({
|
|
"enabledPlugins": {
|
|
"plain-name": true,
|
|
"other-name": false
|
|
}
|
|
});
|
|
let (enabled, disabled) = parse_enabled_disabled_plugins(&json);
|
|
assert_eq!(enabled.len(), 1);
|
|
assert!(enabled.contains(&"plain-name".to_string()));
|
|
assert_eq!(disabled.len(), 1);
|
|
assert!(disabled.contains(&"other-name".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_enabled_disabled_skips_non_bool() {
|
|
let json = serde_json::json!({
|
|
"enabledPlugins": {
|
|
"good@m": true,
|
|
"bad@m": "yes",
|
|
"ugly@m": 42
|
|
}
|
|
});
|
|
let (enabled, disabled) = parse_enabled_disabled_plugins(&json);
|
|
assert_eq!(enabled.len(), 1);
|
|
assert!(enabled.contains(&"good".to_string()));
|
|
assert!(disabled.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn load_enabled_disabled_missing_file() {
|
|
let (enabled, disabled) =
|
|
load_enabled_disabled_plugins(Path::new("/nonexistent/settings.json"));
|
|
assert!(enabled.is_empty());
|
|
assert!(disabled.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn load_enabled_disabled_from_file() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let path = tmp.path().join("settings.json");
|
|
std::fs::write(
|
|
&path,
|
|
r#"{"enabledPlugins": {"foo@m": true, "bar@m": false}}"#,
|
|
)
|
|
.unwrap();
|
|
let (enabled, disabled) = load_enabled_disabled_plugins(&path);
|
|
assert_eq!(enabled.len(), 1);
|
|
assert!(enabled.contains(&"foo".to_string()));
|
|
assert_eq!(disabled.len(), 1);
|
|
assert!(disabled.contains(&"bar".to_string()));
|
|
}
|
|
|
|
/// Build a `~/.claude`-style dir with one known marketplace named `mp`
|
|
/// containing `plugins/{alpha,beta}` and `external_plugins/gamma`, plus a
|
|
/// `settings.json` with the given content (skipped when `None`).
|
|
fn make_known_marketplace(
|
|
tmp: &Path,
|
|
settings_json: Option<&str>,
|
|
) -> (std::path::PathBuf, std::path::PathBuf) {
|
|
let claude_dir = tmp.join(".claude");
|
|
let mp_dir = tmp.join("mp-repo");
|
|
for plugin in ["plugins/alpha", "plugins/beta", "external_plugins/gamma"] {
|
|
std::fs::create_dir_all(mp_dir.join(plugin)).unwrap();
|
|
}
|
|
std::fs::create_dir_all(claude_dir.join("plugins")).unwrap();
|
|
let known = serde_json::json!({
|
|
"mp": { "installLocation": mp_dir.to_string_lossy() }
|
|
});
|
|
std::fs::write(
|
|
claude_dir.join("plugins").join("known_marketplaces.json"),
|
|
serde_json::to_string(&known).unwrap(),
|
|
)
|
|
.unwrap();
|
|
if let Some(settings) = settings_json {
|
|
std::fs::write(claude_dir.join("settings.json"), settings).unwrap();
|
|
}
|
|
(claude_dir, mp_dir)
|
|
}
|
|
|
|
fn plugin_dir_names(marketplaces: &[ResolvedMarketplace]) -> Vec<String> {
|
|
let mut names: Vec<String> = marketplaces
|
|
.iter()
|
|
.flat_map(|m| &m.plugin_dirs)
|
|
.filter_map(|d| d.file_name().and_then(|n| n.to_str()).map(String::from))
|
|
.collect();
|
|
names.sort();
|
|
names
|
|
}
|
|
|
|
#[test]
|
|
fn known_marketplaces_filtered_to_enabled_plugins_including_false() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
// `gamma@mp: false` = installed-but-disabled: still discovered.
|
|
// `beta` is not listed at all: a never-installed catalog entry.
|
|
let (claude_dir, mp_dir) = make_known_marketplace(
|
|
tmp.path(),
|
|
Some(r#"{"enabledPlugins": {"alpha@mp": true, "gamma@mp": false}}"#),
|
|
);
|
|
|
|
let resolved = resolve_known_marketplaces_in(&claude_dir);
|
|
assert_eq!(resolved.len(), 1);
|
|
assert_eq!(resolved[0].name, "mp");
|
|
assert_eq!(resolved[0].path, mp_dir);
|
|
assert_eq!(
|
|
plugin_dir_names(&resolved),
|
|
vec!["alpha".to_string(), "gamma".to_string()]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn known_marketplaces_key_with_other_marketplace_does_not_match() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let (claude_dir, _) = make_known_marketplace(
|
|
tmp.path(),
|
|
Some(r#"{"enabledPlugins": {"alpha@other": true}}"#),
|
|
);
|
|
|
|
let resolved = resolve_known_marketplaces_in(&claude_dir);
|
|
assert_eq!(resolved.len(), 1);
|
|
assert!(plugin_dir_names(&resolved).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn known_marketplaces_unqualified_key_matches_any_marketplace() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let (claude_dir, _) =
|
|
make_known_marketplace(tmp.path(), Some(r#"{"enabledPlugins": {"alpha": true}}"#));
|
|
|
|
let resolved = resolve_known_marketplaces_in(&claude_dir);
|
|
assert_eq!(plugin_dir_names(&resolved), vec!["alpha".to_string()]);
|
|
}
|
|
|
|
#[test]
|
|
fn known_marketplaces_no_settings_yields_no_plugin_dirs() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let (claude_dir, _) = make_known_marketplace(tmp.path(), None);
|
|
|
|
let resolved = resolve_known_marketplaces_in(&claude_dir);
|
|
assert_eq!(resolved.len(), 1, "marketplace entry itself is kept");
|
|
assert!(plugin_dir_names(&resolved).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn known_marketplaces_reads_settings_local_json_too() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let (claude_dir, _) = make_known_marketplace(
|
|
tmp.path(),
|
|
Some(r#"{"enabledPlugins": {"alpha@mp": true}}"#),
|
|
);
|
|
std::fs::write(
|
|
claude_dir.join("settings.local.json"),
|
|
r#"{"enabledPlugins": {"gamma@mp": false}}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let resolved = resolve_known_marketplaces_in(&claude_dir);
|
|
assert_eq!(
|
|
plugin_dir_names(&resolved),
|
|
vec!["alpha".to_string(), "gamma".to_string()],
|
|
"keys from settings.local.json and settings.json must both count"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn known_marketplaces_non_bool_enabled_value_skipped() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let (claude_dir, _) = make_known_marketplace(
|
|
tmp.path(),
|
|
Some(r#"{"enabledPlugins": {"alpha@mp": "yes", "beta@mp": true}}"#),
|
|
);
|
|
|
|
let resolved = resolve_known_marketplaces_in(&claude_dir);
|
|
assert_eq!(plugin_dir_names(&resolved), vec!["beta".to_string()]);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_enabled_disabled_conflict_disabled_wins() {
|
|
// Same plugin name from different marketplaces with conflicting values:
|
|
// disabled (false) should win.
|
|
let json = serde_json::json!({
|
|
"enabledPlugins": {
|
|
"conflict@market1": true,
|
|
"conflict@market2": false
|
|
}
|
|
});
|
|
let (enabled, disabled) = parse_enabled_disabled_plugins(&json);
|
|
assert!(enabled.is_empty());
|
|
assert_eq!(disabled.len(), 1);
|
|
assert!(disabled.contains(&"conflict".to_string()));
|
|
}
|
|
}
|