autoresearch 0.2.3

Universal autoresearch CLI — install skills, track experiments, view results across any AI coding agent
use crate::errors::CliError;
use crate::git;
use crate::output::format::OutputFormat;

pub fn run(run_a: usize, run_b: usize, json: bool) -> Result<(), CliError> {
    let format = OutputFormat::detect(json);
    let branch = load_branch()?.unwrap_or_else(|| "autoresearch".to_string());

    let experiments = git::parse_experiments(&branch, 10000).unwrap_or_default();
    if experiments.is_empty() {
        return Err(CliError::NoExperiments(branch));
    }

    let exp_a = experiments
        .iter()
        .find(|e| e.run == run_a)
        .ok_or(CliError::RunNotFound(run_a))?;
    let exp_b = experiments
        .iter()
        .find(|e| e.run == run_b)
        .ok_or(CliError::RunNotFound(run_b))?;

    if exp_a.hash.is_empty() || exp_b.hash.is_empty() {
        return Err(CliError::Git(
            "Cannot diff: missing commit hashes".to_string(),
        ));
    }

    let diff_output = git::diff_commits(&exp_a.hash, &exp_b.hash)?;

    match format {
        OutputFormat::Json => {
            let out = serde_json::json!({
                "status": "success",
                "data": {
                    "run_a": exp_a,
                    "run_b": exp_b,
                    "diff": diff_output,
                }
            });
            println!("{}", serde_json::to_string_pretty(&out).unwrap());
        }
        OutputFormat::Table => {
            println!("Comparing experiment #{run_a} vs #{run_b}:");
            println!();
            println!(
                "  #{run_a} ({}) metric={}{}",
                exp_a.short_hash,
                exp_a
                    .metric
                    .map(|m| format!("{:.4}", m))
                    .unwrap_or("-".into()),
                exp_a.summary
            );
            println!(
                "  #{run_b} ({}) metric={}{}",
                exp_b.short_hash,
                exp_b
                    .metric
                    .map(|m| format!("{:.4}", m))
                    .unwrap_or("-".into()),
                exp_b.summary
            );
            println!();

            if diff_output.is_empty() {
                println!("  No differences found.");
            } else {
                let lines: Vec<&str> = diff_output.lines().collect();
                if lines.len() > 80 {
                    for line in &lines[..80] {
                        println!("{line}");
                    }
                    println!("... ({} more lines)", lines.len() - 80);
                } else {
                    print!("{diff_output}");
                }
            }
        }
    }

    Ok(())
}

fn load_branch() -> Result<Option<String>, CliError> {
    let path = std::path::Path::new("autoresearch.toml");
    if !path.exists() {
        return Ok(None);
    }
    let content = std::fs::read_to_string(path)?;
    let table: toml::Table =
        toml::from_str(&content).map_err(|e| CliError::Config(e.to_string()))?;
    Ok(table
        .get("branch")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string()))
}