orcs-app 0.1.0

ORCS Application Layer - Re-exports and AppError
Documentation
//! Builtin components shipped with the binary.
//!
//! Components are embedded at compile time via `build.rs` auto-generation.
//! At runtime, [`ensure_expanded`] writes them to a versioned directory
//! under the configured builtins path (default `~/.orcs/builtins/vX.Y.Z/components/`).
//!
//! Users never need to touch this directory — it is fully managed by ORCS.
//! User components and customizations live in separate paths (e.g. `~/.orcs/components/`).

use std::path::{Path, PathBuf};

// Auto-generated by build.rs: array of (relative_path, source_content).
include!(concat!(env!("OUT_DIR"), "/builtins_gen.rs"));

/// Returns the versioned builtins directory: `{base}/v{CARGO_PKG_VERSION}/components`.
#[must_use]
pub fn versioned_dir(base: &Path) -> PathBuf {
    base.join(format!("v{}", env!("CARGO_PKG_VERSION")))
        .join("components")
}

/// Ensures all builtin components are expanded to disk at the versioned directory.
///
/// - If the versioned directory already exists, returns `Ok(vec![])` immediately.
/// - Otherwise creates the full directory tree and writes all files.
/// - Directory structure is preserved (e.g. `skill_manager/init.lua`).
///
/// # Errors
///
/// Returns `std::io::Error` if directory creation or file writes fail.
pub fn ensure_expanded(base_dir: &Path) -> Result<Vec<PathBuf>, std::io::Error> {
    let target = versioned_dir(base_dir);

    if target.exists() {
        return Ok(vec![]);
    }

    write_all(&target)
}

/// Force-expands builtin components, removing any existing versioned directory first.
///
/// Unlike [`ensure_expanded`], this always writes fresh copies of all files,
/// even if the versioned directory already exists.
///
/// # Errors
///
/// Returns `std::io::Error` if removal, directory creation, or file writes fail.
pub fn force_expand(base_dir: &Path) -> Result<Vec<PathBuf>, std::io::Error> {
    let target = versioned_dir(base_dir);

    if target.exists() {
        std::fs::remove_dir_all(&target)?;
    }

    write_all(&target)
}

/// Writes all embedded builtin files to the target directory.
fn write_all(target: &Path) -> Result<Vec<PathBuf>, std::io::Error> {
    let mut written = Vec::new();

    for (rel_path, content) in FILES {
        let dest = target.join(rel_path);
        if let Some(parent) = dest.parent() {
            std::fs::create_dir_all(parent)?;
        }
        std::fs::write(&dest, content)?;
        written.push(dest);
    }

    Ok(written)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn files_is_not_empty() {
        assert!(
            !FILES.is_empty(),
            "build.rs should have generated at least one entry"
        );
    }

    #[test]
    fn files_contains_echo() {
        let has_echo = FILES.iter().any(|(path, _)| *path == "echo.lua");
        assert!(has_echo, "builtins should include echo.lua");
    }

    #[test]
    fn files_contains_skill_manager_init() {
        let has = FILES
            .iter()
            .any(|(path, _)| *path == "skill_manager/init.lua");
        assert!(has, "builtins should include skill_manager/init.lua");
    }

    #[test]
    fn versioned_dir_includes_version() {
        let dir = versioned_dir(Path::new("/base"));
        let dir_str = dir.to_string_lossy();
        assert!(
            dir_str.contains(env!("CARGO_PKG_VERSION")),
            "should contain crate version: {dir_str}"
        );
        assert!(
            dir_str.ends_with("components"),
            "should end with components: {dir_str}"
        );
    }

    #[test]
    fn ensure_expanded_creates_files() {
        let tmp = tempfile::tempdir().expect("create temp dir");
        let written = ensure_expanded(tmp.path()).expect("expansion should succeed");

        assert_eq!(written.len(), FILES.len(), "should write all files");

        // Verify directory component exists
        let sm_init = versioned_dir(tmp.path()).join("skill_manager/init.lua");
        assert!(sm_init.exists(), "skill_manager/init.lua should exist");
    }

    #[test]
    fn ensure_expanded_skips_existing_version() {
        let tmp = tempfile::tempdir().expect("create temp dir");
        ensure_expanded(tmp.path()).expect("first expansion");

        let second = ensure_expanded(tmp.path()).expect("second expansion");
        assert!(second.is_empty(), "should skip existing version");
    }

    #[test]
    fn force_expand_overwrites_existing() {
        let tmp = tempfile::tempdir().expect("create temp dir");

        // First expansion
        let first = ensure_expanded(tmp.path()).expect("initial expansion");
        assert_eq!(first.len(), FILES.len(), "should write all files initially");

        // Tamper with a file to verify it gets overwritten
        let echo_path = versioned_dir(tmp.path()).join("echo.lua");
        std::fs::write(&echo_path, "-- tampered").expect("should write tamper");
        let tampered = std::fs::read_to_string(&echo_path).expect("should read tampered");
        assert_eq!(tampered, "-- tampered");

        // Force expand should overwrite
        let forced = force_expand(tmp.path()).expect("force expansion");
        assert_eq!(forced.len(), FILES.len(), "should write all files again");

        // Verify tampered file is restored
        let restored = std::fs::read_to_string(&echo_path).expect("should read restored");
        assert_ne!(
            restored, "-- tampered",
            "tampered file should be overwritten"
        );
    }

    #[test]
    fn force_expand_works_on_empty_dir() {
        let tmp = tempfile::tempdir().expect("create temp dir");

        // Force expand on fresh directory (no existing version)
        let written = force_expand(tmp.path()).expect("force expansion on empty");
        assert_eq!(written.len(), FILES.len(), "should write all files");

        let sm_init = versioned_dir(tmp.path()).join("skill_manager/init.lua");
        assert!(sm_init.exists(), "skill_manager/init.lua should exist");
    }
}