use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PluginId {
pub name: &'static str,
pub marketplace: &'static str,
}
pub const DEVBOY_PLUGIN: PluginId = PluginId {
name: "devboy",
marketplace: "meteora-devboy",
};
pub fn is_claude_plugin_enabled(home: &Path, plugin: &PluginId) -> bool {
is_plugin_enabled_in(&home.join(".claude").join("settings.json"), plugin)
}
pub fn is_codex_plugin_enabled(home: &Path, plugin: &PluginId) -> bool {
is_plugin_enabled_in(&home.join(".codex").join("settings.json"), plugin)
}
#[doc(hidden)]
pub fn is_claude_plugin_installed(home: &Path, plugin_name: &str) -> bool {
if plugin_name == DEVBOY_PLUGIN.name {
return is_claude_plugin_enabled(home, &DEVBOY_PLUGIN);
}
false
}
#[doc(hidden)]
pub fn is_codex_plugin_installed(home: &Path, plugin_name: &str) -> bool {
if plugin_name == DEVBOY_PLUGIN.name {
return is_codex_plugin_enabled(home, &DEVBOY_PLUGIN);
}
false
}
fn is_plugin_enabled_in(settings_path: &Path, plugin: &PluginId) -> bool {
let bytes = match fs::read(settings_path) {
Ok(b) => b,
Err(_) => return false,
};
let json: serde_json::Value = match serde_json::from_slice(&bytes) {
Ok(v) => v,
Err(_) => return false,
};
let Some(enabled) = json.get("enabledPlugins") else {
return false;
};
enabled_plugins_contains(enabled, plugin)
}
fn enabled_plugins_contains(value: &serde_json::Value, plugin: &PluginId) -> bool {
use serde_json::Value;
let qualified = format!("{}@{}", plugin.name, plugin.marketplace);
match value {
Value::Object(map) => {
if let Some(Value::Object(inner)) = map.get(plugin.marketplace)
&& let Some(v) = inner.get(plugin.name)
&& is_truthy(v)
{
return true;
}
if let Some(v) = map.get(&qualified)
&& is_truthy(v)
{
return true;
}
false
}
Value::Array(arr) => arr
.iter()
.any(|v| matches!(v, Value::String(s) if s == &qualified)),
_ => false,
}
}
fn is_truthy(value: &serde_json::Value) -> bool {
use serde_json::Value;
match value {
Value::Bool(b) => *b,
Value::Object(map) => map
.get("enabled")
.map(|v| !matches!(v, Value::Bool(false)))
.unwrap_or(true),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs as stdfs;
use tempfile::tempdir;
fn write_settings(home: &Path, agent_dir: &str, body: &str) {
let dir = home.join(agent_dir);
stdfs::create_dir_all(&dir).unwrap();
stdfs::write(dir.join("settings.json"), body).unwrap();
}
#[test]
fn missing_file_returns_false() {
let home = tempdir().unwrap();
assert!(!is_claude_plugin_enabled(home.path(), &DEVBOY_PLUGIN));
assert!(!is_codex_plugin_enabled(home.path(), &DEVBOY_PLUGIN));
}
#[test]
fn unrelated_settings_returns_false() {
let home = tempdir().unwrap();
write_settings(home.path(), ".claude", r#"{"theme":"dark"}"#);
assert!(!is_claude_plugin_enabled(home.path(), &DEVBOY_PLUGIN));
}
#[test]
fn malformed_json_returns_false() {
let home = tempdir().unwrap();
write_settings(home.path(), ".claude", "not json {{{");
assert!(!is_claude_plugin_enabled(home.path(), &DEVBOY_PLUGIN));
}
#[test]
fn pattern1_nested_object_marketplace_then_name() {
let home = tempdir().unwrap();
write_settings(
home.path(),
".claude",
r#"{
"enabledPlugins": {
"meteora-devboy": { "devboy": true }
}
}"#,
);
assert!(is_claude_plugin_enabled(home.path(), &DEVBOY_PLUGIN));
}
#[test]
fn pattern2_qualified_key_at_top_level() {
let home = tempdir().unwrap();
write_settings(
home.path(),
".claude",
r#"{ "enabledPlugins": { "devboy@meteora-devboy": true } }"#,
);
assert!(is_claude_plugin_enabled(home.path(), &DEVBOY_PLUGIN));
}
#[test]
fn pattern3_qualified_array_element() {
let home = tempdir().unwrap();
write_settings(
home.path(),
".claude",
r#"{ "enabledPlugins": ["other", "devboy@meteora-devboy"] }"#,
);
assert!(is_claude_plugin_enabled(home.path(), &DEVBOY_PLUGIN));
}
#[test]
fn unrelated_plugin_with_devboy_substring_does_not_match() {
let home = tempdir().unwrap();
write_settings(
home.path(),
".claude",
r#"{
"enabledPlugins": {
"third-party": { "devboy-helper": true }
}
}"#,
);
assert!(!is_claude_plugin_enabled(home.path(), &DEVBOY_PLUGIN));
}
#[test]
fn correct_name_wrong_marketplace_does_not_match() {
let home = tempdir().unwrap();
write_settings(
home.path(),
".claude",
r#"{
"enabledPlugins": {
"fork-marketplace": { "devboy": true }
}
}"#,
);
assert!(!is_claude_plugin_enabled(home.path(), &DEVBOY_PLUGIN));
}
#[test]
fn explicitly_disabled_plugin_does_not_match() {
let home = tempdir().unwrap();
write_settings(
home.path(),
".claude",
r#"{
"enabledPlugins": {
"meteora-devboy": { "devboy": false }
}
}"#,
);
assert!(!is_claude_plugin_enabled(home.path(), &DEVBOY_PLUGIN));
}
#[test]
fn explicitly_disabled_via_qualified_key_does_not_match() {
let home = tempdir().unwrap();
write_settings(
home.path(),
".claude",
r#"{ "enabledPlugins": { "devboy@meteora-devboy": false } }"#,
);
assert!(!is_claude_plugin_enabled(home.path(), &DEVBOY_PLUGIN));
}
#[test]
fn enabled_object_with_enabled_field_is_truthy() {
let home = tempdir().unwrap();
write_settings(
home.path(),
".claude",
r#"{
"enabledPlugins": {
"meteora-devboy": {
"devboy": { "enabled": true, "version": "0.24.0" }
}
}
}"#,
);
assert!(is_claude_plugin_enabled(home.path(), &DEVBOY_PLUGIN));
}
#[test]
fn enabled_object_with_enabled_false_is_skipped() {
let home = tempdir().unwrap();
write_settings(
home.path(),
".claude",
r#"{
"enabledPlugins": {
"meteora-devboy": {
"devboy": { "enabled": false }
}
}
}"#,
);
assert!(!is_claude_plugin_enabled(home.path(), &DEVBOY_PLUGIN));
}
#[test]
fn codex_settings_independent_of_claude() {
let home = tempdir().unwrap();
write_settings(
home.path(),
".codex",
r#"{ "enabledPlugins": { "devboy@meteora-devboy": true } }"#,
);
assert!(is_codex_plugin_enabled(home.path(), &DEVBOY_PLUGIN));
assert!(!is_claude_plugin_enabled(home.path(), &DEVBOY_PLUGIN));
}
}