unified-agent-api 0.2.3

Agent-agnostic facade and registry for wrapper backends
Documentation
use std::{
    collections::BTreeMap,
    env, fs,
    path::{Path, PathBuf},
    process::Command,
    sync::OnceLock,
    time::Duration,
};

use crate::{
    backends::opencode::{OpencodeBackend, OpencodeBackendConfig},
    AgentWrapperRunRequest,
};

static FAKE_BINARY: OnceLock<PathBuf> = OnceLock::new();

pub(super) fn repo_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")
}

pub(super) fn target_dir() -> PathBuf {
    env::var_os("CARGO_TARGET_DIR")
        .map(PathBuf::from)
        .unwrap_or_else(|| repo_root().join("target"))
}

pub(super) fn target_debug_binary(name: &str) -> PathBuf {
    let binary_name = if cfg!(windows) {
        format!("{name}.exe")
    } else {
        name.to_string()
    };
    target_dir().join("debug").join(binary_name)
}

pub(super) fn fake_opencode_run_json_binary() -> PathBuf {
    FAKE_BINARY
        .get_or_init(|| {
            let binary = target_debug_binary("fake_opencode_run_json");
            if binary.exists() {
                return binary;
            }

            let output = Command::new("cargo")
                .args([
                    "build",
                    "-p",
                    "unified-agent-api-opencode",
                    "--bin",
                    "fake_opencode_run_json",
                    "--all-features",
                ])
                .current_dir(repo_root())
                .output()
                .expect("spawn cargo build for fake opencode binary");

            assert!(
                output.status.success(),
                "cargo build failed: status={:?}, stderr={}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
            assert!(
                binary.exists(),
                "fake opencode binary should exist after cargo build"
            );
            binary
        })
        .clone()
}

pub(super) fn backend_with_env(env: BTreeMap<String, String>) -> OpencodeBackend {
    backend_with_config(OpencodeBackendConfig {
        binary: Some(fake_opencode_run_json_binary()),
        default_timeout: None,
        env,
    })
}

pub(super) fn backend_with_timeout(
    env: BTreeMap<String, String>,
    timeout: Duration,
) -> OpencodeBackend {
    backend_with_config(OpencodeBackendConfig {
        binary: Some(fake_opencode_run_json_binary()),
        default_timeout: Some(timeout),
        env,
    })
}

pub(super) fn backend_with_config(config: OpencodeBackendConfig) -> OpencodeBackend {
    OpencodeBackend::new(config)
}

pub(super) fn request(prompt: &str, working_dir: Option<&Path>) -> AgentWrapperRunRequest {
    AgentWrapperRunRequest {
        prompt: prompt.to_string(),
        working_dir: working_dir.map(Path::to_path_buf),
        ..Default::default()
    }
}

pub(super) fn capture_json(path: &Path) -> serde_json::Value {
    let bytes = fs::read(path).expect("read capture file");
    serde_json::from_slice(&bytes).expect("parse capture json")
}