openlatch-provider 0.1.0

Self-service onboarding CLI + runtime daemon for OpenLatch Editors and Providers
//! CLI surface — clap derive tree per PRD §8.1.
//!
//! The bodies of each subcommand live in submodules under `src/cli/commands/`.
//! At T0 every command is a stub returning `OlError::new(OL_42xx, "not yet
//! implemented")`; tasks T1–T8 fill them in.
//!
//! The dispatcher is intentionally thin — global flags are parsed once into
//! [`GlobalArgs`] and threaded into each command, so commands don't reach into
//! `Cli` themselves.

use crate::error::{OlError, OL_4270_CONFIG_UNREADABLE};
use clap::{Parser, Subcommand, ValueEnum};

pub mod commands;

// ---------------------------------------------------------------------------
// Top-level CLI definition
// ---------------------------------------------------------------------------

#[derive(Parser, Debug)]
#[command(
    name = "openlatch-provider",
    version,
    about = "Publish and run security tools on OpenLatch",
    long_about = None,
    disable_help_subcommand = true,
)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Option<Command>,

    /// Activate a config profile (default: "default").
    #[arg(long, global = true, value_name = "NAME")]
    pub profile: Option<String>,

    /// Output format (TTY default = `table`, non-TTY default = `json`).
    #[arg(long, value_enum, global = true, value_name = "FORMAT")]
    pub output: Option<OutputFormat>,

    /// Suppress info/warn output. Errors still print.
    #[arg(long, short = 'q', global = true)]
    pub quiet: bool,

    /// Verbose human output (extra operational detail).
    #[arg(long, short = 'v', global = true)]
    pub verbose: bool,

    /// Internal state, timings, request/response bodies. Implies `--verbose`.
    #[arg(long, global = true)]
    pub debug: bool,

    /// Strip ANSI colors. Equivalent to setting `NO_COLOR=1`.
    #[arg(long, global = true)]
    pub no_color: bool,

    /// Assume "yes" to all interactive prompts.
    #[arg(long, short = 'y', global = true)]
    pub yes: bool,

    /// Show what would happen without actually doing it.
    #[arg(long, global = true)]
    pub dry_run: bool,

    /// Force-disable any interactive prompts.
    #[arg(long, global = true)]
    pub non_interactive: bool,

    /// (hidden) emit the entire CLI surface as Markdown to stdout.
    /// Used to regenerate `docs/cli-reference.md`.
    #[arg(long, hide = true)]
    pub markdown_help: bool,
}

/// Snapshot of the global flags. Built once by the dispatcher and threaded
/// into each command so tests can build it directly.
#[derive(Debug, Clone, Default)]
pub struct GlobalArgs {
    pub profile: Option<String>,
    pub output: Option<OutputFormat>,
    pub quiet: bool,
    pub verbose: bool,
    pub debug: bool,
    pub no_color: bool,
    pub yes: bool,
    pub dry_run: bool,
    pub non_interactive: bool,
}

impl GlobalArgs {
    fn from_cli(cli: &Cli) -> Self {
        Self {
            profile: cli.profile.clone(),
            output: cli.output,
            quiet: cli.quiet,
            verbose: cli.verbose || cli.debug,
            debug: cli.debug,
            no_color: cli.no_color,
            yes: cli.yes,
            dry_run: cli.dry_run,
            non_interactive: cli.non_interactive,
        }
    }
}

#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputFormat {
    Table,
    Json,
    Yaml,
    Sarif,
}

// ---------------------------------------------------------------------------
// Subcommands — PRD §8.1
// ---------------------------------------------------------------------------

#[derive(Subcommand, Debug)]
pub enum Command {
    /// Sign in to your OpenLatch account.
    Login(commands::auth::LoginArgs),

    /// Sign out and clear your saved credentials.
    Logout,

    /// Show who you're signed in as.
    Whoami,

    /// Set up a new tool — sign in and create your manifest.
    Init(commands::init::InitArgs),

    /// Create a starter tool project from a template.
    New {
        #[command(subcommand)]
        kind: NewKind,
    },

    /// Manage your publisher profile.
    Editor {
        #[command(subcommand)]
        action: EditorAction,
    },

    /// Submit your manifest to OpenLatch.
    Register(commands::register::RegisterArgs),

    /// Publish a new version of your tool.
    Publish(commands::publish::PublishArgs),

    /// Mark a tool version range as deprecated.
    Deprecate(commands::tools::DeprecateArgs),

    /// List, delete, or deprecate your tools.
    Tools {
        #[command(subcommand)]
        action: ToolsAction,
    },

    /// List, update, or delete your providers.
    Providers {
        #[command(subcommand)]
        action: ProvidersAction,
    },

    /// Manage bindings and webhook secrets.
    Bindings {
        #[command(subcommand)]
        action: BindingsAction,
    },

    /// Start the event listener that handles incoming requests.
    Listen(commands::listen::ListenArgs),

    /// Send a test event to your running listener.
    Trigger(commands::trigger::TriggerArgs),

    /// Watch live events as they arrive.
    Tail(commands::tail::TailArgs),

    /// Check that everything is set up correctly.
    Doctor,

    /// Update openlatch-provider to the latest version.
    #[command(name = "self")]
    SelfCmd {
        #[command(subcommand)]
        action: SelfAction,
    },

    /// View or change your local settings.
    Config {
        #[command(subcommand)]
        action: ConfigAction,
    },
}

// -- nested action enums ----------------------------------------------------

#[derive(Subcommand, Debug)]
pub enum NewKind {
    /// Create a starter tool project (Python, Rust, or Node).
    Tool {
        #[arg(long, value_enum)]
        template: ToolTemplate,
        /// Target directory (default: ./<slug>).
        #[arg(long)]
        out: Option<String>,
    },
}

#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolTemplate {
    Python,
    Rust,
    Node,
}

#[derive(Subcommand, Debug)]
pub enum EditorAction {
    /// Update your publisher profile.
    Update(commands::editor::UpdateArgs),
}

#[derive(Subcommand, Debug)]
pub enum ToolsAction {
    /// Show all your tools.
    List,
    /// Permanently delete a tool.
    Delete {
        slug: String,
        /// Skip confirmation prompt.
        #[arg(long)]
        yes: bool,
    },
    /// Mark a tool version range as deprecated.
    Deprecate(commands::tools::DeprecateArgs),
}

#[derive(Subcommand, Debug)]
pub enum ProvidersAction {
    /// Show all your providers.
    List,
    /// Update a provider's settings.
    Update { slug: String },
    /// Permanently delete a provider.
    Delete {
        slug: String,
        #[arg(long)]
        yes: bool,
    },
}

#[derive(Subcommand, Debug)]
pub enum BindingsAction {
    /// Show all your bindings.
    List,
    /// Generate a new webhook signing secret (shown once).
    RotateSecret {
        id: String,
        #[arg(long)]
        yes: bool,
    },
    /// Remove a binding's webhook signing secret.
    DeleteSecret {
        id: String,
        #[arg(long)]
        yes: bool,
    },
    /// Test that a binding's endpoint is reachable.
    Probe { id: String },
    /// Show traffic and latency metrics for a binding.
    Metrics { id: String },
    /// Permanently delete a binding.
    Delete {
        id: String,
        #[arg(long)]
        yes: bool,
    },
}

#[derive(Subcommand, Debug)]
pub enum SelfAction {
    /// Update openlatch-provider to the latest version.
    Update {
        /// Check for an update without installing it.
        #[arg(long)]
        check: bool,
        /// Install the update without prompting.
        #[arg(long)]
        apply: bool,
        /// Release channel (`stable` or `beta`).
        #[arg(long)]
        channel: Option<String>,
    },
}

#[derive(Subcommand, Debug)]
pub enum ConfigAction {
    /// Read a setting value.
    Get { key: String },
    /// Change a setting value.
    Set { key: String, value: String },
    /// Show all your profiles and their settings.
    List,
}

// ---------------------------------------------------------------------------
// Dispatcher
// ---------------------------------------------------------------------------

/// Parse the CLI and dispatch to the matching command. Bodies are stubs at
/// T0 — most return `OL-42xx not-yet-implemented` errors. Each task fills in
/// its slice in subsequent commits.
pub async fn dispatch() -> Result<(), OlError> {
    let cli = Cli::parse();

    if cli.markdown_help {
        // `--markdown-help` is hidden and synchronously dumps the entire CLI
        // tree to stdout in Markdown form. The clap-markdown crate is added in
        // P3.T6; for T0 we emit a clear "not yet wired" notice on stderr and
        // exit 0 so docs CI can fall back gracefully.
        eprintln!("--markdown-help: clap-markdown integration is wired in P3.T6");
        return Ok(());
    }

    let g = GlobalArgs::from_cli(&cli);

    match cli.command {
        None => {
            let out = crate::ui::output::OutputConfig::resolve(&g);
            crate::ui::header::print_full_banner(&out);
            <Cli as clap::CommandFactory>::command()
                .print_help()
                .map_err(|e| {
                    OlError::new(OL_4270_CONFIG_UNREADABLE, format!("printing help: {e}"))
                })?;
            println!();
            Ok(())
        }
        Some(Command::Login(args)) => commands::auth::login(&g, args).await,
        Some(Command::Logout) => commands::auth::logout(&g).await,
        Some(Command::Whoami) => commands::auth::whoami(&g).await,
        Some(Command::Init(args)) => commands::init::run(&g, args).await,
        Some(Command::New { kind }) => commands::new::run(&g, kind).await,
        Some(Command::Editor { action }) => commands::editor::run(&g, action).await,
        Some(Command::Register(args)) => commands::register::run(&g, args).await,
        Some(Command::Publish(args)) => commands::publish::run(&g, args).await,
        Some(Command::Deprecate(args)) => commands::tools::deprecate(&g, args).await,
        Some(Command::Tools { action }) => commands::tools::run(&g, action).await,
        Some(Command::Providers { action }) => commands::providers::run(&g, action).await,
        Some(Command::Bindings { action }) => commands::bindings::run(&g, action).await,
        Some(Command::Listen(args)) => commands::listen::run(&g, args).await,
        Some(Command::Trigger(args)) => commands::trigger::run(&g, args).await,
        Some(Command::Tail(args)) => commands::tail::run(&g, args).await,
        Some(Command::Doctor) => commands::doctor::run(&g).await,
        Some(Command::SelfCmd { action }) => commands::self_update::run(&g, action).await,
        Some(Command::Config { action }) => commands::config::run(&g, action).await,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use clap::CommandFactory;

    #[test]
    fn cli_definition_compiles() {
        // clap derive macro panics at startup if the definition is malformed;
        // calling debug_assert validates argument shapes.
        Cli::command().debug_assert();
    }
}