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