use std::path::PathBuf;
use crate::config::skillset::SkillsetConfig;
use crate::conventions::ConventionRegistry;
use crate::error::Result;
use crate::skill::FetchedSkill;
use crate::sources::SourceRegistry;
pub struct SkillManager {
convention_registry: ConventionRegistry,
config: SkillsetConfig,
project_path: PathBuf,
source_registry: SourceRegistry,
}
impl SkillManager {
pub fn new(project_path: PathBuf) -> Result<Self> {
let config = Self::load_config(&project_path)?;
let mut convention_registry = ConventionRegistry::new();
let source_registry = SourceRegistry::new()?;
let enabled_conventions = config.get_conventions();
if enabled_conventions.contains(&"autogpt".to_string()) {
convention_registry.register(Box::new(crate::conventions::AutoGptConvention::new()));
}
if enabled_conventions.contains(&"langchain".to_string()) {
convention_registry.register(Box::new(crate::conventions::LangchainConvention::new()));
}
if enabled_conventions.contains(&"agent-skills".to_string()) {
convention_registry
.register(Box::new(crate::conventions::AgentSkillsConvention::new()));
}
Ok(Self {
convention_registry,
config,
project_path,
source_registry,
})
}
fn load_config(project_path: &PathBuf) -> Result<SkillsetConfig> {
let config_path = project_path.join("skillset.json");
if config_path.exists() {
SkillsetConfig::load_from_file(&config_path)
} else {
Ok(SkillsetConfig::default())
}
}
pub fn config_mut(&mut self) -> &mut SkillsetConfig {
&mut self.config
}
pub fn config(&self) -> &SkillsetConfig {
&self.config
}
pub fn save_config(&self) -> Result<()> {
let config_path = self.project_path.join("skillset.json");
self.config.save_to_file(&config_path)
}
pub async fn add_skill(
&mut self,
reference: &str,
convention: Option<String>,
version: Option<String>,
) -> Result<()> {
let (source_type, source_ref, skill_name) = self.parse_reference(reference)?;
let fetched_skill = self.fetch_skill(&source_type, &source_ref, version).await?;
let convention_name = if let Some(conv) = convention {
conv
} else {
self.detect_convention(&fetched_skill.source_path).await?
};
self.organize_skill(&fetched_skill, &convention_name)
.await?;
self.update_config(&fetched_skill, &convention_name)?;
println!("Successfully added skill: {}", fetched_skill.name);
Ok(())
}
pub async fn add_skill_by_name(
&mut self,
skill_name: &str,
skill_config: &crate::config::skillset::SkillConfig,
) -> Result<()> {
let resolved_reference = self
.config
.resolve_skill_reference(skill_name, skill_config)?;
let (source_type, source_ref, actual_name) = self.parse_reference(&resolved_reference)?;
let version = Some(skill_config.get_version().to_string());
let fetched_skill = self.fetch_skill(&source_type, &source_ref, version).await?;
let convention_name = if let Some(conv) = skill_config.get_explicit_convention() {
conv.clone()
} else {
self.detect_convention(&fetched_skill.source_path).await?
};
self.organize_skill(&fetched_skill, &convention_name)
.await?;
self.update_config(&fetched_skill, &convention_name)?;
println!("Successfully added skill: {}", skill_name);
Ok(())
}
fn parse_reference(&self, reference: &str) -> Result<(String, String, String)> {
if reference.starts_with("git:")
|| reference.starts_with("https://github.com")
|| reference.starts_with("git@")
{
let source_ref = reference
.strip_prefix("git:")
.unwrap_or(reference)
.to_string();
let skill_name = self.extract_skill_name_from_git(&source_ref)?;
Ok(("git".to_string(), source_ref, skill_name))
} else if reference.starts_with("oci:")
|| reference.contains("ghcr.io")
|| reference.contains("docker.io")
{
let source_ref = reference
.strip_prefix("oci:")
.unwrap_or(reference)
.to_string();
let skill_name = self.extract_skill_name_from_oci(&source_ref)?;
Ok(("oci".to_string(), source_ref, skill_name))
} else if reference.starts_with("/") || reference.starts_with("./") {
let skill_name = self.extract_skill_name_from_path(reference)?;
Ok(("local".to_string(), reference.to_string(), skill_name))
} else {
Err(crate::error::SkillsetError::Source(format!(
"Simplified skill names should be resolved at CLI level: {}",
reference
)))
}
}
fn extract_skill_name_from_git(&self, git_url: &str) -> Result<String> {
let parts: Vec<&str> = git_url.split('/').collect();
if parts.len() >= 2 {
let last_part = parts.last().unwrap();
let name = last_part.trim_end_matches(".git");
Ok(name.to_string())
} else {
Err(crate::error::SkillsetError::Source(format!(
"Unable to extract skill name from git URL: {}",
git_url
)))
}
}
fn extract_skill_name_from_oci(&self, oci_ref: &str) -> Result<String> {
let parts: Vec<&str> = oci_ref.split('/').collect();
if parts.len() >= 3 {
let repo_part = parts.last().unwrap();
let name_with_tag = repo_part.split(':').next().unwrap_or(repo_part);
Ok(name_with_tag.to_string())
} else {
Err(crate::error::SkillsetError::Source(format!(
"Unable to extract skill name from OCI reference: {}",
oci_ref
)))
}
}
fn extract_skill_name_from_path(&self, path: &str) -> Result<String> {
let path_buf = std::path::PathBuf::from(path);
path_buf
.file_name()
.and_then(|name| name.to_str())
.map(|name| name.to_string())
.ok_or_else(|| {
crate::error::SkillsetError::Source(format!(
"Unable to extract skill name from path: {}",
path
))
})
}
pub async fn remove_skill(&mut self, skill_name: &str) -> Result<()> {
if !self.config.skills.contains_key(skill_name) {
return Err(crate::error::SkillsetError::SkillNotFound(
skill_name.to_string(),
));
}
self.config.skills.remove(skill_name);
self.save_config()?;
println!("Successfully removed skill: {}", skill_name);
Ok(())
}
pub async fn show_skill_info(&self, skill_name: &str) -> Result<()> {
if let Some(skill_config) = self.config.skills.get(skill_name) {
let version = skill_config.get_version();
println!("Skill: {}", skill_name);
println!("Version: {}", version);
if let Some(source) = skill_config.get_explicit_source() {
println!("Source: {}", source);
} else {
let resolved = self
.config
.resolve_skill_reference(skill_name, skill_config)?;
println!("Resolved Source: {}", resolved);
}
if let Some(convention) = skill_config.get_explicit_convention() {
println!("Convention: {}", convention);
}
} else {
return Err(crate::error::SkillsetError::SkillNotFound(
skill_name.to_string(),
));
}
Ok(())
}
async fn fetch_skill(
&self,
source_type: &str,
source_ref: &str,
_version: Option<String>,
) -> Result<FetchedSkill> {
match source_type {
"git" => {
if let Some(source) = self.source_registry.get("git") {
source.fetch(source_ref).await
} else {
Err(crate::error::SkillsetError::SourceNotFound(
"git".to_string(),
))
}
}
"oci" => {
todo!("Implement OCI source fetching")
}
"local" => {
todo!("Implement local source handling")
}
_ => Err(crate::error::SkillsetError::SourceNotFound(
source_type.to_string(),
)),
}
}
async fn detect_convention(&self, path: &PathBuf) -> Result<String> {
if let Some(detected) = self.convention_registry.detect_convention(path).await? {
Ok(detected)
} else {
Ok("autogpt".to_string())
}
}
async fn organize_skill(
&self,
fetched_skill: &FetchedSkill,
convention_name: &str,
) -> Result<()> {
if let Some(convention) = self.convention_registry.get(convention_name) {
convention
.organize(
&fetched_skill.name,
&fetched_skill.source_path,
&self.project_path,
)
.await
} else {
Err(crate::error::SkillsetError::ConventionNotFound(
convention_name.to_string(),
))
}
}
fn update_config(&mut self, fetched_skill: &FetchedSkill, convention_name: &str) -> Result<()> {
self.config.skills.insert(
fetched_skill.name.clone(),
crate::config::skillset::SkillConfig::Detailed {
version: fetched_skill.version.clone(),
source: Some(fetched_skill.source_path.display().to_string()),
convention: Some(convention_name.to_string()),
},
);
let config_path = self.project_path.join("skillset.json");
self.config.save_to_file(&config_path)?;
Ok(())
}
pub fn list_skills(&self, verbose: bool) -> Result<()> {
if self.config.skills.is_empty() {
println!("No skills installed.");
return Ok(());
}
println!("Installed skills:");
for (name, skill_config) in &self.config.skills {
let version = skill_config.get_version();
if verbose {
if let (Some(source), Some(convention)) = (
skill_config.get_explicit_source(),
skill_config.get_explicit_convention(),
) {
println!(
" {} (v{}) - Source: {} - Convention: {}",
name, version, source, convention
);
} else if let Some(source) = skill_config.get_explicit_source() {
println!(" {} (v{}) - Source: {}", name, version, source);
} else {
println!(" {} (v{})", name, version);
}
} else {
println!(" {} (v{})", name, version);
}
}
Ok(())
}
}