liber-cli 0.1.0

AI-agent-readable company directory CLI
//! Clap entrypoint. Wires global flags + subcommands to the verb modules.

use std::path::PathBuf;

use clap::{Args, Parser, Subcommand};

use crate::commands::{chats, customers, people, products, repos, search};
use crate::output::{exit, report, Ctx};
use crate::{data, init, validate};

#[derive(Parser, Debug)]
#[command(
    name = "liber",
    bin_name = "liber",
    version,
    about = "AI-agent-readable company directory CLI",
    long_about = "liber — query a liber data directory (people / products / repos / customers / chats).\n\n\
                  Data dir resolution: --data-dir > $LIBER_DATA_DIR > ./.liber > cwd > ~/.liber.\n\
                  Output is human-friendly on a TTY, JSON otherwise (or with --json).\n\
                  Exit codes: 0=ok, 2=validation, 4=not-found, 5=conflict, 6=data."
)]
struct Cli {
    /// Force JSON output (auto when stdout is not a TTY).
    #[arg(long, global = true)]
    json: bool,

    /// Suppress non-essential stdout (success messages on mutations).
    #[arg(long, global = true)]
    quiet: bool,

    /// Show extra fields on TTY output (no effect on --json).
    #[arg(long, global = true)]
    full: bool,

    /// Never prompt; if a required value is missing, exit instead of asking.
    #[arg(long, global = true)]
    no_interactive: bool,

    /// Override data directory (highest priority).
    #[arg(long, global = true, value_name = "DIR")]
    data_dir: Option<PathBuf>,

    #[command(subcommand)]
    cmd: Cmd,
}

#[derive(Subcommand, Debug)]
enum Cmd {
    /// Scaffold a new liber data directory.
    Init(InitArgs),
    /// Validate every entity file against its JSON schema.
    Validate(ValidateArgs),
    /// Employees / departments.
    People(NounArgs<PeopleVerb>),
    /// Products (slug / name / description).
    Products(NounArgs<ProductsVerb>),
    /// Customers + relations.
    Customers(NounArgs<CustomersVerb>),
    /// Group chats (Feishu / Slack / Teams / …).
    Chats(NounArgs<ChatsVerb>),
    /// Code repositories.
    Repos(NounArgs<ReposVerb>),
    /// Substring search across all entities.
    Search(SearchArgs),
}

#[derive(Args, Debug)]
struct NounArgs<V: Subcommand> {
    #[command(subcommand)]
    verb: V,
}

#[derive(Subcommand, Debug)]
enum PeopleVerb {
    /// List employees.
    List {
        /// Filter by department name (exact match).
        #[arg(long)]
        dept: Option<String>,
    },
    /// Get one person by name / github / git alias.
    Get { name: String },
}

#[derive(Subcommand, Debug)]
enum ProductsVerb {
    /// List all products.
    List,
    /// Get one product by slug.
    Get { slug: String },
}

#[derive(Subcommand, Debug)]
enum CustomersVerb {
    /// List all customers.
    List,
    /// Get a customer (auto-enriched with products_detail + chat_ids).
    Get { slug: String },
}

#[derive(Subcommand, Debug)]
enum ChatsVerb {
    /// List all chats.
    List,
    /// Get chat ID by exact name.
    Get { name: String },
}

#[derive(Subcommand, Debug)]
enum ReposVerb {
    /// List repos (optionally filter by visibility).
    List {
        /// Filter by visibility (public / private).
        #[arg(long)]
        visibility: Option<String>,
    },
    /// Get one repo by slug.
    Get { slug: String },
}

#[derive(Args, Debug)]
struct InitArgs {
    /// Company slug (lowercase, [a-z0-9_-]).
    slug: String,
    /// Path to create (defaults to ./<slug>).
    path: Option<PathBuf>,
    /// Overwrite existing files.
    #[arg(long)]
    force: bool,
}

#[derive(Args, Debug)]
struct ValidateArgs {}

#[derive(Args, Debug)]
struct SearchArgs {
    query: String,
}

pub fn run() -> i32 {
    let cli = match Cli::try_parse() {
        Ok(c) => c,
        Err(e) => {
            // clap handles --help / --version (exits 0 itself). For real
            // parse failures, surface via stderr and return validation code.
            let kind = e.kind();
            e.print().ok();
            return match kind {
                clap::error::ErrorKind::DisplayHelp
                | clap::error::ErrorKind::DisplayVersion => exit::OK,
                _ => exit::VALIDATION,
            };
        }
    };

    let ctx = Ctx::new(cli.json, cli.quiet, cli.full, cli.no_interactive);

    let result = dispatch(&cli, ctx);
    match result {
        Ok(()) => exit::OK,
        Err(err) => {
            report(&err);
            err.code
        }
    }
}

fn dispatch(cli: &Cli, ctx: Ctx) -> Result<(), crate::output::CliError> {
    match &cli.cmd {
        Cmd::Init(a) => {
            let path = a
                .path
                .clone()
                .unwrap_or_else(|| PathBuf::from(format!("./{}", a.slug)));
            init::run(ctx, &a.slug, &path, a.force)
        }
        Cmd::Validate(_) => {
            let dir = data::resolve_data_dir(cli.data_dir.as_deref())?;
            validate::run(&dir, ctx)
        }
        Cmd::People(n) => {
            let dir = data::resolve_data_dir(cli.data_dir.as_deref())?;
            match &n.verb {
                PeopleVerb::List { dept } => people::list(&dir, ctx, dept.as_deref()),
                PeopleVerb::Get { name } => people::get(&dir, ctx, name),
            }
        }
        Cmd::Products(n) => {
            let dir = data::resolve_data_dir(cli.data_dir.as_deref())?;
            match &n.verb {
                ProductsVerb::List => products::list(&dir, ctx),
                ProductsVerb::Get { slug } => products::get(&dir, ctx, slug),
            }
        }
        Cmd::Customers(n) => {
            let dir = data::resolve_data_dir(cli.data_dir.as_deref())?;
            match &n.verb {
                CustomersVerb::List => customers::list(&dir, ctx),
                CustomersVerb::Get { slug } => customers::get(&dir, ctx, slug),
            }
        }
        Cmd::Chats(n) => {
            let dir = data::resolve_data_dir(cli.data_dir.as_deref())?;
            match &n.verb {
                ChatsVerb::List => chats::list(&dir, ctx),
                ChatsVerb::Get { name } => chats::get(&dir, ctx, name),
            }
        }
        Cmd::Repos(n) => {
            let dir = data::resolve_data_dir(cli.data_dir.as_deref())?;
            match &n.verb {
                ReposVerb::List { visibility } => {
                    repos::list(&dir, ctx, visibility.as_deref())
                }
                ReposVerb::Get { slug } => repos::get(&dir, ctx, slug),
            }
        }
        Cmd::Search(a) => {
            let dir = data::resolve_data_dir(cli.data_dir.as_deref())?;
            search::run(&dir, ctx, &a.query)
        }
    }
}