use crate::cli::SkillCommand;
#[allow(clippy::too_many_lines)]
pub(crate) async fn handle_skill_command(
cmd: SkillCommand,
config_path: Option<&std::path::Path>,
) -> anyhow::Result<()> {
use crate::bootstrap::{managed_skills_dir, resolve_config_path};
use std::collections::HashMap;
use zeph_skills::manager::SkillManager;
let config_file = resolve_config_path(config_path);
let config = zeph_core::config::Config::load(&config_file).unwrap_or_default();
let managed_dir = managed_skills_dir();
std::fs::create_dir_all(&managed_dir)
.map_err(|e| anyhow::anyhow!("failed to create managed skills dir: {e}"))?;
let mgr = SkillManager::new(managed_dir.clone());
let sqlite_path = crate::db_url::resolve_db_url(&config).to_owned();
match cmd {
SkillCommand::Install { source } => {
let result = if source.starts_with("http://")
|| source.starts_with("https://")
|| source.starts_with("git@")
{
mgr.install_from_url(&source)
} else {
mgr.install_from_path(std::path::Path::new(&source))
}
.map_err(|e| anyhow::anyhow!("{e}"))?;
let store = zeph_memory::store::SqliteStore::new(&sqlite_path)
.await
.map_err(|e| anyhow::anyhow!("failed to open SQLite: {e}"))?;
let (source_kind, source_url, source_path) = match &result.source {
zeph_skills::SkillSource::Hub { url } => (
zeph_memory::store::SourceKind::Hub,
Some(url.as_str()),
None,
),
zeph_skills::SkillSource::File { path } => (
zeph_memory::store::SourceKind::File,
None,
Some(path.to_string_lossy().into_owned()),
),
zeph_skills::SkillSource::Local => {
(zeph_memory::store::SourceKind::Local, None, None)
}
};
store
.upsert_skill_trust(
&result.name,
"quarantined",
source_kind,
source_url,
source_path.as_deref(),
&result.blake3_hash,
)
.await
.map_err(|e| anyhow::anyhow!("trust upsert failed: {e}"))?;
println!(
"Installed skill \"{}\" (hash: {}..., trust: quarantined)",
result.name,
&result.blake3_hash[..8]
);
let skill_md = managed_dir.join(&result.name).join("SKILL.md");
if let Ok(meta) = zeph_skills::loader::load_skill_meta(&skill_md)
&& !meta.requires_secrets.is_empty()
{
println!(
" Note: this skill requires secrets: {}",
meta.requires_secrets.join(", ")
);
println!(" Run `zeph vault set ZEPH_SECRET_<NAME> <value>` for each.");
}
}
SkillCommand::Remove { name } => {
mgr.remove(&name).map_err(|e| anyhow::anyhow!("{e}"))?;
let store = zeph_memory::store::SqliteStore::new(&sqlite_path)
.await
.map_err(|e| anyhow::anyhow!("failed to open SQLite: {e}"))?;
store
.delete_skill_trust(&name)
.await
.map_err(|e| anyhow::anyhow!("trust delete failed: {e}"))?;
println!("Removed skill \"{name}\".");
}
SkillCommand::List => {
let installed = mgr.list_installed().map_err(|e| anyhow::anyhow!("{e}"))?;
if installed.is_empty() {
println!("No skills installed in {}.", managed_dir.display());
return Ok(());
}
let store = zeph_memory::store::SqliteStore::new(&sqlite_path)
.await
.map_err(|e| anyhow::anyhow!("failed to open SQLite: {e}"))?;
println!("Installed skills ({}):\n", installed.len());
for skill in &installed {
let trust = store
.load_skill_trust(&skill.name)
.await
.ok()
.flatten()
.map_or_else(|| "no trust record".to_owned(), |r| r.trust_level);
if skill.requires_secrets.is_empty() {
println!(" {} — {} [{}]", skill.name, skill.description, trust);
} else {
println!(
" {} — {} [{}] (requires: {})",
skill.name,
skill.description,
trust,
skill.requires_secrets.join(", "),
);
}
}
}
SkillCommand::Verify { name } => {
let store = zeph_memory::store::SqliteStore::new(&sqlite_path)
.await
.map_err(|e| anyhow::anyhow!("failed to open SQLite: {e}"))?;
if let Some(name) = name {
let current_hash = mgr.verify(&name).map_err(|e| anyhow::anyhow!("{e}"))?;
let stored = store
.load_skill_trust(&name)
.await
.ok()
.flatten()
.map(|r| r.blake3_hash);
match stored {
Some(ref h) if h == ¤t_hash => {
println!("{name}: OK (hash matches)");
}
Some(_) => {
println!("{name}: MISMATCH (hash changed, setting to quarantined)");
store
.set_skill_trust_level(&name, "quarantined")
.await
.map_err(|e| anyhow::anyhow!("trust update failed: {e}"))?;
store
.update_skill_hash(&name, ¤t_hash)
.await
.map_err(|e| anyhow::anyhow!("hash update failed: {e}"))?;
}
None => {
println!("{name}: no stored hash (hash: {}...)", ¤t_hash[..8]);
}
}
} else {
let rows = store
.load_all_skill_trust()
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
let stored_hashes: HashMap<String, String> = rows
.into_iter()
.map(|r| (r.skill_name, r.blake3_hash))
.collect();
let results = mgr
.verify_all(&stored_hashes)
.map_err(|e| anyhow::anyhow!("{e}"))?;
for r in &results {
match r.stored_hash_matches {
Some(true) => println!("{}: OK", r.name),
Some(false) => {
println!("{}: MISMATCH (setting to quarantined)", r.name);
store
.set_skill_trust_level(&r.name, "quarantined")
.await
.map_err(|e| anyhow::anyhow!("trust update failed: {e}"))?;
store
.update_skill_hash(&r.name, &r.current_hash)
.await
.map_err(|e| anyhow::anyhow!("hash update failed: {e}"))?;
}
None => println!("{}: no stored hash", r.name),
}
}
}
}
SkillCommand::Trust { name, level } => {
let valid = matches!(
level.as_str(),
"trusted" | "verified" | "quarantined" | "blocked"
);
if !valid {
anyhow::bail!(
"invalid trust level: {level}. Use: trusted, verified, quarantined, blocked"
);
}
if matches!(level.as_str(), "trusted" | "verified") {
let managed_dir = crate::bootstrap::managed_skills_dir();
let mgr = zeph_skills::manager::SkillManager::new(managed_dir.clone());
let name_clone = name.clone();
let current_hash = tokio::task::spawn_blocking(move || mgr.verify(&name_clone))
.await
.map_err(|e| anyhow::anyhow!("spawn_blocking failed: {e}"))??;
let store = zeph_memory::store::SqliteStore::new(&sqlite_path)
.await
.map_err(|e| anyhow::anyhow!("failed to open SQLite: {e}"))?;
let row = store
.load_skill_trust(&name)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
match row {
None => anyhow::bail!("skill \"{name}\" not found in trust database"),
Some(r) if r.blake3_hash != current_hash => {
anyhow::bail!(
"hash mismatch for \"{name}\" — run `zeph skill verify {name}` first"
);
}
Some(_) => {}
}
let updated = store
.set_skill_trust_level(&name, &level)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
if updated {
println!("Trust level for \"{name}\" set to {level}.");
} else {
anyhow::bail!("skill \"{name}\" not found in trust database");
}
} else {
let store = zeph_memory::store::SqliteStore::new(&sqlite_path)
.await
.map_err(|e| anyhow::anyhow!("failed to open SQLite: {e}"))?;
let updated = store
.set_skill_trust_level(&name, &level)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
if updated {
println!("Trust level for \"{name}\" set to {level}.");
} else {
anyhow::bail!("skill \"{name}\" not found in trust database");
}
}
}
SkillCommand::Block { name } => {
let store = zeph_memory::store::SqliteStore::new(&sqlite_path)
.await
.map_err(|e| anyhow::anyhow!("failed to open SQLite: {e}"))?;
let updated = store
.set_skill_trust_level(&name, "blocked")
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
if updated {
println!("Skill \"{name}\" blocked.");
} else {
anyhow::bail!("skill \"{name}\" not found in trust database");
}
}
SkillCommand::Unblock { name } => {
let store = zeph_memory::store::SqliteStore::new(&sqlite_path)
.await
.map_err(|e| anyhow::anyhow!("failed to open SQLite: {e}"))?;
let updated = store
.set_skill_trust_level(&name, "quarantined")
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
if updated {
println!("Skill \"{name}\" unblocked (set to quarantined).");
} else {
anyhow::bail!("skill \"{name}\" not found in trust database");
}
}
SkillCommand::Invoke { name, args } => {
use std::str::FromStr;
use zeph_common::SkillTrustLevel;
use zeph_skills::prompt::{sanitize_skill_text, wrap_quarantined};
let registry = zeph_skills::registry::SkillRegistry::load(&[managed_dir]);
let trust = {
let store = zeph_memory::store::SqliteStore::new(&sqlite_path)
.await
.map_err(|e| anyhow::anyhow!("failed to open SQLite: {e}"))?;
store
.load_skill_trust(&name)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?
.and_then(|r| SkillTrustLevel::from_str(&r.trust_level).ok())
.unwrap_or_default()
};
if trust == SkillTrustLevel::Blocked {
anyhow::bail!("skill is blocked by policy: {name}");
}
let raw = registry
.body(&name)
.map_err(|e| anyhow::anyhow!("{e}"))?
.to_owned();
let sanitized = if trust == SkillTrustLevel::Trusted {
raw
} else {
sanitize_skill_text(&raw)
};
let body = if trust == SkillTrustLevel::Quarantined {
wrap_quarantined(&name, &sanitized)
} else {
sanitized
};
match args {
Some(a) => {
let args_safe = sanitize_skill_text(&a);
println!("{body}\n\n<args>\n{args_safe}\n</args>");
}
None => println!("{body}"),
}
}
}
Ok(())
}