tools-kit 0.0.2

Portable Local and MCP tool execution with narrow extension traits.
Documentation
use std::{
    path::{Path, PathBuf},
    process::Command,
};

use serde_json::Value;

pub fn test_target_executable(target_name: &str) -> PathBuf {
    let output = Command::new(cargo_bin())
        .args([
            "test",
            "--locked",
            "-p",
            "tools-kit",
            "--test",
            target_name,
            "--features",
            "mcp-server",
            "--no-run",
            "--message-format=json",
        ])
        .current_dir(workspace_dir())
        .output()
        .expect("nested cargo should compile the repo-managed MCP server target");

    if !output.status.success() {
        panic!(
            "failed to compile repo-managed MCP server target `{target_name}`\nstdout:\n{}\nstderr:\n{}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );
    }

    parse_executable_path(target_name, &output.stdout).unwrap_or_else(|| {
        panic!(
            "cargo did not report an executable for repo-managed MCP server target `{target_name}`\nstdout:\n{}",
            String::from_utf8_lossy(&output.stdout)
        )
    })
}

fn cargo_bin() -> PathBuf {
    std::env::var_os("CARGO")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("cargo"))
}

fn workspace_dir() -> &'static Path {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("tools-kit manifest should live under the workspace root")
}

fn parse_executable_path(target_name: &str, stdout: &[u8]) -> Option<PathBuf> {
    let stdout = String::from_utf8_lossy(stdout);
    for line in stdout.lines() {
        let Ok(message) = serde_json::from_str::<Value>(line) else {
            continue;
        };

        if message.get("reason").and_then(Value::as_str) != Some("compiler-artifact") {
            continue;
        }
        if message.pointer("/target/name").and_then(Value::as_str) != Some(target_name) {
            continue;
        }
        if let Some(executable) = message.get("executable").and_then(Value::as_str) {
            return Some(PathBuf::from(executable));
        }
    }

    None
}