1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
7pub enum PackageType {
8 #[default]
10 Workflow,
11 Skill,
13 Agent,
15 Mcp,
17 Data,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct PackageRef {
41 pub scope: Option<String>,
43 pub name: String,
45 pub version: Option<String>,
47}
48
49impl PackageRef {
50 pub fn parse(input: &str) -> Option<Self> {
58 let input = input.trim();
59 if input.is_empty() {
60 return None;
61 }
62
63 if let Some(without_at) = input.strip_prefix('@') {
65 let (scope, rest) = without_at.split_once('/')?;
66
67 if let Some((name, version)) = rest.split_once('@') {
69 Some(PackageRef {
70 scope: Some(scope.to_string()),
71 name: name.to_string(),
72 version: Some(version.to_string()),
73 })
74 } else {
75 Some(PackageRef {
76 scope: Some(scope.to_string()),
77 name: rest.to_string(),
78 version: None,
79 })
80 }
81 } else {
82 if let Some((name, version)) = input.split_once('@') {
84 Some(PackageRef {
85 scope: None,
86 name: name.to_string(),
87 version: Some(version.to_string()),
88 })
89 } else {
90 Some(PackageRef {
91 scope: None,
92 name: input.to_string(),
93 version: None,
94 })
95 }
96 }
97 }
98
99 pub fn full_name(&self) -> String {
101 match &self.scope {
102 Some(scope) => format!("@{}/{}", scope, self.name),
103 None => self.name.clone(),
104 }
105 }
106
107 pub fn to_string_with_version(&self) -> String {
109 match &self.version {
110 Some(v) => format!("{}@{}", self.full_name(), v),
111 None => self.full_name(),
112 }
113 }
114}
115
116#[derive(Debug, Clone, Default)]
118pub struct PackageManifest {
119 pub name: String,
121 pub version: String,
123 pub description: Option<String>,
125 pub package_type: PackageType,
127 pub authors: Vec<String>,
129 pub license: Option<String>,
131 pub repository: Option<String>,
133 pub keywords: Vec<String>,
135 pub dependencies: Vec<PackageRef>,
137}
138
139impl PackageManifest {
140 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
142 Self {
143 name: name.into(),
144 version: version.into(),
145 ..Default::default()
146 }
147 }
148
149 pub fn as_ref(&self) -> PackageRef {
151 PackageRef {
152 scope: None, name: self.name.clone(),
154 version: Some(self.version.clone()),
155 }
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn test_parse_simple_name() {
165 let pkg = PackageRef::parse("code-review").unwrap();
166 assert_eq!(pkg.scope, None);
167 assert_eq!(pkg.name, "code-review");
168 assert_eq!(pkg.version, None);
169 }
170
171 #[test]
172 fn test_parse_name_with_version() {
173 let pkg = PackageRef::parse("code-review@1.0.0").unwrap();
174 assert_eq!(pkg.scope, None);
175 assert_eq!(pkg.name, "code-review");
176 assert_eq!(pkg.version, Some("1.0.0".to_string()));
177 }
178
179 #[test]
180 fn test_parse_scoped() {
181 let pkg = PackageRef::parse("@workflows/code-review").unwrap();
182 assert_eq!(pkg.scope, Some("workflows".to_string()));
183 assert_eq!(pkg.name, "code-review");
184 assert_eq!(pkg.version, None);
185 }
186
187 #[test]
188 fn test_parse_scoped_with_version() {
189 let pkg = PackageRef::parse("@workflows/code-review@1.2.3").unwrap();
190 assert_eq!(pkg.scope, Some("workflows".to_string()));
191 assert_eq!(pkg.name, "code-review");
192 assert_eq!(pkg.version, Some("1.2.3".to_string()));
193 }
194
195 #[test]
196 fn test_parse_empty() {
197 assert!(PackageRef::parse("").is_none());
198 assert!(PackageRef::parse(" ").is_none());
199 }
200
201 #[test]
202 fn test_full_name() {
203 let scoped = PackageRef::parse("@workflows/code-review").unwrap();
204 assert_eq!(scoped.full_name(), "@workflows/code-review");
205
206 let unscoped = PackageRef::parse("code-review").unwrap();
207 assert_eq!(unscoped.full_name(), "code-review");
208 }
209
210 #[test]
211 fn test_to_string_with_version() {
212 let pkg = PackageRef::parse("@workflows/code-review@1.0.0").unwrap();
213 assert_eq!(pkg.to_string_with_version(), "@workflows/code-review@1.0.0");
214
215 let pkg = PackageRef::parse("@workflows/code-review").unwrap();
216 assert_eq!(pkg.to_string_with_version(), "@workflows/code-review");
217 }
218
219 #[test]
220 fn test_package_manifest() {
221 let manifest = PackageManifest::new("my-workflow", "1.0.0");
222 assert_eq!(manifest.name, "my-workflow");
223 assert_eq!(manifest.version, "1.0.0");
224 assert_eq!(manifest.package_type, PackageType::Workflow);
225 }
226}