use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentManifest {
pub metadata: AgentMetadata,
#[serde(skip_serializing_if = "Option::is_none")]
pub requirements: Option<Requirements>,
#[serde(default)]
pub config: HashMap<String, toml::Value>,
}
impl AgentManifest {
pub fn load(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read agent manifest: {}", path.display()))?;
let manifest: Self = toml::from_str(&content)
.with_context(|| format!("Failed to parse agent manifest: {}", path.display()))?;
Ok(manifest)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentMetadata {
pub name: String,
pub description: String,
pub author: String,
pub license: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub homepage: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub repository: Option<String>,
#[serde(default)]
pub keywords: Vec<String>,
#[serde(default)]
pub categories: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnippetManifest {
pub metadata: SnippetMetadata,
pub content: SnippetContent,
#[serde(default)]
pub config: HashMap<String, toml::Value>,
}
impl SnippetManifest {
pub fn load(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read snippet manifest: {}", path.display()))?;
let manifest: Self = toml::from_str(&content)
.with_context(|| format!("Failed to parse snippet manifest: {}", path.display()))?;
Ok(manifest)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnippetMetadata {
pub name: String,
pub description: String,
pub author: String,
pub language: String,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub keywords: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SnippetContent {
Inline {
content: String,
},
File {
file: String,
},
Files {
files: Vec<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Requirements {
#[serde(skip_serializing_if = "Option::is_none")]
pub agpm_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub claude_version: Option<String>,
#[serde(default)]
pub dependencies: Vec<Dependency>,
#[serde(skip_serializing_if = "Option::is_none")]
pub platforms: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dependency {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub r#type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(default)]
pub optional: bool,
}
#[allow(dead_code)]
pub fn load_agent_manifest(path: &Path) -> Result<AgentManifest> {
let content = std::fs::read_to_string(path)?;
let manifest: AgentManifest = toml::from_str(&content)?;
Ok(manifest)
}
#[allow(dead_code)]
pub fn load_snippet_manifest(path: &Path) -> Result<SnippetManifest> {
let content = std::fs::read_to_string(path)?;
let manifest: SnippetManifest = toml::from_str(&content)?;
Ok(manifest)
}
#[allow(dead_code)]
pub fn create_agent_manifest(name: String, author: String) -> AgentManifest {
AgentManifest {
metadata: AgentMetadata {
name: name.clone(),
description: format!("{name} agent for Claude Code"),
author,
license: "MIT".to_string(),
homepage: None,
repository: None,
keywords: vec![],
categories: vec![],
},
requirements: None,
config: HashMap::new(),
}
}
#[allow(dead_code)]
pub fn create_snippet_manifest(name: String, author: String, language: String) -> SnippetManifest {
SnippetManifest {
metadata: SnippetMetadata {
name: name.clone(),
description: format!("{name} snippet"),
author,
language,
tags: vec![],
keywords: vec![],
},
content: SnippetContent::File {
file: "snippet.md".to_string(),
},
config: HashMap::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_create_agent_manifest() {
let manifest = create_agent_manifest("test-agent".to_string(), "John Doe".to_string());
assert_eq!(manifest.metadata.name, "test-agent");
assert_eq!(manifest.metadata.author, "John Doe");
assert_eq!(manifest.metadata.license, "MIT");
assert_eq!(manifest.metadata.description, "test-agent agent for Claude Code");
}
#[test]
fn test_create_snippet_manifest() {
let manifest = create_snippet_manifest(
"test-snippet".to_string(),
"Jane Doe".to_string(),
"python".to_string(),
);
assert_eq!(manifest.metadata.name, "test-snippet");
assert_eq!(manifest.metadata.author, "Jane Doe");
assert_eq!(manifest.metadata.language, "python");
assert_eq!(manifest.metadata.description, "test-snippet snippet");
}
#[test]
fn test_snippet_content_variants() {
let inline = SnippetContent::Inline {
content: "print('hello')".to_string(),
};
let file = SnippetContent::File {
file: "snippet.py".to_string(),
};
let files = SnippetContent::Files {
files: vec!["file1.py".to_string(), "file2.py".to_string()],
};
let inline_json = serde_json::to_string(&inline).unwrap();
assert!(inline_json.contains("content"));
let file_json = serde_json::to_string(&file).unwrap();
assert!(file_json.contains("file"));
let files_json = serde_json::to_string(&files).unwrap();
assert!(files_json.contains("files"));
}
#[test]
fn test_dependency() {
let dep = Dependency {
name: "test-dep".to_string(),
version: Some("^1.0.0".to_string()),
r#type: Some("agent".to_string()),
source: Some("github".to_string()),
optional: false,
};
assert_eq!(dep.name, "test-dep");
assert_eq!(dep.version, Some("^1.0.0".to_string()));
assert!(!dep.optional);
}
#[test]
fn test_requirements() {
let req = Requirements {
agpm_version: Some(">=0.1.0".to_string()),
claude_version: Some("latest".to_string()),
dependencies: vec![Dependency {
name: "dep1".to_string(),
version: None,
r#type: None,
source: None,
optional: false,
}],
platforms: Some(vec!["windows".to_string(), "macos".to_string()]),
};
assert_eq!(req.agpm_version, Some(">=0.1.0".to_string()));
assert_eq!(req.dependencies.len(), 1);
assert_eq!(req.platforms.as_ref().unwrap().len(), 2);
}
#[test]
fn test_save_and_load_agent_manifest() {
let temp = tempdir().unwrap();
let manifest_path = temp.path().join("agent.toml");
let manifest = create_agent_manifest("test".to_string(), "author".to_string());
let toml_str = toml::to_string(&manifest).unwrap();
std::fs::write(&manifest_path, toml_str).unwrap();
let loaded = load_agent_manifest(&manifest_path).unwrap();
assert_eq!(loaded.metadata.name, "test");
assert_eq!(loaded.metadata.author, "author");
}
#[test]
fn test_save_and_load_snippet_manifest() {
let temp = tempdir().unwrap();
let manifest_path = temp.path().join("snippet.toml");
let manifest =
create_snippet_manifest("test".to_string(), "author".to_string(), "rust".to_string());
let toml_str = toml::to_string(&manifest).unwrap();
std::fs::write(&manifest_path, toml_str).unwrap();
let loaded = load_snippet_manifest(&manifest_path).unwrap();
assert_eq!(loaded.metadata.name, "test");
assert_eq!(loaded.metadata.language, "rust");
}
}