use std::path::{Path, PathBuf};
use crate::agents::AgentRegistry;
use crate::error::Result;
use crate::installer;
use crate::providers::ProviderRegistry;
use crate::types::{
AgentId, DiscoverOptions, InstallOptions, InstallResult, InstalledSkill, ListOptions,
ParsedSource, RemoveOptions, Skill,
};
#[derive(Debug, Clone, Default)]
pub struct ManagerConfig {
pub cwd: Option<PathBuf>,
}
#[derive(Debug, Default)]
pub struct SkillManagerBuilder {
agents: Option<AgentRegistry>,
providers: Option<ProviderRegistry>,
config: ManagerConfig,
}
impl SkillManagerBuilder {
#[must_use]
pub fn agents(mut self, agents: AgentRegistry) -> Self {
self.agents = Some(agents);
self
}
#[must_use]
pub fn providers(mut self, providers: ProviderRegistry) -> Self {
self.providers = Some(providers);
self
}
#[must_use]
pub fn cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
self.config.cwd = Some(cwd.into());
self
}
#[must_use]
pub fn build(self) -> SkillManager {
SkillManager {
agents: self.agents.unwrap_or_default(),
providers: self.providers.unwrap_or_default(),
config: self.config,
}
}
}
#[derive(Debug)]
pub struct SkillManager {
agents: AgentRegistry,
providers: ProviderRegistry,
config: ManagerConfig,
}
impl Default for SkillManager {
fn default() -> Self {
Self::builder().build()
}
}
impl SkillManager {
#[must_use]
pub fn builder() -> SkillManagerBuilder {
SkillManagerBuilder::default()
}
#[must_use]
pub fn cwd(&self) -> PathBuf {
self.config
.cwd
.clone()
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default())
}
#[must_use]
pub const fn agents(&self) -> &AgentRegistry {
&self.agents
}
pub const fn agents_mut(&mut self) -> &mut AgentRegistry {
&mut self.agents
}
pub async fn detect_installed_agents(&self) -> Vec<AgentId> {
self.agents.detect_installed().await
}
#[must_use]
pub const fn providers(&self) -> &ProviderRegistry {
&self.providers
}
pub fn register_provider(&mut self, provider: impl crate::providers::HostProvider + 'static) {
self.providers.register(provider);
}
#[must_use]
pub fn parse_source(&self, input: &str) -> ParsedSource {
crate::source::parse_source(input)
}
pub async fn discover_skills(
&self,
path: &Path,
options: &DiscoverOptions,
) -> Result<Vec<Skill>> {
crate::skills::discover_skills(path, None, options).await
}
pub async fn discover_skills_with_subpath(
&self,
path: &Path,
subpath: &str,
options: &DiscoverOptions,
) -> Result<Vec<Skill>> {
crate::skills::discover_skills(path, Some(subpath), options).await
}
pub async fn install_skill(
&self,
skill: &Skill,
agent_id: &AgentId,
options: &InstallOptions,
) -> Result<InstallResult> {
let agent = self
.agents
.get(agent_id)
.ok_or_else(|| crate::error::Error::UnknownAgent(agent_id.to_string()))?;
installer::install_skill_for_agent(skill, agent, &self.agents, options).await
}
pub async fn list_installed(&self, options: &ListOptions) -> Result<Vec<InstalledSkill>> {
installer::list_installed_skills(&self.agents, options).await
}
pub async fn remove_skills(
&self,
skill_names: &[String],
options: &RemoveOptions,
) -> Result<()> {
let cwd = options.cwd.clone().unwrap_or_else(|| self.cwd());
let scope = options.scope;
for name in skill_names {
let canonical = installer::get_canonical_path(name, scope, &cwd);
let sanitized = installer::sanitize_name(name);
let target_agents: Vec<AgentId> = if options.agents.is_empty() {
self.agents.all_ids()
} else {
options.agents.clone()
};
for agent_id in &target_agents {
if let Some(agent) = self.agents.get(agent_id) {
let mut paths_to_cleanup = Vec::new();
let agent_base = installer::agent_base_dir(agent, &self.agents, scope, &cwd);
paths_to_cleanup.push(agent_base.join(&sanitized));
let native_dir = match scope {
crate::types::InstallScope::Global => {
agent.global_skills_dir.as_ref().map(|d| d.join(&sanitized))
}
crate::types::InstallScope::Project => {
Some(cwd.join(&agent.skills_dir).join(&sanitized))
}
};
if let Some(nd) = native_dir
&& !paths_to_cleanup.contains(&nd)
{
paths_to_cleanup.push(nd);
}
for path in &paths_to_cleanup {
if *path == canonical {
continue;
}
let _ = tokio::fs::remove_dir_all(path).await;
let _ = tokio::fs::remove_file(path).await;
}
}
}
let all_ids = self.agents.all_ids();
let remaining: Vec<&AgentId> = all_ids
.iter()
.filter(|id| !target_agents.contains(id))
.collect();
let mut still_used = false;
for aid in &remaining {
if let Some(agent) = self.agents.get(aid)
&& installer::is_skill_installed(name, agent, scope, &cwd).await
{
still_used = true;
break;
}
}
if !still_used {
let _ = tokio::fs::remove_dir_all(&canonical).await;
let _ = tokio::fs::remove_file(&canonical).await;
}
}
Ok(())
}
}