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
122#[derive(Debug, Clone, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct PluginMarketplaceOwner {
126 pub name: String,
127 #[serde(skip_serializing_if = "Option::is_none")]
128 pub email: Option<String>,
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub url: Option<String>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct PluginMarketplaceMetadata {
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub plugin_root: Option<String>,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub version: Option<String>,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub description: Option<String>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct PluginMarketplace {
149 pub name: String,
151 pub owner: PluginMarketplaceOwner,
153 #[serde(skip_serializing_if = "Option::is_none")]
154 pub description: Option<String>,
155 pub plugins: Vec<PluginMarketplaceEntry>,
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub force_remove_deleted_plugins: Option<bool>,
159 #[serde(skip_serializing_if = "Option::is_none")]
160 pub metadata: Option<PluginMarketplaceMetadata>,
161 #[serde(skip_serializing_if = "Option::is_none")]
162 pub allow_cross_marketplace_dependencies_on: Option<Vec<String>>,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167#[serde(rename_all = "camelCase")]
168pub struct KnownMarketplace {
169 pub source: PluginSource,
171 pub install_location: String,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 pub auto_update: Option<bool>,
175}
176
177pub type KnownMarketplacesFile = HashMap<String, KnownMarketplace>;
179
180pub type PluginId = String;
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_plugin_source_relative() {
189 let source = PluginSource::Relative("./my-plugin".to_string());
190 assert!(source.is_local());
191 }
192
193 #[test]
194 fn test_plugin_source_npm() {
195 let source = PluginSource::Npm {
196 source: "npm".to_string(),
197 package: "my-npm-plugin".to_string(),
198 version: Some("1.0.0".to_string()),
199 registry: None,
200 };
201 assert!(!source.is_local());
202 }
203
204 #[test]
205 fn test_marketplace_serialization() {
206 let marketplace = PluginMarketplace {
207 name: "my-marketplace".to_string(),
208 owner: PluginMarketplaceOwner {
209 name: "Test Owner".to_string(),
210 email: Some("test@example.com".to_string()),
211 url: None,
212 },
213 description: Some("A test marketplace".to_string()),
214 plugins: vec![PluginMarketplaceEntry {
215 name: "test-plugin".to_string(),
216 source: PluginSource::Relative("./test-plugin".to_string()),
217 description: Some("A test plugin".to_string()),
218 version: Some("1.0.0".to_string()),
219 strict: Some(true),
220 category: None,
221 tags: None,
222 author: None,
223 homepage: None,
224 repository: None,
225 keywords: None,
226 commands: None,
227 agents: None,
228 skills: None,
229 hooks: None,
230 output_styles: None,
231 mcp_servers: None,
232 lsp_servers: None,
233 settings: None,
234 }],
235 force_remove_deleted_plugins: None,
236 metadata: None,
237 allow_cross_marketplace_dependencies_on: None,
238 };
239
240 let json = serde_json::to_string(&marketplace).unwrap();
241 assert!(json.contains("my-marketplace"));
242 assert!(json.contains("test-plugin"));
243 }
244
245 #[test]
246 fn test_known_marketplace_serialization() {
247 let known = KnownMarketplacesFile::from_iter([(
248 "my-marketplace".to_string(),
249 KnownMarketplace {
250 source: PluginSource::Url {
251 source: "url".to_string(),
252 url: "https://example.com/marketplace.json".to_string(),
253 headers: None,
254 },
255 install_location: "/path/to/marketplace".to_string(),
256 auto_update: Some(true),
257 },
258 )]);
259
260 let json = serde_json::to_string(&known).unwrap();
261 assert!(json.contains("my-marketplace"));
262 }
263}