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;
use std::fs;
use std::io::IsTerminal;

pub fn run(
    target_file: Option<String>,
    eval_command: Option<String>,
    metric_name: &str,
    metric_direction: &str,
    time_budget: &str,
    branch: &str,
    json: bool,
) -> Result<(), CliError> {
    let format = OutputFormat::detect(json);

    if !git::is_git_repo() {
        return Err(CliError::NotGitRepo);
    }

    if std::path::Path::new("autoresearch.toml").exists() {
        match format {
            OutputFormat::Json => {
                println!(
                    "{}",
                    serde_json::json!({
                        "status": "already_initialized",
                        "message": "autoresearch.toml already exists"
                    })
                );
            }
            OutputFormat::Table => {
                println!("autoresearch.toml already exists. Edit it directly to change settings.");
            }
        }
        return Ok(());
    }

    // Determine if we can run interactively or need flags
    let is_interactive = std::io::stdin().is_terminal() && target_file.is_none();

    let (target_file, eval_command) = if is_interactive {
        // Interactive mode
        use dialoguer::{Input, Select};

        let tf: String = Input::new()
            .with_prompt("Target file (the single file the agent may modify)")
            .interact_text()
            .map_err(|e| CliError::Config(e.to_string()))?;

        let ec: String = Input::new()
            .with_prompt("Eval command (produces the metric)")
            .interact_text()
            .map_err(|e| CliError::Config(e.to_string()))?;

        let _metric_name_input: String = Input::new()
            .with_prompt("Metric name")
            .default(metric_name.to_string())
            .interact_text()
            .map_err(|e| CliError::Config(e.to_string()))?;

        let directions = &["lower is better", "higher is better"];
        let _direction_idx = Select::new()
            .with_prompt("Metric direction")
            .items(directions)
            .default(0)
            .interact()
            .map_err(|e| CliError::Config(e.to_string()))?;

        (tf, ec)
    } else {
        // Non-interactive mode — flags required
        let tf = target_file.ok_or_else(|| {
            CliError::Config(
                "In non-interactive mode, --target-file is required. \
                 Example: autoresearch init --target-file train.py --eval-command 'python train.py'"
                    .to_string(),
            )
        })?;
        let ec = eval_command.ok_or_else(|| {
            CliError::Config(
                "In non-interactive mode, --eval-command is required. \
                 Example: autoresearch init --target-file train.py --eval-command 'python train.py'"
                    .to_string(),
            )
        })?;
        (tf, ec)
    };

    // Write autoresearch.toml using proper TOML serialization
    let mut config_table = toml::map::Map::new();
    config_table.insert(
        "target_file".to_string(),
        toml::Value::String(target_file.clone()),
    );
    config_table.insert(
        "eval_command".to_string(),
        toml::Value::String(eval_command.clone()),
    );
    config_table.insert(
        "metric_name".to_string(),
        toml::Value::String(metric_name.to_string()),
    );
    config_table.insert(
        "metric_direction".to_string(),
        toml::Value::String(metric_direction.to_string()),
    );
    config_table.insert(
        "time_budget".to_string(),
        toml::Value::String(time_budget.to_string()),
    );
    config_table.insert(
        "branch".to_string(),
        toml::Value::String(branch.to_string()),
    );

    let config_str = format!(
        "# Autoresearch configuration\n# Generated by autoresearch CLI v{}\n\n{}",
        env!("CARGO_PKG_VERSION"),
        toml::to_string_pretty(&toml::Value::Table(config_table))
            .map_err(|e| CliError::Config(e.to_string()))?
    );

    fs::write("autoresearch.toml", &config_str)?;

    // Create .autoresearch directory
    fs::create_dir_all(".autoresearch")?;

    // Create program.md if it doesn't exist
    if !std::path::Path::new("program.md").exists() {
        let program = format!(
            "# Autoresearch Program\n\n\
             ## Goal\n\
             Optimize `{metric_name}` ({metric_direction} is better) by modifying `{target_file}`.\n\n\
             ## Ideas to Explore\n\
             <!-- Add your research directions, ideas, and hypotheses here. -->\n\n\
             1.\n\n\
             ## Constraints\n\
             - Time budget per experiment: {time_budget}\n\
             - Only modify: {target_file}\n\
             - Eval command: `{eval_command}`\n",
        );
        fs::write("program.md", &program)?;
    }

    match format {
        OutputFormat::Json => {
            println!(
                "{}",
                serde_json::json!({
                    "status": "success",
                    "files_created": ["autoresearch.toml", "program.md", ".autoresearch/"],
                    "config": {
                        "target_file": target_file,
                        "eval_command": eval_command,
                        "metric_name": metric_name,
                        "metric_direction": metric_direction,
                        "time_budget": time_budget,
                        "branch": branch,
                    }
                })
            );
        }
        OutputFormat::Table => {
            println!();
            println!("Initialized autoresearch project:");
            println!("  autoresearch.toml  — experiment configuration");
            println!("  program.md         — research direction & ideas (edit this!)");
            println!("  .autoresearch/     — experiment logs");
            println!();
            println!(
                "Next: edit program.md with your ideas, then tell your agent to run /autoresearch"
            );
        }
    }

    Ok(())
}