malvin 0.2.4

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

use super::{MALVIN_CHECKS_LEGACY_REL, MALVIN_CHECKS_REL, MALVIN_DIR};

#[must_use]
pub fn malvin_data_root(work_dir: &Path) -> PathBuf {
    git_worktree_toplevel(work_dir).unwrap_or_else(|| crate::user_home_dir().join(MALVIN_DIR))
}

fn malvin_layout_dir(work_dir: &Path) -> PathBuf {
    if git_worktree_toplevel(work_dir).is_some() {
        malvin_data_root(work_dir).join(MALVIN_DIR)
    } else {
        malvin_data_root(work_dir)
    }
}

#[must_use]
pub fn malvin_checks_path(work_dir: &Path) -> PathBuf {
    malvin_layout_dir(work_dir).join("gates")
}

#[must_use]
pub fn legacy_malvin_checks_path(work_dir: &Path) -> PathBuf {
    work_dir.join(MALVIN_CHECKS_LEGACY_REL)
}

#[must_use]
pub fn resolve_malvin_checks_path(work_dir: &Path) -> PathBuf {
    let primary = malvin_checks_path(work_dir);
    if primary.is_file() {
        return primary;
    }
    let cwd_gates = work_dir.join(MALVIN_CHECKS_REL);
    if cwd_gates.is_file() {
        return cwd_gates;
    }
    let layout_legacy = primary.with_file_name("checks");
    if layout_legacy.is_file() {
        return layout_legacy;
    }
    let legacy = legacy_malvin_checks_path(work_dir);
    if legacy.is_file() {
        return legacy;
    }
    primary
}

#[must_use]
pub fn malvin_acp_spawn_chamber_dir(work_dir: &Path) -> PathBuf {
    let home_chamber = malvin_layout_dir(work_dir).join("acp_spawn");
    if directory_is_writable(home_chamber.parent().unwrap_or(&home_chamber)) {
        home_chamber
    } else {
        work_dir.join(MALVIN_DIR).join("acp_spawn")
    }
}

fn directory_is_writable(dir: &Path) -> bool {
    if std::fs::create_dir_all(dir).is_err() {
        return false;
    }
    let probe = dir.join(format!(".write-probe-{}", std::process::id()));
    let writable = std::fs::write(&probe, []).is_ok();
    let _ = std::fs::remove_file(probe);
    writable
}

#[must_use]
pub fn git_worktree_toplevel(work_dir: &Path) -> Option<PathBuf> {
    let output = Command::new("git")
        .args(["rev-parse", "--show-toplevel"])
        .current_dir(work_dir)
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    let root = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if root.is_empty() {
        return None;
    }
    Some(PathBuf::from(root))
}