Skip to main content

claude_wrapper/command/
marketplace.rs

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