1#[cfg(feature = "async")]
9use crate::Claude;
10use crate::command::ClaudeCommand;
11#[cfg(feature = "async")]
12use crate::error::Result;
13#[cfg(feature = "async")]
14use crate::exec;
15use crate::exec::CommandOutput;
16use crate::types::Scope;
17
18#[derive(Debug, Clone, Default)]
33pub struct PluginListCommand {
34 json: bool,
35 available: bool,
36}
37
38impl PluginListCommand {
39 #[must_use]
41 pub fn new() -> Self {
42 Self::default()
43 }
44
45 #[must_use]
47 pub fn json(mut self) -> Self {
48 self.json = true;
49 self
50 }
51
52 #[must_use]
54 pub fn available(mut self) -> Self {
55 self.available = true;
56 self
57 }
58}
59
60impl ClaudeCommand for PluginListCommand {
61 type Output = CommandOutput;
62
63 fn args(&self) -> Vec<String> {
64 let mut args = vec!["plugin".to_string(), "list".to_string()];
65 if self.json {
66 args.push("--json".to_string());
67 }
68 if self.available {
69 args.push("--available".to_string());
70 }
71 args
72 }
73
74 #[cfg(feature = "async")]
75 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
76 exec::run_claude(claude, self.args()).await
77 }
78}
79
80#[derive(Debug, Clone)]
97pub struct PluginInstallCommand {
98 plugin: String,
99 scope: Option<Scope>,
100 config: Vec<(String, String)>,
101}
102
103impl PluginInstallCommand {
104 #[must_use]
106 pub fn new(plugin: impl Into<String>) -> Self {
107 Self {
108 plugin: plugin.into(),
109 scope: None,
110 config: Vec::new(),
111 }
112 }
113
114 #[must_use]
116 pub fn scope(mut self, scope: Scope) -> Self {
117 self.scope = Some(scope);
118 self
119 }
120
121 #[must_use]
125 pub fn config(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
126 self.config.push((key.into(), value.into()));
127 self
128 }
129}
130
131impl ClaudeCommand for PluginInstallCommand {
132 type Output = CommandOutput;
133
134 fn args(&self) -> Vec<String> {
135 let mut args = vec!["plugin".to_string(), "install".to_string()];
136 if let Some(ref scope) = self.scope {
137 args.push("--scope".to_string());
138 args.push(scope.as_arg().to_string());
139 }
140 for (key, value) in &self.config {
141 args.push("--config".to_string());
142 args.push(format!("{key}={value}"));
143 }
144 args.push(self.plugin.clone());
145 args
146 }
147
148 #[cfg(feature = "async")]
149 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
150 exec::run_claude(claude, self.args()).await
151 }
152}
153
154#[derive(Debug, Clone)]
160pub struct PluginUninstallCommand {
161 plugin: String,
162 scope: Option<Scope>,
163 keep_data: bool,
164 prune: bool,
165 yes: bool,
166}
167
168impl PluginUninstallCommand {
169 #[must_use]
171 pub fn new(plugin: impl Into<String>) -> Self {
172 Self {
173 plugin: plugin.into(),
174 scope: None,
175 keep_data: false,
176 prune: false,
177 yes: false,
178 }
179 }
180
181 #[must_use]
183 pub fn scope(mut self, scope: Scope) -> Self {
184 self.scope = Some(scope);
185 self
186 }
187
188 #[must_use]
192 pub fn keep_data(mut self) -> Self {
193 self.keep_data = true;
194 self
195 }
196
197 #[must_use]
201 pub fn prune(mut self) -> Self {
202 self.prune = true;
203 self
204 }
205
206 #[must_use]
211 pub fn yes(mut self) -> Self {
212 self.yes = true;
213 self
214 }
215}
216
217impl ClaudeCommand for PluginUninstallCommand {
218 type Output = CommandOutput;
219
220 fn args(&self) -> Vec<String> {
221 let mut args = vec!["plugin".to_string(), "uninstall".to_string()];
222 if let Some(ref scope) = self.scope {
223 args.push("--scope".to_string());
224 args.push(scope.as_arg().to_string());
225 }
226 if self.keep_data {
227 args.push("--keep-data".to_string());
228 }
229 if self.prune {
230 args.push("--prune".to_string());
231 }
232 if self.yes {
233 args.push("--yes".to_string());
234 }
235 args.push(self.plugin.clone());
236 args
237 }
238
239 #[cfg(feature = "async")]
240 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
241 exec::run_claude(claude, self.args()).await
242 }
243}
244
245#[derive(Debug, Clone)]
247pub struct PluginEnableCommand {
248 plugin: String,
249 scope: Option<Scope>,
250}
251
252impl PluginEnableCommand {
253 #[must_use]
255 pub fn new(plugin: impl Into<String>) -> Self {
256 Self {
257 plugin: plugin.into(),
258 scope: None,
259 }
260 }
261
262 #[must_use]
264 pub fn scope(mut self, scope: Scope) -> Self {
265 self.scope = Some(scope);
266 self
267 }
268}
269
270impl ClaudeCommand for PluginEnableCommand {
271 type Output = CommandOutput;
272
273 fn args(&self) -> Vec<String> {
274 let mut args = vec!["plugin".to_string(), "enable".to_string()];
275 if let Some(ref scope) = self.scope {
276 args.push("--scope".to_string());
277 args.push(scope.as_arg().to_string());
278 }
279 args.push(self.plugin.clone());
280 args
281 }
282
283 #[cfg(feature = "async")]
284 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
285 exec::run_claude(claude, self.args()).await
286 }
287}
288
289#[derive(Debug, Clone)]
291pub struct PluginDisableCommand {
292 plugin: Option<String>,
293 scope: Option<Scope>,
294 all: bool,
295}
296
297impl PluginDisableCommand {
298 #[must_use]
300 pub fn new(plugin: impl Into<String>) -> Self {
301 Self {
302 plugin: Some(plugin.into()),
303 scope: None,
304 all: false,
305 }
306 }
307
308 #[must_use]
310 pub fn all() -> Self {
311 Self {
312 plugin: None,
313 scope: None,
314 all: true,
315 }
316 }
317
318 #[must_use]
320 pub fn scope(mut self, scope: Scope) -> Self {
321 self.scope = Some(scope);
322 self
323 }
324}
325
326impl ClaudeCommand for PluginDisableCommand {
327 type Output = CommandOutput;
328
329 fn args(&self) -> Vec<String> {
330 let mut args = vec!["plugin".to_string(), "disable".to_string()];
331 if self.all {
332 args.push("--all".to_string());
333 }
334 if let Some(ref scope) = self.scope {
335 args.push("--scope".to_string());
336 args.push(scope.as_arg().to_string());
337 }
338 if let Some(ref plugin) = self.plugin {
339 args.push(plugin.clone());
340 }
341 args
342 }
343
344 #[cfg(feature = "async")]
345 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
346 exec::run_claude(claude, self.args()).await
347 }
348}
349
350#[derive(Debug, Clone)]
352pub struct PluginUpdateCommand {
353 plugin: String,
354 scope: Option<Scope>,
355}
356
357impl PluginUpdateCommand {
358 #[must_use]
360 pub fn new(plugin: impl Into<String>) -> Self {
361 Self {
362 plugin: plugin.into(),
363 scope: None,
364 }
365 }
366
367 #[must_use]
369 pub fn scope(mut self, scope: Scope) -> Self {
370 self.scope = Some(scope);
371 self
372 }
373}
374
375impl ClaudeCommand for PluginUpdateCommand {
376 type Output = CommandOutput;
377
378 fn args(&self) -> Vec<String> {
379 let mut args = vec!["plugin".to_string(), "update".to_string()];
380 if let Some(ref scope) = self.scope {
381 args.push("--scope".to_string());
382 args.push(scope.as_arg().to_string());
383 }
384 args.push(self.plugin.clone());
385 args
386 }
387
388 #[cfg(feature = "async")]
389 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
390 exec::run_claude(claude, self.args()).await
391 }
392}
393
394#[derive(Debug, Clone)]
396pub struct PluginValidateCommand {
397 path: String,
398}
399
400impl PluginValidateCommand {
401 #[must_use]
403 pub fn new(path: impl Into<String>) -> Self {
404 Self { path: path.into() }
405 }
406}
407
408impl ClaudeCommand for PluginValidateCommand {
409 type Output = CommandOutput;
410
411 fn args(&self) -> Vec<String> {
412 vec![
413 "plugin".to_string(),
414 "validate".to_string(),
415 self.path.clone(),
416 ]
417 }
418
419 #[cfg(feature = "async")]
420 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
421 exec::run_claude(claude, self.args()).await
422 }
423}
424
425#[derive(Debug, Clone, Default)]
450pub struct PluginTagCommand {
451 path: Option<String>,
452 dry_run: bool,
453 force: bool,
454 message: Option<String>,
455 push: bool,
456 remote: Option<String>,
457}
458
459impl PluginTagCommand {
460 #[must_use]
463 pub fn new() -> Self {
464 Self::default()
465 }
466
467 #[must_use]
469 pub fn path(mut self, path: impl Into<String>) -> Self {
470 self.path = Some(path.into());
471 self
472 }
473
474 #[must_use]
476 pub fn dry_run(mut self) -> Self {
477 self.dry_run = true;
478 self
479 }
480
481 #[must_use]
483 pub fn force(mut self) -> Self {
484 self.force = true;
485 self
486 }
487
488 #[must_use]
490 pub fn message(mut self, msg: impl Into<String>) -> Self {
491 self.message = Some(msg.into());
492 self
493 }
494
495 #[must_use]
497 pub fn push(mut self) -> Self {
498 self.push = true;
499 self
500 }
501
502 #[must_use]
504 pub fn remote(mut self, remote: impl Into<String>) -> Self {
505 self.remote = Some(remote.into());
506 self
507 }
508}
509
510impl ClaudeCommand for PluginTagCommand {
511 type Output = CommandOutput;
512
513 fn args(&self) -> Vec<String> {
514 let mut args = vec!["plugin".to_string(), "tag".to_string()];
515 if self.dry_run {
516 args.push("--dry-run".to_string());
517 }
518 if self.force {
519 args.push("--force".to_string());
520 }
521 if let Some(ref msg) = self.message {
522 args.push("--message".to_string());
523 args.push(msg.clone());
524 }
525 if self.push {
526 args.push("--push".to_string());
527 }
528 if let Some(ref remote) = self.remote {
529 args.push("--remote".to_string());
530 args.push(remote.clone());
531 }
532 if let Some(ref path) = self.path {
533 args.push(path.clone());
534 }
535 args
536 }
537
538 #[cfg(feature = "async")]
539 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
540 exec::run_claude(claude, self.args()).await
541 }
542}
543
544#[derive(Debug, Clone)]
547pub struct PluginDetailsCommand {
548 plugin: String,
549}
550
551impl PluginDetailsCommand {
552 #[must_use]
554 pub fn new(plugin: impl Into<String>) -> Self {
555 Self {
556 plugin: plugin.into(),
557 }
558 }
559}
560
561impl ClaudeCommand for PluginDetailsCommand {
562 type Output = CommandOutput;
563
564 fn args(&self) -> Vec<String> {
565 vec![
566 "plugin".to_string(),
567 "details".to_string(),
568 self.plugin.clone(),
569 ]
570 }
571
572 #[cfg(feature = "async")]
573 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
574 exec::run_claude(claude, self.args()).await
575 }
576}
577
578#[derive(Debug, Clone, Default)]
585pub struct PluginPruneCommand {
586 dry_run: bool,
587 scope: Option<Scope>,
588 yes: bool,
589}
590
591impl PluginPruneCommand {
592 #[must_use]
594 pub fn new() -> Self {
595 Self::default()
596 }
597
598 #[must_use]
601 pub fn dry_run(mut self) -> Self {
602 self.dry_run = true;
603 self
604 }
605
606 #[must_use]
608 pub fn scope(mut self, scope: Scope) -> Self {
609 self.scope = Some(scope);
610 self
611 }
612
613 #[must_use]
616 pub fn yes(mut self) -> Self {
617 self.yes = true;
618 self
619 }
620}
621
622impl ClaudeCommand for PluginPruneCommand {
623 type Output = CommandOutput;
624
625 fn args(&self) -> Vec<String> {
626 let mut args = vec!["plugin".to_string(), "prune".to_string()];
627 if self.dry_run {
628 args.push("--dry-run".to_string());
629 }
630 if let Some(ref scope) = self.scope {
631 args.push("--scope".to_string());
632 args.push(scope.as_arg().to_string());
633 }
634 if self.yes {
635 args.push("--yes".to_string());
636 }
637 args
638 }
639
640 #[cfg(feature = "async")]
641 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
642 exec::run_claude(claude, self.args()).await
643 }
644}
645
646#[cfg(test)]
647mod tests {
648 use super::*;
649 use crate::command::ClaudeCommand;
650
651 #[test]
652 fn test_plugin_list() {
653 let cmd = PluginListCommand::new().json().available();
654 assert_eq!(
655 ClaudeCommand::args(&cmd),
656 vec!["plugin", "list", "--json", "--available"]
657 );
658 }
659
660 #[test]
661 fn test_plugin_install() {
662 let cmd = PluginInstallCommand::new("my-plugin").scope(Scope::User);
663 assert_eq!(
664 ClaudeCommand::args(&cmd),
665 vec!["plugin", "install", "--scope", "user", "my-plugin"]
666 );
667 }
668
669 #[test]
670 fn test_plugin_install_with_config() {
671 let cmd = PluginInstallCommand::new("my-plugin").config("k", "v");
672 assert_eq!(
673 ClaudeCommand::args(&cmd),
674 vec!["plugin", "install", "--config", "k=v", "my-plugin"]
675 );
676 }
677
678 #[test]
679 fn test_plugin_install_with_multiple_config() {
680 let cmd = PluginInstallCommand::new("my-plugin")
681 .config("a", "1")
682 .config("b", "2");
683 assert_eq!(
684 ClaudeCommand::args(&cmd),
685 vec![
686 "plugin",
687 "install",
688 "--config",
689 "a=1",
690 "--config",
691 "b=2",
692 "my-plugin"
693 ]
694 );
695 }
696
697 #[test]
698 fn test_plugin_install_with_scope_and_config() {
699 let cmd = PluginInstallCommand::new("my-plugin")
700 .scope(Scope::User)
701 .config("k", "v");
702 assert_eq!(
703 ClaudeCommand::args(&cmd),
704 vec![
705 "plugin",
706 "install",
707 "--scope",
708 "user",
709 "--config",
710 "k=v",
711 "my-plugin"
712 ]
713 );
714 }
715
716 #[test]
717 fn test_plugin_uninstall() {
718 let cmd = PluginUninstallCommand::new("old-plugin");
719 assert_eq!(
720 ClaudeCommand::args(&cmd),
721 vec!["plugin", "uninstall", "old-plugin"]
722 );
723 }
724
725 #[test]
726 fn test_plugin_uninstall_with_all_flags() {
727 let cmd = PluginUninstallCommand::new("old-plugin")
728 .scope(Scope::User)
729 .keep_data()
730 .prune()
731 .yes();
732 assert_eq!(
733 ClaudeCommand::args(&cmd),
734 vec![
735 "plugin",
736 "uninstall",
737 "--scope",
738 "user",
739 "--keep-data",
740 "--prune",
741 "--yes",
742 "old-plugin"
743 ]
744 );
745 }
746
747 #[test]
748 fn test_plugin_uninstall_yes_alone() {
749 let cmd = PluginUninstallCommand::new("p").yes();
751 assert_eq!(
752 ClaudeCommand::args(&cmd),
753 vec!["plugin", "uninstall", "--yes", "p"]
754 );
755 }
756
757 #[test]
758 fn test_plugin_enable() {
759 let cmd = PluginEnableCommand::new("my-plugin").scope(Scope::Project);
760 assert_eq!(
761 ClaudeCommand::args(&cmd),
762 vec!["plugin", "enable", "--scope", "project", "my-plugin"]
763 );
764 }
765
766 #[test]
767 fn test_plugin_disable_specific() {
768 let cmd = PluginDisableCommand::new("my-plugin");
769 assert_eq!(
770 ClaudeCommand::args(&cmd),
771 vec!["plugin", "disable", "my-plugin"]
772 );
773 }
774
775 #[test]
776 fn test_plugin_disable_all() {
777 let cmd = PluginDisableCommand::all();
778 assert_eq!(
779 ClaudeCommand::args(&cmd),
780 vec!["plugin", "disable", "--all"]
781 );
782 }
783
784 #[test]
785 fn test_plugin_update() {
786 let cmd = PluginUpdateCommand::new("my-plugin").scope(Scope::Local);
787 assert_eq!(
788 ClaudeCommand::args(&cmd),
789 vec!["plugin", "update", "--scope", "local", "my-plugin"]
790 );
791 }
792
793 #[test]
794 fn test_plugin_validate() {
795 let cmd = PluginValidateCommand::new("/path/to/manifest");
796 assert_eq!(
797 ClaudeCommand::args(&cmd),
798 vec!["plugin", "validate", "/path/to/manifest"]
799 );
800 }
801
802 #[test]
803 fn plugin_tag_defaults_to_just_subcommand() {
804 let cmd = PluginTagCommand::new();
805 assert_eq!(ClaudeCommand::args(&cmd), vec!["plugin", "tag"]);
806 }
807
808 #[test]
809 fn plugin_tag_with_all_options() {
810 let cmd = PluginTagCommand::new()
811 .path("./plugin")
812 .dry_run()
813 .force()
814 .message("release %s")
815 .push()
816 .remote("upstream");
817 assert_eq!(
818 ClaudeCommand::args(&cmd),
819 vec![
820 "plugin",
821 "tag",
822 "--dry-run",
823 "--force",
824 "--message",
825 "release %s",
826 "--push",
827 "--remote",
828 "upstream",
829 "./plugin",
830 ]
831 );
832 }
833
834 #[test]
835 fn test_plugin_details() {
836 let cmd = PluginDetailsCommand::new("some-plugin");
837 assert_eq!(
838 ClaudeCommand::args(&cmd),
839 vec!["plugin", "details", "some-plugin"]
840 );
841 }
842
843 #[test]
844 fn test_plugin_prune_default() {
845 let cmd = PluginPruneCommand::new();
846 assert_eq!(ClaudeCommand::args(&cmd), vec!["plugin", "prune"]);
847 }
848
849 #[test]
850 fn test_plugin_prune_all_flags() {
851 let cmd = PluginPruneCommand::new().dry_run().scope(Scope::User).yes();
852 assert_eq!(
853 ClaudeCommand::args(&cmd),
854 vec!["plugin", "prune", "--dry-run", "--scope", "user", "--yes"]
855 );
856 }
857
858 #[test]
859 fn test_scope_managed_renders_as_arg() {
860 let cmd = PluginUpdateCommand::new("p").scope(Scope::Managed);
862 assert_eq!(
863 ClaudeCommand::args(&cmd),
864 vec!["plugin", "update", "--scope", "managed", "p"]
865 );
866 }
867}