1use crate::Codex;
12use crate::command::CodexCommand;
13use crate::error::Result;
14use crate::exec::{self, CommandOutput};
15
16fn push_config(
18 args: &mut Vec<String>,
19 config_overrides: &[String],
20 enabled: &[String],
21 disabled: &[String],
22) {
23 for value in config_overrides {
24 args.push("-c".into());
25 args.push(value.clone());
26 }
27 for value in enabled {
28 args.push("--enable".into());
29 args.push(value.clone());
30 }
31 for value in disabled {
32 args.push("--disable".into());
33 args.push(value.clone());
34 }
35}
36
37#[derive(Debug, Clone)]
40pub struct PluginAddCommand {
41 selector: String,
42 marketplace: Option<String>,
43 config_overrides: Vec<String>,
44 enabled_features: Vec<String>,
45 disabled_features: Vec<String>,
46 retry_policy: Option<crate::retry::RetryPolicy>,
47}
48
49impl PluginAddCommand {
50 #[must_use]
53 pub fn new(selector: impl Into<String>) -> Self {
54 Self {
55 selector: selector.into(),
56 marketplace: None,
57 config_overrides: Vec::new(),
58 enabled_features: Vec::new(),
59 disabled_features: Vec::new(),
60 retry_policy: None,
61 }
62 }
63
64 #[must_use]
66 pub fn marketplace(mut self, name: impl Into<String>) -> Self {
67 self.marketplace = Some(name.into());
68 self
69 }
70
71 #[must_use]
73 pub fn config(mut self, key_value: impl Into<String>) -> Self {
74 self.config_overrides.push(key_value.into());
75 self
76 }
77
78 #[must_use]
80 pub fn enable(mut self, feature: impl Into<String>) -> Self {
81 self.enabled_features.push(feature.into());
82 self
83 }
84
85 #[must_use]
87 pub fn disable(mut self, feature: impl Into<String>) -> Self {
88 self.disabled_features.push(feature.into());
89 self
90 }
91
92 #[must_use]
94 pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
95 self.retry_policy = Some(policy);
96 self
97 }
98}
99
100impl CodexCommand for PluginAddCommand {
101 type Output = CommandOutput;
102
103 fn args(&self) -> Vec<String> {
104 let mut args = vec!["plugin".to_string(), "add".to_string()];
105 push_config(
106 &mut args,
107 &self.config_overrides,
108 &self.enabled_features,
109 &self.disabled_features,
110 );
111 if let Some(name) = &self.marketplace {
112 args.push("--marketplace".into());
113 args.push(name.clone());
114 }
115 args.push(self.selector.clone());
116 args
117 }
118
119 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
120 exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
121 }
122}
123
124#[derive(Debug, Clone)]
127pub struct PluginListCommand {
128 marketplace: Option<String>,
129 json: bool,
130 available: bool,
131 config_overrides: Vec<String>,
132 enabled_features: Vec<String>,
133 disabled_features: Vec<String>,
134 retry_policy: Option<crate::retry::RetryPolicy>,
135}
136
137impl PluginListCommand {
138 #[must_use]
140 pub fn new() -> Self {
141 Self {
142 marketplace: None,
143 json: false,
144 available: false,
145 config_overrides: Vec::new(),
146 enabled_features: Vec::new(),
147 disabled_features: Vec::new(),
148 retry_policy: None,
149 }
150 }
151
152 #[must_use]
154 pub fn marketplace(mut self, name: impl Into<String>) -> Self {
155 self.marketplace = Some(name.into());
156 self
157 }
158
159 #[must_use]
161 pub fn json(mut self) -> Self {
162 self.json = true;
163 self
164 }
165
166 #[must_use]
169 pub fn available(mut self) -> Self {
170 self.available = true;
171 self
172 }
173
174 #[must_use]
176 pub fn config(mut self, key_value: impl Into<String>) -> Self {
177 self.config_overrides.push(key_value.into());
178 self
179 }
180
181 #[must_use]
183 pub fn enable(mut self, feature: impl Into<String>) -> Self {
184 self.enabled_features.push(feature.into());
185 self
186 }
187
188 #[must_use]
190 pub fn disable(mut self, feature: impl Into<String>) -> Self {
191 self.disabled_features.push(feature.into());
192 self
193 }
194
195 #[must_use]
197 pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
198 self.retry_policy = Some(policy);
199 self
200 }
201}
202
203impl Default for PluginListCommand {
204 fn default() -> Self {
205 Self::new()
206 }
207}
208
209impl CodexCommand for PluginListCommand {
210 type Output = CommandOutput;
211
212 fn args(&self) -> Vec<String> {
213 let mut args = vec!["plugin".to_string(), "list".to_string()];
214 push_config(
215 &mut args,
216 &self.config_overrides,
217 &self.enabled_features,
218 &self.disabled_features,
219 );
220 if let Some(name) = &self.marketplace {
221 args.push("--marketplace".into());
222 args.push(name.clone());
223 }
224 if self.json {
225 args.push("--json".into());
226 }
227 if self.available {
228 args.push("--available".into());
229 }
230 args
231 }
232
233 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
234 exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
235 }
236}
237
238#[derive(Debug, Clone)]
241pub struct PluginRemoveCommand {
242 selector: String,
243 marketplace: Option<String>,
244 config_overrides: Vec<String>,
245 enabled_features: Vec<String>,
246 disabled_features: Vec<String>,
247 retry_policy: Option<crate::retry::RetryPolicy>,
248}
249
250impl PluginRemoveCommand {
251 #[must_use]
254 pub fn new(selector: impl Into<String>) -> Self {
255 Self {
256 selector: selector.into(),
257 marketplace: None,
258 config_overrides: Vec::new(),
259 enabled_features: Vec::new(),
260 disabled_features: Vec::new(),
261 retry_policy: None,
262 }
263 }
264
265 #[must_use]
267 pub fn marketplace(mut self, name: impl Into<String>) -> Self {
268 self.marketplace = Some(name.into());
269 self
270 }
271
272 #[must_use]
274 pub fn config(mut self, key_value: impl Into<String>) -> Self {
275 self.config_overrides.push(key_value.into());
276 self
277 }
278
279 #[must_use]
281 pub fn enable(mut self, feature: impl Into<String>) -> Self {
282 self.enabled_features.push(feature.into());
283 self
284 }
285
286 #[must_use]
288 pub fn disable(mut self, feature: impl Into<String>) -> Self {
289 self.disabled_features.push(feature.into());
290 self
291 }
292
293 #[must_use]
295 pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
296 self.retry_policy = Some(policy);
297 self
298 }
299}
300
301impl CodexCommand for PluginRemoveCommand {
302 type Output = CommandOutput;
303
304 fn args(&self) -> Vec<String> {
305 let mut args = vec!["plugin".to_string(), "remove".to_string()];
306 push_config(
307 &mut args,
308 &self.config_overrides,
309 &self.enabled_features,
310 &self.disabled_features,
311 );
312 if let Some(name) = &self.marketplace {
313 args.push("--marketplace".into());
314 args.push(name.clone());
315 }
316 args.push(self.selector.clone());
317 args
318 }
319
320 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
321 exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
322 }
323}
324
325#[derive(Debug, Clone)]
328pub struct PluginMarketplaceAddCommand {
329 source: String,
330 git_ref: Option<String>,
331 config_overrides: Vec<String>,
332 enabled_features: Vec<String>,
333 disabled_features: Vec<String>,
334 retry_policy: Option<crate::retry::RetryPolicy>,
335}
336
337impl PluginMarketplaceAddCommand {
338 #[must_use]
341 pub fn new(source: impl Into<String>) -> Self {
342 Self {
343 source: source.into(),
344 git_ref: None,
345 config_overrides: Vec::new(),
346 enabled_features: Vec::new(),
347 disabled_features: Vec::new(),
348 retry_policy: None,
349 }
350 }
351
352 #[must_use]
354 pub fn git_ref(mut self, git_ref: impl Into<String>) -> Self {
355 self.git_ref = Some(git_ref.into());
356 self
357 }
358
359 #[must_use]
361 pub fn config(mut self, key_value: impl Into<String>) -> Self {
362 self.config_overrides.push(key_value.into());
363 self
364 }
365
366 #[must_use]
368 pub fn enable(mut self, feature: impl Into<String>) -> Self {
369 self.enabled_features.push(feature.into());
370 self
371 }
372
373 #[must_use]
375 pub fn disable(mut self, feature: impl Into<String>) -> Self {
376 self.disabled_features.push(feature.into());
377 self
378 }
379
380 #[must_use]
382 pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
383 self.retry_policy = Some(policy);
384 self
385 }
386}
387
388impl CodexCommand for PluginMarketplaceAddCommand {
389 type Output = CommandOutput;
390
391 fn args(&self) -> Vec<String> {
392 let mut args = vec![
393 "plugin".to_string(),
394 "marketplace".to_string(),
395 "add".to_string(),
396 ];
397 push_config(
398 &mut args,
399 &self.config_overrides,
400 &self.enabled_features,
401 &self.disabled_features,
402 );
403 if let Some(git_ref) = &self.git_ref {
404 args.push("--ref".into());
405 args.push(git_ref.clone());
406 }
407 args.push(self.source.clone());
408 args
409 }
410
411 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
412 exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
413 }
414}
415
416#[derive(Debug, Clone)]
418pub struct PluginMarketplaceListCommand {
419 json: bool,
420 config_overrides: Vec<String>,
421 enabled_features: Vec<String>,
422 disabled_features: Vec<String>,
423 retry_policy: Option<crate::retry::RetryPolicy>,
424}
425
426impl PluginMarketplaceListCommand {
427 #[must_use]
429 pub fn new() -> Self {
430 Self {
431 json: false,
432 config_overrides: Vec::new(),
433 enabled_features: Vec::new(),
434 disabled_features: Vec::new(),
435 retry_policy: None,
436 }
437 }
438
439 #[must_use]
441 pub fn json(mut self) -> Self {
442 self.json = true;
443 self
444 }
445
446 #[must_use]
448 pub fn config(mut self, key_value: impl Into<String>) -> Self {
449 self.config_overrides.push(key_value.into());
450 self
451 }
452
453 #[must_use]
455 pub fn enable(mut self, feature: impl Into<String>) -> Self {
456 self.enabled_features.push(feature.into());
457 self
458 }
459
460 #[must_use]
462 pub fn disable(mut self, feature: impl Into<String>) -> Self {
463 self.disabled_features.push(feature.into());
464 self
465 }
466
467 #[must_use]
469 pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
470 self.retry_policy = Some(policy);
471 self
472 }
473}
474
475impl Default for PluginMarketplaceListCommand {
476 fn default() -> Self {
477 Self::new()
478 }
479}
480
481impl CodexCommand for PluginMarketplaceListCommand {
482 type Output = CommandOutput;
483
484 fn args(&self) -> Vec<String> {
485 let mut args = vec![
486 "plugin".to_string(),
487 "marketplace".to_string(),
488 "list".to_string(),
489 ];
490 push_config(
491 &mut args,
492 &self.config_overrides,
493 &self.enabled_features,
494 &self.disabled_features,
495 );
496 if self.json {
497 args.push("--json".into());
498 }
499 args
500 }
501
502 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
503 exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
504 }
505}
506
507#[derive(Debug, Clone)]
512pub struct PluginMarketplaceUpgradeCommand {
513 name: Option<String>,
514 json: bool,
515 config_overrides: Vec<String>,
516 enabled_features: Vec<String>,
517 disabled_features: Vec<String>,
518 retry_policy: Option<crate::retry::RetryPolicy>,
519}
520
521impl PluginMarketplaceUpgradeCommand {
522 #[must_use]
525 pub fn new() -> Self {
526 Self {
527 name: None,
528 json: false,
529 config_overrides: Vec::new(),
530 enabled_features: Vec::new(),
531 disabled_features: Vec::new(),
532 retry_policy: None,
533 }
534 }
535
536 #[must_use]
538 pub fn name(mut self, name: impl Into<String>) -> Self {
539 self.name = Some(name.into());
540 self
541 }
542
543 #[must_use]
545 pub fn json(mut self) -> Self {
546 self.json = true;
547 self
548 }
549
550 #[must_use]
552 pub fn config(mut self, key_value: impl Into<String>) -> Self {
553 self.config_overrides.push(key_value.into());
554 self
555 }
556
557 #[must_use]
559 pub fn enable(mut self, feature: impl Into<String>) -> Self {
560 self.enabled_features.push(feature.into());
561 self
562 }
563
564 #[must_use]
566 pub fn disable(mut self, feature: impl Into<String>) -> Self {
567 self.disabled_features.push(feature.into());
568 self
569 }
570
571 #[must_use]
573 pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
574 self.retry_policy = Some(policy);
575 self
576 }
577}
578
579impl Default for PluginMarketplaceUpgradeCommand {
580 fn default() -> Self {
581 Self::new()
582 }
583}
584
585impl CodexCommand for PluginMarketplaceUpgradeCommand {
586 type Output = CommandOutput;
587
588 fn args(&self) -> Vec<String> {
589 let mut args = vec![
590 "plugin".to_string(),
591 "marketplace".to_string(),
592 "upgrade".to_string(),
593 ];
594 push_config(
595 &mut args,
596 &self.config_overrides,
597 &self.enabled_features,
598 &self.disabled_features,
599 );
600 if self.json {
601 args.push("--json".into());
602 }
603 if let Some(name) = &self.name {
604 args.push(name.clone());
605 }
606 args
607 }
608
609 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
610 exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
611 }
612}
613
614#[derive(Debug, Clone)]
617pub struct PluginMarketplaceRemoveCommand {
618 name: String,
619 json: bool,
620 config_overrides: Vec<String>,
621 enabled_features: Vec<String>,
622 disabled_features: Vec<String>,
623 retry_policy: Option<crate::retry::RetryPolicy>,
624}
625
626impl PluginMarketplaceRemoveCommand {
627 #[must_use]
629 pub fn new(name: impl Into<String>) -> Self {
630 Self {
631 name: name.into(),
632 json: false,
633 config_overrides: Vec::new(),
634 enabled_features: Vec::new(),
635 disabled_features: Vec::new(),
636 retry_policy: None,
637 }
638 }
639
640 #[must_use]
642 pub fn json(mut self) -> Self {
643 self.json = true;
644 self
645 }
646
647 #[must_use]
649 pub fn config(mut self, key_value: impl Into<String>) -> Self {
650 self.config_overrides.push(key_value.into());
651 self
652 }
653
654 #[must_use]
656 pub fn enable(mut self, feature: impl Into<String>) -> Self {
657 self.enabled_features.push(feature.into());
658 self
659 }
660
661 #[must_use]
663 pub fn disable(mut self, feature: impl Into<String>) -> Self {
664 self.disabled_features.push(feature.into());
665 self
666 }
667
668 #[must_use]
670 pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
671 self.retry_policy = Some(policy);
672 self
673 }
674}
675
676impl CodexCommand for PluginMarketplaceRemoveCommand {
677 type Output = CommandOutput;
678
679 fn args(&self) -> Vec<String> {
680 let mut args = vec![
681 "plugin".to_string(),
682 "marketplace".to_string(),
683 "remove".to_string(),
684 ];
685 push_config(
686 &mut args,
687 &self.config_overrides,
688 &self.enabled_features,
689 &self.disabled_features,
690 );
691 if self.json {
692 args.push("--json".into());
693 }
694 args.push(self.name.clone());
695 args
696 }
697
698 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
699 exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
700 }
701}
702
703#[cfg(test)]
704mod tests {
705 use super::*;
706
707 #[test]
708 fn plugin_add_args() {
709 let args = PluginAddCommand::new("hello@official").args();
710 assert_eq!(args, vec!["plugin", "add", "hello@official"]);
711 }
712
713 #[test]
714 fn plugin_add_args_with_marketplace_and_config() {
715 let args = PluginAddCommand::new("hello")
716 .marketplace("official")
717 .config("foo=bar")
718 .args();
719 assert_eq!(
720 args,
721 vec![
722 "plugin",
723 "add",
724 "-c",
725 "foo=bar",
726 "--marketplace",
727 "official",
728 "hello",
729 ]
730 );
731 }
732
733 #[test]
734 fn plugin_list_args() {
735 let args = PluginListCommand::new().json().available().args();
736 assert_eq!(args, vec!["plugin", "list", "--json", "--available"]);
737 }
738
739 #[test]
740 fn plugin_remove_args() {
741 let args = PluginRemoveCommand::new("hello")
742 .marketplace("official")
743 .args();
744 assert_eq!(
745 args,
746 vec!["plugin", "remove", "--marketplace", "official", "hello"]
747 );
748 }
749
750 #[test]
751 fn marketplace_add_args() {
752 let args = PluginMarketplaceAddCommand::new("owner/repo")
753 .git_ref("main")
754 .args();
755 assert_eq!(
756 args,
757 vec![
758 "plugin",
759 "marketplace",
760 "add",
761 "--ref",
762 "main",
763 "owner/repo",
764 ]
765 );
766 }
767
768 #[test]
769 fn marketplace_list_args() {
770 let args = PluginMarketplaceListCommand::new().json().args();
771 assert_eq!(args, vec!["plugin", "marketplace", "list", "--json"]);
772 }
773
774 #[test]
775 fn marketplace_upgrade_args_all() {
776 let args = PluginMarketplaceUpgradeCommand::new().args();
777 assert_eq!(args, vec!["plugin", "marketplace", "upgrade"]);
778 }
779
780 #[test]
781 fn marketplace_upgrade_args_named() {
782 let args = PluginMarketplaceUpgradeCommand::new()
783 .name("official")
784 .args();
785 assert_eq!(args, vec!["plugin", "marketplace", "upgrade", "official"]);
786 }
787
788 #[test]
789 fn marketplace_remove_args() {
790 let args = PluginMarketplaceRemoveCommand::new("official")
791 .json()
792 .args();
793 assert_eq!(
794 args,
795 vec!["plugin", "marketplace", "remove", "--json", "official"]
796 );
797 }
798}