Skip to main content

claude_wrapper/command/
plugin.rs

1#[cfg(feature = "async")]
2use crate::Claude;
3use crate::command::ClaudeCommand;
4#[cfg(feature = "async")]
5use crate::error::Result;
6#[cfg(feature = "async")]
7use crate::exec;
8use crate::exec::CommandOutput;
9use crate::types::Scope;
10
11/// List installed plugins.
12///
13/// # Example
14///
15/// ```no_run
16/// use claude_wrapper::{Claude, ClaudeCommand, PluginListCommand};
17///
18/// # async fn example() -> claude_wrapper::Result<()> {
19/// let claude = Claude::builder().build()?;
20/// let output = PluginListCommand::new().json().execute(&claude).await?;
21/// println!("{}", output.stdout);
22/// # Ok(())
23/// # }
24/// ```
25#[derive(Debug, Clone, Default)]
26pub struct PluginListCommand {
27    json: bool,
28    available: bool,
29}
30
31impl PluginListCommand {
32    /// Creates a new plugin list command.
33    #[must_use]
34    pub fn new() -> Self {
35        Self::default()
36    }
37
38    /// Output as JSON.
39    #[must_use]
40    pub fn json(mut self) -> Self {
41        self.json = true;
42        self
43    }
44
45    /// Include available plugins from marketplaces (requires `json()`).
46    #[must_use]
47    pub fn available(mut self) -> Self {
48        self.available = true;
49        self
50    }
51}
52
53impl ClaudeCommand for PluginListCommand {
54    type Output = CommandOutput;
55
56    fn args(&self) -> Vec<String> {
57        let mut args = vec!["plugin".to_string(), "list".to_string()];
58        if self.json {
59            args.push("--json".to_string());
60        }
61        if self.available {
62            args.push("--available".to_string());
63        }
64        args
65    }
66
67    #[cfg(feature = "async")]
68    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
69        exec::run_claude(claude, self.args()).await
70    }
71}
72
73/// Install a plugin.
74///
75/// # Example
76///
77/// ```no_run
78/// use claude_wrapper::{Claude, ClaudeCommand, PluginInstallCommand, Scope};
79///
80/// # async fn example() -> claude_wrapper::Result<()> {
81/// let claude = Claude::builder().build()?;
82/// PluginInstallCommand::new("my-plugin")
83///     .scope(Scope::User)
84///     .execute(&claude)
85///     .await?;
86/// # Ok(())
87/// # }
88/// ```
89#[derive(Debug, Clone)]
90pub struct PluginInstallCommand {
91    plugin: String,
92    scope: Option<Scope>,
93}
94
95impl PluginInstallCommand {
96    /// Creates a command to install a plugin by name.
97    #[must_use]
98    pub fn new(plugin: impl Into<String>) -> Self {
99        Self {
100            plugin: plugin.into(),
101            scope: None,
102        }
103    }
104
105    /// Set the installation scope.
106    #[must_use]
107    pub fn scope(mut self, scope: Scope) -> Self {
108        self.scope = Some(scope);
109        self
110    }
111}
112
113impl ClaudeCommand for PluginInstallCommand {
114    type Output = CommandOutput;
115
116    fn args(&self) -> Vec<String> {
117        let mut args = vec!["plugin".to_string(), "install".to_string()];
118        if let Some(ref scope) = self.scope {
119            args.push("--scope".to_string());
120            args.push(scope.as_arg().to_string());
121        }
122        args.push(self.plugin.clone());
123        args
124    }
125
126    #[cfg(feature = "async")]
127    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
128        exec::run_claude(claude, self.args()).await
129    }
130}
131
132/// Uninstall a plugin.
133#[derive(Debug, Clone)]
134pub struct PluginUninstallCommand {
135    plugin: String,
136    scope: Option<Scope>,
137}
138
139impl PluginUninstallCommand {
140    /// Creates a command to uninstall a plugin by name.
141    #[must_use]
142    pub fn new(plugin: impl Into<String>) -> Self {
143        Self {
144            plugin: plugin.into(),
145            scope: None,
146        }
147    }
148
149    /// Set the scope.
150    #[must_use]
151    pub fn scope(mut self, scope: Scope) -> Self {
152        self.scope = Some(scope);
153        self
154    }
155}
156
157impl ClaudeCommand for PluginUninstallCommand {
158    type Output = CommandOutput;
159
160    fn args(&self) -> Vec<String> {
161        let mut args = vec!["plugin".to_string(), "uninstall".to_string()];
162        if let Some(ref scope) = self.scope {
163            args.push("--scope".to_string());
164            args.push(scope.as_arg().to_string());
165        }
166        args.push(self.plugin.clone());
167        args
168    }
169
170    #[cfg(feature = "async")]
171    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
172        exec::run_claude(claude, self.args()).await
173    }
174}
175
176/// Enable a disabled plugin.
177#[derive(Debug, Clone)]
178pub struct PluginEnableCommand {
179    plugin: String,
180    scope: Option<Scope>,
181}
182
183impl PluginEnableCommand {
184    /// Creates a command to enable a plugin by name.
185    #[must_use]
186    pub fn new(plugin: impl Into<String>) -> Self {
187        Self {
188            plugin: plugin.into(),
189            scope: None,
190        }
191    }
192
193    /// Set the scope.
194    #[must_use]
195    pub fn scope(mut self, scope: Scope) -> Self {
196        self.scope = Some(scope);
197        self
198    }
199}
200
201impl ClaudeCommand for PluginEnableCommand {
202    type Output = CommandOutput;
203
204    fn args(&self) -> Vec<String> {
205        let mut args = vec!["plugin".to_string(), "enable".to_string()];
206        if let Some(ref scope) = self.scope {
207            args.push("--scope".to_string());
208            args.push(scope.as_arg().to_string());
209        }
210        args.push(self.plugin.clone());
211        args
212    }
213
214    #[cfg(feature = "async")]
215    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
216        exec::run_claude(claude, self.args()).await
217    }
218}
219
220/// Disable an enabled plugin.
221#[derive(Debug, Clone)]
222pub struct PluginDisableCommand {
223    plugin: Option<String>,
224    scope: Option<Scope>,
225    all: bool,
226}
227
228impl PluginDisableCommand {
229    /// Creates a command to disable a plugin by name. To disable all plugins, use [`PluginDisableCommand::all`].
230    #[must_use]
231    pub fn new(plugin: impl Into<String>) -> Self {
232        Self {
233            plugin: Some(plugin.into()),
234            scope: None,
235            all: false,
236        }
237    }
238
239    /// Disable all enabled plugins.
240    #[must_use]
241    pub fn all() -> Self {
242        Self {
243            plugin: None,
244            scope: None,
245            all: true,
246        }
247    }
248
249    /// Set the scope.
250    #[must_use]
251    pub fn scope(mut self, scope: Scope) -> Self {
252        self.scope = Some(scope);
253        self
254    }
255}
256
257impl ClaudeCommand for PluginDisableCommand {
258    type Output = CommandOutput;
259
260    fn args(&self) -> Vec<String> {
261        let mut args = vec!["plugin".to_string(), "disable".to_string()];
262        if self.all {
263            args.push("--all".to_string());
264        }
265        if let Some(ref scope) = self.scope {
266            args.push("--scope".to_string());
267            args.push(scope.as_arg().to_string());
268        }
269        if let Some(ref plugin) = self.plugin {
270            args.push(plugin.clone());
271        }
272        args
273    }
274
275    #[cfg(feature = "async")]
276    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
277        exec::run_claude(claude, self.args()).await
278    }
279}
280
281/// Update a plugin to the latest version.
282#[derive(Debug, Clone)]
283pub struct PluginUpdateCommand {
284    plugin: String,
285    scope: Option<Scope>,
286}
287
288impl PluginUpdateCommand {
289    /// Creates a command to update a plugin to the latest version.
290    #[must_use]
291    pub fn new(plugin: impl Into<String>) -> Self {
292        Self {
293            plugin: plugin.into(),
294            scope: None,
295        }
296    }
297
298    /// Set the scope.
299    #[must_use]
300    pub fn scope(mut self, scope: Scope) -> Self {
301        self.scope = Some(scope);
302        self
303    }
304}
305
306impl ClaudeCommand for PluginUpdateCommand {
307    type Output = CommandOutput;
308
309    fn args(&self) -> Vec<String> {
310        let mut args = vec!["plugin".to_string(), "update".to_string()];
311        if let Some(ref scope) = self.scope {
312            args.push("--scope".to_string());
313            args.push(scope.as_arg().to_string());
314        }
315        args.push(self.plugin.clone());
316        args
317    }
318
319    #[cfg(feature = "async")]
320    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
321        exec::run_claude(claude, self.args()).await
322    }
323}
324
325/// Validate a plugin or marketplace manifest.
326#[derive(Debug, Clone)]
327pub struct PluginValidateCommand {
328    path: String,
329}
330
331impl PluginValidateCommand {
332    /// Creates a command to validate a plugin manifest at the given path.
333    #[must_use]
334    pub fn new(path: impl Into<String>) -> Self {
335        Self { path: path.into() }
336    }
337}
338
339impl ClaudeCommand for PluginValidateCommand {
340    type Output = CommandOutput;
341
342    fn args(&self) -> Vec<String> {
343        vec![
344            "plugin".to_string(),
345            "validate".to_string(),
346            self.path.clone(),
347        ]
348    }
349
350    #[cfg(feature = "async")]
351    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
352        exec::run_claude(claude, self.args()).await
353    }
354}
355
356/// Create a `{name}--v{version}` git tag for a plugin release.
357///
358/// Runs `claude plugin tag [path]`, validating that the plugin's
359/// `plugin.json` and any enclosing marketplace entry agree on the
360/// version before tagging.
361///
362/// # Example
363///
364/// ```no_run
365/// # #[cfg(feature = "async")] {
366/// use claude_wrapper::{Claude, ClaudeCommand, PluginTagCommand};
367///
368/// # async fn example() -> claude_wrapper::Result<()> {
369/// let claude = Claude::builder().build()?;
370/// let out = PluginTagCommand::new()
371///     .path("./my-plugin")
372///     .message("release %s")
373///     .push()
374///     .execute(&claude)
375///     .await?;
376/// println!("{}", out.stdout);
377/// # Ok(()) }
378/// # }
379/// ```
380#[derive(Debug, Clone, Default)]
381pub struct PluginTagCommand {
382    path: Option<String>,
383    dry_run: bool,
384    force: bool,
385    message: Option<String>,
386    push: bool,
387    remote: Option<String>,
388}
389
390impl PluginTagCommand {
391    /// Create a new tag command. Without [`path`](Self::path), the CLI
392    /// uses the current directory.
393    #[must_use]
394    pub fn new() -> Self {
395        Self::default()
396    }
397
398    /// Path to the plugin directory.
399    #[must_use]
400    pub fn path(mut self, path: impl Into<String>) -> Self {
401        self.path = Some(path.into());
402        self
403    }
404
405    /// Print what would be tagged without creating anything.
406    #[must_use]
407    pub fn dry_run(mut self) -> Self {
408        self.dry_run = true;
409        self
410    }
411
412    /// Skip dirty-working-tree and tag-already-exists checks.
413    #[must_use]
414    pub fn force(mut self) -> Self {
415        self.force = true;
416        self
417    }
418
419    /// Tag annotation message; `%s` is substituted with the version.
420    #[must_use]
421    pub fn message(mut self, msg: impl Into<String>) -> Self {
422        self.message = Some(msg.into());
423        self
424    }
425
426    /// Push the tag after creating it.
427    #[must_use]
428    pub fn push(mut self) -> Self {
429        self.push = true;
430        self
431    }
432
433    /// Override the remote pushed to with [`push`](Self::push) (default `origin`).
434    #[must_use]
435    pub fn remote(mut self, remote: impl Into<String>) -> Self {
436        self.remote = Some(remote.into());
437        self
438    }
439}
440
441impl ClaudeCommand for PluginTagCommand {
442    type Output = CommandOutput;
443
444    fn args(&self) -> Vec<String> {
445        let mut args = vec!["plugin".to_string(), "tag".to_string()];
446        if self.dry_run {
447            args.push("--dry-run".to_string());
448        }
449        if self.force {
450            args.push("--force".to_string());
451        }
452        if let Some(ref msg) = self.message {
453            args.push("--message".to_string());
454            args.push(msg.clone());
455        }
456        if self.push {
457            args.push("--push".to_string());
458        }
459        if let Some(ref remote) = self.remote {
460            args.push("--remote".to_string());
461            args.push(remote.clone());
462        }
463        if let Some(ref path) = self.path {
464            args.push(path.clone());
465        }
466        args
467    }
468
469    #[cfg(feature = "async")]
470    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
471        exec::run_claude(claude, self.args()).await
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478    use crate::command::ClaudeCommand;
479
480    #[test]
481    fn test_plugin_list() {
482        let cmd = PluginListCommand::new().json().available();
483        assert_eq!(
484            ClaudeCommand::args(&cmd),
485            vec!["plugin", "list", "--json", "--available"]
486        );
487    }
488
489    #[test]
490    fn test_plugin_install() {
491        let cmd = PluginInstallCommand::new("my-plugin").scope(Scope::User);
492        assert_eq!(
493            ClaudeCommand::args(&cmd),
494            vec!["plugin", "install", "--scope", "user", "my-plugin"]
495        );
496    }
497
498    #[test]
499    fn test_plugin_uninstall() {
500        let cmd = PluginUninstallCommand::new("old-plugin");
501        assert_eq!(
502            ClaudeCommand::args(&cmd),
503            vec!["plugin", "uninstall", "old-plugin"]
504        );
505    }
506
507    #[test]
508    fn test_plugin_enable() {
509        let cmd = PluginEnableCommand::new("my-plugin").scope(Scope::Project);
510        assert_eq!(
511            ClaudeCommand::args(&cmd),
512            vec!["plugin", "enable", "--scope", "project", "my-plugin"]
513        );
514    }
515
516    #[test]
517    fn test_plugin_disable_specific() {
518        let cmd = PluginDisableCommand::new("my-plugin");
519        assert_eq!(
520            ClaudeCommand::args(&cmd),
521            vec!["plugin", "disable", "my-plugin"]
522        );
523    }
524
525    #[test]
526    fn test_plugin_disable_all() {
527        let cmd = PluginDisableCommand::all();
528        assert_eq!(
529            ClaudeCommand::args(&cmd),
530            vec!["plugin", "disable", "--all"]
531        );
532    }
533
534    #[test]
535    fn test_plugin_update() {
536        let cmd = PluginUpdateCommand::new("my-plugin").scope(Scope::Local);
537        assert_eq!(
538            ClaudeCommand::args(&cmd),
539            vec!["plugin", "update", "--scope", "local", "my-plugin"]
540        );
541    }
542
543    #[test]
544    fn test_plugin_validate() {
545        let cmd = PluginValidateCommand::new("/path/to/manifest");
546        assert_eq!(
547            ClaudeCommand::args(&cmd),
548            vec!["plugin", "validate", "/path/to/manifest"]
549        );
550    }
551
552    #[test]
553    fn plugin_tag_defaults_to_just_subcommand() {
554        let cmd = PluginTagCommand::new();
555        assert_eq!(ClaudeCommand::args(&cmd), vec!["plugin", "tag"]);
556    }
557
558    #[test]
559    fn plugin_tag_with_all_options() {
560        let cmd = PluginTagCommand::new()
561            .path("./plugin")
562            .dry_run()
563            .force()
564            .message("release %s")
565            .push()
566            .remote("upstream");
567        assert_eq!(
568            ClaudeCommand::args(&cmd),
569            vec![
570                "plugin",
571                "tag",
572                "--dry-run",
573                "--force",
574                "--message",
575                "release %s",
576                "--push",
577                "--remote",
578                "upstream",
579                "./plugin",
580            ]
581        );
582    }
583}