Skip to main content

claude_wrapper/command/
plugin.rs

1use crate::Claude;
2use crate::command::ClaudeCommand;
3use crate::error::Result;
4use crate::exec::{self, CommandOutput};
5use crate::types::Scope;
6
7/// List installed plugins.
8///
9/// # Example
10///
11/// ```no_run
12/// use claude_wrapper::{Claude, ClaudeCommand, PluginListCommand};
13///
14/// # async fn example() -> claude_wrapper::Result<()> {
15/// let claude = Claude::builder().build()?;
16/// let output = PluginListCommand::new().json().execute(&claude).await?;
17/// println!("{}", output.stdout);
18/// # Ok(())
19/// # }
20/// ```
21#[derive(Debug, Clone, Default)]
22pub struct PluginListCommand {
23    json: bool,
24    available: bool,
25}
26
27impl PluginListCommand {
28    #[must_use]
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Output as JSON.
34    #[must_use]
35    pub fn json(mut self) -> Self {
36        self.json = true;
37        self
38    }
39
40    /// Include available plugins from marketplaces (requires `json()`).
41    #[must_use]
42    pub fn available(mut self) -> Self {
43        self.available = true;
44        self
45    }
46}
47
48impl ClaudeCommand for PluginListCommand {
49    type Output = CommandOutput;
50
51    fn args(&self) -> Vec<String> {
52        let mut args = vec!["plugin".to_string(), "list".to_string()];
53        if self.json {
54            args.push("--json".to_string());
55        }
56        if self.available {
57            args.push("--available".to_string());
58        }
59        args
60    }
61
62    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
63        exec::run_claude(claude, self.args()).await
64    }
65}
66
67/// Install a plugin.
68///
69/// # Example
70///
71/// ```no_run
72/// use claude_wrapper::{Claude, ClaudeCommand, PluginInstallCommand, Scope};
73///
74/// # async fn example() -> claude_wrapper::Result<()> {
75/// let claude = Claude::builder().build()?;
76/// PluginInstallCommand::new("my-plugin")
77///     .scope(Scope::User)
78///     .execute(&claude)
79///     .await?;
80/// # Ok(())
81/// # }
82/// ```
83#[derive(Debug, Clone)]
84pub struct PluginInstallCommand {
85    plugin: String,
86    scope: Option<Scope>,
87}
88
89impl PluginInstallCommand {
90    #[must_use]
91    pub fn new(plugin: impl Into<String>) -> Self {
92        Self {
93            plugin: plugin.into(),
94            scope: None,
95        }
96    }
97
98    /// Set the installation scope.
99    #[must_use]
100    pub fn scope(mut self, scope: Scope) -> Self {
101        self.scope = Some(scope);
102        self
103    }
104}
105
106impl ClaudeCommand for PluginInstallCommand {
107    type Output = CommandOutput;
108
109    fn args(&self) -> Vec<String> {
110        let mut args = vec!["plugin".to_string(), "install".to_string()];
111        if let Some(ref scope) = self.scope {
112            args.push("--scope".to_string());
113            args.push(scope.as_arg().to_string());
114        }
115        args.push(self.plugin.clone());
116        args
117    }
118
119    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
120        exec::run_claude(claude, self.args()).await
121    }
122}
123
124/// Uninstall a plugin.
125#[derive(Debug, Clone)]
126pub struct PluginUninstallCommand {
127    plugin: String,
128    scope: Option<Scope>,
129}
130
131impl PluginUninstallCommand {
132    #[must_use]
133    pub fn new(plugin: impl Into<String>) -> Self {
134        Self {
135            plugin: plugin.into(),
136            scope: None,
137        }
138    }
139
140    /// Set the scope.
141    #[must_use]
142    pub fn scope(mut self, scope: Scope) -> Self {
143        self.scope = Some(scope);
144        self
145    }
146}
147
148impl ClaudeCommand for PluginUninstallCommand {
149    type Output = CommandOutput;
150
151    fn args(&self) -> Vec<String> {
152        let mut args = vec!["plugin".to_string(), "uninstall".to_string()];
153        if let Some(ref scope) = self.scope {
154            args.push("--scope".to_string());
155            args.push(scope.as_arg().to_string());
156        }
157        args.push(self.plugin.clone());
158        args
159    }
160
161    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
162        exec::run_claude(claude, self.args()).await
163    }
164}
165
166/// Enable a disabled plugin.
167#[derive(Debug, Clone)]
168pub struct PluginEnableCommand {
169    plugin: String,
170    scope: Option<Scope>,
171}
172
173impl PluginEnableCommand {
174    #[must_use]
175    pub fn new(plugin: impl Into<String>) -> Self {
176        Self {
177            plugin: plugin.into(),
178            scope: None,
179        }
180    }
181
182    /// Set the scope.
183    #[must_use]
184    pub fn scope(mut self, scope: Scope) -> Self {
185        self.scope = Some(scope);
186        self
187    }
188}
189
190impl ClaudeCommand for PluginEnableCommand {
191    type Output = CommandOutput;
192
193    fn args(&self) -> Vec<String> {
194        let mut args = vec!["plugin".to_string(), "enable".to_string()];
195        if let Some(ref scope) = self.scope {
196            args.push("--scope".to_string());
197            args.push(scope.as_arg().to_string());
198        }
199        args.push(self.plugin.clone());
200        args
201    }
202
203    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
204        exec::run_claude(claude, self.args()).await
205    }
206}
207
208/// Disable an enabled plugin.
209#[derive(Debug, Clone)]
210pub struct PluginDisableCommand {
211    plugin: Option<String>,
212    scope: Option<Scope>,
213    all: bool,
214}
215
216impl PluginDisableCommand {
217    /// Disable a specific plugin.
218    #[must_use]
219    pub fn new(plugin: impl Into<String>) -> Self {
220        Self {
221            plugin: Some(plugin.into()),
222            scope: None,
223            all: false,
224        }
225    }
226
227    /// Disable all enabled plugins.
228    #[must_use]
229    pub fn all() -> Self {
230        Self {
231            plugin: None,
232            scope: None,
233            all: true,
234        }
235    }
236
237    /// Set the scope.
238    #[must_use]
239    pub fn scope(mut self, scope: Scope) -> Self {
240        self.scope = Some(scope);
241        self
242    }
243}
244
245impl ClaudeCommand for PluginDisableCommand {
246    type Output = CommandOutput;
247
248    fn args(&self) -> Vec<String> {
249        let mut args = vec!["plugin".to_string(), "disable".to_string()];
250        if self.all {
251            args.push("--all".to_string());
252        }
253        if let Some(ref scope) = self.scope {
254            args.push("--scope".to_string());
255            args.push(scope.as_arg().to_string());
256        }
257        if let Some(ref plugin) = self.plugin {
258            args.push(plugin.clone());
259        }
260        args
261    }
262
263    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
264        exec::run_claude(claude, self.args()).await
265    }
266}
267
268/// Update a plugin to the latest version.
269#[derive(Debug, Clone)]
270pub struct PluginUpdateCommand {
271    plugin: String,
272    scope: Option<Scope>,
273}
274
275impl PluginUpdateCommand {
276    #[must_use]
277    pub fn new(plugin: impl Into<String>) -> Self {
278        Self {
279            plugin: plugin.into(),
280            scope: None,
281        }
282    }
283
284    /// Set the scope.
285    #[must_use]
286    pub fn scope(mut self, scope: Scope) -> Self {
287        self.scope = Some(scope);
288        self
289    }
290}
291
292impl ClaudeCommand for PluginUpdateCommand {
293    type Output = CommandOutput;
294
295    fn args(&self) -> Vec<String> {
296        let mut args = vec!["plugin".to_string(), "update".to_string()];
297        if let Some(ref scope) = self.scope {
298            args.push("--scope".to_string());
299            args.push(scope.as_arg().to_string());
300        }
301        args.push(self.plugin.clone());
302        args
303    }
304
305    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
306        exec::run_claude(claude, self.args()).await
307    }
308}
309
310/// Validate a plugin or marketplace manifest.
311#[derive(Debug, Clone)]
312pub struct PluginValidateCommand {
313    path: String,
314}
315
316impl PluginValidateCommand {
317    #[must_use]
318    pub fn new(path: impl Into<String>) -> Self {
319        Self { path: path.into() }
320    }
321}
322
323impl ClaudeCommand for PluginValidateCommand {
324    type Output = CommandOutput;
325
326    fn args(&self) -> Vec<String> {
327        vec![
328            "plugin".to_string(),
329            "validate".to_string(),
330            self.path.clone(),
331        ]
332    }
333
334    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
335        exec::run_claude(claude, self.args()).await
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342    use crate::command::ClaudeCommand;
343
344    #[test]
345    fn test_plugin_list() {
346        let cmd = PluginListCommand::new().json().available();
347        assert_eq!(
348            ClaudeCommand::args(&cmd),
349            vec!["plugin", "list", "--json", "--available"]
350        );
351    }
352
353    #[test]
354    fn test_plugin_install() {
355        let cmd = PluginInstallCommand::new("my-plugin").scope(Scope::User);
356        assert_eq!(
357            ClaudeCommand::args(&cmd),
358            vec!["plugin", "install", "--scope", "user", "my-plugin"]
359        );
360    }
361
362    #[test]
363    fn test_plugin_uninstall() {
364        let cmd = PluginUninstallCommand::new("old-plugin");
365        assert_eq!(
366            ClaudeCommand::args(&cmd),
367            vec!["plugin", "uninstall", "old-plugin"]
368        );
369    }
370
371    #[test]
372    fn test_plugin_enable() {
373        let cmd = PluginEnableCommand::new("my-plugin").scope(Scope::Project);
374        assert_eq!(
375            ClaudeCommand::args(&cmd),
376            vec!["plugin", "enable", "--scope", "project", "my-plugin"]
377        );
378    }
379
380    #[test]
381    fn test_plugin_disable_specific() {
382        let cmd = PluginDisableCommand::new("my-plugin");
383        assert_eq!(
384            ClaudeCommand::args(&cmd),
385            vec!["plugin", "disable", "my-plugin"]
386        );
387    }
388
389    #[test]
390    fn test_plugin_disable_all() {
391        let cmd = PluginDisableCommand::all();
392        assert_eq!(
393            ClaudeCommand::args(&cmd),
394            vec!["plugin", "disable", "--all"]
395        );
396    }
397
398    #[test]
399    fn test_plugin_update() {
400        let cmd = PluginUpdateCommand::new("my-plugin").scope(Scope::Local);
401        assert_eq!(
402            ClaudeCommand::args(&cmd),
403            vec!["plugin", "update", "--scope", "local", "my-plugin"]
404        );
405    }
406
407    #[test]
408    fn test_plugin_validate() {
409        let cmd = PluginValidateCommand::new("/path/to/manifest");
410        assert_eq!(
411            ClaudeCommand::args(&cmd),
412            vec!["plugin", "validate", "/path/to/manifest"]
413        );
414    }
415}