cleanlib-cli 0.1.5

Terminal interface to CleanLibrary — query dependency verdicts and scan package manifests for ALLOW / DENY / WARN signals from the terminal or CI pipelines.
//! `cleanlib audit` (cycle-7 Cli2). Migrates `cmd_audit` from `main.rs`.

use anyhow::Result;
use cleanlib_client::{config, transport};
use comfy_table::{presets, ContentArrangement, Table};

use crate::render::terminal::{stdout_is_tty, style_decision};

pub async fn run(
    since: Option<String>,
    decision: Option<String>,
    ecosystem: Option<String>,
    output: String,
) -> Result<()> {
    let path = config::default_path();
    let cfg = config::load_with_env_overrides(path.as_deref())?;
    let client = transport::Client::from_config(&cfg)?;

    let resp = client
        .audit(since.as_deref(), decision.as_deref(), ecosystem.as_deref())
        .await?;

    match output.as_str() {
        "json" => println!("{}", serde_json::to_string_pretty(&resp)?),
        _ => {
            if resp.records.is_empty() {
                println!("(no audit entries match the given filters)");
            } else {
                let mut table = Table::new();
                let preset = if stdout_is_tty() {
                    presets::UTF8_BORDERS_ONLY
                } else {
                    presets::NOTHING
                };
                table
                    .load_preset(preset)
                    .set_content_arrangement(ContentArrangement::Dynamic)
                    .set_header(vec![
                        "AT", "REQUEST_ID", "ECOSYSTEM", "PACKAGE", "VERSION", "DECISION", "REASON",
                    ]);
                // CLEANLIB-366: field names below track the App-side `AuditRow`
                // shape (request_at / package_name / package_version /
                // policy_decision / reasoning). Prior code referenced `at` /
                // `package` / `version` / `decision` / `reason` and silently
                // rendered empty cells because deserialize-default filled them
                // with `""`.
                for e in &resp.records {
                    table.add_row(vec![
                        e.request_at.clone(),
                        e.request_id.clone(),
                        e.ecosystem.clone(),
                        e.package_name.clone(),
                        e.package_version.clone(),
                        style_decision(&e.policy_decision),
                        e.reasoning.clone(),
                    ]);
                }
                println!("{table}");
            }
            // Honesty signal from the App: surface `not_wired` / `read_error`
            // so the customer can distinguish "no matching rows" from "the
            // audit backend is offline" (CLEANLIB-101 wire-in). Silent on
            // `wired` — that's the normal path.
            match resp.backend_status.as_str() {
                "" | "wired" => {}
                other => eprintln!("# audit backend status: {}", other),
            }
        }
    }
    Ok(())
}