repopilot 0.20.0

Local-first CLI for reviewing Git changes, security boundaries, and blast radius before merge.
Documentation
pub(crate) fn render_languages_section(output: &mut String, summary: &ScanSummary) {
    output.push_str("Languages:\n");

    if summary.metrics.languages.is_empty() {
        output.push_str("  No languages detected\n\n");
        return;
    }

    for language in &summary.metrics.languages {
        writeln!(
            output,
            "  {}: {} files",
            language.name, language.files_analyzed
        )
        .unwrap();
    }
    output.push('\n');
}

pub(crate) fn render_frameworks_section(output: &mut String, frameworks: &[DetectedFramework]) {
    if frameworks.is_empty() {
        return;
    }
    let labels: Vec<String> = frameworks.iter().map(|f| f.label()).collect();
    writeln!(output, "Frameworks: {}\n", labels.join(" | ")).unwrap();
}

pub(crate) fn render_framework_projects_section(
    output: &mut String,
    projects: &[FrameworkProject],
) {
    let nested_projects: Vec<_> = projects
        .iter()
        .filter(|project| project.path.as_path() != std::path::Path::new("."))
        .collect();
    if nested_projects.is_empty() {
        return;
    }

    output.push_str("Framework projects:\n");
    for project in nested_projects {
        let labels: Vec<String> = project.frameworks.iter().map(|f| f.label()).collect();
        writeln!(
            output,
            "  {}: {}",
            project.path.display(),
            labels.join(" | ")
        )
        .unwrap();
    }
    output.push('\n');
}

fn render_scan_input(output: &mut String, summary: &ScanSummary) {
    output.push_str("Scan input:\n");

    if let Some(path) = &summary.repopilotignore_path {
        writeln!(output, " .repopilotignore: {}", path.display()).unwrap();
    }

    if summary.metrics.files_skipped_repopilotignore > 0 {
        writeln!(
            output,
            " Files skipped (.repopilotignore): {:>7}",
            summary.metrics.files_skipped_repopilotignore
        )
        .unwrap();
    }

    writeln!(output, " Files discovered: {:>7}", summary.metrics.files_discovered).unwrap();

    if summary.metrics.files_skipped_by_limit > 0 {
        writeln!(
            output,
            " Files skipped (limit): {:>7}",
            summary.metrics.files_skipped_by_limit
        )
        .unwrap();
    }

    writeln!(output, " Files analyzed: {:>7}", summary.metrics.files_analyzed).unwrap();

    if summary.metrics.large_files_skipped > 0 {
        writeln!(
            output,
            " Large files skipped: {:>7}",
            summary.metrics.large_files_skipped
        )
        .unwrap();
    }

    if summary.metrics.binary_files_skipped > 0 {
        writeln!(
            output,
            " Binary files skipped: {:>7}",
            summary.metrics.binary_files_skipped
        )
        .unwrap();
    }

    if summary.metrics.files_skipped_low_signal > 0 {
        writeln!(
            output,
            " Low-signal files skipped:{:>7}",
            summary.metrics.files_skipped_low_signal
        )
        .unwrap();
    }
}

fn health_score_bar(score: u8) -> &'static str {
    match score {
        90..=100 => "[##########] Excellent",
        75..=89 => "[########  ] Good",
        60..=74 => "[######    ] Fair",
        40..=59 => "[####      ] Poor",
        _ => "[##        ] Critical",
    }
}