use std::path::{Path, PathBuf};
use anyhow::Result;
use super::SkillLoaderConfig;
use super::discovery::{find_git_root, skill_roots_with_home_dir};
use crate::skills::cli_bridge::CliToolBridge;
use crate::skills::command_skills::{
BuiltInCommandSkill, CommandSkillBackend, built_in_command_skill, find_command_skill_by_skill_name,
merge_built_in_command_skill_contexts,
};
use crate::skills::container_validation::{
ContainerSkillsValidator, ContainerValidationReport, ContainerValidationResult,
};
use crate::skills::discovery::{DiscoveryConfig, DiscoveryResult, SkillDiscovery};
use crate::skills::model::SkillScope;
use crate::skills::system::{install_system_skills, system_cache_root_dir};
use crate::skills::types::{Skill, SkillContext};
use crate::tools::error_messages::skill_ops;
pub enum EnhancedSkill {
Traditional(Box<Skill>),
CliTool(Box<CliToolBridge>),
BuiltInCommand(Box<BuiltInCommandSkill>),
NativePlugin(Box<dyn crate::skills::native_plugin::NativePluginTrait>),
}
impl std::fmt::Debug for EnhancedSkill {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Traditional(skill) => f.debug_tuple("Traditional").field(skill).finish(),
Self::CliTool(tool) => f.debug_tuple("CliTool").field(tool).finish(),
Self::BuiltInCommand(skill) => f.debug_tuple("BuiltInCommand").field(skill).finish(),
Self::NativePlugin(plugin) => f.debug_tuple("NativePlugin").field(plugin).finish(),
}
}
}
pub struct EnhancedSkillLoader {
workspace_root: PathBuf,
codex_home: PathBuf,
discovery: SkillDiscovery,
}
fn default_codex_home() -> PathBuf {
std::env::var_os("CODEX_HOME")
.filter(|value| !value.is_empty())
.map(PathBuf::from)
.or_else(|| dirs::home_dir().map(|home| home.join(".codex")))
.unwrap_or_else(|| PathBuf::from(".codex"))
}
pub(crate) fn discovery_config_for_codex_home(workspace_root: &Path, codex_home: &Path) -> DiscoveryConfig {
let home_dir = dirs::home_dir();
let loader_config = SkillLoaderConfig {
codex_home: codex_home.to_path_buf(),
cwd: workspace_root.to_path_buf(),
project_root: find_git_root(workspace_root),
include_bundled_system_skills: true,
};
let roots = skill_roots_with_home_dir(&loader_config, home_dir.as_deref());
DiscoveryConfig {
skill_paths: roots
.iter()
.filter(|root| !root.is_tool_root && !root.is_plugin_root)
.map(|root| root.path.clone())
.collect(),
tool_paths: roots
.iter()
.filter(|root| root.is_tool_root)
.map(|root| root.path.clone())
.collect(),
..Default::default()
}
}
impl EnhancedSkillLoader {
pub fn new(workspace_root: PathBuf) -> Self {
let codex_home = default_codex_home();
let discovery = SkillDiscovery::with_config(discovery_config_for_codex_home(&workspace_root, &codex_home));
Self { workspace_root, codex_home, discovery }
}
pub fn with_codex_home(workspace_root: PathBuf, codex_home: PathBuf) -> Self {
let discovery = SkillDiscovery::with_config(discovery_config_for_codex_home(&workspace_root, &codex_home));
Self { workspace_root, codex_home, discovery }
}
fn ensure_system_skills_installed(&self) {
if let Err(err) = install_system_skills(&self.codex_home) {
tracing::warn!("enhanced skill loader failed to install bundled system skills: {err}");
}
}
pub async fn discover_all_skills(&mut self) -> Result<DiscoveryResult> {
self.ensure_system_skills_installed();
let mut result = self.discovery.discover_all(&self.workspace_root).await?;
merge_built_in_command_skill_contexts(&mut result.skills);
Ok(result)
}
pub async fn get_skill(&mut self, name: &str) -> Result<EnhancedSkill> {
self.ensure_system_skills_installed();
let result = self.discovery.discover_all(&self.workspace_root).await?;
if let Some(skill_ctx) = select_traditional_skill_ctx(&result.skills, name, &self.workspace_root) {
let path = skill_ctx.path();
let (manifest, instructions) = crate::skills::manifest::parse_skill_file(path)?;
let skill = Skill::with_scope(
manifest,
path.clone(),
infer_scope_from_skill_path(path, &self.workspace_root),
instructions,
)?;
return Ok(EnhancedSkill::Traditional(Box::new(skill)));
}
for tool_config in &result.tools {
if tool_config.name == name {
let bridge = CliToolBridge::new(tool_config.clone())?;
return Ok(EnhancedSkill::CliTool(Box::new(bridge)));
}
}
if let Some(skill) = built_in_command_skill(name) {
return Ok(EnhancedSkill::BuiltInCommand(Box::new(skill)));
}
Err(skill_ops::skill_not_found_error(name))
}
pub async fn generate_validation_report(&mut self) -> Result<ContainerValidationReport> {
let result = self.discovery.discover_all(&self.workspace_root).await?;
let mut report = ContainerValidationReport::new();
let validator = ContainerSkillsValidator::new();
for skill_ctx in &result.skills {
match self.load_full_skill_from_ctx(skill_ctx) {
Ok(skill) => {
let analysis = validator.analyze_skill(&skill);
report.add_skill_analysis(skill.name().to_string(), analysis);
}
Err(e) => {
report.add_incompatible_skill(
skill_ctx.manifest().name.clone(),
skill_ctx.manifest().description.clone(),
format!("Load error: {e}"),
);
}
}
}
report.finalize();
Ok(report)
}
pub fn check_container_requirements(&self, skill: &Skill) -> ContainerValidationResult {
let validator = ContainerSkillsValidator::new();
validator.analyze_skill(skill)
}
fn load_full_skill_from_ctx(&self, ctx: &SkillContext) -> Result<Skill> {
let path = ctx.path();
let (manifest, instructions) = crate::skills::manifest::parse_skill_file(path)?;
Skill::with_scope(manifest, path.clone(), infer_scope_from_skill_path(path, &self.workspace_root), instructions)
}
}
fn infer_scope_from_skill_path(path: &Path, workspace_root: &Path) -> SkillScope {
if path.starts_with(Path::new("/etc/codex/skills")) {
return SkillScope::Admin;
}
if path.starts_with(system_cache_root_dir(&default_codex_home())) {
return SkillScope::System;
}
if let Some(home) = dirs::home_dir()
&& path.starts_with(home.join(".agents/skills"))
{
return SkillScope::User;
}
if path.starts_with(workspace_root) {
return SkillScope::Repo;
}
if has_agents_skills_ancestor(path) {
return SkillScope::Repo;
}
SkillScope::User
}
fn has_agents_skills_ancestor(path: &Path) -> bool {
let mut ancestors = path.parent().map(Path::new).into_iter().peekable();
while let Some(current) = ancestors.next() {
let is_skills = current.file_name().and_then(|f| f.to_str()) == Some("skills");
let parent_is_agents = ancestors.peek().and_then(|p| p.file_name().and_then(|f| f.to_str())) == Some(".agents");
if is_skills && parent_is_agents {
return true;
}
}
false
}
fn is_bundled_command_skill(name: &str) -> bool {
find_command_skill_by_skill_name(name)
.is_some_and(|spec| matches!(spec.backend, CommandSkillBackend::TraditionalSkill { .. }))
}
fn is_system_skill_path(path: &Path) -> bool {
let mut components = path.components().peekable();
while let Some(component) = components.next() {
let is_skills = component.as_os_str() == "skills";
let next_is_system = components.peek().is_some_and(|next| next.as_os_str() == ".system");
if is_skills && next_is_system {
return true;
}
}
false
}
pub(crate) fn select_traditional_skill_ctx<'a>(
skills: &'a [SkillContext],
name: &str,
workspace_root: &Path,
) -> Option<&'a SkillContext> {
let mut first_match: Option<&'a SkillContext> = None;
let mut system_match: Option<&'a SkillContext> = None;
for skill_ctx in skills {
if skill_ctx.manifest().name != name {
continue;
}
if first_match.is_none() {
first_match = Some(skill_ctx);
}
let scope = infer_scope_from_skill_path(skill_ctx.path(), workspace_root);
if system_match.is_none()
&& (scope == SkillScope::System || (scope != SkillScope::Repo && is_system_skill_path(skill_ctx.path())))
{
system_match = Some(skill_ctx);
}
if first_match.is_some() && system_match.is_some() {
break;
}
}
if is_bundled_command_skill(name) {
system_match.or(first_match)
} else {
first_match
}
}