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