Skip to main content

claude_wrapper/command/
marketplace.rs

1use crate::Claude;
2use crate::command::ClaudeCommand;
3use crate::error::Result;
4use crate::exec::{self, CommandOutput};
5use crate::types::Scope;
6
7/// List configured plugin marketplaces.
8///
9/// # Example
10///
11/// ```no_run
12/// use claude_wrapper::{Claude, ClaudeCommand, MarketplaceListCommand};
13///
14/// # async fn example() -> claude_wrapper::Result<()> {
15/// let claude = Claude::builder().build()?;
16/// let output = MarketplaceListCommand::new().json().execute(&claude).await?;
17/// println!("{}", output.stdout);
18/// # Ok(())
19/// # }
20/// ```
21#[derive(Debug, Clone, Default)]
22pub struct MarketplaceListCommand {
23    json: bool,
24}
25
26impl MarketplaceListCommand {
27    #[must_use]
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    /// Output as JSON.
33    #[must_use]
34    pub fn json(mut self) -> Self {
35        self.json = true;
36        self
37    }
38}
39
40impl ClaudeCommand for MarketplaceListCommand {
41    type Output = CommandOutput;
42
43    fn args(&self) -> Vec<String> {
44        let mut args = vec![
45            "plugin".to_string(),
46            "marketplace".to_string(),
47            "list".to_string(),
48        ];
49        if self.json {
50            args.push("--json".to_string());
51        }
52        args
53    }
54
55    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
56        exec::run_claude(claude, self.args()).await
57    }
58}
59
60/// Add a plugin marketplace from a URL, path, or GitHub repo.
61///
62/// # Example
63///
64/// ```no_run
65/// use claude_wrapper::{Claude, ClaudeCommand, MarketplaceAddCommand, Scope};
66///
67/// # async fn example() -> claude_wrapper::Result<()> {
68/// let claude = Claude::builder().build()?;
69/// MarketplaceAddCommand::new("https://github.com/org/marketplace")
70///     .scope(Scope::User)
71///     .execute(&claude)
72///     .await?;
73/// # Ok(())
74/// # }
75/// ```
76#[derive(Debug, Clone)]
77pub struct MarketplaceAddCommand {
78    source: String,
79    scope: Option<Scope>,
80    sparse: Vec<String>,
81}
82
83impl MarketplaceAddCommand {
84    #[must_use]
85    pub fn new(source: impl Into<String>) -> Self {
86        Self {
87            source: source.into(),
88            scope: None,
89            sparse: Vec::new(),
90        }
91    }
92
93    /// Set the scope.
94    #[must_use]
95    pub fn scope(mut self, scope: Scope) -> Self {
96        self.scope = Some(scope);
97        self
98    }
99
100    /// Limit checkout to specific directories via git sparse-checkout (for monorepos).
101    #[must_use]
102    pub fn sparse(mut self, paths: impl IntoIterator<Item = impl Into<String>>) -> Self {
103        self.sparse.extend(paths.into_iter().map(Into::into));
104        self
105    }
106}
107
108impl ClaudeCommand for MarketplaceAddCommand {
109    type Output = CommandOutput;
110
111    fn args(&self) -> Vec<String> {
112        let mut args = vec![
113            "plugin".to_string(),
114            "marketplace".to_string(),
115            "add".to_string(),
116        ];
117        if let Some(ref scope) = self.scope {
118            args.push("--scope".to_string());
119            args.push(scope.as_arg().to_string());
120        }
121        if !self.sparse.is_empty() {
122            args.push("--sparse".to_string());
123            args.extend(self.sparse.clone());
124        }
125        args.push(self.source.clone());
126        args
127    }
128
129    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
130        exec::run_claude(claude, self.args()).await
131    }
132}
133
134/// Remove a configured marketplace.
135#[derive(Debug, Clone)]
136pub struct MarketplaceRemoveCommand {
137    name: String,
138}
139
140impl MarketplaceRemoveCommand {
141    #[must_use]
142    pub fn new(name: impl Into<String>) -> Self {
143        Self { name: name.into() }
144    }
145}
146
147impl ClaudeCommand for MarketplaceRemoveCommand {
148    type Output = CommandOutput;
149
150    fn args(&self) -> Vec<String> {
151        vec![
152            "plugin".to_string(),
153            "marketplace".to_string(),
154            "remove".to_string(),
155            self.name.clone(),
156        ]
157    }
158
159    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
160        exec::run_claude(claude, self.args()).await
161    }
162}
163
164/// Update marketplace(s) from their source.
165#[derive(Debug, Clone, Default)]
166pub struct MarketplaceUpdateCommand {
167    name: Option<String>,
168}
169
170impl MarketplaceUpdateCommand {
171    /// Update all marketplaces.
172    #[must_use]
173    pub fn all() -> Self {
174        Self { name: None }
175    }
176
177    /// Update a specific marketplace.
178    #[must_use]
179    pub fn new(name: impl Into<String>) -> Self {
180        Self {
181            name: Some(name.into()),
182        }
183    }
184}
185
186impl ClaudeCommand for MarketplaceUpdateCommand {
187    type Output = CommandOutput;
188
189    fn args(&self) -> Vec<String> {
190        let mut args = vec![
191            "plugin".to_string(),
192            "marketplace".to_string(),
193            "update".to_string(),
194        ];
195        if let Some(ref name) = self.name {
196            args.push(name.clone());
197        }
198        args
199    }
200
201    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
202        exec::run_claude(claude, self.args()).await
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use crate::command::ClaudeCommand;
210
211    #[test]
212    fn test_marketplace_list() {
213        let cmd = MarketplaceListCommand::new().json();
214        assert_eq!(
215            ClaudeCommand::args(&cmd),
216            vec!["plugin", "marketplace", "list", "--json"]
217        );
218    }
219
220    #[test]
221    fn test_marketplace_add() {
222        let cmd = MarketplaceAddCommand::new("https://github.com/org/mp").scope(Scope::User);
223        assert_eq!(
224            ClaudeCommand::args(&cmd),
225            vec![
226                "plugin",
227                "marketplace",
228                "add",
229                "--scope",
230                "user",
231                "https://github.com/org/mp"
232            ]
233        );
234    }
235
236    #[test]
237    fn test_marketplace_add_sparse() {
238        let cmd = MarketplaceAddCommand::new("https://github.com/org/monorepo")
239            .sparse([".claude-plugin", "plugins"]);
240        let args = ClaudeCommand::args(&cmd);
241        assert!(args.contains(&"--sparse".to_string()));
242        assert!(args.contains(&".claude-plugin".to_string()));
243        assert!(args.contains(&"plugins".to_string()));
244    }
245
246    #[test]
247    fn test_marketplace_remove() {
248        let cmd = MarketplaceRemoveCommand::new("old-mp");
249        assert_eq!(
250            ClaudeCommand::args(&cmd),
251            vec!["plugin", "marketplace", "remove", "old-mp"]
252        );
253    }
254
255    #[test]
256    fn test_marketplace_update_all() {
257        let cmd = MarketplaceUpdateCommand::all();
258        assert_eq!(
259            ClaudeCommand::args(&cmd),
260            vec!["plugin", "marketplace", "update"]
261        );
262    }
263
264    #[test]
265    fn test_marketplace_update_specific() {
266        let cmd = MarketplaceUpdateCommand::new("my-mp");
267        assert_eq!(
268            ClaudeCommand::args(&cmd),
269            vec!["plugin", "marketplace", "update", "my-mp"]
270        );
271    }
272}