1use clap::{Args, Subcommand};
9use indicatif::{ProgressBar, ProgressStyle};
10use tabled::{Table, Tabled};
11
12use crate::error::CliError;
13
14const DEFAULT_MARKET_URL: &str = "http://localhost:8080";
16
17#[derive(Subcommand, Debug)]
19pub enum PluginCommand {
20 Search(SearchArgs),
22 Install(InstallArgs),
24 Publish(PublishArgs),
26 Uninstall(UninstallArgs),
28 Update(UpdateArgs),
30 List,
32 Login(LoginArgs),
34}
35
36#[derive(Args, Debug)]
38pub struct SearchArgs {
39 pub keyword: String,
41 #[arg(long, value_delimiter = ',')]
43 pub tags: Vec<String>,
44 #[arg(long)]
46 pub source: Option<String>,
47 #[arg(long, default_value = "relevance")]
49 pub sort: String,
50 #[arg(long, default_value_t = 1)]
52 pub page: u32,
53 #[arg(long, default_value_t = 20)]
55 pub page_size: u32,
56}
57
58#[derive(Args, Debug)]
60pub struct InstallArgs {
61 pub identifier: String,
63}
64
65#[derive(Args, Debug)]
67pub struct PublishArgs {
68 #[arg(short = 'p', long)]
70 pub path: String,
71 #[arg(short = 's', long)]
73 pub sign: String,
74 #[arg(short = 'c', long)]
76 pub changelog: Option<String>,
77}
78
79#[derive(Args, Debug)]
81pub struct UninstallArgs {
82 pub identifier: String,
84}
85
86#[derive(Args, Debug)]
88pub struct UpdateArgs {
89 pub identifier: String,
91 #[arg(long)]
93 pub version: Option<String>,
94}
95
96#[derive(Args, Debug)]
98pub struct LoginArgs {
99 #[arg(long)]
101 pub token: Option<String>,
102 #[arg(long)]
104 pub url: Option<String>,
105}
106
107pub async fn execute(cmd: &PluginCommand) -> Result<i32, CliError> {
109 match cmd {
110 PluginCommand::Search(args) => execute_search(args).await,
111 PluginCommand::Install(args) => execute_install(args).await,
112 PluginCommand::Publish(args) => execute_publish(args).await,
113 PluginCommand::Uninstall(args) => execute_uninstall(args).await,
114 PluginCommand::Update(args) => execute_update(args).await,
115 PluginCommand::List => execute_list().await,
116 PluginCommand::Login(args) => execute_login(args).await,
117 }
118}
119
120fn parse_identifier(id: &str) -> (&str, &str) {
122 if let Some((name, version)) = id.split_once('@') {
123 (name, version)
124 } else {
125 (id, "latest")
126 }
127}
128
129async fn create_client() -> Result<sz_rust_marketplace::client::MarketplaceClient, CliError> {
131 match sz_rust_marketplace::client::MarketplaceClient::from_credentials().await {
132 Ok(client) => Ok(client),
133 Err(_) => Ok(sz_rust_marketplace::client::MarketplaceClient::new(
134 DEFAULT_MARKET_URL,
135 None,
136 )),
137 }
138}
139
140#[derive(Tabled)]
142struct PluginRow {
143 name: String,
144 title: String,
145 author: String,
146 tags: String,
147 price: String,
148}
149
150async fn execute_search(args: &SearchArgs) -> Result<i32, CliError> {
151 let client = create_client().await?;
152
153 let tag = args.tags.first().map(|s| s.as_str());
154 let offset = ((args.page - 1) * args.page_size) as i64;
155
156 let response = client
157 .search(&args.keyword, tag, args.page_size as i64, offset)
158 .await
159 .map_err(|e| CliError::Marketplace(e.to_string()))?;
160
161 if response.plugins.is_empty() {
162 println!("未找到匹配插件");
163 return Ok(0);
164 }
165
166 let rows: Vec<PluginRow> = response
167 .plugins
168 .iter()
169 .map(|p| PluginRow {
170 name: p.name.clone(),
171 title: p.title.clone(),
172 author: p.author.clone(),
173 tags: p.tags.join(", "),
174 price: if p.price == 0.0 {
175 "免费".to_string()
176 } else {
177 format!("¥{:.2}", p.price)
178 },
179 })
180 .collect();
181
182 let table = Table::new(rows);
183 println!("{table}");
184 println!("\n共 {} 个插件", response.total);
185
186 Ok(0)
187}
188
189async fn execute_install(args: &InstallArgs) -> Result<i32, CliError> {
190 let (name, version) = parse_identifier(&args.identifier);
191 let client = create_client().await?;
192
193 let pb = ProgressBar::new_spinner();
194 pb.set_style(
195 ProgressStyle::with_template("{spinner} [{elapsed}] {msg}")
196 .unwrap_or_else(|_| ProgressStyle::default_spinner()),
197 );
198 pb.set_message(format!("下载 {name}@{version}..."));
199
200 let archive = client
201 .install(name, version)
202 .await
203 .map_err(|e| CliError::Marketplace(e.to_string()))?;
204
205 pb.finish_with_message(format!("已下载 {name}@{version} ({} bytes)", archive.len()));
206 println!("插件 {name}@{version} 安装成功");
207
208 Ok(0)
209}
210
211async fn execute_publish(args: &PublishArgs) -> Result<i32, CliError> {
212 let client = create_client().await?;
213
214 let pb = ProgressBar::new_spinner();
215 pb.set_style(
216 ProgressStyle::with_template("{spinner} [{elapsed}] {msg}")
217 .unwrap_or_else(|_| ProgressStyle::default_spinner()),
218 );
219 pb.set_message("发布中...".to_string());
220
221 client
222 .publish(&args.path)
223 .await
224 .map_err(|e| CliError::Marketplace(e.to_string()))?;
225
226 pb.finish_with_message("发布请求已提交");
227 println!("插件已发布,等待审核");
228
229 Ok(0)
230}
231
232async fn execute_uninstall(args: &UninstallArgs) -> Result<i32, CliError> {
233 let (name, _) = parse_identifier(&args.identifier);
234 let client = create_client().await?;
235
236 client
237 .uninstall(name)
238 .await
239 .map_err(|e| CliError::Marketplace(e.to_string()))?;
240
241 println!("插件 {name} 已卸载");
242 Ok(0)
243}
244
245async fn execute_update(args: &UpdateArgs) -> Result<i32, CliError> {
246 let (name, _) = parse_identifier(&args.identifier);
247 let client = create_client().await?;
248
249 let pb = ProgressBar::new_spinner();
250 pb.set_style(
251 ProgressStyle::with_template("{spinner} [{elapsed}] {msg}")
252 .unwrap_or_else(|_| ProgressStyle::default_spinner()),
253 );
254 pb.set_message(format!("更新 {name}..."));
255
256 let archive = client
257 .update(name)
258 .await
259 .map_err(|e| CliError::Marketplace(e.to_string()))?;
260
261 pb.finish_with_message(format!("已更新 {name} ({} bytes)", archive.len()));
262 println!("插件 {name} 更新成功");
263
264 Ok(0)
265}
266
267async fn execute_list() -> Result<i32, CliError> {
268 let client = create_client().await?;
269
270 let entries = client
271 .list()
272 .await
273 .map_err(|e| CliError::Marketplace(e.to_string()))?;
274
275 if entries.is_empty() {
276 println!("未安装任何插件");
277 return Ok(0);
278 }
279
280 #[derive(Tabled)]
281 struct InstalledRow {
282 name: String,
283 version: String,
284 sha256: String,
285 installed_at: String,
286 }
287
288 let rows: Vec<InstalledRow> = entries
289 .iter()
290 .map(|e| InstalledRow {
291 name: e.name.clone(),
292 version: e.version.clone(),
293 sha256: e.sha256.chars().take(16).collect(),
294 installed_at: e.installed_at.format("%Y-%m-%d %H:%M").to_string(),
295 })
296 .collect();
297
298 let table = Table::new(rows);
299 println!("{table}");
300 println!("\n共 {} 个已安装插件", entries.len());
301
302 Ok(0)
303}
304
305async fn execute_login(args: &LoginArgs) -> Result<i32, CliError> {
306 let url = args.url.as_deref().unwrap_or(DEFAULT_MARKET_URL);
307
308 if let Some(token) = &args.token {
309 let client = sz_rust_marketplace::client::MarketplaceClient::new(url, None);
310 client
311 .save_token(token)
312 .await
313 .map_err(|e| CliError::Marketplace(e.to_string()))?;
314 println!("登录成功(token 已保存)");
315 Ok(0)
316 } else {
317 println!("请提供 --token 参数");
318 Ok(1)
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[tokio::test]
327 async fn test_execute_list_no_lock() {
328 let result = execute_list().await;
329 assert!(result.is_ok());
330 }
331
332 #[tokio::test]
333 async fn test_execute_login_with_token() {
334 let args = LoginArgs {
335 token: Some("test".into()),
336 url: Some("http://localhost:9999".into()),
337 };
338 let result = execute_login(&args).await;
339 assert!(result.is_ok());
340 }
341
342 #[tokio::test]
343 async fn test_execute_login_without_token() {
344 let args = LoginArgs {
345 token: None,
346 url: None,
347 };
348 let result = execute_login(&args).await;
349 assert!(result.is_ok());
350 assert_eq!(result.unwrap(), 1);
351 }
352
353 #[test]
354 fn test_parse_identifier_with_version() {
355 let (name, version) = parse_identifier("orm@1.0.0");
356 assert_eq!(name, "orm");
357 assert_eq!(version, "1.0.0");
358 }
359
360 #[test]
361 fn test_parse_identifier_without_version() {
362 let (name, version) = parse_identifier("orm");
363 assert_eq!(name, "orm");
364 assert_eq!(version, "latest");
365 }
366
367 #[test]
368 fn test_plugin_row_tabled() {
369 let row = PluginRow {
370 name: "crm".to_string(),
371 title: "CRM".to_string(),
372 author: "alice".to_string(),
373 tags: "business".to_string(),
374 price: "免费".to_string(),
375 };
376 let table = Table::new(vec![row]);
377 let s = table.to_string();
378 assert!(s.contains("crm"));
379 assert!(s.contains("CRM"));
380 }
381}