carp_cli/utils/
manifest.rs1use crate::utils::error::{CarpError, CarpResult};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::Path;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct AgentManifest {
9 pub name: String,
11 pub version: String,
13 pub description: String,
15 pub author: String,
17 pub license: Option<String>,
19 pub homepage: Option<String>,
21 pub repository: Option<String>,
23 pub tags: Vec<String>,
25 pub files: Vec<String>,
27 pub main: Option<String>,
29 pub dependencies: Option<std::collections::HashMap<String, String>>,
31}
32
33impl AgentManifest {
34 #[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 #[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 #[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 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 #[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 manifest.name = "".to_string();
147 assert!(manifest.validate().is_err());
148
149 manifest.name = "test agent!".to_string();
151 assert!(manifest.validate().is_err());
152
153 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}