use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::OnceLock;
static DEFAULT_TOOLS_CONFIG: OnceLock<ToolsConfig> = OnceLock::new();
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ResourceConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "merge-target")]
pub merge_target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub flatten: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WellKnownTool {
ClaudeCode,
OpenCode,
Agpm,
Generic,
}
impl WellKnownTool {
pub fn from_name(tool_name: &str) -> Self {
match tool_name {
"claude-code" => WellKnownTool::ClaudeCode,
"opencode" => WellKnownTool::OpenCode,
"agpm" => WellKnownTool::Agpm,
_ => WellKnownTool::Generic,
}
}
pub const fn default_enabled(self) -> bool {
match self {
WellKnownTool::ClaudeCode => true,
WellKnownTool::OpenCode => true,
WellKnownTool::Agpm => true,
WellKnownTool::Generic => true,
}
}
}
#[derive(Debug, Clone, Deserialize)]
struct ArtifactTypeConfigRaw {
path: PathBuf,
#[serde(default)]
resources: HashMap<String, ResourceConfig>,
#[serde(default)]
enabled: Option<bool>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ArtifactTypeConfig {
pub path: PathBuf,
pub resources: HashMap<String, ResourceConfig>,
pub enabled: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct ToolsConfig {
#[serde(flatten)]
pub types: HashMap<String, ArtifactTypeConfig>,
}
impl<'de> serde::Deserialize<'de> for ToolsConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw_types: HashMap<String, ArtifactTypeConfigRaw> = HashMap::deserialize(deserializer)?;
let defaults = DEFAULT_TOOLS_CONFIG.get_or_init(ToolsConfig::default);
let types = raw_types
.into_iter()
.map(|(tool_name, raw_config)| {
let well_known_tool = WellKnownTool::from_name(&tool_name);
let enabled =
raw_config.enabled.unwrap_or_else(|| well_known_tool.default_enabled());
let merged_resources = if let Some(default_config) = defaults.types.get(&tool_name)
{
let mut resources = default_config.resources.clone();
resources.extend(raw_config.resources);
resources
} else {
raw_config.resources
};
let config = ArtifactTypeConfig {
path: raw_config.path,
resources: merged_resources,
enabled,
};
(tool_name, config)
})
.collect();
Ok(ToolsConfig {
types,
})
}
}
impl Default for ToolsConfig {
fn default() -> Self {
use crate::core::ResourceType;
let mut types = HashMap::new();
let mut claude_resources = HashMap::new();
claude_resources.insert(
ResourceType::Agent.to_plural().to_string(),
ResourceConfig {
path: Some("agents/agpm".to_string()),
merge_target: None,
flatten: Some(true), },
);
claude_resources.insert(
ResourceType::Snippet.to_plural().to_string(),
ResourceConfig {
path: Some("snippets/agpm".to_string()),
merge_target: None,
flatten: Some(false), },
);
claude_resources.insert(
ResourceType::Command.to_plural().to_string(),
ResourceConfig {
path: Some("commands/agpm".to_string()),
merge_target: None,
flatten: Some(true), },
);
claude_resources.insert(
ResourceType::Script.to_plural().to_string(),
ResourceConfig {
path: Some("scripts/agpm".to_string()),
merge_target: None,
flatten: Some(false), },
);
claude_resources.insert(
ResourceType::Hook.to_plural().to_string(),
ResourceConfig {
path: None, merge_target: Some(".claude/settings.local.json".to_string()),
flatten: None, },
);
claude_resources.insert(
ResourceType::McpServer.to_plural().to_string(),
ResourceConfig {
path: None, merge_target: Some(".mcp.json".to_string()),
flatten: None, },
);
claude_resources.insert(
ResourceType::Skill.to_plural().to_string(),
ResourceConfig {
path: Some("skills/agpm".to_string()),
merge_target: None,
flatten: Some(false), },
);
types.insert(
"claude-code".to_string(),
ArtifactTypeConfig {
path: PathBuf::from(".claude"),
resources: claude_resources,
enabled: WellKnownTool::ClaudeCode.default_enabled(),
},
);
let mut opencode_resources = HashMap::new();
opencode_resources.insert(
ResourceType::Agent.to_plural().to_string(),
ResourceConfig {
path: Some("agent/agpm".to_string()), merge_target: None,
flatten: Some(true), },
);
opencode_resources.insert(
ResourceType::Snippet.to_plural().to_string(),
ResourceConfig {
path: Some("snippet/agpm".to_string()), merge_target: None,
flatten: Some(false), },
);
opencode_resources.insert(
ResourceType::Command.to_plural().to_string(),
ResourceConfig {
path: Some("command/agpm".to_string()), merge_target: None,
flatten: Some(true), },
);
opencode_resources.insert(
ResourceType::McpServer.to_plural().to_string(),
ResourceConfig {
path: None, merge_target: Some(".opencode/opencode.json".to_string()),
flatten: None, },
);
types.insert(
"opencode".to_string(),
ArtifactTypeConfig {
path: PathBuf::from(".opencode"),
resources: opencode_resources,
enabled: WellKnownTool::OpenCode.default_enabled(),
},
);
let mut agpm_resources = HashMap::new();
agpm_resources.insert(
ResourceType::Snippet.to_plural().to_string(),
ResourceConfig {
path: Some("snippets".to_string()),
merge_target: None,
flatten: Some(false), },
);
types.insert(
"agpm".to_string(),
ArtifactTypeConfig {
path: PathBuf::from(".agpm"),
resources: agpm_resources,
enabled: WellKnownTool::Agpm.default_enabled(),
},
);
Self {
types,
}
}
}