bot-forge 1.0.0

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

use std::fs;
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};

#[test]
fn launcher_exits_cleanly_in_a_pseudo_terminal() {
    if Command::new("script").arg("--version").output().is_err() {
        return;
    }
    let binary = env!("CARGO_BIN_EXE_bot-forge");
    let command = if cfg!(target_os = "macos") {
        format!("printf 0 | script -q /dev/null {binary}")
    } else {
        format!("printf 0 | script -q -c {binary} /dev/null")
    };
    let output = Command::new("sh").args(["-c", &command]).output().unwrap();
    assert!(
        output.status.success(),
        "{}",
        String::from_utf8_lossy(&output.stderr)
    );
    let combined = format!(
        "{}{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(combined.contains("bot-forge"));
}

#[test]
fn launcher_restores_terminal_after_resize_and_ctrl_c() {
    if Command::new("expect").arg("-v").output().is_err() {
        return;
    }
    let binary = env!("CARGO_BIN_EXE_bot-forge");
    let script = format!(
        r#"
set timeout 5
spawn {binary}
expect "↑↓ navigate"
stty rows 12 columns 40
set child [exp_pid]
exec kill -INT $child
expect eof
catch wait result
set code [lindex $result 3]
if {{$code != 130 && $code != 1}} {{ exit 1 }}
"#
    );
    let path = temp_file("resize-ctrl-c.exp");
    fs::write(&path, script).unwrap();
    let output = Command::new("expect").arg(&path).output().unwrap();
    fs::remove_file(path).unwrap();
    assert!(
        output.status.success(),
        "stdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    let combined = format!(
        "{}{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(combined.contains("bot-forge"));
}

#[test]
fn component_selection_restores_the_main_screen_before_confirmation() {
    if Command::new("expect").arg("-v").output().is_err() {
        return;
    }
    let binary = env!("CARGO_BIN_EXE_bot-forge");
    let config = temp_file("selection.toml");
    let transcript = temp_file("selection.log");
    fs::write(
        &config,
        r#"catalog = "rust-dev"
[policy]
allow_shell = true
[profiles.smoke]
components = ["terminal-test"]
[profiles.standard]
inherits = ["smoke"]
[[components]]
id = "terminal-test"
platforms = ["*"]
[components.detect]
kind = "command"
program = "false"
[components.install]
backend = "shell"
command = "true"
resources = ["terminal-test"]
"#,
    )
    .unwrap();
    let script = format!(
        r#"
set timeout 10
log_user 1
log_file -noappend {{{}}}
spawn sh -c "stty rows 24 columns 80; exec {binary} install smoke --config {}"
expect "Enter install"
send "\r"
expect "Selected components: 1"
expect "Y confirm"
send "n"
expect eof
catch wait result
set code [lindex $result 3]
if {{$code != 3}} {{ exit 1 }}
"#,
        transcript.display(),
        config.display(),
    );
    let path = temp_file("selection.exp");
    fs::write(&path, script).unwrap();
    let output = Command::new("expect").arg(&path).output().unwrap();
    let captured = fs::read(&transcript).unwrap_or_default();
    fs::remove_file(path).unwrap();
    fs::remove_file(config).unwrap();
    fs::remove_file(transcript).unwrap();
    assert!(
        output.status.success(),
        "stdout:\n{}\nstderr:\n{}\ntranscript:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr),
        String::from_utf8_lossy(&captured)
    );

    let leave_screen = captured
        .windows(b"\x1b[?1049l".len())
        .position(|window| window == b"\x1b[?1049l")
        .expect("selection did not leave the alternate screen");
    let selected = captured
        .windows(b"Selected components: 1".len())
        .position(|window| window == b"Selected components: 1")
        .expect("selection summary was not printed");
    assert!(leave_screen < selected);
}

#[test]
fn install_progress_stays_on_the_current_line_with_theme_color() {
    if Command::new("expect").arg("-v").output().is_err() {
        return;
    }
    let binary = env!("CARGO_BIN_EXE_bot-forge");
    let config = temp_file("progress.toml");
    let transcript = temp_file("progress.log");
    let state_home = temp_file("progress-home");
    fs::create_dir_all(&state_home).unwrap();
    fs::write(
        &config,
        r#"catalog = "rust-dev"
[policy]
allow_shell = true
[profiles.smoke]
components = ["terminal-progress"]
[profiles.standard]
inherits = ["smoke"]
[[components]]
id = "terminal-progress"
platforms = ["*"]
[components.detect]
kind = "shell"
command = 'test -f "$BOT_FORGE_HOME/terminal-progress"'
[components.install]
backend = "shell"
command = 'printf "Compiling terminal-progress\\n"; sleep 1; touch "$BOT_FORGE_HOME/terminal-progress"'
resources = ["terminal-progress"]
"#,
    )
    .unwrap();
    let script = format!(
        r#"
set timeout 10
log_user 1
log_file -noappend {{{}}}
spawn sh -c "stty rows 24 columns 80; exec env -u NO_COLOR TERM=xterm-256color BOT_FORGE_HOME={} HOME={} {binary} install smoke --config {}"
expect "Enter install"
send "\r"
expect "Y confirm"
send "y"
expect "Compiling terminal-progress"
expect eof
"#,
        transcript.display(),
        state_home.display(),
        state_home.display(),
        config.display(),
    );
    let path = temp_file("progress.exp");
    fs::write(&path, script).unwrap();
    let output = Command::new("expect").arg(&path).output().unwrap();
    let captured = fs::read(&transcript).unwrap_or_default();
    fs::remove_file(path).unwrap();
    fs::remove_file(config).unwrap();
    fs::remove_file(transcript).unwrap();
    fs::remove_dir_all(state_home).unwrap();
    assert!(
        output.status.success(),
        "stdout:\n{}\nstderr:\n{}\ntranscript:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr),
        String::from_utf8_lossy(&captured)
    );

    assert!(
        captured
            .windows(b"\r\x1b[2K".len())
            .any(|window| window == b"\r\x1b[2K")
    );
    assert!(
        captured
            .windows(b"\x1b[1;38;2;255;165;0m".len())
            .any(|window| window == b"\x1b[1;38;2;255;165;0m")
    );
    assert!(
        !captured
            .windows(b"\r\n\r\n\r\n\r\n".len())
            .any(|window| window == b"\r\n\r\n\r\n\r\n")
    );
    assert!(
        !captured
            .windows(b"0/0".len())
            .any(|window| window == b"0/0")
    );
}

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