Skip to main content

ai_agent/utils/plugins/
types.rs

1// Source: /data/home/swei/claudecode/openclaudecode/src/utils/filePersistence/types.ts
2//! Plugin types - ported from ~/claudecode/openclaudecode/src/utils/plugins/schemas.ts
3//!
4//! This module provides types for plugin marketplaces and sources.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Plugin source - can be a local path (relative) or a remote source
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(untagged)]
12pub enum PluginSource {
13    /// Local path relative to marketplace root (starts with "./")
14    Relative(String),
15    /// NPM package source
16    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 package source
25    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 repository source
34    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    /// Git subdirectory source
45    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 URL source
53    Git {
54        source: String,
55        url: String,
56        #[serde(skip_serializing_if = "Option::is_none")]
57        ref_: Option<String>,
58    },
59    /// Direct URL source
60    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 (inline manifest from settings.json)
67    Settings { source: String },
68}
69
70impl PluginSource {
71    /// Check if this is a local plugin source (relative path starting with "./")
72    pub fn is_local(&self) -> bool {
73        matches!(self, PluginSource::Relative(s) if s.starts_with("./"))
74    }
75}
76
77/// Plugin marketplace entry
78#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80pub struct PluginMarketplaceEntry {
81    /// Unique identifier matching the plugin name
82    pub name: String,
83    /// Where to fetch the plugin from
84    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    // Inherited from PluginManifest partial
96    #[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/// Plugin marketplace owner
123#[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/// Plugin marketplace metadata
134#[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/// Plugin marketplace configuration
146#[derive(Debug, Clone, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct PluginMarketplace {
149    /// Marketplace name
150    pub name: String,
151    /// Marketplace owner
152    pub owner: PluginMarketplaceOwner,
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub description: Option<String>,
155    /// Collection of available plugins in this marketplace
156    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/// Known marketplace configuration (for known_marketplaces.json)
166#[derive(Debug, Clone, Serialize, Deserialize)]
167#[serde(rename_all = "camelCase")]
168pub struct KnownMarketplace {
169    /// Marketplace source configuration
170    pub source: PluginSource,
171    /// Installation location path
172    pub install_location: String,
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub auto_update: Option<bool>,
175}
176
177/// Known marketplaces configuration file
178pub type KnownMarketplacesFile = HashMap<String, KnownMarketplace>;
179
180/// Plugin ID in "plugin@marketplace" format
181pub 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}