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