runx-runtime 0.6.19

Native Rust runtime for local runx execution, adapters, harness replay, receipts, and sandboxing.
Documentation
use std::fs;
use std::path::{Path, PathBuf};

use super::RunxExportLoadError;

pub(super) fn resolve_skill_ref(
    root: &Path,
    reference: &str,
    official_roots: &[PathBuf],
) -> Result<PathBuf, RunxExportLoadError> {
    let reference_path = Path::new(reference);
    let candidates = if reference_path.is_absolute() {
        vec![reference_path.to_path_buf()]
    } else {
        vec![
            root.join("skills").join(reference_path),
            root.join(reference_path),
        ]
    };
    for candidate in candidates {
        if let Some(skill_dir) = skill_dir_if_exists(&candidate) {
            return canonicalize(&skill_dir, "canonicalizing skill reference");
        }
    }
    if let Some(skill_dir) = resolve_official_skill_ref(reference, official_roots)? {
        return canonicalize(&skill_dir, "canonicalizing official skill reference");
    }
    Err(RunxExportLoadError::InvalidArgs(format!(
        "skill reference {reference} does not resolve to a SKILL.md package"
    )))
}

fn skill_dir_if_exists(candidate: &Path) -> Option<PathBuf> {
    let skill_dir = if candidate
        .file_name()
        .and_then(|name| name.to_str())
        .is_some_and(|name| name.eq_ignore_ascii_case("SKILL.md"))
    {
        candidate.parent().map(Path::to_path_buf)
    } else {
        Some(candidate.to_path_buf())
    }?;
    skill_dir.join("SKILL.md").exists().then_some(skill_dir)
}

fn resolve_official_skill_ref(
    reference: &str,
    official_roots: &[PathBuf],
) -> Result<Option<PathBuf>, RunxExportLoadError> {
    let Some(name) = official_skill_name(reference) else {
        return Ok(None);
    };
    for root in official_roots {
        for candidate in [root.join(name), root.join("runx").join(name)] {
            if let Some(skill_dir) = skill_dir_if_exists(&candidate) {
                return Ok(Some(skill_dir));
            }
            let versioned = versioned_skill_dirs(&candidate)?;
            if versioned.len() == 1 {
                return Ok(versioned.into_iter().next());
            }
            if versioned.len() > 1 {
                return Err(RunxExportLoadError::InvalidArgs(format!(
                    "official skill reference {reference} is ambiguous in {}; use an explicit skill path",
                    display_path(&candidate)
                )));
            }
        }
    }
    Ok(None)
}

fn official_skill_name(reference: &str) -> Option<&str> {
    if reference
        .chars()
        .all(|character| character.is_ascii_alphanumeric() || matches!(character, '-' | '_' | '.'))
    {
        return Some(reference);
    }
    reference.strip_prefix("runx/").filter(|name| {
        !name.is_empty()
            && name.chars().all(|character| {
                character.is_ascii_alphanumeric() || matches!(character, '-' | '_' | '.')
            })
    })
}

fn versioned_skill_dirs(root: &Path) -> Result<Vec<PathBuf>, RunxExportLoadError> {
    let entries = match fs::read_dir(root) {
        Ok(entries) => entries,
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
        Err(source) => {
            return Err(RunxExportLoadError::Io {
                context: format!("reading {}", display_path(root)),
                source,
            });
        }
    };
    let mut dirs = Vec::new();
    for entry in entries {
        let entry = entry.map_err(|source| RunxExportLoadError::Io {
            context: format!("reading {}", display_path(root)),
            source,
        })?;
        if let Some(skill_dir) = skill_dir_if_exists(&entry.path()) {
            dirs.push(skill_dir);
        }
    }
    dirs.sort();
    Ok(dirs)
}

fn canonicalize(path: &Path, context: &str) -> Result<PathBuf, RunxExportLoadError> {
    fs::canonicalize(path).map_err(|source| RunxExportLoadError::Io {
        context: format!("{context} {}", display_path(path)),
        source,
    })
}

fn display_path(path: &Path) -> String {
    path.to_string_lossy().into_owned()
}