liber-cli 0.1.0

AI-agent-readable company directory CLI
//! `liber search <query>` cross-entity substring search.

use std::path::Path;

use serde_json::{json, Value};

use crate::data;
use crate::output::{emit, exit, print_brief, CliError, Ctx};
use super::people;

pub fn run(dir: &Path, ctx: Ctx, query: &str) -> Result<(), CliError> {
    let needle = query.to_lowercase();

    let people_data = data::load(dir, "people")?;
    let people_iter = people::all_people(&people_data);
    let mut people_hits: Vec<Value> = Vec::new();
    for (dept, p) in &people_iter {
        let s = serde_json::to_string(&Value::Object(p.clone())).unwrap_or_default();
        if s.to_lowercase().contains(&needle) {
            let mut obj = p.clone();
            obj.insert("department".into(), Value::String(dept.clone()));
            people_hits.push(Value::Object(obj));
        }
    }

    let products_data = data::load(dir, "products")?;
    let products: Vec<Value> = products_data
        .get("products")
        .and_then(|v| v.as_array())
        .cloned()
        .unwrap_or_default()
        .into_iter()
        .filter(|p| {
            serde_json::to_string(p)
                .map(|s| s.to_lowercase().contains(&needle))
                .unwrap_or(false)
        })
        .collect();

    let customers_data = data::load(dir, "customers")?;
    let customers: Vec<Value> = customers_data
        .get("customers")
        .and_then(|v| v.as_array())
        .cloned()
        .unwrap_or_default()
        .into_iter()
        .filter(|c| {
            serde_json::to_string(c)
                .map(|s| s.to_lowercase().contains(&needle))
                .unwrap_or(false)
        })
        .collect();

    let repos_data = data::load(dir, "repos")?;
    let repos: Vec<Value> = repos_data
        .get("repos")
        .and_then(|v| v.as_array())
        .cloned()
        .unwrap_or_default()
        .into_iter()
        .filter(|r| {
            serde_json::to_string(r)
                .map(|s| s.to_lowercase().contains(&needle))
                .unwrap_or(false)
        })
        .collect();

    let chats_data = data::load(dir, "chats")?;
    let mut chats_hits: Vec<Value> = Vec::new();
    if let Some(map) = chats_data.get("group_chats").and_then(|v| v.as_object()) {
        for (name, val) in map {
            let id = match val {
                Value::String(s) => Value::String(s.clone()),
                Value::Object(o) => o.get("id").cloned().unwrap_or(Value::Null),
                other => other.clone(),
            };
            if name.to_lowercase().contains(&needle)
                || id
                    .as_str()
                    .map(|s| s.to_lowercase().contains(&needle))
                    .unwrap_or(false)
            {
                chats_hits.push(json!({ "name": name, "id": id }));
            }
        }
    }

    let total = people_hits.len()
        + products.len()
        + customers.len()
        + chats_hits.len()
        + repos.len();
    if total == 0 {
        return Err(CliError {
            code: exit::NOT_FOUND,
            message: format!("no matches for: {query}"),
            hint: Some("widen the query or `liber <noun> list` to browse".into()),
            retryable: false,
        });
    }

    let results = json!({
        "people": people_hits,
        "products": products,
        "customers": customers,
        "chats": chats_hits,
        "repos": repos,
    });

    if ctx.json {
        emit(ctx, &results);
        return Ok(());
    }
    for kind in ["people", "products", "customers", "chats", "repos"] {
        let arr = results
            .get(kind)
            .and_then(|v| v.as_array())
            .cloned()
            .unwrap_or_default();
        if arr.is_empty() {
            continue;
        }
        println!("== {kind} ({}) ==", arr.len());
        for item in arr {
            print_brief(&item, false);
        }
    }
    Ok(())
}