agntcy-shadi-cli 0.1.6

Command-line interface for SHADI policy, secrets, memory, and SLIM operations.
use super::*;

pub(crate) fn run_trace_command(cli: TraceCli) -> ExitCode {
    let path = resolve_trace_file(cli.file);
    match &cli.command {
        TraceCommand::List {
            limit,
            name,
            command,
            exit_code,
        } => match trace_list(&path, *limit, name.as_deref(), command.as_deref(), *exit_code) {
            Ok(()) => ExitCode::SUCCESS,
            Err(err) => {
                eprintln!("{}", err);
                ExitCode::from(1)
            }
        },
        TraceCommand::Summary { limit } => match trace_summary(&path, *limit) {
            Ok(()) => ExitCode::SUCCESS,
            Err(err) => {
                eprintln!("{}", err);
                ExitCode::from(1)
            }
        },
    }
}

pub(crate) fn resolve_trace_file(cli_path: Option<PathBuf>) -> PathBuf {
    if let Some(path) = cli_path {
        return path;
    }
    if let Ok(path) = std::env::var("SHADI_OTEL_FILE") {
        if !path.trim().is_empty() {
            return PathBuf::from(path);
        }
    }
    PathBuf::from(".shadi/traces.jsonl")
}

pub(crate) fn trace_list(
    path: &Path,
    limit: usize,
    name: Option<&str>,
    command: Option<&str>,
    exit_code: Option<i32>,
) -> Result<(), String> {
    let lines = read_trace_lines(path, limit)?;
    for line in lines {
        if let Some(value) = parse_trace_line(&line) {
            if !trace_matches(&value, name, command, exit_code) {
                continue;
            }
        }
        println!("{}", line);
    }
    Ok(())
}

pub(crate) fn trace_summary(path: &Path, limit: usize) -> Result<(), String> {
    let lines = read_trace_lines(path, limit)?;
    let mut counts: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
    for line in lines {
        if let Some(value) = parse_trace_line(&line) {
            if let Some(name) = trace_span_name(&value) {
                *counts.entry(name).or_insert(0) += 1;
            }
        }
    }

    for (name, count) in counts {
        println!("{}\t{}", count, name);
    }
    Ok(())
}

pub(crate) fn read_trace_lines(path: &Path, limit: usize) -> Result<Vec<String>, String> {
    let file = std::fs::File::open(path)
        .map_err(|err| format!("failed to open trace file {}: {}", path.display(), err))?;
    let reader = std::io::BufReader::new(file);
    let mut lines: std::collections::VecDeque<String> = std::collections::VecDeque::new();
    for line in reader.lines() {
        let line = line.map_err(|err| format!("failed to read trace file: {}", err))?;
        if limit == 0 {
            continue;
        }
        lines.push_back(line);
        if lines.len() > limit {
            lines.pop_front();
        }
    }
    Ok(lines.into_iter().collect())
}

pub(crate) fn parse_trace_line(line: &str) -> Option<serde_json::Value> {
    serde_json::from_str(line).ok()
}

pub(crate) fn trace_span_name(value: &serde_json::Value) -> Option<String> {
    if let Some(name) = value
        .get("span")
        .and_then(|span| span.get("name"))
        .and_then(|name| name.as_str())
    {
        return Some(name.to_string());
    }

    if let Some(spans) = value.get("spans").and_then(|spans| spans.as_array()) {
        if let Some(name) = spans
            .iter()
            .filter_map(|span| span.get("name"))
            .filter_map(|name| name.as_str())
            .next()
        {
            return Some(name.to_string());
        }
    }

    None
}

pub(crate) fn trace_matches(
    value: &serde_json::Value,
    name: Option<&str>,
    command: Option<&str>,
    exit_code: Option<i32>,
) -> bool {
    if let Some(expected) = name {
        if trace_span_name(value)
            .as_deref()
            .map(|value| !value.contains(expected))
            .unwrap_or(true)
        {
            return false;
        }
    }

    if let Some(expected) = command {
        let found = value
            .get("fields")
            .and_then(|fields| fields.get("command"))
            .and_then(|value| value.as_str())
            .map(|value| value.contains(expected))
            .unwrap_or(false);
        if !found {
            return false;
        }
    }

    if let Some(expected) = exit_code {
        let found = value
            .get("fields")
            .and_then(|fields| fields.get("exit.code"))
            .and_then(|value| value.as_i64())
            .map(|value| value == expected as i64)
            .unwrap_or(false);
        if !found {
            return false;
        }
    }

    true
}