malvin 0.2.5

Non-interactive research and coding agent
use std::path::{Path, PathBuf};

const ENV_BRIDGE: &str = "MALVIN_CURSOR_SDK_BRIDGE";
const BRIDGE_JS: &str = "cursor-sdk-bridge/dist/bridge.js";
const MODELS_JS: &str = "cursor-sdk-bridge/dist/models.js";
const SDK_MARKER: &str = "cursor-sdk-bridge/node_modules/@cursor/sdk/package.json";

pub fn resolve_bridge_js() -> Result<PathBuf, String> {
    if let Some(p) = std::env::var_os(ENV_BRIDGE).filter(|v| !v.is_empty()) {
        let path = PathBuf::from(p);
        if path.is_file() {
            return Ok(path);
        }
        return Err(format!(
            "{ENV_BRIDGE} is set but not a file: {}",
            path.display()
        ));
    }
    if let Some(path) = cursor_first_ready_bridge_js() {
        return Ok(path);
    }
    if let Some(path) = cursor_first_any_bridge_js() {
        return Ok(path);
    }
    Err(
        "cursor-sdk-bridge/dist/bridge.js not found (Cursor SDK bridge). \
         Reinstall with `cargo install malvin` (build.rs installs @cursor/sdk under \
         ~/.malvin_home/sdk-bridges/), or run `npm ci && npm run build` in cursor-sdk-bridge/, \
         or set MALVIN_CURSOR_SDK_BRIDGE"
            .to_string(),
    )
}

pub fn resolve_models_js() -> Result<PathBuf, String> {
    if let Ok(bridge) = resolve_bridge_js() {
        let models = bridge.with_file_name("models.js");
        if models.is_file() {
            return Ok(models);
        }
    }
    if let Some(path) = cursor_first_ready_models_js() {
        return Ok(path);
    }
    if let Some(path) = cursor_first_any_models_js() {
        return Ok(path);
    }
    Err("cursor-sdk-bridge/dist/models.js not found".to_string())
}

fn cursor_first_ready_bridge_js() -> Option<PathBuf> {
    for root in cursor_candidate_roots() {
        if !cursor_sdk_marker_present(&root) {
            continue;
        }
        let candidate = root.join(BRIDGE_JS);
        if candidate.is_file() {
            return Some(candidate);
        }
    }
    None
}

fn cursor_first_any_bridge_js() -> Option<PathBuf> {
    for root in cursor_candidate_roots() {
        let candidate = root.join(BRIDGE_JS);
        if candidate.is_file() {
            return Some(candidate);
        }
    }
    None
}

fn cursor_first_ready_models_js() -> Option<PathBuf> {
    for root in cursor_candidate_roots() {
        if !cursor_sdk_marker_present(&root) {
            continue;
        }
        let candidate = root.join(MODELS_JS);
        if candidate.is_file() {
            return Some(candidate);
        }
    }
    None
}

fn cursor_first_any_models_js() -> Option<PathBuf> {
    for root in cursor_candidate_roots() {
        let candidate = root.join(MODELS_JS);
        if candidate.is_file() {
            return Some(candidate);
        }
    }
    None
}

pub(crate) fn cursor_sdk_marker_present(root: &Path) -> bool {
    root.join(SDK_MARKER).is_file()
}

fn cursor_candidate_roots() -> Vec<PathBuf> {
    let mut roots = Vec::new();
    if let Ok(cwd) = std::env::current_dir() {
        roots.push(cwd);
    }
    roots.push(PathBuf::from(env!("CARGO_MANIFEST_DIR")));
    roots.push(
        crate::user_home::user_home_dir()
            .join(".malvin_home")
            .join("sdk-bridges"),
    );
    if let Ok(exe) = std::env::current_exe()
        && let Some(dir) = exe.parent()
    {
        roots.push(dir.to_path_buf());
        if let Some(parent) = dir.parent() {
            roots.push(parent.to_path_buf());
        }
    }
    roots
}