use std::env;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use crate::constants::SKILL_FILE;
use crate::error::ForgeError;
use crate::fsutil::{read_dir, read_to_string};
use crate::model::Agent;
use crate::util::{home_dir, unquote};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SkillCandidate {
pub(crate) name: String,
pub(crate) path: PathBuf,
}
pub(crate) fn discover(root: &Path) -> Result<Vec<SkillCandidate>, ForgeError> {
let mut candidates = Vec::new();
if root.join(SKILL_FILE).is_file() {
candidates.push(candidate(root));
}
for base in [root.join("skills"), root.join(".claude").join("skills")] {
if base.is_dir() {
for entry in read_dir(&base)? {
let path = entry?;
if path.join(SKILL_FILE).is_file() {
candidates.push(candidate(&path));
}
}
}
}
candidates.sort_by(|left, right| left.name.cmp(&right.name).then(left.path.cmp(&right.path)));
candidates.dedup_by(|left, right| left.path == right.path);
Ok(candidates)
}
pub(crate) fn agent_dir(agent: Agent) -> Option<PathBuf> {
let override_var = match agent {
Agent::Claude => "BOT_FORGE_CLAUDE_DIR",
Agent::OpenCode => "BOT_FORGE_OPENCODE_DIR",
};
if let Ok(path) = env::var(override_var) {
let path = PathBuf::from(path);
return path.is_dir().then_some(path);
}
let path = match agent {
Agent::Claude => home_dir().join(".claude").join("skills"),
Agent::OpenCode if cfg!(windows) => env::var("APPDATA")
.map(PathBuf::from)
.unwrap_or_else(|_| home_dir().join("AppData").join("Roaming"))
.join("opencode")
.join("skills"),
Agent::OpenCode => home_dir().join(".config").join("opencode").join("skills"),
};
path.is_dir().then_some(path)
}
fn candidate(path: &Path) -> SkillCandidate {
let fallback = path
.file_name()
.and_then(OsStr::to_str)
.unwrap_or("unnamed")
.to_string();
let name = read_to_string(&path.join(SKILL_FILE))
.ok()
.and_then(|content| parse_frontmatter_name(&content))
.unwrap_or(fallback);
SkillCandidate {
name,
path: path.to_path_buf(),
}
}
fn parse_frontmatter_name(content: &str) -> Option<String> {
let mut lines = content.lines();
if lines.next()?.trim() != "---" {
return None;
}
for line in lines {
let line = line.trim();
if line == "---" {
return None;
}
if let Some((key, value)) = line.split_once(':')
&& key.trim() == "name"
{
return Some(unquote(value.trim()).to_string());
}
}
None
}