1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(untagged)]
12pub enum PluginSource {
13 Relative(String),
15 Npm {
17 source: String,
18 package: String,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 version: Option<String>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 registry: Option<String>,
23 },
24 Pip {
26 source: String,
27 package: String,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 version: Option<String>,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 index_url: Option<String>,
32 },
33 Github {
35 source: String,
36 repo: String,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 ref_: Option<String>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 path: Option<String>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 sparse_paths: Option<Vec<String>>,
43 },
44 GitSubdir {
46 source: String,
47 repo: String,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 ref_: Option<String>,
50 subdir: String,
51 },
52 Git {
54 source: String,
55 url: String,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 ref_: Option<String>,
58 },
59 Url {
61 source: String,
62 url: String,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 headers: Option<HashMap<String, String>>,
65 },
66 Settings { source: String },
68}
69
70impl PluginSource {
71 pub fn is_local(&self) -> bool {
73 matches!(self, PluginSource::Relative(s) if s.starts_with("./"))
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80pub struct PluginMarketplaceEntry {
81 pub name: String,
83 pub source: PluginSource,
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub description: Option<String>,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub version: Option<String>,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub strict: Option<bool>,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub category: Option<String>,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub tags: Option<Vec<String>>,
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub author: Option<super::super::super::plugin::types::PluginAuthor>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub homepage: Option<String>,
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub repository: Option<String>,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 pub keywords: Option<Vec<String>>,
104 #[serde(skip_serializing_if = "Option::is_none")]
105 pub commands: Option<serde_json::Value>,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub agents: Option<serde_json::Value>,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub skills: Option<serde_json::Value>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub hooks: Option<serde_json::Value>,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub output_styles: Option<serde_json::Value>,
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub mcp_servers: Option<serde_json::Value>,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub lsp_servers: Option<serde_json::Value>,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub settings: Option<HashMap<String, serde_json::Value>>,
120}
121
122impl PluginMarketplaceEntry {
123 pub fn source_as_path(&self) -> &str {
126 match &self.source {
127 PluginSource::Relative(path) => path,
128 _ => "",
129 }
130 }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct PluginMarketplaceOwner {
137 pub name: String,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub email: Option<String>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub url: Option<String>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct PluginMarketplaceMetadata {
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub plugin_root: Option<String>,
150 #[serde(skip_serializing_if = "Option::is_none")]
151 pub version: Option<String>,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub description: Option<String>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158#[serde(rename_all = "camelCase")]
159pub struct PluginMarketplace {
160 pub name: String,
162 pub owner: PluginMarketplaceOwner,
164 #[serde(skip_serializing_if = "Option::is_none")]
165 pub description: Option<String>,
166 pub plugins: Vec<PluginMarketplaceEntry>,
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub force_remove_deleted_plugins: Option<bool>,
170 #[serde(skip_serializing_if = "Option::is_none")]
171 pub metadata: Option<PluginMarketplaceMetadata>,
172 #[serde(skip_serializing_if = "Option::is_none")]
173 pub allow_cross_marketplace_dependencies_on: Option<Vec<String>>,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178#[serde(rename_all = "camelCase")]
179pub struct KnownMarketplace {
180 pub source: PluginSource,
182 pub install_location: String,
184 #[serde(skip_serializing_if = "Option::is_none")]
185 pub auto_update: Option<bool>,
186}
187
188pub type KnownMarketplacesFile = HashMap<String, KnownMarketplace>;
190
191pub type PluginId = String;
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn test_plugin_source_relative() {
200 let source = PluginSource::Relative("./my-plugin".to_string());
201 assert!(source.is_local());
202 }
203
204 #[test]
205 fn test_plugin_source_npm() {
206 let source = PluginSource::Npm {
207 source: "npm".to_string(),
208 package: "my-npm-plugin".to_string(),
209 version: Some("1.0.0".to_string()),
210 registry: None,
211 };
212 assert!(!source.is_local());
213 }
214
215 #[test]
216 fn test_marketplace_serialization() {
217 let marketplace = PluginMarketplace {
218 name: "my-marketplace".to_string(),
219 owner: PluginMarketplaceOwner {
220 name: "Test Owner".to_string(),
221 email: Some("test@example.com".to_string()),
222 url: None,
223 },
224 description: Some("A test marketplace".to_string()),
225 plugins: vec![PluginMarketplaceEntry {
226 name: "test-plugin".to_string(),
227 source: PluginSource::Relative("./test-plugin".to_string()),
228 description: Some("A test plugin".to_string()),
229 version: Some("1.0.0".to_string()),
230 strict: Some(true),
231 category: None,
232 tags: None,
233 author: None,
234 homepage: None,
235 repository: None,
236 keywords: None,
237 commands: None,
238 agents: None,
239 skills: None,
240 hooks: None,
241 output_styles: None,
242 mcp_servers: None,
243 lsp_servers: None,
244 settings: None,
245 }],
246 force_remove_deleted_plugins: None,
247 metadata: None,
248 allow_cross_marketplace_dependencies_on: None,
249 };
250
251 let json = serde_json::to_string(&marketplace).unwrap();
252 assert!(json.contains("my-marketplace"));
253 assert!(json.contains("test-plugin"));
254 }
255
256 #[test]
257 fn test_known_marketplace_serialization() {
258 let known = KnownMarketplacesFile::from_iter([(
259 "my-marketplace".to_string(),
260 KnownMarketplace {
261 source: PluginSource::Url {
262 source: "url".to_string(),
263 url: "https://example.com/marketplace.json".to_string(),
264 headers: None,
265 },
266 install_location: "/path/to/marketplace".to_string(),
267 auto_update: Some(true),
268 },
269 )]);
270
271 let json = serde_json::to_string(&known).unwrap();
272 assert!(json.contains("my-marketplace"));
273 }
274}