albatross-cli 2.1.0

Open source, terminal-first AI coding agent with fully transparent multi-model routing. Local (Ollama, LM Studio, MLX, llama.cpp) or cloud, your keys, one TUI.
use super::*;
use std::path::Path;

pub(super) fn cmd_scorecard(args: &str, state: &AppState) -> Result<()> {
    if !state.config.scorecard.enabled {
        println!("  {DIM}scorecard disabled in config (scorecard.enabled = false){RESET}");
        return Ok(());
    }

    let mut parts = args.split_whitespace();
    let action = parts.next().unwrap_or("daily");

    match action {
        "" | "daily" | "show" => {
            let report = crate::scorecard::load_report(&state.config.workspace_root)?;
            print!("{}", crate::scorecard::render_report(&report));
        }
        "current" => {
            let summary = crate::scorecard::current_summary(&state.config.workspace_root)?;
            print!("{}", crate::scorecard::render_current(&summary));
        }
        "prs" | "list" => {
            let limit = parts
                .next()
                .and_then(|value| value.parse::<usize>().ok())
                .unwrap_or(12)
                .clamp(1, 50);
            let prs = crate::scorecard::recent_prs(limit)?;
            print!("{}", crate::scorecard::render_recent_prs(&prs));
        }
        "pr" => {
            let index = parts
                .next()
                .and_then(|value| value.parse::<usize>().ok())
                .filter(|value| *value > 0)
                .ok_or_else(|| anyhow::anyhow!("usage: /scorecard pr <n>"))?;
            match crate::scorecard::recent_pr_by_index(index)? {
                Some(pr) => print!("{}", crate::scorecard::render_pr_detail(&pr, index)),
                None => println!(
                    "  {YELLOW}!{RESET} {DIM}scorecard PR #{index} not found (try /scorecard prs){RESET}"
                ),
            }
        }
        "verify" => {
            let target = parts.next().unwrap_or("");
            if target == "--all" {
                match crate::scorecard::verify_all_prs(&state.config.workspace_root)? {
                    Some(summary) => {
                        print!("{}", crate::scorecard::render_verify_all_summary(&summary))
                    }
                    None => println!(
                        "  {YELLOW}!{RESET} {DIM}scorecard store unavailable: HOME is unset{RESET}"
                    ),
                }
            } else {
                let index = target
                    .parse::<usize>()
                    .ok()
                    .filter(|value| *value > 0)
                    .ok_or_else(|| anyhow::anyhow!("usage: /scorecard verify <n>|--all"))?;
                match crate::scorecard::verify_recent_pr(index, &state.config.workspace_root)? {
                    Some(summary) => {
                        print!("{}", crate::scorecard::render_verification_summary(&summary))
                    }
                    None => println!(
                        "  {YELLOW}!{RESET} {DIM}scorecard PR #{index} not found (try /scorecard prs){RESET}"
                    ),
                }
            }
        }
        "close" => {
            let rest: Vec<&str> = parts.collect();
            let (label, url, run_tests) = parse_scorecard_close_args(&rest)?;
            let snapshot = if run_tests {
                crate::shipcheck::collect_shipcheck_with_tests(&state.config.workspace_root, true)?
            } else {
                crate::shipcheck::collect_shipcheck(&state.config.workspace_root)?
            };
            let has_url = url.is_some();
            let quality =
                crate::shipcheck::quality_input_from_shipcheck(&snapshot, false, false, has_url);
            close_scorecard_pr(state, &label, url.as_deref(), "manual", Some(quality), None)?;
        }
        "path" => match crate::scorecard::scorecard_path() {
            Some(path) => println!("  {DIM}scorecardStore{RESET} {}", path.display()),
            None => println!(
                "  {YELLOW}!{RESET} {DIM}scorecard store unavailable: HOME is unset{RESET}"
            ),
        },
        "doctor" => match crate::scorecard::scorecard_diagnostics()? {
            Some(diagnostics) => print!("{}", crate::scorecard::render_diagnostics(&diagnostics)),
            None => println!(
                "  {YELLOW}!{RESET} {DIM}scorecard store unavailable: HOME is unset{RESET}"
            ),
        },
        "export" => {
            let path = parts.next().map(std::path::PathBuf::from);
            match crate::scorecard::export_store(path.as_deref())? {
                Some(path) => println!(
                    "  {GREEN}✓{RESET} {DIM}scorecard exported →{RESET} {}",
                    path.display()
                ),
                None => {
                    println!("  {YELLOW}!{RESET} {DIM}scorecard store unavailable or empty{RESET}")
                }
            }
        }
        "reset" => {
            if parts.next() != Some("--yes") {
                println!("  {DIM}Usage: /scorecard reset --yes{RESET}");
                return Ok(());
            }
            match crate::scorecard::reset_store()? {
                Some(summary) => {
                    println!(
                        "  {GREEN}✓{RESET} {DIM}scorecard reset:{RESET} {}",
                        summary.path.display()
                    );
                    if let Some(backup) = summary.backup_path {
                        println!(
                            "  {GREEN}✓{RESET} {DIM}backup saved →{RESET} {}",
                            backup.display()
                        );
                    }
                }
                None => println!(
                    "  {YELLOW}!{RESET} {DIM}scorecard store unavailable: HOME is unset{RESET}"
                ),
            }
        }
        _ => print_scorecard_usage(),
    }

    Ok(())
}

pub(super) fn close_scorecard_pr(
    state: &AppState,
    label: &str,
    url: Option<&str>,
    status: &str,
    quality: Option<crate::scorecard::PrQualityInput>,
    ship_record_path: Option<&Path>,
) -> Result<()> {
    if !state.config.scorecard.enabled {
        return Ok(());
    }

    let title = if label.trim().is_empty() {
        "Untitled PR"
    } else {
        label.trim()
    };
    let ship_record_path = ship_record_path.map(|path| path.display().to_string());
    match crate::scorecard::close_pr_for_workspace(crate::scorecard::PrCloseInput {
        workspace_root: &state.config.workspace_root,
        session_dir: &state.config.session_dir,
        title,
        url,
        status,
        quality,
        ship_record_path: ship_record_path.as_deref(),
        quality_threshold: state.config.scorecard.quality_threshold,
    })? {
        Some(summary) => {
            let quality = summary
                .pr
                .quality
                .as_ref()
                .map(|quality| {
                    format!(
                        "{} {}/100 · {}",
                        quality.grade, quality.score, quality.status
                    )
                })
                .unwrap_or_else(|| "unrated quality".to_string());
            println!(
                "  {GREEN}✓{RESET} {DIM}scorecard PR closed:{RESET} {quality} · {} tokens · {} turn(s)",
                summary.pr.total_tokens, summary.pr.turn_count
            );
            if let Some(quality) = summary.pr.quality.as_ref() {
                if !quality.counts && !quality.reasons.is_empty() {
                    println!(
                        "  {YELLOW}!{RESET} {DIM}not counted:{RESET} {}",
                        quality.reasons.join(" · ")
                    );
                }
            }
            if !summary.pr.session_ids.is_empty() {
                println!(
                    "  {DIM}scorecard sessions{RESET}  {}",
                    summary.pr.session_ids.join(", ")
                );
            }
            println!(
                "  {GREEN}✓{RESET} {DIM}scorecard saved →{RESET} {}",
                summary.path.display()
            );
            println!("  {DIM}detail → /scorecard pr 1{RESET}");
        }
        None => {
            println!(
                "  {YELLOW}!{RESET} {DIM}scorecard not closed: no tracked tokens for this repo/branch yet{RESET}"
            );
        }
    }
    Ok(())
}

fn parse_scorecard_close_args(args: &[&str]) -> Result<(String, Option<String>, bool)> {
    let mut label_parts = Vec::new();
    let mut url = None;
    let mut run_tests = false;
    let mut index = 0;
    while index < args.len() {
        match args[index] {
            "--url" => {
                index += 1;
                if index >= args.len() {
                    anyhow::bail!("missing value for --url");
                }
                url = Some(args[index].to_string());
            }
            "--tests" => run_tests = true,
            value => label_parts.push(value),
        }
        index += 1;
    }
    Ok((label_parts.join(" "), url, run_tests))
}

fn print_scorecard_usage() {
    println!(
        "  {DIM}Usage: /scorecard [daily|current|prs [limit]|pr <n>|verify <n>|verify --all|close <label> [--url <url>] [--tests]|doctor|export [path]|path|reset --yes]{RESET}"
    );
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_close_args_extracts_flags() {
        let (label, url, tests) = parse_scorecard_close_args(&[
            "OAuth",
            "login",
            "PR",
            "--url",
            "https://github.com/org/repo/pull/1",
            "--tests",
        ])
        .unwrap();
        assert_eq!(label, "OAuth login PR");
        assert_eq!(url.as_deref(), Some("https://github.com/org/repo/pull/1"));
        assert!(tests);
    }
}