Skip to main content

clankers_cli/
lib.rs

1//! Command-line interface for [clankeRS](https://docs.rs/clankers).
2//!
3//! Install the `clankers` binary with `cargo install clankers-cli`, then scaffold
4//! projects, replay MCAP logs, validate ONNX models, and run bundled demos.
5//!
6//! ## Commands
7//!
8//! | Command | Purpose |
9//! |---------|---------|
10//! | `new` | Scaffold from `basic-node`, `perception-node`, `ml-inference-node`, or `replay-test-node` |
11//! | `run` | Run the current project's node |
12//! | `test` | Run replay tests and `cargo test` |
13//! | `inspect` | Summarize an MCAP file |
14//! | `replay` | Replay a log (optionally through a node) |
15//! | `latency` | Latency stats from a replay |
16//! | `compare` | Diff two MCAP files |
17//! | `validate-model` | ONNX vs PyTorch reference outputs |
18//! | `bench` | Benchmark inference latency and copy/allocation stats |
19//! | `import-pytorch` | Export a checkpoint to ONNX |
20//! | `add-model` | Register a model in `clankeRS.toml` |
21//! | `visualize` | MCAP summary for Foxglove / Rerun |
22//! | `demo` | Run bundled demos (`camera-perception`) |
23//! | `record` | Run the node with MCAP recording enabled |
24//!
25//! See the [crate README](https://github.com/PvRao-29/clankeRS/blob/main/crates/clankers-cli/README.md)
26//! for install steps and examples.
27
28mod commands;
29
30use clap::{Parser, Subcommand};
31use tracing_subscriber::EnvFilter;
32
33use commands::{
34    add_model, bench, compare, import_pytorch, inspect, latency, new_project, record, replay, run,
35    test, validate_model, visualize,
36};
37
38const BRAND: &str = "clankeRS";
39
40/// `clankers` CLI parsed from `std::env::args`.
41#[derive(Parser)]
42#[command(name = "clankers", about = "clankeRS — Rust robotics SDK CLI", version)]
43pub struct Cli {
44    #[command(subcommand)]
45    command: Commands,
46}
47
48/// Subcommands for the `clankers` binary.
49#[derive(Subcommand)]
50pub enum Commands {
51    /// Create a new clankeRS project from a template
52    New {
53        name: String,
54        #[arg(long, default_value = "basic-node")]
55        template: String,
56    },
57    /// Run the current clankeRS node
58    Run,
59    /// Run replay tests and cargo test
60    Test,
61    /// Inspect an MCAP file
62    Inspect { file: String },
63    /// Replay an MCAP file through a node
64    Replay {
65        file: String,
66        #[arg(long)]
67        node: Option<String>,
68    },
69    /// Report latency statistics from an MCAP replay
70    Latency { file: String },
71    /// Compare two MCAP files
72    Compare { expected: String, actual: String },
73    /// Record node I/O to MCAP
74    Record {
75        #[arg(long, default_value = "logs/run.mcap")]
76        output: String,
77    },
78    /// Benchmark model inference latency (p50/p95/p99) and copy/alloc accounting
79    Bench {
80        #[arg(long)]
81        model: String,
82        #[arg(long, default_value = "onnxruntime")]
83        backend: String,
84        #[arg(long)]
85        input: Option<String>,
86        #[arg(long, default_value = "50")]
87        warmup: u32,
88        #[arg(long, default_value = "1000")]
89        iters: u32,
90    },
91    /// Validate ONNX model output against PyTorch reference
92    ValidateModel {
93        #[arg(long)]
94        pytorch: Option<String>,
95        #[arg(long)]
96        checkpoint: Option<String>,
97        #[arg(long)]
98        onnx: String,
99        #[arg(long, default_value = "sample_data/policy_inputs/")]
100        samples: String,
101        #[arg(long, default_value = "0.001")]
102        tolerance: f32,
103    },
104    /// Import a PyTorch model and export to ONNX
105    ImportPytorch {
106        #[arg(long)]
107        model: String,
108        #[arg(long)]
109        checkpoint: String,
110        #[arg(long)]
111        output: String,
112        #[arg(long)]
113        opset: Option<u32>,
114    },
115    /// Add a model to the current project
116    AddModel { path: String },
117    /// Visualization hook — export MCAP summary for Foxglove/Rerun
118    Visualize { file: String },
119    /// Run the camera perception demo
120    Demo {
121        #[arg(default_value = "camera-perception")]
122        name: String,
123    },
124}
125
126/// Parse CLI arguments and dispatch the selected subcommand.
127pub async fn run() -> anyhow::Result<()> {
128    tracing_subscriber::fmt()
129        .with_env_filter(
130            EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
131        )
132        .init();
133
134    let cli = Cli::parse();
135
136    match cli.command {
137        Commands::New { name, template } => new_project::execute(&name, &template)?,
138        Commands::Run => run::execute()?,
139        Commands::Test => test::execute().await?,
140        Commands::Inspect { file } => inspect::execute(&file)?,
141        Commands::Replay { file, node } => replay::execute(&file, node.as_deref()).await?,
142        Commands::Latency { file } => latency::execute(&file).await?,
143        Commands::Compare { expected, actual } => compare::execute(&expected, &actual)?,
144        Commands::Bench {
145            model,
146            backend,
147            input,
148            warmup,
149            iters,
150        } => bench::execute(&model, &backend, input.as_deref(), warmup, iters)?,
151        Commands::Record { output } => record::execute(&output).await?,
152        Commands::ValidateModel {
153            pytorch,
154            checkpoint,
155            onnx,
156            samples,
157            tolerance,
158        } => validate_model::execute(
159            pytorch.as_deref(),
160            checkpoint.as_deref(),
161            &onnx,
162            &samples,
163            tolerance,
164        )?,
165        Commands::ImportPytorch {
166            model,
167            checkpoint,
168            output,
169            opset,
170        } => import_pytorch::execute(&model, &checkpoint, &output, opset)?,
171        Commands::AddModel { path } => add_model::execute(&path)?,
172        Commands::Visualize { file } => visualize::execute(&file)?,
173        Commands::Demo { name } => commands::demo::execute(&name).await?,
174    }
175
176    Ok(())
177}
178
179/// Print a branded status line to stdout.
180pub fn print_banner(msg: &str) {
181    println!("{BRAND} {msg}");
182}