bot-forge 1.0.2

Rust CLI for installing agent skills and developer tools from configurable forms.
Documentation
#![cfg(target_os = "macos")]

use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::time::{SystemTime, UNIX_EPOCH};

#[test]
fn default_paths_use_application_support_and_allow_spaces() {
    let temp = test_dir("paths with spaces");
    let user_home = temp.join("user home");
    fs::create_dir_all(&user_home).unwrap();
    let output = Command::new(bin())
        .env_remove("BOT_FORGE_HOME")
        .env_remove("BOT_FORGE_CONFIG_DIR")
        .env("HOME", &user_home)
        .env("USERPROFILE", &user_home)
        .args(["doctor", "--format", "json"])
        .output()
        .unwrap();
    let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
    let data = value
        .as_array()
        .unwrap()
        .iter()
        .find(|check| check["id"] == "filesystem.data-dir-permission")
        .unwrap();
    assert_eq!(
        data["summary"],
        format!(
            "{} writable",
            user_home
                .join("Library/Application Support/bot-forge")
                .display()
        )
    );
    assert!(
        user_home
            .join("Library/Application Support/bot-forge")
            .is_dir()
    );
    fs::remove_dir_all(temp).unwrap();
}

#[test]
fn home_takes_precedence_over_a_foreign_userprofile() {
    let temp = test_dir("home precedence");
    let user_home = temp.join("mac home");
    let foreign_home = temp.join("foreign profile");
    let output = Command::new(bin())
        .env_remove("BOT_FORGE_HOME")
        .env_remove("BOT_FORGE_CONFIG_DIR")
        .env("HOME", &user_home)
        .env("USERPROFILE", &foreign_home)
        .args(["doctor", "--format", "json"])
        .output()
        .unwrap();
    assert_success(&output);
    assert!(
        user_home
            .join("Library/Application Support/bot-forge")
            .is_dir()
    );
    assert!(!foreign_home.exists());
    fs::remove_dir_all(temp).unwrap();
}

#[test]
fn doctor_reports_the_explicit_config_and_checks_clang_with_xcrun() {
    let temp = test_dir("doctor config");
    let config = temp.join("selected.toml");
    fs::create_dir_all(&temp).unwrap();
    fs::write(&config, "catalog = \"rust-dev\"\n").unwrap();
    let output = Command::new(bin())
        .args(["doctor", "--format", "json", "--config", text(&config)])
        .output()
        .unwrap();
    assert_success(&output);
    let checks: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
    let source = checks
        .as_array()
        .unwrap()
        .iter()
        .find(|check| check["id"] == "config.source")
        .unwrap();
    assert_eq!(source["summary"], config.display().to_string());
    assert!(
        checks
            .as_array()
            .unwrap()
            .iter()
            .any(|check| check["id"] == "system.macos-translation")
    );
    fs::remove_dir_all(temp).unwrap();
}

#[test]
fn default_catalog_selects_only_macos_backends() {
    let temp = test_dir("default-plan");
    let config = temp.join("bot-forge.toml");
    fs::create_dir_all(&temp).unwrap();
    fs::write(&config, "catalog = \"rust-dev\"\n").unwrap();
    for profile in ["minimal", "standard", "advanced"] {
        let output = Command::new(bin())
            .args([
                "plan",
                profile,
                "--format",
                "json",
                "--config",
                text(&config),
            ])
            .output()
            .unwrap();
        assert_success(&output);
        let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
        assert_eq!(value["target"]["os"], "macos");
        let encoded = String::from_utf8(output.stdout).unwrap();
        assert!(encoded.contains("macos-brew"));
        assert!(!encoded.contains("linux-apt"));
        assert!(!encoded.contains("windows-winget"));
    }
    fs::remove_dir_all(temp).unwrap();
}

#[test]
fn brew_install_is_typed_and_persists_zsh_path() {
    let temp = test_dir("brew typed");
    let fake_bin = temp.join("fake bin");
    let home = temp.join("state with spaces");
    let user_home = temp.join("user home");
    fs::create_dir_all(&fake_bin).unwrap();
    let log = temp.join("brew.log");
    write_executable(
        &fake_bin.join("brew"),
        r#"#!/bin/sh
printf '%s\n' "$HOMEBREW_NO_AUTO_UPDATE|$*" >> "$BOT_FORGE_TEST_LOG"
printf '#!/bin/sh\nexit 0\n' > "$BOT_FORGE_TEST_BIN/demo-tool"
chmod +x "$BOT_FORGE_TEST_BIN/demo-tool"
"#,
    );
    let config = write_brew_config(&temp, "demo", "demo-formula", "demo-tool");
    let path = format!("{}:{}", fake_bin.display(), std::env::var("PATH").unwrap());
    let output = Command::new(bin())
        .env("BOT_FORGE_HOME", &home)
        .env("HOME", &user_home)
        .env("USERPROFILE", &user_home)
        .env("SHELL", "/bin/zsh")
        .env("PATH", path)
        .env("BOT_FORGE_TEST_LOG", &log)
        .env("BOT_FORGE_TEST_BIN", &fake_bin)
        .args(["install", "macos-test", "--yes", "--config", text(&config)])
        .output()
        .unwrap();
    assert_success(&output);
    assert_eq!(
        fs::read_to_string(log).unwrap(),
        "1|install --formula demo-formula\n"
    );
    let profile = fs::read_to_string(user_home.join(".zprofile")).unwrap();
    assert!(profile.contains(&home.join("bin").display().to_string()));
    assert!(!user_home.join(".bashrc").exists());
    fs::remove_dir_all(temp).unwrap();
}

#[test]
fn brew_install_persists_fish_path_with_fish_syntax() {
    let temp = test_dir("brew fish");
    let fake_bin = temp.join("fake bin");
    let home = temp.join("state");
    let user_home = temp.join("user home");
    fs::create_dir_all(&fake_bin).unwrap();
    write_executable(
        &fake_bin.join("brew"),
        "#!/bin/sh\nprintf '#!/bin/sh\\nexit 0\\n' > \"$BOT_FORGE_TEST_BIN/demo-tool\"\nchmod +x \"$BOT_FORGE_TEST_BIN/demo-tool\"\n",
    );
    let config = write_brew_config(&temp, "fish-demo", "demo-formula", "demo-tool");
    let path = format!("{}:{}", fake_bin.display(), std::env::var("PATH").unwrap());
    let output = Command::new(bin())
        .env("BOT_FORGE_HOME", &home)
        .env("HOME", &user_home)
        .env("SHELL", "/opt/homebrew/bin/fish")
        .env("PATH", path)
        .env("BOT_FORGE_TEST_BIN", &fake_bin)
        .args(["install", "macos-test", "--yes", "--config", text(&config)])
        .output()
        .unwrap();
    assert_success(&output);
    let profile = fs::read_to_string(user_home.join(".config/fish/config.fish")).unwrap();
    assert!(profile.contains("set -gx PATH"));
    assert!(!profile.contains("export PATH="));
    fs::remove_dir_all(temp).unwrap();
}

#[test]
fn brew_installs_are_serialized_across_processes() {
    let temp = test_dir("brew-lock");
    let fake_bin = temp.join("bin");
    let home = temp.join("shared-home");
    let log = temp.join("events.log");
    fs::create_dir_all(&fake_bin).unwrap();
    write_executable(
        &fake_bin.join("brew"),
        r#"#!/bin/sh
formula="$3"
printf 'start-%s\n' "$formula" >> "$BOT_FORGE_TEST_LOG"
sleep 1
printf '#!/bin/sh\nexit 0\n' > "$BOT_FORGE_TEST_BIN/tool-$formula"
chmod +x "$BOT_FORGE_TEST_BIN/tool-$formula"
printf 'end-%s\n' "$formula" >> "$BOT_FORGE_TEST_LOG"
"#,
    );
    let first = write_brew_config(&temp, "one", "one", "tool-one");
    let second = write_brew_config(&temp, "two", "two", "tool-two");
    let path = format!("{}:{}", fake_bin.display(), std::env::var("PATH").unwrap());
    let launch = |config: &Path| {
        let mut command = Command::new(bin());
        command
            .env("BOT_FORGE_HOME", &home)
            .env("HOME", temp.join("user-home"))
            .env("SHELL", "/bin/zsh")
            .env("PATH", &path)
            .env("BOT_FORGE_TEST_LOG", &log)
            .env("BOT_FORGE_TEST_BIN", &fake_bin)
            .args(["install", "macos-test", "--yes", "--config", text(config)]);
        command.spawn().unwrap()
    };
    let mut left = launch(&first);
    let mut right = launch(&second);
    assert!(left.wait().unwrap().success());
    assert!(right.wait().unwrap().success());
    let events = fs::read_to_string(log).unwrap();
    let lines = events.lines().collect::<Vec<_>>();
    assert_eq!(lines.len(), 4, "{events}");
    assert!(lines[0].starts_with("start-"), "{events}");
    assert!(lines[1].starts_with("end-"), "{events}");
    assert!(lines[2].starts_with("start-"), "{events}");
    assert!(lines[3].starts_with("end-"), "{events}");
    fs::remove_dir_all(temp).unwrap();
}

fn write_brew_config(temp: &Path, id: &str, formula: &str, program: &str) -> PathBuf {
    let path = temp.join(format!("{id}.toml"));
    fs::write(
        &path,
        format!(
            r#"catalog = "rust-dev"
[profiles.macos-test]
components = ["{id}"]
[[components]]
id = "{id}"
platforms = ["macos-*"]
[components.detect]
kind = "command"
program = "{program}"
[components.install]
backend = "brew"
formulae = ["{formula}"]
"#
        ),
    )
    .unwrap();
    path
}

fn write_executable(path: &Path, contents: &str) {
    fs::write(path, contents).unwrap();
    fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap();
}

fn bin() -> &'static str {
    env!("CARGO_BIN_EXE_bot-forge")
}

fn text(path: &Path) -> &str {
    path.to_str().unwrap()
}

fn assert_success(output: &Output) {
    assert!(
        output.status.success(),
        "stdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
}

fn test_dir(name: &str) -> PathBuf {
    let nonce = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    std::env::temp_dir().join(format!(
        "bot-forge-macos-{}-{nonce}-{name}",
        std::process::id()
    ))
}