fastskill_core/core/registry/
client.rs1use crate::core::metadata::SkillMetadata;
4use crate::core::registry::auth::Auth;
5use crate::core::registry::config::RegistryConfig;
6use crate::core::registry_index::{Dependency as RegistryDependency, IndexMetadata};
7use crate::core::service::ServiceError;
8use reqwest::Client;
9use serde::{Deserialize, Serialize};
10use sha2::Digest;
11use std::collections::HashMap;
12
13pub struct RegistryClient {
15 config: RegistryConfig,
16 client: Client,
17 auth: Option<Box<dyn Auth>>,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct IndexEntry {
23 pub name: String,
24 pub vers: String,
25 pub deps: Vec<RegistryDependency>,
26 pub cksum: String,
27 pub features: HashMap<String, Vec<String>>,
28 pub yanked: bool,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub links: Option<String>,
31 pub download_url: String,
32 #[serde(default)]
33 pub metadata: Option<IndexMetadata>,
34}
35
36impl RegistryClient {
41 pub fn new(config: RegistryConfig) -> Result<Self, ServiceError> {
43 let client = Client::builder()
44 .user_agent("fastskill/0.6.8")
45 .build()
46 .map_err(|e| ServiceError::Custom(format!("Failed to create HTTP client: {}", e)))?;
47
48 let auth: Option<Box<dyn Auth>> = if let Some(ref auth_config) = config.auth {
50 match auth_config {
51 crate::core::registry::config::AuthConfig::Pat { env_var } => Some(Box::new(
52 crate::core::registry::auth::GitHubPat::new(env_var.clone()),
53 )),
54 crate::core::registry::config::AuthConfig::Ssh { key_path } => Some(Box::new(
55 crate::core::registry::auth::SshKey::new(key_path.clone()),
56 )),
57 crate::core::registry::config::AuthConfig::ApiKey { env_var } => Some(Box::new(
58 crate::core::registry::auth::ApiKey::new(env_var.clone()),
59 )),
60 }
61 } else {
62 None
63 };
64
65 Ok(Self {
66 config,
67 client,
68 auth,
69 })
70 }
71
72 fn get_index_url(&self, skill_id: &str) -> String {
74 format!(
77 "{}/{}",
78 self.config.index_url.trim_end_matches('/'),
79 skill_id
80 )
81 }
82
83 pub async fn get_skill(&self, name: &str) -> Result<Vec<IndexEntry>, ServiceError> {
86 let url = self.get_index_url(name);
87
88 let mut request = self.client.get(&url);
89
90 if let Some(ref auth) = self.auth {
92 if auth.is_configured() {
93 if let Ok(header_value) = auth.get_auth_header() {
94 request = request.header("Authorization", header_value);
95 }
96 }
97 }
98
99 let response = request
100 .send()
101 .await
102 .map_err(|e| ServiceError::Custom(format!("Failed to fetch skill index: {}", e)))?;
103
104 if !response.status().is_success() {
105 if response.status() == 404 {
106 return Ok(Vec::new()); }
108 return Err(ServiceError::Custom(format!(
109 "Failed to fetch skill index: HTTP {}",
110 response.status()
111 )));
112 }
113
114 let content = response
115 .text()
116 .await
117 .map_err(|e| ServiceError::Custom(format!("Failed to read index file: {}", e)))?;
118
119 let mut entries = Vec::new();
121 for line in content.lines() {
122 let line = line.trim();
123 if line.is_empty() {
124 continue;
125 }
126
127 match serde_json::from_str::<IndexEntry>(line) {
128 Ok(entry) => entries.push(entry),
129 Err(e) => {
130 eprintln!(
132 "Warning: Failed to parse index entry: {} (line: {})",
133 e, line
134 );
135 }
136 }
137 }
138
139 Ok(entries)
140 }
141
142 async fn get_index_entry(
144 &self,
145 name: &str,
146 version: &str,
147 ) -> Result<Option<IndexEntry>, ServiceError> {
148 let entries = self.get_skill(name).await?;
150 Ok(entries.into_iter().find(|e| e.vers == version))
151 }
152
153 pub async fn get_versions(&self, name: &str) -> Result<Vec<String>, ServiceError> {
155 let entries = self.get_skill(name).await?;
156 let mut versions: Vec<String> = entries.iter().map(|e| e.vers.clone()).collect();
157 versions.sort_by(|a, b| {
159 b.cmp(a)
162 });
163 Ok(versions)
164 }
165
166 pub async fn get_latest_version(
168 &self,
169 name: &str,
170 include_pre_release: bool,
171 ) -> Result<Option<String>, ServiceError> {
172 let versions = self.get_versions(name).await?;
173
174 if versions.is_empty() {
175 return Ok(None);
176 }
177
178 let candidates: Vec<&String> = if include_pre_release {
180 versions.iter().collect()
181 } else {
182 versions
183 .iter()
184 .filter(|v| !v.contains('-')) .collect()
186 };
187
188 if candidates.is_empty() {
189 return Ok(None);
191 }
192
193 use semver::Version;
195 let mut parsed_versions: Vec<(Version, &String)> = candidates
196 .iter()
197 .filter_map(|v| {
198 Version::parse(v).ok().map(|ver| (ver, *v))
200 })
201 .collect();
202
203 if parsed_versions.is_empty() {
204 return Ok(Some(candidates[0].clone()));
206 }
207
208 parsed_versions.sort_by(|a, b| b.0.cmp(&a.0));
210
211 Ok(Some(parsed_versions[0].1.clone()))
212 }
213
214 pub async fn get_version(
216 &self,
217 name: &str,
218 version: &str,
219 ) -> Result<Option<IndexEntry>, ServiceError> {
220 self.get_index_entry(name, version).await
221 }
222
223 pub async fn download(&self, name: &str, version: &str) -> Result<Vec<u8>, ServiceError> {
225 let entry = self.get_version(name, version).await?.ok_or_else(|| {
226 ServiceError::Custom(format!(
227 "Skill {} version {} not found in registry",
228 name, version
229 ))
230 })?;
231
232 if entry.yanked {
233 return Err(ServiceError::Custom(format!(
234 "Skill {} version {} has been yanked",
235 name, version
236 )));
237 }
238
239 let mut request = self.client.get(&entry.download_url);
240
241 if let Some(ref auth) = self.auth {
243 if auth.is_configured() {
244 if let Ok(header_value) = auth.get_auth_header() {
245 request = request.header("Authorization", header_value);
246 }
247 }
248 }
249
250 let response = request
251 .send()
252 .await
253 .map_err(|e| ServiceError::Custom(format!("Failed to download package: {}", e)))?;
254
255 if !response.status().is_success() {
256 return Err(ServiceError::Custom(format!(
257 "Failed to download package: HTTP {}",
258 response.status()
259 )));
260 }
261
262 let bytes = response
263 .bytes()
264 .await
265 .map_err(|e| ServiceError::Custom(format!("Failed to read package data: {}", e)))?;
266
267 let calculated = format!("sha256:{:x}", sha2::Sha256::digest(&bytes));
269 if calculated != entry.cksum {
270 return Err(ServiceError::Custom(format!(
271 "Checksum mismatch: expected {}, got {}",
272 entry.cksum, calculated
273 )));
274 }
275
276 Ok(bytes.to_vec())
277 }
278
279 pub async fn search(&self, _query: &str) -> Result<Vec<SkillMetadata>, ServiceError> {
281 Ok(Vec::new())
285 }
286}