crabmate 0.5.0

Rust AI agent: OpenAI-compatible chat/completions, function calling, HTTP serve, ops CLI
Documentation
//! 原子写盘与目录权限。

use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};

use serde::Serialize;
use serde::de::DeserializeOwned;

pub fn ensure_tree(root: &Path) -> Result<(), String> {
    for sub in [
        root,
        &root.join("global"),
        &root.join("workspaces"),
        &root.join("secrets"),
    ] {
        if sub.exists() {
            restrict_dir(sub)?;
        } else {
            fs::create_dir_all(sub).map_err(|e| format!("创建目录 {}: {e}", sub.display()))?;
            restrict_dir(sub)?;
        }
    }
    Ok(())
}

pub fn restrict_dir(p: &Path) -> Result<(), String> {
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let meta = fs::metadata(p).map_err(|e| format!("metadata {}: {e}", p.display()))?;
        let mut perm = meta.permissions();
        perm.set_mode(0o700);
        fs::set_permissions(p, perm).map_err(|e| format!("chmod {}: {e}", p.display()))?;
    }
    Ok(())
}

pub fn read_json_file<T: DeserializeOwned>(path: &Path) -> Result<T, String> {
    if !path.is_file() {
        return Err(format!("文件不存在: {}", path.display()));
    }
    let raw = fs::read_to_string(path).map_err(|e| format!("读取 {}: {e}", path.display()))?;
    serde_json::from_str(&raw).map_err(|e| format!("解析 {}: {e}", path.display()))
}

pub fn read_json_file_or_default<T: DeserializeOwned + Default>(path: &Path) -> T {
    if !path.is_file() {
        return T::default();
    }
    read_json_file(path).unwrap_or_default()
}

pub fn write_json_atomic<T: Serialize>(path: &Path, value: &T) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).map_err(|e| format!("创建目录 {}: {e}", parent.display()))?;
    }
    let tmp: PathBuf = path.with_extension("json.tmp");
    let json = serde_json::to_string_pretty(value)
        .map_err(|e| format!("序列化 {}: {e}", path.display()))?;
    {
        let mut f =
            fs::File::create(&tmp).map_err(|e| format!("创建临时文件 {}: {e}", tmp.display()))?;
        f.write_all(json.as_bytes())
            .map_err(|e| format!("写入 {}: {e}", tmp.display()))?;
        f.sync_all()
            .map_err(|e| format!("sync {}: {e}", tmp.display()))?;
    }
    fs::rename(&tmp, path).map_err(|e| format!("rename {}: {e}", path.display()))?;
    Ok(())
}

pub fn read_secret_line(path: &Path) -> Option<String> {
    let raw = fs::read_to_string(path).ok()?;
    let t = raw.trim();
    if t.is_empty() {
        None
    } else {
        Some(t.to_string())
    }
}