percli 0.2.1

Offline CLI simulator for the Percolator risk engine
mod commands;
mod format;

use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::{generate, Shell};
use format::OutputFormat;

#[derive(Parser)]
#[command(
    name = "percli",
    about = "Offline simulator for the Percolator risk engine",
    long_about = "Simulate haircuts, liquidations, margin checks, and conservation proofs\nfor the Percolator risk engine — no chain required.",
    version
)]
struct Cli {
    #[command(subcommand)]
    command: Commands,

    /// Output format (table or json). Also settable via PERC_FORMAT env var.
    #[arg(long, short, global = true)]
    format: Option<OutputFormat>,
}

#[derive(Subcommand)]
enum Commands {
    /// Create a new scenario template
    Init {
        /// Template to use
        #[arg(long, default_value = "basic")]
        template: String,
    },
    /// Run a scenario file
    Sim {
        /// Path to scenario TOML file
        path: String,
        /// Print state after each step
        #[arg(long)]
        step_by_step: bool,
        /// Disable conservation checks after each step
        #[arg(long)]
        no_check_conservation: bool,
        /// Verbose output with deltas
        #[arg(long, short)]
        verbose: bool,
        /// Override a parameter: key=value (e.g. maintenance_margin_bps=300)
        #[arg(long = "override", value_name = "KEY=VALUE")]
        overrides: Vec<String>,
    },
    /// Execute a single operation on a saved engine state
    Step {
        /// Path to engine state JSON file
        #[arg(long)]
        state: String,
        #[command(subcommand)]
        action: StepAction,
    },
    /// Read-only queries on engine state
    Query {
        /// Path to engine state JSON file
        #[arg(long)]
        state: String,
        /// Metric to query
        metric: String,
        /// Account name (for account-specific queries)
        #[arg(long)]
        account: Option<String>,
    },
    /// Validate a scenario file without running it
    Inspect {
        /// Path to scenario TOML file
        path: String,
    },
    /// Generate shell completions
    Completions {
        /// Shell to generate completions for
        shell: Shell,
    },
}

#[derive(Subcommand)]
enum StepAction {
    /// Deposit to an account
    Deposit { account: String, amount: u128 },
    /// Withdraw from an account
    Withdraw { account: String, amount: u128 },
    /// Execute a trade
    Trade {
        long: String,
        short: String,
        #[arg(long)]
        size: i128,
        #[arg(long)]
        price: u64,
    },
    /// Run a keeper crank
    Crank {
        #[arg(long)]
        oracle: u64,
        #[arg(long)]
        slot: u64,
    },
    /// Liquidate an account
    Liquidate { account: String },
    /// Set oracle price
    SetOracle { price: u64 },
    /// Set current slot
    SetSlot { slot: u64 },
}

fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();
    let fmt = cli.format.unwrap_or_else(OutputFormat::from_env);

    match cli.command {
        Commands::Init { template } => commands::init::run(&template),
        Commands::Sim {
            path,
            step_by_step,
            no_check_conservation,
            verbose,
            overrides,
        } => commands::sim::run(
            &path,
            fmt,
            step_by_step,
            !no_check_conservation,
            verbose,
            &overrides,
        ),
        Commands::Step { state, action } => commands::step::run(&state, action, fmt),
        Commands::Query {
            state,
            metric,
            account,
        } => commands::query::run(&state, &metric, account.as_deref(), fmt),
        Commands::Inspect { path } => commands::inspect::run(&path),
        Commands::Completions { shell } => {
            let mut cmd = Cli::command();
            generate(shell, &mut cmd, "percli", &mut std::io::stdout());
            Ok(())
        }
    }
}