bot-forge 1.0.2

Rust CLI for installing agent skills and developer tools from configurable forms.
Documentation
use sha2::{Digest, Sha256};

use crate::config::schema::{ConfigDocument, OriginMap};
use crate::constants::BUILTIN_CATALOG;
use crate::error::ForgeError;

pub(crate) fn load_builtin() -> Result<(ConfigDocument, OriginMap), ForgeError> {
    let mut document = ConfigDocument::parse_catalog(BUILTIN_CATALOG)?;
    let digest = Sha256::digest(BUILTIN_CATALOG.as_bytes())
        .iter()
        .map(|byte| format!("{byte:02x}"))
        .collect::<String>();
    document.catalog = "rust-dev".to_string();
    document.catalog_digest = digest.clone();
    let mut origins = OriginMap::default();
    let origin = format!("builtin:rust-dev@sha256:{digest}");
    for field in [
        "policy.network",
        "policy.allow_shell",
        "policy.allow_unlocked_cargo",
        "policy.max_parallel",
        "policy.max_downloads",
        "environment.mutations.managed-bin",
    ] {
        origins.record(field, &origin);
    }
    for name in document.versions.keys() {
        origins.record(format!("versions.{name}"), &origin);
    }
    for name in document.groups.keys() {
        origins.record(format!("groups.{name}"), &origin);
    }
    for name in document.profiles.keys() {
        origins.record(format!("profiles.{name}"), &origin);
    }
    for component in &document.components {
        origins.record(format!("components.{}", component.id), &origin);
    }
    record_shorthand_origins(BUILTIN_CATALOG, &origin, &mut origins)?;
    origins.record("catalog.rust-dev", origin);
    Ok((document, origins))
}

fn record_shorthand_origins(
    input: &str,
    origin: &str,
    origins: &mut OriginMap,
) -> Result<(), ForgeError> {
    let value: toml::Value = toml::from_str(input)
        .map_err(|error| ForgeError::Parse(format!("invalid TOML: {error}")))?;
    for section in ["cargo-tools", "package-tools", "rustup-tools"] {
        if let Some(table) = value.get(section).and_then(toml::Value::as_table) {
            for id in table.keys() {
                origins.record(
                    format!("components.{id}"),
                    format!("{origin} (expanded from {section}.{id})"),
                );
            }
        }
    }
    if let Some(toolsets) = value.get("cargo-toolsets").and_then(toml::Value::as_table) {
        for (group, tools) in toolsets {
            if let Some(tools) = tools.as_table() {
                for id in tools.keys() {
                    origins.record(
                        format!("components.{id}"),
                        format!("{origin} (expanded from cargo-toolsets.{group}.{id})"),
                    );
                }
            }
        }
    }
    Ok(())
}

pub(crate) fn validate_selection(input: &str, origin: &str) -> Result<(), ForgeError> {
    let value: toml::Value = toml::from_str(input)
        .map_err(|error| ForgeError::Parse(format!("invalid TOML: {error}")))?;
    if value.get("catalog").and_then(toml::Value::as_str) != Some("rust-dev") {
        return Err(ForgeError::Config(format!(
            "main configuration {origin} must declare catalog = \"rust-dev\""
        )));
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use crate::config::catalog::load_builtin;
    use crate::config::schema::InstallSpec;
    use crate::planning::{PlanRequest, TargetPlatform, build_plan};

    #[test]
    fn supported_platform_profiles_always_plan() {
        let (document, origins) = load_builtin().unwrap();
        for target in [
            TargetPlatform {
                os: "linux".into(),
                arch: "x86_64".into(),
                abi: "gnu".into(),
            },
            TargetPlatform {
                os: "windows".into(),
                arch: "x86_64".into(),
                abi: "msvc".into(),
            },
            TargetPlatform {
                os: "windows".into(),
                arch: "aarch64".into(),
                abi: "msvc".into(),
            },
            TargetPlatform {
                os: "macos".into(),
                arch: "x86_64".into(),
                abi: "native".into(),
            },
            TargetPlatform {
                os: "macos".into(),
                arch: "aarch64".into(),
                abi: "native".into(),
            },
        ] {
            for profile in ["minimal", "standard", "advanced"] {
                let plan = build_plan(PlanRequest {
                    document: &document,
                    origins: &origins,
                    profile,
                    target: target.clone(),
                    only: &[],
                    exclude: &[],
                    source_root: None,
                })
                .unwrap_or_else(|error| {
                    panic!("{profile} must plan for {}: {error}", target.selector())
                });
                if profile == "advanced" {
                    let has_valgrind = plan.components.iter().any(|component| {
                        matches!(component.id.as_str(), "valgrind" | "cargo-valgrind")
                    });
                    assert_eq!(has_valgrind, target.os == "linux");
                    let node = plan
                        .components
                        .iter()
                        .find(|component| component.id == "nodejs")
                        .expect("advanced must include Node.js 22");
                    match (&target.os[..], &node.install) {
                        ("linux", Some(InstallSpec::Archive(_)))
                        | ("macos", Some(InstallSpec::Brew(_)))
                        | ("windows", Some(InstallSpec::Archive(_))) => {}
                        _ => panic!("unexpected Node.js backend for {}", target.selector()),
                    }
                }
            }
        }
        let plan = build_plan(PlanRequest {
            document: &document,
            origins: &origins,
            profile: "advanced",
            target: TargetPlatform {
                os: "linux".into(),
                arch: "x86_64".into(),
                abi: "gnu".into(),
            },
            only: &[],
            exclude: &[],
            source_root: None,
        })
        .unwrap();
        assert!(
            plan.components
                .iter()
                .any(|component| component.id == "valgrind")
        );
        assert!(
            plan.components
                .iter()
                .any(|component| component.id == "cargo-valgrind")
        );
        for (id, version) in [("bot-gate", "=1.2.1"), ("bot-metric", "=1.2.0")] {
            let component = plan
                .components
                .iter()
                .find(|component| component.id == id)
                .unwrap_or_else(|| panic!("standard profile must include {id}"));
            let Some(InstallSpec::Cargo(install)) = &component.install else {
                panic!("{id} must use the Cargo backend");
            };
            assert_eq!(install.crate_name, id);
            assert_eq!(install.version, version);
            assert!(install.source.is_none(), "{id} must use crates.io");
            assert!(
                !component
                    .dependencies
                    .iter()
                    .any(|dependency| dependency == "git"),
                "{id} must not require Git"
            );
            assert!(
                component
                    .dependencies
                    .iter()
                    .any(|dependency| dependency == "rust-toolchain")
            );
        }
        let rust_bot = plan
            .components
            .iter()
            .find(|component| component.id == "rust-bot")
            .expect("minimal level must include rust-bot");
        let Some(InstallSpec::Cargo(rust_bot_install)) = &rust_bot.install else {
            panic!("rust-bot must use the Cargo backend");
        };
        assert_eq!(rust_bot_install.crate_name, "rust-bot");
        assert_eq!(rust_bot_install.version, "=2.0.0");
        assert!(
            rust_bot_install.source.is_none(),
            "rust-bot must use crates.io"
        );
        assert!(rust_bot_install.revision.is_none());
        assert!(
            !rust_bot
                .dependencies
                .iter()
                .any(|dependency| dependency == "git")
        );
        for component in &plan.components {
            if let Some(InstallSpec::Cargo(cargo)) = &component.install {
                assert_eq!(
                    cargo.toolchain.as_deref(),
                    Some("1.89.0"),
                    "{} must use the pinned Rust toolchain",
                    component.id
                );
            }
        }
        for component in plan.components.iter().filter(|component| {
            matches!(component.install, Some(InstallSpec::Cargo(_)))
                || matches!(
                    component.install,
                    Some(InstallSpec::Rustup(ref rustup))
                        if rustup.bootstrap.is_none()
                )
        }) {
            assert!(
                component
                    .dependencies
                    .iter()
                    .any(|dependency| dependency == "rust-toolchain"),
                "{} must depend on the typed Rust provider",
                component.id
            );
        }
    }
}