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
122impl PluginMarketplaceEntry {
123    /// Get the relative path from the source for local plugins.
124    /// Returns the path component from a Relative source, or empty string for others.
125    pub fn source_as_path(&self) -> &str {
126        match &self.source {
127            PluginSource::Relative(path) => path,
128            _ => "",
129        }
130    }
131}
132
133/// Plugin marketplace owner
134#[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/// Plugin marketplace metadata
145#[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/// Plugin marketplace configuration
157#[derive(Debug, Clone, Serialize, Deserialize)]
158#[serde(rename_all = "camelCase")]
159pub struct PluginMarketplace {
160    /// Marketplace name
161    pub name: String,
162    /// Marketplace owner
163    pub owner: PluginMarketplaceOwner,
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub description: Option<String>,
166    /// Collection of available plugins in this marketplace
167    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/// Known marketplace configuration (for known_marketplaces.json)
177#[derive(Debug, Clone, Serialize, Deserialize)]
178#[serde(rename_all = "camelCase")]
179pub struct KnownMarketplace {
180    /// Marketplace source configuration
181    pub source: PluginSource,
182    /// Installation location path
183    pub install_location: String,
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub auto_update: Option<bool>,
186}
187
188/// Known marketplaces configuration file
189pub type KnownMarketplacesFile = HashMap<String, KnownMarketplace>;
190
191/// Plugin ID in "plugin@marketplace" format
192pub 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}