bot-forge 1.0.2

Rust CLI for installing agent skills and developer tools from configurable forms.
Documentation
//! Merge Cargo configuration fragments without losing TOML table boundaries.

use std::fs;
use std::path::Path;

use toml_edit::{DocumentMut, TableLike};

use crate::error::ForgeError;
use crate::ui::{self, StatusKind};

pub(super) fn is_cargo_config(path: &Path) -> bool {
    if !matches!(
        path.file_name().and_then(|name| name.to_str()),
        Some("config" | "config.toml")
    ) {
        return false;
    }
    let Some(parent) = path.parent() else {
        return false;
    };
    if parent.file_name().is_some_and(|name| name == ".cargo") {
        return true;
    }
    std::env::var_os("CARGO_HOME")
        .filter(|home| !home.is_empty())
        .and_then(|home| std::path::absolute(home).ok())
        .zip(std::path::absolute(parent).ok())
        .is_some_and(|(home, parent)| super::paths_equal(&home, &parent))
}

pub(super) fn merge(label: &str, path: &Path, lines: &[String]) -> Result<(), ForgeError> {
    let existing = match fs::read_to_string(path) {
        Ok(text) => text,
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => String::new(),
        Err(source) => {
            return Err(ForgeError::Io {
                path: path.to_owned(),
                source,
            });
        }
    };
    let parse = |text: &str| {
        text.parse::<DocumentMut>().map_err(|error| {
            ForgeError::Config(format!(
                "cannot merge Cargo configuration {}: {error}",
                path.display()
            ))
        })
    };
    let mut document = parse(&existing)?;
    let fragment = parse(&lines.join("\n"))?;
    merge_tables(document.as_table_mut(), fragment.as_table());
    let merged = document.to_string();
    // Compare values as well as validating the final document. Repeated application must
    // not rewrite a user's file merely because the fragment uses different formatting.
    let values = |text: &str| {
        toml::from_str::<toml::Value>(text).map_err(|error| {
            ForgeError::Config(format!(
                "invalid Cargo configuration {}: {error}",
                path.display()
            ))
        })
    };
    if values(&existing)? == values(&merged)? {
        ui::stdout_status(
            StatusKind::Info,
            &format!(
                "{label} already contains bot-forge configuration · {}",
                path.display()
            ),
        );
        return Ok(());
    }
    super::write_text(path, &merged)?;
    ui::stdout_status(
        StatusKind::Success,
        &format!("{label} updated · {}", path.display()),
    );
    Ok(())
}

fn merge_tables(target: &mut dyn TableLike, fragment: &dyn TableLike) {
    for (key, incoming) in fragment.iter() {
        if let Some(current) = target.get_mut(key) {
            if let (Some(target_table), Some(source_table)) =
                (current.as_table_like_mut(), incoming.as_table_like())
            {
                merge_tables(target_table, source_table);
                continue;
            }
            let mut replacement = incoming.clone();
            if let (Some(old), Some(new)) = (current.as_value(), replacement.as_value_mut()) {
                *new.decor_mut() = old.decor().clone();
            }
            *current = replacement;
        } else {
            target.insert(key, incoming.clone());
        }
    }
}