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#[derive(Debug, Clone, Default)]
26pub struct PluginListCommand {
27 json: bool,
28 available: bool,
29}
30
31impl PluginListCommand {
32 #[must_use]
34 pub fn new() -> Self {
35 Self::default()
36 }
37
38 #[must_use]
40 pub fn json(mut self) -> Self {
41 self.json = true;
42 self
43 }
44
45 #[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#[derive(Debug, Clone)]
90pub struct PluginInstallCommand {
91 plugin: String,
92 scope: Option<Scope>,
93}
94
95impl PluginInstallCommand {
96 #[must_use]
98 pub fn new(plugin: impl Into<String>) -> Self {
99 Self {
100 plugin: plugin.into(),
101 scope: None,
102 }
103 }
104
105 #[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#[derive(Debug, Clone)]
138pub struct PluginUninstallCommand {
139 plugin: String,
140 scope: Option<Scope>,
141 keep_data: bool,
142 prune: bool,
143 yes: bool,
144}
145
146impl PluginUninstallCommand {
147 #[must_use]
149 pub fn new(plugin: impl Into<String>) -> Self {
150 Self {
151 plugin: plugin.into(),
152 scope: None,
153 keep_data: false,
154 prune: false,
155 yes: false,
156 }
157 }
158
159 #[must_use]
161 pub fn scope(mut self, scope: Scope) -> Self {
162 self.scope = Some(scope);
163 self
164 }
165
166 #[must_use]
170 pub fn keep_data(mut self) -> Self {
171 self.keep_data = true;
172 self
173 }
174
175 #[must_use]
179 pub fn prune(mut self) -> Self {
180 self.prune = true;
181 self
182 }
183
184 #[must_use]
189 pub fn yes(mut self) -> Self {
190 self.yes = true;
191 self
192 }
193}
194
195impl ClaudeCommand for PluginUninstallCommand {
196 type Output = CommandOutput;
197
198 fn args(&self) -> Vec<String> {
199 let mut args = vec!["plugin".to_string(), "uninstall".to_string()];
200 if let Some(ref scope) = self.scope {
201 args.push("--scope".to_string());
202 args.push(scope.as_arg().to_string());
203 }
204 if self.keep_data {
205 args.push("--keep-data".to_string());
206 }
207 if self.prune {
208 args.push("--prune".to_string());
209 }
210 if self.yes {
211 args.push("--yes".to_string());
212 }
213 args.push(self.plugin.clone());
214 args
215 }
216
217 #[cfg(feature = "async")]
218 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
219 exec::run_claude(claude, self.args()).await
220 }
221}
222
223#[derive(Debug, Clone)]
225pub struct PluginEnableCommand {
226 plugin: String,
227 scope: Option<Scope>,
228}
229
230impl PluginEnableCommand {
231 #[must_use]
233 pub fn new(plugin: impl Into<String>) -> Self {
234 Self {
235 plugin: plugin.into(),
236 scope: None,
237 }
238 }
239
240 #[must_use]
242 pub fn scope(mut self, scope: Scope) -> Self {
243 self.scope = Some(scope);
244 self
245 }
246}
247
248impl ClaudeCommand for PluginEnableCommand {
249 type Output = CommandOutput;
250
251 fn args(&self) -> Vec<String> {
252 let mut args = vec!["plugin".to_string(), "enable".to_string()];
253 if let Some(ref scope) = self.scope {
254 args.push("--scope".to_string());
255 args.push(scope.as_arg().to_string());
256 }
257 args.push(self.plugin.clone());
258 args
259 }
260
261 #[cfg(feature = "async")]
262 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
263 exec::run_claude(claude, self.args()).await
264 }
265}
266
267#[derive(Debug, Clone)]
269pub struct PluginDisableCommand {
270 plugin: Option<String>,
271 scope: Option<Scope>,
272 all: bool,
273}
274
275impl PluginDisableCommand {
276 #[must_use]
278 pub fn new(plugin: impl Into<String>) -> Self {
279 Self {
280 plugin: Some(plugin.into()),
281 scope: None,
282 all: false,
283 }
284 }
285
286 #[must_use]
288 pub fn all() -> Self {
289 Self {
290 plugin: None,
291 scope: None,
292 all: true,
293 }
294 }
295
296 #[must_use]
298 pub fn scope(mut self, scope: Scope) -> Self {
299 self.scope = Some(scope);
300 self
301 }
302}
303
304impl ClaudeCommand for PluginDisableCommand {
305 type Output = CommandOutput;
306
307 fn args(&self) -> Vec<String> {
308 let mut args = vec!["plugin".to_string(), "disable".to_string()];
309 if self.all {
310 args.push("--all".to_string());
311 }
312 if let Some(ref scope) = self.scope {
313 args.push("--scope".to_string());
314 args.push(scope.as_arg().to_string());
315 }
316 if let Some(ref plugin) = self.plugin {
317 args.push(plugin.clone());
318 }
319 args
320 }
321
322 #[cfg(feature = "async")]
323 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
324 exec::run_claude(claude, self.args()).await
325 }
326}
327
328#[derive(Debug, Clone)]
330pub struct PluginUpdateCommand {
331 plugin: String,
332 scope: Option<Scope>,
333}
334
335impl PluginUpdateCommand {
336 #[must_use]
338 pub fn new(plugin: impl Into<String>) -> Self {
339 Self {
340 plugin: plugin.into(),
341 scope: None,
342 }
343 }
344
345 #[must_use]
347 pub fn scope(mut self, scope: Scope) -> Self {
348 self.scope = Some(scope);
349 self
350 }
351}
352
353impl ClaudeCommand for PluginUpdateCommand {
354 type Output = CommandOutput;
355
356 fn args(&self) -> Vec<String> {
357 let mut args = vec!["plugin".to_string(), "update".to_string()];
358 if let Some(ref scope) = self.scope {
359 args.push("--scope".to_string());
360 args.push(scope.as_arg().to_string());
361 }
362 args.push(self.plugin.clone());
363 args
364 }
365
366 #[cfg(feature = "async")]
367 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
368 exec::run_claude(claude, self.args()).await
369 }
370}
371
372#[derive(Debug, Clone)]
374pub struct PluginValidateCommand {
375 path: String,
376}
377
378impl PluginValidateCommand {
379 #[must_use]
381 pub fn new(path: impl Into<String>) -> Self {
382 Self { path: path.into() }
383 }
384}
385
386impl ClaudeCommand for PluginValidateCommand {
387 type Output = CommandOutput;
388
389 fn args(&self) -> Vec<String> {
390 vec![
391 "plugin".to_string(),
392 "validate".to_string(),
393 self.path.clone(),
394 ]
395 }
396
397 #[cfg(feature = "async")]
398 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
399 exec::run_claude(claude, self.args()).await
400 }
401}
402
403#[derive(Debug, Clone, Default)]
428pub struct PluginTagCommand {
429 path: Option<String>,
430 dry_run: bool,
431 force: bool,
432 message: Option<String>,
433 push: bool,
434 remote: Option<String>,
435}
436
437impl PluginTagCommand {
438 #[must_use]
441 pub fn new() -> Self {
442 Self::default()
443 }
444
445 #[must_use]
447 pub fn path(mut self, path: impl Into<String>) -> Self {
448 self.path = Some(path.into());
449 self
450 }
451
452 #[must_use]
454 pub fn dry_run(mut self) -> Self {
455 self.dry_run = true;
456 self
457 }
458
459 #[must_use]
461 pub fn force(mut self) -> Self {
462 self.force = true;
463 self
464 }
465
466 #[must_use]
468 pub fn message(mut self, msg: impl Into<String>) -> Self {
469 self.message = Some(msg.into());
470 self
471 }
472
473 #[must_use]
475 pub fn push(mut self) -> Self {
476 self.push = true;
477 self
478 }
479
480 #[must_use]
482 pub fn remote(mut self, remote: impl Into<String>) -> Self {
483 self.remote = Some(remote.into());
484 self
485 }
486}
487
488impl ClaudeCommand for PluginTagCommand {
489 type Output = CommandOutput;
490
491 fn args(&self) -> Vec<String> {
492 let mut args = vec!["plugin".to_string(), "tag".to_string()];
493 if self.dry_run {
494 args.push("--dry-run".to_string());
495 }
496 if self.force {
497 args.push("--force".to_string());
498 }
499 if let Some(ref msg) = self.message {
500 args.push("--message".to_string());
501 args.push(msg.clone());
502 }
503 if self.push {
504 args.push("--push".to_string());
505 }
506 if let Some(ref remote) = self.remote {
507 args.push("--remote".to_string());
508 args.push(remote.clone());
509 }
510 if let Some(ref path) = self.path {
511 args.push(path.clone());
512 }
513 args
514 }
515
516 #[cfg(feature = "async")]
517 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
518 exec::run_claude(claude, self.args()).await
519 }
520}
521
522#[derive(Debug, Clone)]
525pub struct PluginDetailsCommand {
526 plugin: String,
527}
528
529impl PluginDetailsCommand {
530 #[must_use]
532 pub fn new(plugin: impl Into<String>) -> Self {
533 Self {
534 plugin: plugin.into(),
535 }
536 }
537}
538
539impl ClaudeCommand for PluginDetailsCommand {
540 type Output = CommandOutput;
541
542 fn args(&self) -> Vec<String> {
543 vec![
544 "plugin".to_string(),
545 "details".to_string(),
546 self.plugin.clone(),
547 ]
548 }
549
550 #[cfg(feature = "async")]
551 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
552 exec::run_claude(claude, self.args()).await
553 }
554}
555
556#[derive(Debug, Clone, Default)]
563pub struct PluginPruneCommand {
564 dry_run: bool,
565 scope: Option<Scope>,
566 yes: bool,
567}
568
569impl PluginPruneCommand {
570 #[must_use]
572 pub fn new() -> Self {
573 Self::default()
574 }
575
576 #[must_use]
579 pub fn dry_run(mut self) -> Self {
580 self.dry_run = true;
581 self
582 }
583
584 #[must_use]
586 pub fn scope(mut self, scope: Scope) -> Self {
587 self.scope = Some(scope);
588 self
589 }
590
591 #[must_use]
594 pub fn yes(mut self) -> Self {
595 self.yes = true;
596 self
597 }
598}
599
600impl ClaudeCommand for PluginPruneCommand {
601 type Output = CommandOutput;
602
603 fn args(&self) -> Vec<String> {
604 let mut args = vec!["plugin".to_string(), "prune".to_string()];
605 if self.dry_run {
606 args.push("--dry-run".to_string());
607 }
608 if let Some(ref scope) = self.scope {
609 args.push("--scope".to_string());
610 args.push(scope.as_arg().to_string());
611 }
612 if self.yes {
613 args.push("--yes".to_string());
614 }
615 args
616 }
617
618 #[cfg(feature = "async")]
619 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
620 exec::run_claude(claude, self.args()).await
621 }
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627 use crate::command::ClaudeCommand;
628
629 #[test]
630 fn test_plugin_list() {
631 let cmd = PluginListCommand::new().json().available();
632 assert_eq!(
633 ClaudeCommand::args(&cmd),
634 vec!["plugin", "list", "--json", "--available"]
635 );
636 }
637
638 #[test]
639 fn test_plugin_install() {
640 let cmd = PluginInstallCommand::new("my-plugin").scope(Scope::User);
641 assert_eq!(
642 ClaudeCommand::args(&cmd),
643 vec!["plugin", "install", "--scope", "user", "my-plugin"]
644 );
645 }
646
647 #[test]
648 fn test_plugin_uninstall() {
649 let cmd = PluginUninstallCommand::new("old-plugin");
650 assert_eq!(
651 ClaudeCommand::args(&cmd),
652 vec!["plugin", "uninstall", "old-plugin"]
653 );
654 }
655
656 #[test]
657 fn test_plugin_uninstall_with_all_flags() {
658 let cmd = PluginUninstallCommand::new("old-plugin")
659 .scope(Scope::User)
660 .keep_data()
661 .prune()
662 .yes();
663 assert_eq!(
664 ClaudeCommand::args(&cmd),
665 vec![
666 "plugin",
667 "uninstall",
668 "--scope",
669 "user",
670 "--keep-data",
671 "--prune",
672 "--yes",
673 "old-plugin"
674 ]
675 );
676 }
677
678 #[test]
679 fn test_plugin_uninstall_yes_alone() {
680 let cmd = PluginUninstallCommand::new("p").yes();
682 assert_eq!(
683 ClaudeCommand::args(&cmd),
684 vec!["plugin", "uninstall", "--yes", "p"]
685 );
686 }
687
688 #[test]
689 fn test_plugin_enable() {
690 let cmd = PluginEnableCommand::new("my-plugin").scope(Scope::Project);
691 assert_eq!(
692 ClaudeCommand::args(&cmd),
693 vec!["plugin", "enable", "--scope", "project", "my-plugin"]
694 );
695 }
696
697 #[test]
698 fn test_plugin_disable_specific() {
699 let cmd = PluginDisableCommand::new("my-plugin");
700 assert_eq!(
701 ClaudeCommand::args(&cmd),
702 vec!["plugin", "disable", "my-plugin"]
703 );
704 }
705
706 #[test]
707 fn test_plugin_disable_all() {
708 let cmd = PluginDisableCommand::all();
709 assert_eq!(
710 ClaudeCommand::args(&cmd),
711 vec!["plugin", "disable", "--all"]
712 );
713 }
714
715 #[test]
716 fn test_plugin_update() {
717 let cmd = PluginUpdateCommand::new("my-plugin").scope(Scope::Local);
718 assert_eq!(
719 ClaudeCommand::args(&cmd),
720 vec!["plugin", "update", "--scope", "local", "my-plugin"]
721 );
722 }
723
724 #[test]
725 fn test_plugin_validate() {
726 let cmd = PluginValidateCommand::new("/path/to/manifest");
727 assert_eq!(
728 ClaudeCommand::args(&cmd),
729 vec!["plugin", "validate", "/path/to/manifest"]
730 );
731 }
732
733 #[test]
734 fn plugin_tag_defaults_to_just_subcommand() {
735 let cmd = PluginTagCommand::new();
736 assert_eq!(ClaudeCommand::args(&cmd), vec!["plugin", "tag"]);
737 }
738
739 #[test]
740 fn plugin_tag_with_all_options() {
741 let cmd = PluginTagCommand::new()
742 .path("./plugin")
743 .dry_run()
744 .force()
745 .message("release %s")
746 .push()
747 .remote("upstream");
748 assert_eq!(
749 ClaudeCommand::args(&cmd),
750 vec![
751 "plugin",
752 "tag",
753 "--dry-run",
754 "--force",
755 "--message",
756 "release %s",
757 "--push",
758 "--remote",
759 "upstream",
760 "./plugin",
761 ]
762 );
763 }
764
765 #[test]
766 fn test_plugin_details() {
767 let cmd = PluginDetailsCommand::new("some-plugin");
768 assert_eq!(
769 ClaudeCommand::args(&cmd),
770 vec!["plugin", "details", "some-plugin"]
771 );
772 }
773
774 #[test]
775 fn test_plugin_prune_default() {
776 let cmd = PluginPruneCommand::new();
777 assert_eq!(ClaudeCommand::args(&cmd), vec!["plugin", "prune"]);
778 }
779
780 #[test]
781 fn test_plugin_prune_all_flags() {
782 let cmd = PluginPruneCommand::new().dry_run().scope(Scope::User).yes();
783 assert_eq!(
784 ClaudeCommand::args(&cmd),
785 vec!["plugin", "prune", "--dry-run", "--scope", "user", "--yes"]
786 );
787 }
788
789 #[test]
790 fn test_scope_managed_renders_as_arg() {
791 let cmd = PluginUpdateCommand::new("p").scope(Scope::Managed);
793 assert_eq!(
794 ClaudeCommand::args(&cmd),
795 vec!["plugin", "update", "--scope", "managed", "p"]
796 );
797 }
798}