carp_cli/utils/
manifest.rs

1use crate::utils::error::{CarpError, CarpResult};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::Path;
5
6/// Agent manifest structure
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct AgentManifest {
9    /// Agent name
10    pub name: String,
11    /// Agent version (semver)
12    pub version: String,
13    /// Short description
14    pub description: String,
15    /// Author information
16    pub author: String,
17    /// License identifier
18    pub license: Option<String>,
19    /// Homepage URL
20    pub homepage: Option<String>,
21    /// Repository URL
22    pub repository: Option<String>,
23    /// Tags for categorization
24    pub tags: Vec<String>,
25    /// List of files to include in the package
26    pub files: Vec<String>,
27    /// Entry point script or configuration
28    pub main: Option<String>,
29    /// Dependencies on other agents
30    pub dependencies: Option<std::collections::HashMap<String, String>>,
31}
32
33impl AgentManifest {
34    /// Load manifest from a TOML file
35    #[allow(dead_code)]
36    pub fn load<P: AsRef<Path>>(path: P) -> CarpResult<Self> {
37        let contents = fs::read_to_string(&path)
38            .map_err(|e| CarpError::ManifestError(format!("Failed to read manifest: {e}")))?;
39
40        let manifest: AgentManifest = toml::from_str(&contents)
41            .map_err(|e| CarpError::ManifestError(format!("Failed to parse manifest: {e}")))?;
42
43        manifest.validate()?;
44        Ok(manifest)
45    }
46
47    /// Save manifest to a TOML file
48    #[allow(dead_code)]
49    pub fn save<P: AsRef<Path>>(&self, path: P) -> CarpResult<()> {
50        self.validate()?;
51
52        let contents = toml::to_string_pretty(self)
53            .map_err(|e| CarpError::ManifestError(format!("Failed to serialize manifest: {e}")))?;
54
55        fs::write(&path, contents)
56            .map_err(|e| CarpError::ManifestError(format!("Failed to write manifest: {e}")))?;
57
58        Ok(())
59    }
60
61    /// Validate the manifest
62    #[allow(dead_code)]
63    pub fn validate(&self) -> CarpResult<()> {
64        if self.name.is_empty() {
65            return Err(CarpError::ManifestError(
66                "Agent name cannot be empty".to_string(),
67            ));
68        }
69
70        if !self
71            .name
72            .chars()
73            .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
74        {
75            return Err(CarpError::ManifestError(
76                "Agent name can only contain alphanumeric characters, hyphens, and underscores"
77                    .to_string(),
78            ));
79        }
80
81        if self.version.is_empty() {
82            return Err(CarpError::ManifestError(
83                "Version cannot be empty".to_string(),
84            ));
85        }
86
87        // Basic semver validation
88        if !self
89            .version
90            .split('.')
91            .all(|part| part.chars().all(|c| c.is_numeric()))
92        {
93            return Err(CarpError::ManifestError(
94                "Version must be in semver format (e.g., 1.0.0)".to_string(),
95            ));
96        }
97
98        if self.description.is_empty() {
99            return Err(CarpError::ManifestError(
100                "Description cannot be empty".to_string(),
101            ));
102        }
103
104        if self.author.is_empty() {
105            return Err(CarpError::ManifestError(
106                "Author cannot be empty".to_string(),
107            ));
108        }
109
110        Ok(())
111    }
112
113    /// Create a default manifest template
114    #[allow(dead_code)]
115    pub fn template(name: &str) -> Self {
116        Self {
117            name: name.to_string(),
118            version: "0.1.0".to_string(),
119            description: format!("A Claude AI agent named {name}"),
120            author: "Your Name <your.email@example.com>".to_string(),
121            license: Some("MIT".to_string()),
122            homepage: None,
123            repository: None,
124            tags: vec!["claude".to_string(), "ai".to_string()],
125            files: vec![
126                "README.md".to_string(),
127                "agent.py".to_string(),
128                "config.toml".to_string(),
129            ],
130            main: Some("agent.py".to_string()),
131            dependencies: None,
132        }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_manifest_validation() {
142        let mut manifest = AgentManifest::template("test-agent");
143        assert!(manifest.validate().is_ok());
144
145        // Test empty name
146        manifest.name = "".to_string();
147        assert!(manifest.validate().is_err());
148
149        // Test invalid name
150        manifest.name = "test agent!".to_string();
151        assert!(manifest.validate().is_err());
152
153        // Test invalid version
154        manifest.name = "test-agent".to_string();
155        manifest.version = "invalid".to_string();
156        assert!(manifest.validate().is_err());
157    }
158
159    #[test]
160    fn test_manifest_serialization() {
161        let manifest = AgentManifest::template("test-agent");
162        let toml_str = toml::to_string(&manifest).unwrap();
163        let deserialized: AgentManifest = toml::from_str(&toml_str).unwrap();
164
165        assert_eq!(manifest.name, deserialized.name);
166        assert_eq!(manifest.version, deserialized.version);
167        assert_eq!(manifest.description, deserialized.description);
168    }
169}