repopilot 0.15.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
mod baseline_flow;
mod timing;
mod validation;

use crate::cli::ScanOptions;
use crate::commands::filters::{scan_pre_visibility_filter, scan_priority_filter};
use crate::commands::product_scan::{
    ProductScanRequest, enforce_diagnostics_exit_policy, run_product_scan,
};
use crate::commands::scan_config::ScanConfigOverrides;
use repopilot::output::{
    ColorChoice, ColorDestination, ConsoleOutputStyle, FindingRenderLimit, RenderOptions,
    render_scan_summary_with_options,
};
use repopilot::receipt::{build_audit_receipt, render_receipt_json};
use repopilot::report::writer::write_report;
use repopilot::scan::types::ScanSummary;
use std::path::Path;
use std::time::Instant;

pub fn run(options: ScanOptions) -> Result<(), Box<dyn std::error::Error>> {
    validation::validate_scan_options(&options)?;

    let pre_visibility_filter = scan_pre_visibility_filter(&options);
    let priority_filter = scan_priority_filter(&options);
    let fail_on_priority = options.fail_on_priority.map(Into::into);
    let visibility_profile = validation::scan_visibility_profile(&options);
    let scan_mode = validation::scan_mode_from_options(&options);

    let scan_result = run_product_scan(ProductScanRequest {
        path: options.path.clone(),
        config_path: options.config.clone(),
        overrides: ScanConfigOverrides {
            max_file_loc: options.max_file_loc,
            max_directory_modules: options.max_directory_modules,
            max_directory_depth: options.max_directory_depth,
            exclude_patterns: options.exclude.clone(),
            include_low_signal: options.include_low_signal,
            max_file_size: options.max_file_size,
            max_files: options.max_files,
        },
        preset: options.preset.clone(),
        mode: scan_mode,
        no_progress: options.no_progress || options.quiet,
        ignore_feedback: options.ignore_feedback,
        visibility_profile,
        pre_visibility_filter,
    })?;

    let mut summary = scan_result.summary;
    let scan_elapsed = scan_result.scan_elapsed;
    let output_format = options
        .format
        .map(Into::into)
        .unwrap_or(scan_result.repo_config.output.default_format);

    if baseline_flow::uses_baseline_flow(&options) {
        return baseline_flow::run_baseline_scan_flow(
            summary,
            &options,
            priority_filter,
            fail_on_priority,
            output_format,
            scan_elapsed,
        );
    }

    if !priority_filter.is_empty() {
        priority_filter.apply_to_summary(&mut summary);
    }

    let render_options = render_options_for_scan(&options);
    let render_start = Instant::now();
    let rendered_report =
        render_scan_summary_with_options(&summary, output_format, render_options)?;
    let render_elapsed = render_start.elapsed();

    write_scan_receipt_if_requested(&summary, options.receipt.as_deref())?;
    write_report(&rendered_report, options.output.as_deref())?;

    if options.verbose {
        timing::print_verbose_scan_timing(&summary, scan_elapsed, render_elapsed);
    }

    if options.timing {
        timing::print_timing_breakdown(&summary);
    }

    enforce_diagnostics_exit_policy(&summary)?;

    Ok(())
}

pub(super) fn render_options_for_scan(options: &ScanOptions) -> RenderOptions {
    RenderOptions {
        console_output_style: options
            .output_style
            .map(Into::into)
            .unwrap_or(ConsoleOutputStyle::Compact),
        color_choice: if options.no_color {
            ColorChoice::Never
        } else {
            options.color.map(Into::into).unwrap_or(ColorChoice::Auto)
        },
        color_destination: if options.output.is_some() {
            ColorDestination::File
        } else {
            ColorDestination::Stdout
        },
        quiet: options.quiet,
        findings_limit: options
            .max_findings
            .map(Into::into)
            .unwrap_or(FindingRenderLimit::Default),
    }
}

pub(super) fn write_scan_receipt_if_requested(
    summary: &ScanSummary,
    receipt_path: Option<&Path>,
) -> Result<(), Box<dyn std::error::Error>> {
    let Some(receipt_path) = receipt_path else {
        return Ok(());
    };

    let receipt = build_audit_receipt(summary);
    let rendered = render_receipt_json(&receipt)?;

    write_report(&rendered, Some(receipt_path))?;

    Ok(())
}