use crate::utils::error::{CarpError, CarpResult};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentManifest {
pub name: String,
pub version: String,
pub description: String,
pub author: String,
pub license: Option<String>,
pub homepage: Option<String>,
pub repository: Option<String>,
pub tags: Vec<String>,
pub files: Vec<String>,
pub main: Option<String>,
pub dependencies: Option<std::collections::HashMap<String, String>>,
}
impl AgentManifest {
#[allow(dead_code)]
pub fn load<P: AsRef<Path>>(path: P) -> CarpResult<Self> {
let contents = fs::read_to_string(&path)
.map_err(|e| CarpError::ManifestError(format!("Failed to read manifest: {e}")))?;
let manifest: AgentManifest = toml::from_str(&contents)
.map_err(|e| CarpError::ManifestError(format!("Failed to parse manifest: {e}")))?;
manifest.validate()?;
Ok(manifest)
}
#[allow(dead_code)]
pub fn save<P: AsRef<Path>>(&self, path: P) -> CarpResult<()> {
self.validate()?;
let contents = toml::to_string_pretty(self)
.map_err(|e| CarpError::ManifestError(format!("Failed to serialize manifest: {e}")))?;
fs::write(&path, contents)
.map_err(|e| CarpError::ManifestError(format!("Failed to write manifest: {e}")))?;
Ok(())
}
#[allow(dead_code)]
pub fn validate(&self) -> CarpResult<()> {
if self.name.is_empty() {
return Err(CarpError::ManifestError(
"Agent name cannot be empty".to_string(),
));
}
if !self
.name
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
return Err(CarpError::ManifestError(
"Agent name can only contain alphanumeric characters, hyphens, and underscores"
.to_string(),
));
}
if self.version.is_empty() {
return Err(CarpError::ManifestError(
"Version cannot be empty".to_string(),
));
}
if !self
.version
.split('.')
.all(|part| part.chars().all(|c| c.is_numeric()))
{
return Err(CarpError::ManifestError(
"Version must be in semver format (e.g., 1.0.0)".to_string(),
));
}
if self.description.is_empty() {
return Err(CarpError::ManifestError(
"Description cannot be empty".to_string(),
));
}
if self.author.is_empty() {
return Err(CarpError::ManifestError(
"Author cannot be empty".to_string(),
));
}
Ok(())
}
#[allow(dead_code)]
pub fn template(name: &str) -> Self {
Self {
name: name.to_string(),
version: "0.1.0".to_string(),
description: format!("A Claude AI agent named {name}"),
author: "Your Name <your.email@example.com>".to_string(),
license: Some("MIT".to_string()),
homepage: None,
repository: None,
tags: vec!["claude".to_string(), "ai".to_string()],
files: vec![
"README.md".to_string(),
"agent.py".to_string(),
"config.toml".to_string(),
],
main: Some("agent.py".to_string()),
dependencies: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_manifest_validation() {
let mut manifest = AgentManifest::template("test-agent");
assert!(manifest.validate().is_ok());
manifest.name = "".to_string();
assert!(manifest.validate().is_err());
manifest.name = "test agent!".to_string();
assert!(manifest.validate().is_err());
manifest.name = "test-agent".to_string();
manifest.version = "invalid".to_string();
assert!(manifest.validate().is_err());
}
#[test]
fn test_manifest_serialization() {
let manifest = AgentManifest::template("test-agent");
let toml_str = toml::to_string(&manifest).unwrap();
let deserialized: AgentManifest = toml::from_str(&toml_str).unwrap();
assert_eq!(manifest.name, deserialized.name);
assert_eq!(manifest.version, deserialized.version);
assert_eq!(manifest.description, deserialized.description);
}
}