Skip to main content

claude_wrapper/command/
marketplace.rs

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