use super::commands::SkillsCommands;
use super::runtime::*;
use crate::api::SkillStore;
use crate::paths::AsobiPaths;
use anyhow::Result;
use std::io::IsTerminal;
use tracing::{info, warn};
fn classify_skill_source(source: &str) -> (String, bool) {
let mut git_url = source.to_string();
let is_git = if source.contains("://") || source.contains("git@") {
true
} else if source.contains("github.com/") || source.contains("gitlab.com/") {
git_url = format!("https://{source}");
true
} else {
!std::path::Path::new(source).is_dir() && source.ends_with(".git")
};
(git_url, is_git)
}
pub(crate) fn run(
backend: &crate::storage::Storage,
paths: &AsobiPaths,
subcommand: Option<SkillsCommands>,
) -> Result<()> {
match subcommand {
None => {
let skills = backend.list_skills()?;
if skills.is_empty() {
println!("No skills installed.");
} else {
let mut grouped: std::collections::BTreeMap<String, Vec<crate::api::SkillRecord>> =
std::collections::BTreeMap::new();
for s in skills {
grouped.entry(s.source.clone()).or_default().push(s);
}
println!("Installed Skills:");
for (source, list) in grouped {
println!("Source: {}", source);
for s in list {
println!(" {} · {} · {}", s.entity_name, s.description, s.version);
}
}
}
}
Some(SkillsCommands::Install {
source,
all,
select,
}) => {
let (git_url, is_git) = classify_skill_source(&source);
let (target_path, version) = if is_git {
let (cache_path, ver) = get_or_update_cached_repo(&git_url, &paths.caches_dir())?;
(cache_path, ver)
} else {
let local_path = std::path::Path::new(&source);
if !local_path.exists() {
anyhow::bail!("Local path {} does not exist", source);
}
(local_path.to_path_buf(), "local".to_string())
};
let mode = if all {
crate::skills::SelectionMode::All
} else if let Some(sel) = select {
crate::skills::SelectionMode::Select(sel)
} else {
crate::skills::SelectionMode::Interactive
};
let is_tty = std::io::stdin().is_terminal();
let prune = matches!(mode, crate::skills::SelectionMode::All);
crate::skills::install_skills_from_dir(
backend,
&target_path,
&git_url,
&version,
mode,
is_tty,
prune,
)?;
info!("Skills installed successfully.");
}
Some(SkillsCommands::Update { source }) => {
let skills = backend.list_skills()?;
let mut unique_sources = std::collections::HashSet::new();
for s in skills {
if let Some(ref filter_src) = source {
let slug = crate::skills::derive_source_slug(&s.source);
if &s.source == filter_src || &slug == filter_src {
unique_sources.insert(s.source.clone());
}
} else {
unique_sources.insert(s.source.clone());
}
}
if unique_sources.is_empty() {
if let Some(src_val) = source {
anyhow::bail!(
"No installed skills found matching source/slug {:?}",
src_val
);
} else {
info!("No skills currently installed.");
return Ok(());
}
}
for src in unique_sources {
info!("Updating skills from {}...", src);
let (git_url, is_git) = classify_skill_source(&src);
let (target_path, version) = if is_git {
let (cache_path, ver) =
get_or_update_cached_repo(&git_url, &paths.caches_dir())?;
(cache_path, ver)
} else {
let local_path = std::path::Path::new(&src);
if !local_path.exists() {
warn!("Local path {} does not exist, skipping update", src);
continue;
}
(local_path.to_path_buf(), "local".to_string())
};
crate::skills::install_skills_from_dir(
backend,
&target_path,
&git_url,
&version,
crate::skills::SelectionMode::All,
false,
true,
)?;
info!("Successfully updated skills from {}.", src);
}
}
Some(SkillsCommands::Remove { target }) => {
let skills = backend.list_skills()?;
let mut entities_to_delete = Vec::new();
for s in skills {
let slug = crate::skills::derive_source_slug(&s.source);
if s.entity_name == target || s.source == target || slug == target {
entities_to_delete.push(s.entity_name.clone());
}
}
if !entities_to_delete.is_empty() {
info!("Deleting {} skill entities...", entities_to_delete.len());
backend.remove_skills(entities_to_delete)?;
info!("Skills removed successfully.");
} else if target.starts_with("skill:") {
info!("Deleting skill entity {}...", target);
backend.remove_skills(vec![target.clone()])?;
info!("Skills removed successfully.");
} else {
anyhow::bail!("No installed skills found matching target {:?}", target);
}
}
Some(SkillsCommands::Show { name }) => {
let mut entity_name = name.clone();
if !entity_name.starts_with("skill:") {
let skills = backend.list_skills()?;
let matches: Vec<_> = skills
.iter()
.filter(|s| {
s.entity_name == name || s.entity_name.ends_with(&format!(":{}", name))
})
.collect();
if matches.len() == 1 {
entity_name = matches[0].entity_name.clone();
} else if matches.len() > 1 {
anyhow::bail!(
"Ambiguous skill name '{}'. Matches: {}",
name,
matches
.iter()
.map(|s| &s.entity_name)
.cloned()
.collect::<Vec<_>>()
.join(", ")
);
} else {
entity_name = format!("skill:{}", name);
}
}
match backend.skill_body(&entity_name)? {
Some(body) => {
print!("{}", body);
}
None => {
anyhow::bail!("Skill '{}' not found", name);
}
}
}
}
Ok(())
}