o7 0.1.0

O7 workflow DSL runner
Documentation
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;

/// Embedded builtin workflow files (compiled into the binary).
pub struct BuiltinFiles {
    pub files: HashMap<&'static str, &'static str>,
}

pub fn get_generate_builtin() -> BuiltinFiles {
    let mut files = HashMap::new();
    files.insert("main.7", include_str!("../builtins/generate/main.7"));
    files.insert(
        ".7/harnesses.toml",
        include_str!("../builtins/generate/.7/harnesses.toml"),
    );
    files.insert(
        "prompts/gather-requirements.md",
        include_str!("../builtins/generate/prompts/gather-requirements.md"),
    );
    files.insert(
        "scripts/has-unanswered-questions.sh",
        include_str!("../builtins/generate/scripts/has-unanswered-questions.sh"),
    );
    BuiltinFiles { files }
}

/// List all available builtin names.
pub fn list_builtins() -> Vec<&'static str> {
    vec!["generate"]
}

/// Extract a builtin to a temporary directory and return its path.
pub fn extract_builtin_to_temp(name: &str) -> Result<PathBuf, String> {
    let builtin = match name {
        "generate" => get_generate_builtin(),
        _ => return Err(format!("Unknown builtin: @{}", name)),
    };

    // Use a unique suffix per invocation to avoid collisions between concurrent calls
    // (e.g. parallel tests or multiple concurrent runs in the same process).
    use std::sync::atomic::{AtomicU64, Ordering};
    static COUNTER: AtomicU64 = AtomicU64::new(0);
    let unique = COUNTER.fetch_add(1, Ordering::Relaxed);
    let temp_dir = std::env::temp_dir().join(format!("o7-builtin-{}-{}-{}", name, std::process::id(), unique));
    fs::create_dir_all(&temp_dir)
        .map_err(|e| format!("Failed to create temp dir: {}", e))?;

    for (path, content) in &builtin.files {
        let file_path = temp_dir.join(path);
        if let Some(parent) = file_path.parent() {
            fs::create_dir_all(parent)
                .map_err(|e| format!("Failed to create dir: {}", e))?;
        }
        fs::write(&file_path, content)
            .map_err(|e| format!("Failed to write file: {}", e))?;

        // Make scripts executable
        if path.ends_with(".sh") {
            #[cfg(unix)]
            {
                use std::os::unix::fs::PermissionsExt;
                let perms = fs::Permissions::from_mode(0o755);
                fs::set_permissions(&file_path, perms)
                    .map_err(|e| format!("Failed to set permissions: {}", e))?;
            }
        }
    }

    Ok(temp_dir.join("main.7"))
}

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

    #[test]
    fn test_list_builtins() {
        let builtins = list_builtins();
        assert!(builtins.contains(&"generate"));
    }

    #[test]
    fn test_extract_generate_builtin() {
        let path = extract_builtin_to_temp("generate").unwrap();
        assert!(path.exists());
        assert!(path.ends_with("main.7"));
        // Verify the file content is valid
        let content = std::fs::read_to_string(&path).unwrap();
        assert!(content.contains("version 1"));
        assert!(content.contains("workflow main"));
        // Clean up
        if let Some(parent) = path.parent() {
            let _ = std::fs::remove_dir_all(parent);
        }
    }

    #[test]
    fn test_extract_unknown_builtin() {
        let result = extract_builtin_to_temp("nonexistent");
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("Unknown builtin"));
    }

    #[test]
    fn test_resolve_workflow_path_builtin() {
        let (path, project_root) = resolve_workflow_path("@generate").unwrap();
        assert!(path.exists());
        assert!(path.ends_with("main.7"));
        assert!(project_root.exists());
        // Clean up
        let _ = std::fs::remove_dir_all(&project_root);
    }

    #[test]
    fn test_resolve_workflow_path_unknown_builtin() {
        let result = resolve_workflow_path("@nonexistent");
        assert!(result.is_err());
    }

    #[test]
    fn test_harnesses_toml_is_extracted() {
        let path = extract_builtin_to_temp("generate").unwrap();
        let harnesses_path = path.parent().unwrap().join(".7/harnesses.toml");
        assert!(harnesses_path.exists(), "harnesses.toml should be extracted");
        let content = std::fs::read_to_string(&harnesses_path).unwrap();
        assert!(content.contains("[harness.check-script]"), "Should define check-script harness");
        assert!(content.contains("[harness.claude-code]"), "Should define claude-code harness");
        // Verify it loads as valid config
        let config = crate::harness::config::load_harness_config(
            path.parent().unwrap().to_str().unwrap()
        ).unwrap();
        assert!(config.harness.contains_key("check-script"));
        assert!(config.harness.contains_key("claude-code"));
        // check-script should have no prompt_slot
        assert!(config.harness["check-script"].prompt_slot.is_none());
        // Clean up
        if let Some(parent) = path.parent() {
            let _ = std::fs::remove_dir_all(parent);
        }
    }

    #[test]
    fn test_check_script_is_executable() {
        let path = extract_builtin_to_temp("generate").unwrap();
        let script_path = path.parent().unwrap().join("scripts/has-unanswered-questions.sh");
        assert!(script_path.exists());

        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let perms = std::fs::metadata(&script_path).unwrap().permissions();
            assert!(perms.mode() & 0o111 != 0, "Script should be executable");
        }

        // Clean up
        if let Some(parent) = path.parent() {
            let _ = std::fs::remove_dir_all(parent);
        }
    }
}

/// Resolve a workflow path that might be a builtin (@name) or a regular file path.
pub fn resolve_workflow_path(input: &str) -> Result<(PathBuf, PathBuf), String> {
    if input.starts_with('@') {
        let builtin_name = &input[1..];
        let main_path = extract_builtin_to_temp(builtin_name)?;
        let project_root = main_path.parent()
            .ok_or_else(|| "Invalid builtin path".to_string())?
            .to_path_buf();
        Ok((main_path, project_root))
    } else {
        let path = PathBuf::from(input).canonicalize()
            .map_err(|e| format!("File not found: {}: {}", input, e))?;
        let project_root = path.parent()
            .ok_or_else(|| "Cannot determine project root".to_string())?
            .to_path_buf();
        Ok((path, project_root))
    }
}