Skip to main content

codex_wrapper/command/
plugin.rs

1//! Manage Codex plugins (`codex plugin`).
2//!
3//! Covers the direct plugin operations (`add`, `list`, `remove`) and the
4//! nested marketplace-source management commands (`plugin marketplace add /
5//! list / upgrade / remove`).
6//!
7//! Plugin selectors follow the CLI's `PLUGIN@MARKETPLACE` form; the
8//! [`marketplace`](PluginAddCommand::marketplace) builder covers the
9//! `PLUGIN` + `-m MARKETPLACE` alternative.
10
11use crate::Codex;
12use crate::command::CodexCommand;
13use crate::error::Result;
14use crate::exec::{self, CommandOutput};
15
16/// Append the `-c`/`--enable`/`--disable` passthrough shared by every builder.
17fn 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/// Install a plugin from a configured marketplace snapshot
38/// (`codex plugin add <PLUGIN[@MARKETPLACE]>`).
39#[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    /// Create an add command for a plugin selector (`PLUGIN` or
51    /// `PLUGIN@MARKETPLACE`).
52    #[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    /// Marketplace name to use when the selector omits `@MARKETPLACE` (`-m`).
65    #[must_use]
66    pub fn marketplace(mut self, name: impl Into<String>) -> Self {
67        self.marketplace = Some(name.into());
68        self
69    }
70
71    /// Override a config key (`-c key=value`). May be called multiple times.
72    #[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    /// Enable an optional feature flag (`--enable <feature>`).
79    #[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    /// Disable an optional feature flag (`--disable <feature>`).
86    #[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    /// Override the retry policy for this command.
93    #[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/// List plugins available from configured marketplace snapshots
125/// (`codex plugin list`).
126#[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    /// Create a plugin list command.
139    #[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    /// Only list plugins from this configured marketplace name (`-m`).
153    #[must_use]
154    pub fn marketplace(mut self, name: impl Into<String>) -> Self {
155        self.marketplace = Some(name.into());
156        self
157    }
158
159    /// Output the plugin list as JSON (`--json`).
160    #[must_use]
161    pub fn json(mut self) -> Self {
162        self.json = true;
163        self
164    }
165
166    /// Include uninstalled marketplace plugins in the JSON output
167    /// (`--available`).
168    #[must_use]
169    pub fn available(mut self) -> Self {
170        self.available = true;
171        self
172    }
173
174    /// Override a config key (`-c key=value`). May be called multiple times.
175    #[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    /// Enable an optional feature flag (`--enable <feature>`).
182    #[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    /// Disable an optional feature flag (`--disable <feature>`).
189    #[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    /// Override the retry policy for this command.
196    #[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/// Remove an installed plugin from local config and cache
239/// (`codex plugin remove <PLUGIN[@MARKETPLACE]>`).
240#[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    /// Create a remove command for a plugin selector (`PLUGIN` or
252    /// `PLUGIN@MARKETPLACE`).
253    #[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    /// Marketplace name to use when the selector omits `@MARKETPLACE` (`-m`).
266    #[must_use]
267    pub fn marketplace(mut self, name: impl Into<String>) -> Self {
268        self.marketplace = Some(name.into());
269        self
270    }
271
272    /// Override a config key (`-c key=value`). May be called multiple times.
273    #[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    /// Enable an optional feature flag (`--enable <feature>`).
280    #[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    /// Disable an optional feature flag (`--disable <feature>`).
287    #[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    /// Override the retry policy for this command.
294    #[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/// Add a local or Git marketplace source
326/// (`codex plugin marketplace add <SOURCE>`).
327#[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    /// Create a marketplace-add command for a source (local path,
339    /// `owner/repo[@ref]`, HTTPS Git URL, or SSH Git URL).
340    #[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    /// Git ref to fetch for Git marketplace sources (`--ref`).
353    #[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    /// Override a config key (`-c key=value`). May be called multiple times.
360    #[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    /// Enable an optional feature flag (`--enable <feature>`).
367    #[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    /// Disable an optional feature flag (`--disable <feature>`).
374    #[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    /// Override the retry policy for this command.
381    #[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/// List configured plugin marketplaces (`codex plugin marketplace list`).
417#[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    /// Create a marketplace list command.
428    #[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    /// Output the marketplace list as JSON (`--json`).
440    #[must_use]
441    pub fn json(mut self) -> Self {
442        self.json = true;
443        self
444    }
445
446    /// Override a config key (`-c key=value`). May be called multiple times.
447    #[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    /// Enable an optional feature flag (`--enable <feature>`).
454    #[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    /// Disable an optional feature flag (`--disable <feature>`).
461    #[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    /// Override the retry policy for this command.
468    #[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/// Refresh configured Git marketplace snapshots
508/// (`codex plugin marketplace upgrade [MARKETPLACE_NAME]`).
509///
510/// Omit the name to upgrade all configured Git marketplaces.
511#[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    /// Create a marketplace upgrade command (upgrades all Git marketplaces
523    /// unless [`name`](PluginMarketplaceUpgradeCommand::name) is set).
524    #[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    /// Upgrade only this configured marketplace name.
537    #[must_use]
538    pub fn name(mut self, name: impl Into<String>) -> Self {
539        self.name = Some(name.into());
540        self
541    }
542
543    /// Output the upgrade result as JSON (`--json`).
544    #[must_use]
545    pub fn json(mut self) -> Self {
546        self.json = true;
547        self
548    }
549
550    /// Override a config key (`-c key=value`). May be called multiple times.
551    #[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    /// Enable an optional feature flag (`--enable <feature>`).
558    #[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    /// Disable an optional feature flag (`--disable <feature>`).
565    #[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    /// Override the retry policy for this command.
572    #[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/// Remove a configured marketplace source by name
615/// (`codex plugin marketplace remove <MARKETPLACE_NAME>`).
616#[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    /// Create a marketplace remove command for the given marketplace name.
628    #[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    /// Output the remove result as JSON (`--json`).
641    #[must_use]
642    pub fn json(mut self) -> Self {
643        self.json = true;
644        self
645    }
646
647    /// Override a config key (`-c key=value`). May be called multiple times.
648    #[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    /// Enable an optional feature flag (`--enable <feature>`).
655    #[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    /// Disable an optional feature flag (`--disable <feature>`).
662    #[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    /// Override the retry policy for this command.
669    #[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}