liber-cli 0.1.0

AI-agent-readable company directory CLI
//! Data directory resolution + JSON loading.

use std::path::{Path, PathBuf};

use serde_json::Value;

use crate::output::CliError;

/// Resolution order (first that exists wins):
/// 1. `--data-dir` flag
/// 2. `$LIBER_DATA_DIR`
/// 3. `./.liber/`
/// 4. `~/.liber/`
pub fn resolve_data_dir(flag: Option<&Path>) -> Result<PathBuf, CliError> {
    if let Some(p) = flag {
        if !p.exists() {
            return Err(CliError::not_found(
                format!("--data-dir path does not exist: {}", p.display()),
                Some("run `liber init <slug> <path>` to scaffold a fresh data directory".into()),
            ));
        }
        return Ok(p.to_path_buf());
    }
    if let Ok(env) = std::env::var("LIBER_DATA_DIR") {
        let p = PathBuf::from(env);
        if !p.exists() {
            return Err(CliError::not_found(
                format!("LIBER_DATA_DIR path does not exist: {}", p.display()),
                Some("unset LIBER_DATA_DIR or point it at an existing directory".into()),
            ));
        }
        return Ok(p);
    }
    let local = PathBuf::from(".liber");
    if local.exists() {
        return Ok(local);
    }
    // Also accept the cwd itself when it's a data directory (people.json + at
    // least one other entity file present). This makes `cd <path> && liber …`
    // work after `liber init <slug> <path>`, without forcing a .liber/ subdir.
    let cwd = PathBuf::from(".");
    if looks_like_data_dir(&cwd) {
        return Ok(cwd);
    }
    if let Some(home) = home_dir() {
        let p = home.join(".liber");
        if p.exists() {
            return Ok(p);
        }
    }
    Err(CliError::not_found(
        "no liber data directory found".into(),
        Some(
            "set --data-dir, LIBER_DATA_DIR, or create one with `liber init <slug> <path>`"
                .into(),
        ),
    ))
}

fn home_dir() -> Option<PathBuf> {
    std::env::var_os("HOME").map(PathBuf::from)
}

fn looks_like_data_dir(dir: &Path) -> bool {
    let must = dir.join("people.json");
    if !must.exists() {
        return false;
    }
    ["products.json", "customers.json", "chats.json"]
        .iter()
        .any(|n| dir.join(n).exists())
}

/// Load `<dir>/<name>.json` and parse it.
pub fn load(dir: &Path, name: &str) -> Result<Value, CliError> {
    let path = dir.join(format!("{name}.json"));
    let bytes = std::fs::read(&path).map_err(|e| {
        CliError::not_found(
            format!("data file missing: {} ({e})", path.display()),
            Some(format!(
                "create {name}.json in {} (see `liber init` for a template)",
                dir.display()
            )),
        )
    })?;
    serde_json::from_slice(&bytes).map_err(|e| {
        CliError::data(
            format!("invalid JSON in {}: {e}", path.display()),
            Some("fix the JSON syntax and re-run".into()),
        )
    })
}