cleanlib-cli 0.1.1

Terminal interface to CleanLibrary — query dependency verdicts and scan package manifests for ALLOW / DENY / WARN signals from the terminal or CI pipelines.
//! Cycle-7 Cli1-Cli8 — `cleanlib` CLI bin entry point.
//!
//! As of cycle-7 the handler logic lives in `commands/*.rs`, the envelope
//! renderer in `render/terminal.rs`, the bearer resolver in `auth/bearer.rs`,
//! the persistent verdict cache in `cache/persistent.rs`, and the per-pkg-mgr
//! wrappers in `wrappers/{npm,pip,cargo,go}.rs`. This file is the thin
//! clap-router into those modules.

mod auth;
mod cache;
mod commands;
mod render;
#[allow(dead_code)]
mod wrappers;

use std::path::PathBuf;

use clap::{Parser, Subcommand};

/// CleanLibrary CLI — verdict-aware package proxy companion.
///
/// CLEANLIB-132 / Jira CLEANLIB-29: `arg_required_else_help = true` so
/// invoking `cleanlib` with no subcommand prints help text + exits non-
/// zero (clap convention exit 2 = "user error"), instead of the pre-fix
/// behavior of silently exiting 0 with no output. Sister of CLEANLIB-130
/// exit-code semantics philosophy (fail loud, never silent fail-open).
#[derive(Parser)]
#[command(name = "cleanlib", version, about, long_about = None, arg_required_else_help = true)]
struct Cli {
    #[command(subcommand)]
    command: Option<Command>,
}

#[derive(Subcommand)]
enum Command {
    /// Display CleanLibrary client state — config file, endpoint, telemetry,
    /// auth source (does NOT print the bearer or API key).
    Status,
    /// Config subcommand (currently exposes `init`).
    Config {
        #[command(subcommand)]
        action: ConfigAction,
    },
    /// Store API key. Default writes to `~/.cleanlibrary/config.toml`;
    /// `--keyring` stores in the OS keyring instead (cycle-7 Cli6).
    Login {
        /// Opaque CleanLibrary API key (sent as `Authorization: Bearer ...`).
        #[arg(long)]
        api_key: String,
        /// Store in the OS keyring (Keychain / Secret Service / Credential
        /// Manager) under `cleanstart.com/cleanlib-enrich` instead of the
        /// config file.
        #[arg(long)]
        keyring: bool,
    },
    /// Remove API key from `~/.cleanlibrary/config.toml`. With `--keyring`,
    /// also removes the keyring entry.
    Logout {
        /// Also remove the OS keyring entry (cycle-7 Cli6).
        #[arg(long)]
        keyring: bool,
    },
    /// Emit a risk-acceptance rule YAML.
    #[command(name = "risk-accept")]
    RiskAccept {
        #[arg(long)]
        package: String,
        #[arg(long)]
        version: String,
        #[arg(long)]
        justification: String,
        #[arg(long)]
        proposed_by: Option<String>,
        #[arg(long)]
        write_to: Option<PathBuf>,
    },
    /// Fetch + display the verdict for a (ecosystem, package, version) tuple.
    Verdict {
        ecosystem: String,
        package: String,
        version: String,
        #[arg(long, default_value = "text")]
        output: String,
    },
    /// Scan a packages file against the active customer policy.
    Scan {
        #[arg(long)]
        ecosystem: String,
        #[arg(long)]
        packages: PathBuf,
        #[arg(long, default_value = "text")]
        output: String,
    },
    /// Query customer audit log.
    Audit {
        #[arg(long)]
        since: Option<String>,
        #[arg(long)]
        decision: Option<String>,
        #[arg(long)]
        ecosystem: Option<String>,
        #[arg(long, default_value = "text")]
        output: String,
    },
    /// `cleanlib policy <action>` — currently exposes `preview`.
    Policy {
        #[command(subcommand)]
        action: PolicyAction,
    },
    /// Fetch artifact bytes for (ecosystem, package, version) via App's
    /// per-ecosystem proxy.
    Fetch {
        ecosystem: String,
        package: String,
        version: String,
        #[arg(long)]
        output: Option<PathBuf>,
    },
    /// Wrap `npm install/i/add` — parse the wrapped command's positional
    /// args, extract package coordinates, and emit verdicts for each
    /// targeted package. Cycle-9 CLEANLIB-112 runtime exposure of the
    /// `cleanlib-cli::wrappers::npm` parser substrate (33 unit tests prove
    /// the parser; this variant makes it user-reachable).
    Npm {
        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
        args: Vec<String>,
    },
    /// Wrap `pip install` — sister of `Npm`.
    Pip {
        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
        args: Vec<String>,
    },
    /// Wrap `cargo install/add` — sister of `Npm`.
    Cargo {
        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
        args: Vec<String>,
    },
    /// Wrap `go get/install` — sister of `Npm`.
    Go {
        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
        args: Vec<String>,
    },
}

#[derive(Subcommand)]
enum PolicyAction {
    /// Preview decisions under a hypothetical policy via App
    /// `POST /v1/customer/policy/preview`.
    Preview {
        #[arg(long)]
        policy: PathBuf,
        #[arg(long)]
        packages: PathBuf,
        #[arg(long)]
        ecosystem: String,
        #[arg(long, default_value = "text")]
        output: String,
    },
}

#[derive(Subcommand)]
enum ConfigAction {
    /// Emit per-ecosystem proxy-config snippets for `.npmrc`, `pip.conf`, or
    /// Go shell-env.
    Init {
        #[arg(long, value_delimiter = ',', default_values_t = vec!["npm".to_string(), "pypi".to_string(), "go".to_string()])]
        ecosystem: Vec<String>,
        #[arg(long)]
        scope: Option<String>,
        #[arg(long)]
        write: bool,
        #[arg(long)]
        write_to: Option<PathBuf>,
        #[arg(long)]
        inline_token: bool,
        #[arg(long)]
        force: bool,
    },
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();
    match cli.command {
        Some(Command::Status) => commands::status::run(),
        Some(Command::Config { action }) => match action {
            ConfigAction::Init {
                ecosystem,
                scope,
                write,
                write_to,
                inline_token,
                force,
            } => commands::config_init::run(ecosystem, scope, write, write_to, inline_token, force),
        },
        Some(Command::Login { api_key, keyring }) => commands::login::run(api_key, keyring),
        Some(Command::Logout { keyring }) => commands::logout::run(keyring),
        Some(Command::RiskAccept {
            package,
            version,
            justification,
            proposed_by,
            write_to,
        }) => commands::risk_accept::run(package, version, justification, proposed_by, write_to),
        Some(Command::Verdict {
            ecosystem,
            package,
            version,
            output,
        }) => commands::verdict::run(ecosystem, package, version, output).await,
        Some(Command::Scan {
            ecosystem,
            packages,
            output,
        }) => commands::scan::run(ecosystem, packages, output).await,
        Some(Command::Audit {
            since,
            decision,
            ecosystem,
            output,
        }) => commands::audit::run(since, decision, ecosystem, output).await,
        Some(Command::Policy { action }) => match action {
            PolicyAction::Preview {
                policy,
                packages,
                ecosystem,
                output,
            } => commands::policy::preview(policy, packages, ecosystem, output).await,
        },
        Some(Command::Fetch {
            ecosystem,
            package,
            version,
            output,
        }) => commands::fetch::run(ecosystem, package, version, output).await,
        Some(Command::Npm { args }) => commands::wrap::run("npm", args).await,
        Some(Command::Pip { args }) => commands::wrap::run("pip", args).await,
        Some(Command::Cargo { args }) => commands::wrap::run("cargo", args).await,
        Some(Command::Go { args }) => commands::wrap::run("go", args).await,
        None => Ok(()),
    }
}