use std::collections::HashMap;
use std::future::Future;
use std::hash::BuildHasher;
use std::path::{Path, PathBuf};
use super::fs::{clean_and_create, copy_directory, create_symlink};
use super::paths::{agent_base_dir, canonical_skills_dir, is_path_safe, sanitize_name};
use crate::agents::AgentRegistry;
use crate::error::{Result, SkillError};
use crate::types::{AgentConfig, InstallMode, InstallOptions, InstallResult, InstallScope, Skill};
struct InstallContext {
canonical_dir: PathBuf,
agent_dir: PathBuf,
mode: InstallMode,
scope: InstallScope,
is_universal: bool,
}
trait SkillWriter {
fn write(&self, dir: &Path) -> impl Future<Output = Result<()>> + Send;
}
fn prepare_install(
install_name: &str,
agent: &AgentConfig,
registry: &AgentRegistry,
options: &InstallOptions,
) -> Result<InstallContext> {
let scope = options.scope;
let cwd = options
.cwd
.as_deref()
.unwrap_or_else(|| Path::new(""))
.to_path_buf();
let cwd = if cwd.as_os_str().is_empty() {
std::env::current_dir().unwrap_or_default()
} else {
cwd
};
if scope == InstallScope::Global && agent.global_skills_dir.is_none() {
return Err(SkillError::AgentUnsupported {
agent: agent.display_name.clone(),
operation: "global skill installation",
});
}
let skill_name = sanitize_name(install_name);
let canonical_base = canonical_skills_dir(scope, &cwd);
let canonical_dir = canonical_base.join(&skill_name);
let agent_base = agent_base_dir(agent, registry, scope, &cwd);
let agent_dir = agent_base.join(&skill_name);
if !is_path_safe(&canonical_base, &canonical_dir) || !is_path_safe(&agent_base, &agent_dir) {
return Err(SkillError::PathTraversal {
context: "skill name",
path: skill_name,
});
}
Ok(InstallContext {
canonical_dir,
agent_dir,
mode: options.mode,
scope,
is_universal: registry.is_universal(&agent.name),
})
}
async fn install<W: SkillWriter>(ctx: InstallContext, writer: W) -> Result<InstallResult> {
if ctx.mode == InstallMode::Copy {
clean_and_create(&ctx.agent_dir).await?;
writer.write(&ctx.agent_dir).await?;
return Ok(InstallResult {
path: ctx.agent_dir,
canonical_path: None,
mode: InstallMode::Copy,
symlink_failed: false,
});
}
clean_and_create(&ctx.canonical_dir).await?;
writer.write(&ctx.canonical_dir).await?;
if ctx.scope == InstallScope::Global && ctx.is_universal {
return Ok(InstallResult {
path: ctx.canonical_dir.clone(),
canonical_path: Some(ctx.canonical_dir),
mode: InstallMode::Symlink,
symlink_failed: false,
});
}
let symlink_ok = create_symlink(&ctx.canonical_dir, &ctx.agent_dir).await;
if !symlink_ok {
clean_and_create(&ctx.agent_dir).await?;
writer.write(&ctx.agent_dir).await?;
}
Ok(InstallResult {
path: ctx.agent_dir,
canonical_path: Some(ctx.canonical_dir),
mode: InstallMode::Symlink,
symlink_failed: !symlink_ok,
})
}
struct CopyTree<'a> {
source: &'a Path,
}
impl SkillWriter for CopyTree<'_> {
async fn write(&self, dir: &Path) -> Result<()> {
copy_directory(self.source, dir).await
}
}
struct InlineSkillMd<'a> {
content: &'a str,
}
impl SkillWriter for InlineSkillMd<'_> {
async fn write(&self, dir: &Path) -> Result<()> {
tokio::fs::write(dir.join("SKILL.md"), self.content)
.await
.map_err(|e| SkillError::io(dir, e))
}
}
struct FileMap<'a, S: BuildHasher + Sync> {
files: &'a HashMap<String, String, S>,
}
impl<S: BuildHasher + Sync> SkillWriter for FileMap<'_, S> {
async fn write(&self, dir: &Path) -> Result<()> {
for (file_path, content) in self.files {
let full = dir.join(file_path);
if !is_path_safe(dir, &full) {
continue;
}
if let Some(parent) = full.parent()
&& parent != dir
{
tokio::fs::create_dir_all(parent)
.await
.map_err(|e| SkillError::io(parent, e))?;
}
tokio::fs::write(&full, content)
.await
.map_err(|e| SkillError::io(&full, e))?;
}
Ok(())
}
}
pub async fn install_skill_for_agent(
skill: &Skill,
agent: &AgentConfig,
registry: &AgentRegistry,
options: &InstallOptions,
) -> Result<InstallResult> {
let ctx = prepare_install(&skill.name, agent, registry, options)?;
install(
ctx,
CopyTree {
source: &skill.path,
},
)
.await
}
pub async fn install_remote_skill_content(
install_name: &str,
content: &str,
agent: &AgentConfig,
registry: &AgentRegistry,
options: &InstallOptions,
) -> Result<InstallResult> {
let ctx = prepare_install(install_name, agent, registry, options)?;
install(ctx, InlineSkillMd { content }).await
}
pub async fn install_wellknown_skill_files<S: BuildHasher + Clone + Send + Sync>(
install_name: &str,
files: &HashMap<String, String, S>,
agent: &AgentConfig,
registry: &AgentRegistry,
options: &InstallOptions,
) -> Result<InstallResult> {
let ctx = prepare_install(install_name, agent, registry, options)?;
install(ctx, FileMap { files }).await
}