use std::collections::HashMap;
#[derive(Default, Debug)]
pub struct ManifestBuilder {
sources: HashMap<String, String>,
target_config: Option<TargetConfig>,
tools_config: Option<ToolsConfig>,
agents: Vec<DependencyEntry>,
snippets: Vec<DependencyEntry>,
commands: Vec<DependencyEntry>,
scripts: Vec<DependencyEntry>,
hooks: Vec<DependencyEntry>,
mcp_servers: Vec<DependencyEntry>,
skills: Vec<DependencyEntry>,
}
#[derive(Debug, Clone)]
struct TargetConfig {
agents: Option<String>,
snippets: Option<String>,
commands: Option<String>,
scripts: Option<String>,
hooks: Option<String>,
mcp_servers: Option<String>,
gitignore: Option<bool>,
}
#[derive(Debug, Clone)]
struct ToolsConfig {
tools: HashMap<String, ToolConfig>,
}
#[derive(Debug, Clone)]
struct ToolConfig {
path: Option<String>,
enabled: Option<bool>,
resources: HashMap<String, ResourceConfig>,
}
#[derive(Debug, Clone)]
struct ResourceConfig {
path: Option<String>,
merge_target: Option<String>,
flatten: Option<bool>,
}
#[derive(Default, Debug)]
pub struct ToolsConfigBuilder {
tools: HashMap<String, ToolConfig>,
}
#[derive(Debug)]
pub struct ToolConfigBuilder {
name: String,
path: Option<String>,
enabled: Option<bool>,
resources: HashMap<String, ResourceConfig>,
}
impl ToolConfigBuilder {
pub fn path(mut self, path: &str) -> Self {
self.path = Some(path.to_string());
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = Some(enabled);
self
}
pub fn agents(mut self, config: ResourceConfigBuilder) -> Self {
self.resources.insert("agents".to_string(), config.build());
self
}
pub fn snippets(mut self, config: ResourceConfigBuilder) -> Self {
self.resources.insert("snippets".to_string(), config.build());
self
}
pub fn commands(mut self, config: ResourceConfigBuilder) -> Self {
self.resources.insert("commands".to_string(), config.build());
self
}
pub fn scripts(mut self, config: ResourceConfigBuilder) -> Self {
self.resources.insert("scripts".to_string(), config.build());
self
}
pub fn hooks(mut self, config: ResourceConfigBuilder) -> Self {
self.resources.insert("hooks".to_string(), config.build());
self
}
pub fn mcp_servers(mut self, config: ResourceConfigBuilder) -> Self {
self.resources.insert("mcp-servers".to_string(), config.build());
self
}
pub fn skills(mut self, config: ResourceConfigBuilder) -> Self {
self.resources.insert("skills".to_string(), config.build());
self
}
fn build(self) -> ToolConfig {
ToolConfig {
path: self.path,
enabled: self.enabled,
resources: self.resources,
}
}
}
#[derive(Default, Debug)]
pub struct ResourceConfigBuilder {
path: Option<String>,
merge_target: Option<String>,
flatten: Option<bool>,
}
impl ResourceConfigBuilder {
pub fn path(mut self, path: &str) -> Self {
self.path = Some(path.to_string());
self
}
pub fn merge_target(mut self, target: &str) -> Self {
self.merge_target = Some(target.to_string());
self
}
pub fn flatten(mut self, flatten: bool) -> Self {
self.flatten = Some(flatten);
self
}
fn build(self) -> ResourceConfig {
ResourceConfig {
path: self.path,
merge_target: self.merge_target,
flatten: self.flatten,
}
}
}
impl ToolsConfigBuilder {
pub fn tool<F>(mut self, name: &str, config: F) -> Self
where
F: FnOnce(ToolConfigBuilder) -> ToolConfigBuilder,
{
let builder = ToolConfigBuilder {
name: name.to_string(),
path: None,
enabled: None,
resources: HashMap::new(),
};
let tool_config = config(builder).build();
self.tools.insert(name.to_string(), tool_config);
self
}
fn build(self) -> ToolsConfig {
ToolsConfig {
tools: self.tools,
}
}
}
#[derive(Default, Debug)]
pub struct TargetConfigBuilder {
agents: Option<String>,
snippets: Option<String>,
commands: Option<String>,
scripts: Option<String>,
hooks: Option<String>,
mcp_servers: Option<String>,
gitignore: Option<bool>,
}
impl TargetConfigBuilder {
pub fn agents(mut self, path: &str) -> Self {
self.agents = Some(path.to_string());
self
}
pub fn snippets(mut self, path: &str) -> Self {
self.snippets = Some(path.to_string());
self
}
pub fn commands(mut self, path: &str) -> Self {
self.commands = Some(path.to_string());
self
}
pub fn scripts(mut self, path: &str) -> Self {
self.scripts = Some(path.to_string());
self
}
pub fn hooks(mut self, path: &str) -> Self {
self.hooks = Some(path.to_string());
self
}
pub fn mcp_servers(mut self, path: &str) -> Self {
self.mcp_servers = Some(path.to_string());
self
}
pub fn gitignore(mut self, enabled: bool) -> Self {
self.gitignore = Some(enabled);
self
}
fn build(self) -> TargetConfig {
TargetConfig {
agents: self.agents,
snippets: self.snippets,
commands: self.commands,
scripts: self.scripts,
hooks: self.hooks,
mcp_servers: self.mcp_servers,
gitignore: self.gitignore,
}
}
}
#[derive(Debug, Clone)]
struct DependencyEntry {
name: String,
source: Option<String>,
path: String,
version: Option<String>,
branch: Option<String>,
rev: Option<String>,
tool: Option<String>,
target: Option<String>,
filename: Option<String>,
flatten: Option<bool>,
}
#[derive(Debug)]
pub struct DependencyBuilder {
name: String,
source: Option<String>,
path: Option<String>,
version: Option<String>,
branch: Option<String>,
rev: Option<String>,
tool: Option<String>,
target: Option<String>,
filename: Option<String>,
flatten: Option<bool>,
}
impl DependencyBuilder {
pub fn source(mut self, source: &str) -> Self {
self.source = Some(source.to_string());
self
}
pub fn path(mut self, path: &str) -> Self {
self.path = Some(path.to_string());
self
}
pub fn version(mut self, version: &str) -> Self {
self.version = Some(version.to_string());
self
}
pub fn branch(mut self, branch: &str) -> Self {
self.branch = Some(branch.to_string());
self
}
pub fn rev(mut self, rev: &str) -> Self {
self.rev = Some(rev.to_string());
self
}
pub fn tool(mut self, tool: &str) -> Self {
self.tool = Some(tool.to_string());
self
}
pub fn target(mut self, target: &str) -> Self {
self.target = Some(target.to_string());
self
}
pub fn flatten(mut self, flatten: bool) -> Self {
self.flatten = Some(flatten);
self
}
pub fn filename(mut self, filename: &str) -> Self {
self.filename = Some(filename.to_string());
self
}
fn build(self) -> DependencyEntry {
DependencyEntry {
name: self.name,
source: self.source,
path: self.path.expect("path is required for dependency"),
version: self.version,
branch: self.branch,
rev: self.rev,
tool: self.tool,
target: self.target,
filename: self.filename,
flatten: self.flatten,
}
}
}
impl ManifestBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn add_source(mut self, name: &str, url: &str) -> Self {
self.sources.insert(name.to_string(), url.to_string());
self
}
pub fn add_sources(mut self, sources: &[(&str, &str)]) -> Self {
for (name, url) in sources {
self.sources.insert(name.to_string(), url.to_string());
}
self
}
pub fn with_target_config<F>(mut self, config: F) -> Self
where
F: FnOnce(TargetConfigBuilder) -> TargetConfigBuilder,
{
let builder = TargetConfigBuilder::default();
self.target_config = Some(config(builder).build());
self
}
pub fn with_tools_config<F>(mut self, config: F) -> Self
where
F: FnOnce(ToolsConfigBuilder) -> ToolsConfigBuilder,
{
let builder = ToolsConfigBuilder::default();
self.tools_config = Some(config(builder).build());
self
}
pub fn with_gitignore(mut self, enabled: bool) -> Self {
if let Some(ref mut config) = self.target_config {
config.gitignore = Some(enabled);
} else {
self.target_config = Some(TargetConfig {
agents: None,
snippets: None,
commands: None,
scripts: None,
hooks: None,
mcp_servers: None,
gitignore: Some(enabled),
});
}
self
}
pub fn add_agent<F>(mut self, name: &str, config: F) -> Self
where
F: FnOnce(DependencyBuilder) -> DependencyBuilder,
{
let builder = DependencyBuilder {
name: name.to_string(),
source: None,
path: None,
version: None,
branch: None,
rev: None,
tool: None,
target: None,
filename: None,
flatten: None,
};
let entry = config(builder).build();
self.agents.push(entry);
self
}
pub fn add_snippet<F>(mut self, name: &str, config: F) -> Self
where
F: FnOnce(DependencyBuilder) -> DependencyBuilder,
{
let builder = DependencyBuilder {
name: name.to_string(),
source: None,
path: None,
version: None,
branch: None,
rev: None,
tool: None,
target: None,
filename: None,
flatten: None,
};
let entry = config(builder).build();
self.snippets.push(entry);
self
}
pub fn add_command<F>(mut self, name: &str, config: F) -> Self
where
F: FnOnce(DependencyBuilder) -> DependencyBuilder,
{
let builder = DependencyBuilder {
name: name.to_string(),
source: None,
path: None,
version: None,
branch: None,
rev: None,
tool: None,
target: None,
filename: None,
flatten: None,
};
let entry = config(builder).build();
self.commands.push(entry);
self
}
pub fn add_script<F>(mut self, name: &str, config: F) -> Self
where
F: FnOnce(DependencyBuilder) -> DependencyBuilder,
{
let builder = DependencyBuilder {
name: name.to_string(),
source: None,
path: None,
version: None,
branch: None,
rev: None,
tool: None,
target: None,
filename: None,
flatten: None,
};
let entry = config(builder).build();
self.scripts.push(entry);
self
}
pub fn add_hook<F>(mut self, name: &str, config: F) -> Self
where
F: FnOnce(DependencyBuilder) -> DependencyBuilder,
{
let builder = DependencyBuilder {
name: name.to_string(),
source: None,
path: None,
version: None,
branch: None,
rev: None,
tool: None,
target: None,
filename: None,
flatten: None,
};
let entry = config(builder).build();
self.hooks.push(entry);
self
}
pub fn add_mcp_server<F>(mut self, name: &str, config: F) -> Self
where
F: FnOnce(DependencyBuilder) -> DependencyBuilder,
{
let builder = DependencyBuilder {
name: name.to_string(),
source: None,
path: None,
version: None,
branch: None,
rev: None,
tool: None,
target: None,
filename: None,
flatten: None,
};
let entry = config(builder).build();
self.mcp_servers.push(entry);
self
}
pub fn add_skill<F>(mut self, name: &str, config: F) -> Self
where
F: FnOnce(DependencyBuilder) -> DependencyBuilder,
{
let builder = DependencyBuilder {
name: name.to_string(),
source: None,
path: None,
version: None,
branch: None,
rev: None,
tool: None,
target: None,
filename: None,
flatten: None,
};
let entry = config(builder).build();
self.skills.push(entry);
self
}
pub fn build(self) -> String {
fn escape_toml_string(s: &str) -> String {
s.replace('\\', "\\\\")
}
let mut toml = String::new();
if !self.sources.is_empty() {
toml.push_str("[sources]\n");
for (name, url) in &self.sources {
toml.push_str(&format!("{} = \"{}\"\n", name, escape_toml_string(url)));
}
toml.push('\n');
}
fn format_dependencies(toml: &mut String, section: &str, deps: &[DependencyEntry]) {
if !deps.is_empty() {
toml.push_str(&format!("[{}]\n", section));
for dep in deps {
toml.push_str(&format!("{} = {{ ", dep.name));
if let Some(source) = &dep.source {
toml.push_str(&format!("source = \"{}\", ", escape_toml_string(source)));
}
toml.push_str(&format!("path = \"{}\"", escape_toml_string(&dep.path)));
if let Some(version) = &dep.version {
toml.push_str(&format!(", version = \"{}\"", escape_toml_string(version)));
}
if let Some(branch) = &dep.branch {
toml.push_str(&format!(", branch = \"{}\"", escape_toml_string(branch)));
}
if let Some(rev) = &dep.rev {
toml.push_str(&format!(", rev = \"{}\"", escape_toml_string(rev)));
}
if let Some(tool) = &dep.tool {
toml.push_str(&format!(", tool = \"{}\"", escape_toml_string(tool)));
}
if let Some(target) = &dep.target {
toml.push_str(&format!(", target = \"{}\"", escape_toml_string(target)));
}
if let Some(filename) = &dep.filename {
toml.push_str(&format!(
", filename = \"{}\"",
escape_toml_string(filename)
));
}
if let Some(flatten) = dep.flatten {
toml.push_str(&format!(", flatten = {}", flatten));
}
toml.push_str(" }\n");
}
toml.push('\n');
}
}
format_dependencies(&mut toml, "agents", &self.agents);
format_dependencies(&mut toml, "snippets", &self.snippets);
format_dependencies(&mut toml, "commands", &self.commands);
format_dependencies(&mut toml, "scripts", &self.scripts);
format_dependencies(&mut toml, "hooks", &self.hooks);
format_dependencies(&mut toml, "mcp-servers", &self.mcp_servers);
format_dependencies(&mut toml, "skills", &self.skills);
if let Some(config) = self.tools_config {
if !config.tools.is_empty() {
toml.push_str("[tools]\n");
for (tool_name, tool_config) in &config.tools {
toml.push_str(&format!("[tools.{}]\n", tool_name));
if let Some(path) = &tool_config.path {
toml.push_str(&format!("path = \"{}\"\n", escape_toml_string(path)));
}
if let Some(enabled) = tool_config.enabled {
toml.push_str(&format!("enabled = {}\n", enabled));
}
if !tool_config.resources.is_empty() {
toml.push_str("[tools.");
toml.push_str(tool_name);
toml.push_str(".resources]\n");
for (resource_name, resource_config) in &tool_config.resources {
toml.push_str(resource_name);
toml.push_str(" = { ");
let mut has_fields = false;
if let Some(path) = &resource_config.path {
toml.push_str(&format!("path = \"{}\"", escape_toml_string(path)));
has_fields = true;
}
if let Some(merge_target) = &resource_config.merge_target {
if has_fields {
toml.push_str(", ");
}
toml.push_str(&format!(
"merge-target = \"{}\"",
escape_toml_string(merge_target)
));
has_fields = true;
}
if let Some(flatten) = resource_config.flatten {
if has_fields {
toml.push_str(", ");
}
toml.push_str(&format!("flatten = {}", flatten));
}
toml.push_str(" }\n");
}
}
toml.push('\n');
}
}
}
if let Some(config) = self.target_config {
let mut has_fields = false;
let mut target_section = String::from("[target]\n");
if let Some(path) = config.agents {
target_section.push_str(&format!("agents = \"{}\"\n", escape_toml_string(&path)));
has_fields = true;
}
if let Some(path) = config.snippets {
target_section.push_str(&format!("snippets = \"{}\"\n", escape_toml_string(&path)));
has_fields = true;
}
if let Some(path) = config.commands {
target_section.push_str(&format!("commands = \"{}\"\n", escape_toml_string(&path)));
has_fields = true;
}
if let Some(path) = config.scripts {
target_section.push_str(&format!("scripts = \"{}\"\n", escape_toml_string(&path)));
has_fields = true;
}
if let Some(path) = config.hooks {
target_section.push_str(&format!("hooks = \"{}\"\n", escape_toml_string(&path)));
has_fields = true;
}
if let Some(path) = config.mcp_servers {
target_section
.push_str(&format!("mcp-servers = \"{}\"\n", escape_toml_string(&path)));
has_fields = true;
}
if let Some(enabled) = config.gitignore {
target_section.push_str(&format!("gitignore = {}\n", enabled));
has_fields = true;
}
if has_fields {
toml.push_str(&target_section);
toml.push('\n');
}
}
toml
}
}
impl ManifestBuilder {
pub fn add_standard_agent(self, name: &str, source: &str, path: &str) -> Self {
self.add_agent(name, |d| d.source(source).path(path).version("v1.0.0"))
}
pub fn add_standard_snippet(self, name: &str, source: &str, path: &str) -> Self {
self.add_snippet(name, |d| d.source(source).path(path).version("v1.0.0"))
}
pub fn add_standard_command(self, name: &str, source: &str, path: &str) -> Self {
self.add_command(name, |d| d.source(source).path(path).version("v1.0.0"))
}
pub fn add_local_agent(self, name: &str, path: &str) -> Self {
self.add_agent(name, |d| d.path(path))
}
pub fn add_local_snippet(self, name: &str, path: &str) -> Self {
self.add_snippet(name, |d| d.path(path))
}
pub fn add_local_command(self, name: &str, path: &str) -> Self {
self.add_command(name, |d| d.path(path))
}
pub fn add_agent_pattern(self, name: &str, source: &str, pattern: &str, version: &str) -> Self {
self.add_agent(name, |d| d.source(source).path(pattern).version(version))
}
pub fn add_snippet_pattern(
self,
name: &str,
source: &str,
pattern: &str,
version: &str,
) -> Self {
self.add_snippet(name, |d| d.source(source).path(pattern).version(version))
}
pub fn with_claude_code_tool(self) -> Self {
self.with_tools_config(|t| {
t.tool("claude-code", |c| {
c.path(".claude")
.agents(ResourceConfigBuilder::default().path("agents/agpm"))
.snippets(ResourceConfigBuilder::default().path("snippets/agpm"))
.commands(ResourceConfigBuilder::default().path("commands/agpm"))
.scripts(ResourceConfigBuilder::default().path("scripts/agpm"))
.hooks(
ResourceConfigBuilder::default()
.merge_target(".claude/settings.local.json"),
)
.mcp_servers(ResourceConfigBuilder::default().merge_target(".mcp.json"))
.skills(ResourceConfigBuilder::default().path("skills/agpm"))
})
})
}
pub fn with_opencode_tool(self) -> Self {
self.with_tools_config(|t| {
t.tool("opencode", |c| {
c.path(".opencode")
.enabled(false)
.agents(ResourceConfigBuilder::default().path("agent"))
.commands(ResourceConfigBuilder::default().path("command"))
.mcp_servers(
ResourceConfigBuilder::default().merge_target(".opencode/opencode.json"),
)
})
})
}
pub fn with_malformed_hooks_tool(self) -> Self {
self.with_tools_config(|t| {
t.tool("claude-code", |c| {
c.path(".claude")
.agents(ResourceConfigBuilder::default().path("agents"))
.snippets(ResourceConfigBuilder::default().path("snippets"))
.commands(ResourceConfigBuilder::default().path("commands"))
.scripts(ResourceConfigBuilder::default().path("scripts"))
.hooks(ResourceConfigBuilder::default()) .mcp_servers(ResourceConfigBuilder::default().merge_target(".mcp.json"))
})
})
}
pub fn with_missing_hooks_tool(self) -> Self {
self.with_tools_config(|t| {
t.tool("claude-code", |c| {
c.path(".claude")
.agents(ResourceConfigBuilder::default().path("agents"))
.snippets(ResourceConfigBuilder::default().path("snippets"))
.commands(ResourceConfigBuilder::default().path("commands"))
.scripts(ResourceConfigBuilder::default().path("scripts"))
.mcp_servers(ResourceConfigBuilder::default().merge_target(".mcp.json"))
})
})
}
pub fn with_empty_hooks_tool(self) -> Self {
self.with_tools_config(|t| {
t.tool("claude-code", |c| {
c.path(".claude")
.agents(ResourceConfigBuilder::default().path("agents"))
.snippets(ResourceConfigBuilder::default().path("snippets"))
.commands(ResourceConfigBuilder::default().path("commands"))
.scripts(ResourceConfigBuilder::default().path("scripts"))
.hooks(ResourceConfigBuilder::default()) .mcp_servers(ResourceConfigBuilder::default().merge_target(".mcp.json"))
})
})
}
}