1use crate::core::metadata::SkillMetadata;
4use crate::core::registry::{RegistryClient, RegistryConfig as OldRegistryConfig};
5use crate::core::registry_index::{ListSkillsOptions, SkillSummary};
6use crate::core::repository::{RepositoryConfig, RepositoryDefinition, RepositoryType};
7use crate::core::service::{ServiceError, SkillId};
8use crate::core::sources::{SourceConfig, SourceDefinition, SourcesManager};
9use reqwest::Client;
10use std::sync::Arc;
11
12#[derive(Debug, thiserror::Error)]
14pub enum RepositoryClientError {
15 #[error("Repository error: {0}")]
16 Service(#[from] ServiceError),
17 #[error("Client error: {0}")]
18 Client(String),
19 #[error("Not implemented for this repository type")]
20 NotImplemented,
21}
22
23#[async_trait::async_trait]
25pub trait RepositoryClient: Send + Sync {
26 async fn list_skills(&self) -> Result<Vec<SkillMetadata>, RepositoryClientError>;
28
29 async fn get_skill(
31 &self,
32 id: &str,
33 version: Option<&str>,
34 ) -> Result<Option<SkillMetadata>, RepositoryClientError>;
35
36 async fn search(&self, query: &str) -> Result<Vec<SkillMetadata>, RepositoryClientError>;
38
39 async fn download(&self, id: &str, version: &str) -> Result<Vec<u8>, RepositoryClientError>;
41
42 async fn get_versions(&self, id: &str) -> Result<Vec<String>, RepositoryClientError>;
44}
45
46pub async fn create_client(
48 repo: &RepositoryDefinition,
49) -> Result<Arc<dyn RepositoryClient + Send + Sync>, ServiceError> {
50 match repo.repo_type {
51 RepositoryType::GitMarketplace | RepositoryType::ZipUrl | RepositoryType::Local => {
52 Ok(Arc::new(MarketplaceRepositoryClient::new(repo)?))
53 }
54 RepositoryType::HttpRegistry => Ok(Arc::new(CratesRegistryClient::new(repo)?)),
55 }
56}
57
58pub struct MarketplaceRepositoryClient {
60 sources_manager: SourcesManager,
61 source_name: String,
62}
63
64impl MarketplaceRepositoryClient {
65 pub fn new(repo: &RepositoryDefinition) -> Result<Self, ServiceError> {
66 let source_config = match &repo.config {
68 RepositoryConfig::GitMarketplace { url, branch, tag } => {
69 let auth = repo.auth.as_ref().and_then(|a| {
71 match a {
72 crate::core::repository::RepositoryAuth::Pat { env_var } => {
73 Some(crate::core::sources::SourceAuth::Pat {
74 env_var: env_var.clone(),
75 })
76 }
77 crate::core::repository::RepositoryAuth::SshKey { path } => {
78 Some(crate::core::sources::SourceAuth::SshKey { path: path.clone() })
79 }
80 crate::core::repository::RepositoryAuth::Basic {
81 username,
82 password_env,
83 } => Some(crate::core::sources::SourceAuth::Basic {
84 username: username.clone(),
85 password_env: password_env.clone(),
86 }),
87 _ => None, }
89 });
90
91 SourceConfig::Git {
92 url: url.clone(),
93 branch: branch.clone(),
94 tag: tag.clone(),
95 auth,
96 }
97 }
98 RepositoryConfig::ZipUrl { base_url } => {
99 let auth = repo.auth.as_ref().and_then(|a| match a {
100 crate::core::repository::RepositoryAuth::Pat { env_var } => {
101 Some(crate::core::sources::SourceAuth::Pat {
102 env_var: env_var.clone(),
103 })
104 }
105 crate::core::repository::RepositoryAuth::Basic {
106 username,
107 password_env,
108 } => Some(crate::core::sources::SourceAuth::Basic {
109 username: username.clone(),
110 password_env: password_env.clone(),
111 }),
112 _ => None,
113 });
114
115 SourceConfig::ZipUrl {
116 base_url: base_url.clone(),
117 auth,
118 }
119 }
120 RepositoryConfig::Local { path } => SourceConfig::Local { path: path.clone() },
121 RepositoryConfig::HttpRegistry { .. } => {
122 return Err(ServiceError::Custom(
123 "HttpRegistry should use CratesRegistryClient".to_string(),
124 ));
125 }
126 };
127
128 let temp_path = std::env::temp_dir().join(format!("fastskill-repo-{}", repo.name));
130 let mut sources_manager = SourcesManager::new(temp_path);
131 sources_manager
132 .load()
133 .map_err(|e| ServiceError::Custom(format!("Failed to load sources manager: {}", e)))?;
134
135 let source_def = SourceDefinition {
136 name: repo.name.clone(),
137 priority: repo.priority,
138 source: source_config,
139 };
140
141 sources_manager
142 .add_source_with_priority(repo.name.clone(), source_def.source.clone(), repo.priority)
143 .map_err(|e| ServiceError::Custom(format!("Failed to add source: {}", e)))?;
144
145 Ok(Self {
146 sources_manager,
147 source_name: repo.name.clone(),
148 })
149 }
150}
151
152#[async_trait::async_trait]
153impl RepositoryClient for MarketplaceRepositoryClient {
154 async fn list_skills(&self) -> Result<Vec<SkillMetadata>, RepositoryClientError> {
155 let source_def = self
156 .sources_manager
157 .get_source(&self.source_name)
158 .ok_or_else(|| RepositoryClientError::Client("Source not found".to_string()))?;
159
160 let skill_infos = self
161 .sources_manager
162 .get_skills_from_source(&self.source_name, source_def)
163 .await
164 .map_err(|e| RepositoryClientError::Client(format!("Failed to get skills: {}", e)))?;
165
166 use crate::core::service::SkillId;
167 use chrono::Utc;
168
169 Ok(skill_infos
170 .into_iter()
171 .filter_map(|info| {
172 SkillId::new(info.id.clone()).ok().map(|id| SkillMetadata {
173 id,
174 name: info.name,
175 description: info.description,
176 version: info.version.unwrap_or_else(|| "1.0.0".to_string()),
177 author: None,
178 enabled: true,
179 token_estimate: 0,
180 last_updated: Utc::now(),
181 })
182 })
183 .collect())
184 }
185
186 async fn get_skill(
187 &self,
188 id: &str,
189 version: Option<&str>,
190 ) -> Result<Option<SkillMetadata>, RepositoryClientError> {
191 use crate::core::service::SkillId;
192 let skill_id = SkillId::new(id.to_string())
193 .map_err(|e| RepositoryClientError::Client(format!("Invalid skill ID: {}", e)))?;
194 let skills = self.list_skills().await?;
195 Ok(skills
196 .into_iter()
197 .find(|s| s.id == skill_id && version.map(|v| s.version == v).unwrap_or(true)))
198 }
199
200 async fn search(&self, query: &str) -> Result<Vec<SkillMetadata>, RepositoryClientError> {
201 let skills = self.list_skills().await?;
202 let query_lower = query.to_lowercase();
203 Ok(skills
204 .into_iter()
205 .filter(|s| {
206 s.id.to_string().to_lowercase().contains(&query_lower)
207 || s.name.to_lowercase().contains(&query_lower)
208 || s.description.to_lowercase().contains(&query_lower)
209 })
210 .collect())
211 }
212
213 async fn download(&self, _id: &str, _version: &str) -> Result<Vec<u8>, RepositoryClientError> {
214 Err(RepositoryClientError::NotImplemented)
217 }
218
219 async fn get_versions(&self, id: &str) -> Result<Vec<String>, RepositoryClientError> {
220 use crate::core::service::SkillId;
221 let skill_id = SkillId::new(id.to_string())
222 .map_err(|e| RepositoryClientError::Client(format!("Invalid skill ID: {}", e)))?;
223 let skills = self.list_skills().await?;
224 Ok(skills
225 .into_iter()
226 .filter(|s| s.id == skill_id)
227 .map(|s| s.version)
228 .collect())
229 }
230}
231
232pub struct CratesRegistryClient {
234 registry_client: RegistryClient,
235 index_url: String,
236 auth: Option<crate::core::registry::config::AuthConfig>,
237}
238
239impl CratesRegistryClient {
240 pub fn new(repo: &RepositoryDefinition) -> Result<Self, ServiceError> {
241 let index_url = match &repo.config {
242 RepositoryConfig::HttpRegistry { index_url } => index_url.clone(),
243 _ => {
244 return Err(ServiceError::Custom(
245 "HttpRegistry requires index_url".to_string(),
246 ))
247 }
248 };
249
250 if index_url.trim().is_empty() {
252 return Err(ServiceError::Custom(
253 "index_url cannot be empty".to_string(),
254 ));
255 }
256
257 if url::Url::parse(&index_url).is_err() {
259 return Err(ServiceError::Custom(format!(
260 "Invalid index_url format: {}",
261 index_url
262 )));
263 }
264
265 let auth = repo.auth.as_ref().map(|a| {
267 match a {
268 crate::core::repository::RepositoryAuth::Pat { env_var } => {
269 crate::core::registry::config::AuthConfig::Pat {
270 env_var: env_var.clone(),
271 }
272 }
273 crate::core::repository::RepositoryAuth::Ssh { key_path } => {
274 crate::core::registry::config::AuthConfig::Ssh {
275 key_path: key_path.clone(),
276 }
277 }
278 crate::core::repository::RepositoryAuth::ApiKey { env_var } => {
279 crate::core::registry::config::AuthConfig::ApiKey {
280 env_var: env_var.clone(),
281 }
282 }
283 _ => {
284 crate::core::registry::config::AuthConfig::Pat {
286 env_var: "GITHUB_TOKEN".to_string(),
287 }
288 }
289 }
290 });
291
292 let registry_config = OldRegistryConfig {
293 name: repo.name.clone(),
294 registry_type: "git".to_string(),
295 index_url,
296 auth,
297 storage: repo
298 .storage
299 .clone()
300 .map(|s| crate::core::registry::config::StorageConfig {
301 storage_type: s.storage_type,
302 repository: s.repository,
303 bucket: s.bucket,
304 region: s.region,
305 endpoint: s.endpoint,
306 base_url: s.base_url,
307 }),
308 };
309
310 let registry_client = RegistryClient::new(registry_config.clone())?;
311
312 Ok(Self {
313 registry_client,
314 index_url: registry_config.index_url.clone(),
315 auth: registry_config.auth.clone(),
316 })
317 }
318
319 pub async fn fetch_skills(
321 &self,
322 options: &ListSkillsOptions,
323 ) -> Result<Vec<SkillSummary>, RepositoryClientError> {
324 use crate::core::registry::auth::Auth;
325
326 let base_url = self.index_url.trim_end_matches('/');
328 let mut url = format!("{}/api/registry/index/skills", base_url);
329
330 let mut query_params = Vec::new();
332 if let Some(ref scope) = options.scope {
333 query_params.push(("scope", scope.clone()));
334 }
335 if options.all_versions {
336 query_params.push(("all_versions", "true".to_string()));
337 }
338 if options.include_pre_release {
339 query_params.push(("include_pre_release", "true".to_string()));
340 }
341
342 if !query_params.is_empty() {
343 let mut url_obj = url::Url::parse(&url)
344 .map_err(|e| RepositoryClientError::Client(format!("Invalid URL: {}", e)))?;
345 for (k, v) in query_params {
346 url_obj.query_pairs_mut().append_pair(k, &v);
347 }
348 url = url_obj.to_string();
349 }
350
351 let client = Client::builder()
353 .user_agent("fastskill/0.8.6")
354 .build()
355 .map_err(|e| {
356 RepositoryClientError::Client(format!("Failed to create HTTP client: {}", e))
357 })?;
358
359 let mut request = client.get(&url);
361
362 if let Some(ref auth_config) = self.auth {
364 let auth: Option<Box<dyn Auth>> = match auth_config {
365 crate::core::registry::config::AuthConfig::Pat { env_var } => Some(Box::new(
366 crate::core::registry::auth::GitHubPat::new(env_var.clone()),
367 )),
368 crate::core::registry::config::AuthConfig::Ssh { key_path } => Some(Box::new(
369 crate::core::registry::auth::SshKey::new(key_path.clone()),
370 )),
371 crate::core::registry::config::AuthConfig::ApiKey { env_var } => Some(Box::new(
372 crate::core::registry::auth::ApiKey::new(env_var.clone()),
373 )),
374 };
375
376 if let Some(auth) = auth {
377 if auth.is_configured() {
378 if let Ok(header_value) = auth.get_auth_header() {
379 request = request.header("Authorization", header_value);
380 }
381 }
382 }
383 }
384
385 let response = request
387 .send()
388 .await
389 .map_err(|e| RepositoryClientError::Client(format!("HTTP request failed: {}", e)))?;
390
391 let status = response.status();
393 match status.as_u16() {
394 200 => {
395 let summaries: Vec<SkillSummary> = response.json().await.map_err(|e| {
397 RepositoryClientError::Client(format!("Failed to parse JSON response: {}", e))
398 })?;
399 Ok(summaries)
400 }
401 400 => Err(RepositoryClientError::Client(
402 "Bad request: Invalid query parameters".to_string(),
403 )),
404 401 => Err(RepositoryClientError::Client(
405 "Unauthorized: Authentication required for this scope".to_string(),
406 )),
407 403 => Err(RepositoryClientError::Client(
408 "Forbidden: Access denied to this scope".to_string(),
409 )),
410 404 => Err(RepositoryClientError::Client(
411 "Not found: Registry endpoint not available".to_string(),
412 )),
413 500..=599 => Err(RepositoryClientError::Client(format!(
414 "Server error: HTTP {}",
415 status
416 ))),
417 _ => Err(RepositoryClientError::Client(format!(
418 "Unexpected HTTP status: {}",
419 status
420 ))),
421 }
422 }
423}
424
425#[async_trait::async_trait]
426impl RepositoryClient for CratesRegistryClient {
427 async fn list_skills(&self) -> Result<Vec<SkillMetadata>, RepositoryClientError> {
428 let options = ListSkillsOptions::default();
430 let summaries = self.fetch_skills(&options).await?;
431
432 let metadata: Vec<SkillMetadata> = summaries
434 .into_iter()
435 .filter_map(|s| {
436 SkillId::new(s.id.clone()).ok().map(|id| SkillMetadata {
437 id,
438 name: s.name.clone(),
439 description: s.description.clone(),
440 version: s.latest_version.clone(),
441 author: None, enabled: true,
443 token_estimate: s.description.len() / 4, last_updated: s.published_at.unwrap_or_else(chrono::Utc::now),
445 })
446 })
447 .collect();
448
449 Ok(metadata)
450 }
451
452 async fn get_skill(
453 &self,
454 id: &str,
455 version: Option<&str>,
456 ) -> Result<Option<SkillMetadata>, RepositoryClientError> {
457 let entries = self
458 .registry_client
459 .get_skill(id)
460 .await
461 .map_err(RepositoryClientError::Service)?;
462
463 if entries.is_empty() {
464 return Ok(None);
465 }
466
467 let entry = if let Some(ver) = version {
469 entries.into_iter().find(|e| e.vers == ver)
470 } else {
471 entries.into_iter().max_by_key(|e| e.vers.clone())
473 };
474
475 use crate::core::service::SkillId;
476 use chrono::Utc;
477
478 Ok(entry.and_then(|e| {
479 SkillId::new(e.name.clone().to_string())
480 .ok()
481 .map(|id| SkillMetadata {
482 id,
483 name: e.name.clone(),
484 description: e
485 .metadata
486 .as_ref()
487 .and_then(|m| m.description.clone())
488 .unwrap_or_else(|| "".to_string()),
489 version: e.vers,
490 author: e.metadata.as_ref().and_then(|m| m.author.clone()),
491 enabled: true,
492 token_estimate: 0,
493 last_updated: Utc::now(),
494 })
495 }))
496 }
497
498 async fn search(&self, query: &str) -> Result<Vec<SkillMetadata>, RepositoryClientError> {
499 let results = self
501 .registry_client
502 .search(query)
503 .await
504 .map_err(RepositoryClientError::Service)?;
505
506 Ok(results)
507 }
508
509 async fn download(&self, id: &str, version: &str) -> Result<Vec<u8>, RepositoryClientError> {
510 let data = self
511 .registry_client
512 .download(id, version)
513 .await
514 .map_err(RepositoryClientError::Service)?;
515 Ok(data)
516 }
517
518 async fn get_versions(&self, id: &str) -> Result<Vec<String>, RepositoryClientError> {
519 let versions = self
520 .registry_client
521 .get_versions(id)
522 .await
523 .map_err(RepositoryClientError::Service)?;
524 Ok(versions)
525 }
526}