1pub mod client;
6
7pub use client::{CratesRegistryClient, RepositoryClient, RepositoryClientError};
8
9use crate::core::service::ServiceError;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::path::PathBuf;
13use std::sync::Arc;
14use tokio::sync::RwLock;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct RepositoriesConfig {
19 #[serde(default)]
20 pub repositories: Vec<RepositoryDefinition>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct RepositoryDefinition {
26 pub name: String,
27 #[serde(rename = "type")]
28 pub repo_type: RepositoryType,
29 #[serde(default = "default_priority")]
30 pub priority: u32,
31 #[serde(flatten)]
32 pub config: RepositoryConfig,
33 #[serde(default)]
34 pub auth: Option<RepositoryAuth>,
35 #[serde(default)]
36 pub storage: Option<StorageConfig>,
37}
38
39fn default_priority() -> u32 {
41 0
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
46#[serde(rename_all = "kebab-case")]
47pub enum RepositoryType {
48 GitMarketplace,
50 HttpRegistry,
52 ZipUrl,
54 Local,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60#[serde(untagged)]
61pub enum RepositoryConfig {
62 GitMarketplace {
64 url: String,
65 #[serde(default)]
66 branch: Option<String>,
67 #[serde(default)]
68 tag: Option<String>,
69 },
70 HttpRegistry { index_url: String },
72 ZipUrl { base_url: String },
74 Local { path: PathBuf },
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80#[serde(tag = "type")]
81pub enum RepositoryAuth {
82 #[serde(rename = "pat")]
83 Pat { env_var: String },
84 #[serde(rename = "ssh-key")]
85 SshKey { path: PathBuf },
86 #[serde(rename = "ssh")]
87 Ssh { key_path: PathBuf },
88 #[serde(rename = "basic")]
89 Basic {
90 username: String,
91 password_env: String,
92 },
93 #[serde(rename = "api_key")]
94 ApiKey { env_var: String },
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct StorageConfig {
100 #[serde(rename = "type")]
101 pub storage_type: String,
102 #[serde(default)]
103 pub repository: Option<String>,
104 #[serde(default)]
105 pub bucket: Option<String>,
106 #[serde(default)]
107 pub region: Option<String>,
108 #[serde(default)]
109 pub endpoint: Option<String>,
110 #[serde(default)]
111 pub base_url: Option<String>,
112}
113
114pub struct RepositoryManager {
116 config_path: PathBuf,
117 repositories: HashMap<String, RepositoryDefinition>,
118 clients: Arc<RwLock<HashMap<String, Arc<dyn RepositoryClient + Send + Sync>>>>,
119}
120
121impl RepositoryManager {
122 pub fn new(config_path: PathBuf) -> Self {
124 Self {
125 config_path,
126 repositories: HashMap::new(),
127 clients: Arc::new(RwLock::new(HashMap::new())),
128 }
129 }
130
131 pub fn from_definitions(definitions: Vec<RepositoryDefinition>) -> Self {
134 let mut repo_map: HashMap<String, RepositoryDefinition> = HashMap::new();
135 let mut sorted_repos = definitions;
136 sorted_repos.sort_by_key(|r| r.priority);
137
138 for repo in sorted_repos {
139 repo_map.entry(repo.name.clone()).or_insert(repo);
140 }
141
142 let config_path = std::env::current_dir()
144 .ok()
145 .and_then(|dir| {
146 let project_file = crate::core::project::resolve_project_file(&dir);
147 if project_file.found {
148 Some(project_file.path)
149 } else {
150 None
151 }
152 })
153 .unwrap_or_default();
154
155 Self {
156 config_path,
157 repositories: repo_map,
158 clients: Arc::new(RwLock::new(HashMap::new())),
159 }
160 }
161
162 pub fn load(&mut self) -> Result<(), ServiceError> {
165 if self.config_path.exists() {
166 let content = std::fs::read_to_string(&self.config_path).map_err(ServiceError::Io)?;
168
169 let config: RepositoriesConfig = toml::from_str(&content).map_err(|e| {
170 ServiceError::Custom(format!("Failed to parse repositories config: {}", e))
171 })?;
172
173 let mut sorted_repos: Vec<RepositoryDefinition> = config.repositories;
176 sorted_repos.sort_by_key(|r| r.priority);
177
178 let mut repo_map: HashMap<String, RepositoryDefinition> = HashMap::new();
180 for repo in sorted_repos {
181 repo_map.entry(repo.name.clone()).or_insert(repo);
182 }
183 self.repositories = repo_map;
184
185 return Ok(());
186 }
187
188 let config = RepositoriesConfig {
190 repositories: Vec::new(),
191 };
192 self.save_config(&config)?;
193 self.repositories = HashMap::new();
194 Ok(())
195 }
196
197 pub fn save(&self) -> Result<(), ServiceError> {
199 if self.config_path.file_name().and_then(|n| n.to_str()) == Some("skill-project.toml") {
201 self.save_to_project_file()
202 } else {
203 let mut repos: Vec<RepositoryDefinition> =
205 self.repositories.values().cloned().collect();
206 repos.sort_by_key(|r| r.priority);
207 let config = RepositoriesConfig {
208 repositories: repos,
209 };
210 self.save_config(&config)
211 }
212 }
213
214 fn save_to_project_file(&self) -> Result<(), ServiceError> {
216 use crate::core::manifest::SkillProjectToml;
217
218 if let Some(parent) = self.config_path.parent() {
220 std::fs::create_dir_all(parent).map_err(ServiceError::Io)?;
221 }
222
223 let mut project = if self.config_path.exists() {
225 SkillProjectToml::load_from_file(&self.config_path).map_err(|e| {
226 ServiceError::Custom(format!("Failed to load skill-project.toml: {}", e))
227 })?
228 } else {
229 SkillProjectToml {
231 metadata: None,
232 dependencies: None,
233 tool: None,
234 }
235 };
236
237 let manifest_repos: Vec<crate::core::manifest::RepositoryDefinition> = self
239 .repositories
240 .values()
241 .map(|repo| self.convert_to_manifest_repo(repo))
242 .collect();
243
244 if project.tool.is_none() {
246 project.tool = Some(crate::core::manifest::ToolSection {
247 fastskill: Some(crate::core::manifest::FastSkillToolConfig {
248 skills_directory: None,
249 embedding: None,
250 repositories: Some(manifest_repos),
251 server: None,
252 install_depth: 5,
253 skip_transitive: false,
254 eval: None,
255 }),
256 });
257 } else if let Some(ref mut tool) = project.tool {
258 if tool.fastskill.is_none() {
259 tool.fastskill = Some(crate::core::manifest::FastSkillToolConfig {
260 skills_directory: None,
261 embedding: None,
262 repositories: Some(manifest_repos),
263 server: None,
264 install_depth: 5,
265 skip_transitive: false,
266 eval: None,
267 });
268 } else if let Some(ref mut fastskill) = tool.fastskill {
269 fastskill.repositories = Some(manifest_repos);
270 }
271 }
272
273 project.save_to_file(&self.config_path).map_err(|e| {
275 ServiceError::Custom(format!("Failed to save skill-project.toml: {}", e))
276 })?;
277
278 Ok(())
279 }
280
281 fn convert_to_manifest_repo(
283 &self,
284 repo: &RepositoryDefinition,
285 ) -> crate::core::manifest::RepositoryDefinition {
286 use crate::core::manifest::{
287 AuthConfig, AuthType, RepositoryConnection, RepositoryType as ManifestType,
288 };
289
290 let repo_type = match repo.repo_type {
291 RepositoryType::HttpRegistry => ManifestType::HttpRegistry,
292 RepositoryType::GitMarketplace => ManifestType::GitMarketplace,
293 RepositoryType::ZipUrl => ManifestType::ZipUrl,
294 RepositoryType::Local => ManifestType::Local,
295 };
296
297 let connection = match &repo.config {
298 RepositoryConfig::HttpRegistry { index_url } => RepositoryConnection::HttpRegistry {
299 index_url: index_url.clone(),
300 },
301 RepositoryConfig::GitMarketplace {
302 url,
303 branch,
304 tag: _,
305 } => RepositoryConnection::GitMarketplace {
306 url: url.clone(),
307 branch: branch.clone(),
308 },
309 RepositoryConfig::ZipUrl { base_url } => RepositoryConnection::ZipUrl {
310 zip_url: base_url.clone(),
311 },
312 RepositoryConfig::Local { path } => RepositoryConnection::Local {
313 path: path.to_string_lossy().to_string(),
314 },
315 };
316
317 let auth = repo.auth.as_ref().and_then(|a| match a {
319 RepositoryAuth::Pat { env_var } => Some(AuthConfig {
320 r#type: AuthType::Pat,
321 env_var: Some(env_var.clone()),
322 }),
323 _ => None,
325 });
326
327 crate::core::manifest::RepositoryDefinition {
328 name: repo.name.clone(),
329 r#type: repo_type,
330 priority: repo.priority,
331 connection,
332 auth,
333 }
334 }
335
336 fn save_config(&self, config: &RepositoriesConfig) -> Result<(), ServiceError> {
338 if let Some(parent) = self.config_path.parent() {
340 std::fs::create_dir_all(parent).map_err(ServiceError::Io)?;
341 }
342
343 let content = toml::to_string_pretty(config).map_err(|e| {
344 ServiceError::Custom(format!("Failed to serialize repositories config: {}", e))
345 })?;
346
347 std::fs::write(&self.config_path, content).map_err(ServiceError::Io)?;
348
349 Ok(())
350 }
351
352 pub fn add_repository(
354 &mut self,
355 name: String,
356 definition: RepositoryDefinition,
357 ) -> Result<(), ServiceError> {
358 if self.repositories.contains_key(&name) {
359 return Err(ServiceError::Custom(format!(
360 "Repository '{}' already exists",
361 name
362 )));
363 }
364
365 self.repositories.insert(name, definition);
366 Ok(())
367 }
368
369 pub fn remove_repository(&mut self, name: &str) -> Result<(), ServiceError> {
371 if self.repositories.remove(name).is_none() {
372 return Err(ServiceError::Custom(format!(
373 "Repository '{}' not found",
374 name
375 )));
376 }
377 if let Ok(mut clients) = self.clients.try_write() {
379 clients.remove(name);
380 }
381 Ok(())
382 }
383
384 pub fn get_repository(&self, name: &str) -> Option<&RepositoryDefinition> {
386 self.repositories.get(name)
387 }
388
389 pub fn list_repositories(&self) -> Vec<&RepositoryDefinition> {
391 let mut repos: Vec<&RepositoryDefinition> = self.repositories.values().collect();
392 repos.sort_by_key(|r| r.priority);
393 repos
394 }
395
396 pub async fn get_client(
398 &self,
399 name: &str,
400 ) -> Result<Arc<dyn RepositoryClient + Send + Sync>, ServiceError> {
401 {
403 let clients = self.clients.read().await;
404 if let Some(client) = clients.get(name) {
405 return Ok(Arc::clone(client));
406 }
407 }
408
409 let repo = self
411 .repositories
412 .get(name)
413 .ok_or_else(|| ServiceError::Custom(format!("Repository '{}' not found", name)))?;
414
415 let client_arc = client::create_client(repo).await?;
416
417 let mut clients = self.clients.write().await;
419 clients.insert(name.to_string(), client_arc.clone());
420 Ok(client_arc)
421 }
422
423 pub fn get_default_repository(&self) -> Option<&RepositoryDefinition> {
425 if let Some(repo) = self.repositories.get("default") {
427 return Some(repo);
428 }
429
430 let mut repos: Vec<&RepositoryDefinition> = self.repositories.values().collect();
432 repos.sort_by_key(|r| r.priority);
433 repos.first().copied()
434 }
435}