use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Parser)]
#[command(
name = "cxpak",
about = "Spends CPU cycles so you don't spend tokens",
version
)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
Overview {
#[arg(long, default_value = "50k")]
tokens: String,
#[arg(long)]
out: Option<PathBuf>,
#[arg(long, default_value = "markdown")]
format: OutputFormat,
#[arg(long)]
verbose: bool,
#[arg(long)]
focus: Option<String>,
#[arg(long)]
timing: bool,
#[arg(default_value = ".")]
path: PathBuf,
},
Clean {
#[arg(default_value = ".")]
path: PathBuf,
},
Diff {
#[arg(long, default_value = "50k")]
tokens: String,
#[arg(long)]
out: Option<PathBuf>,
#[arg(long, default_value = "markdown")]
format: OutputFormat,
#[arg(long)]
verbose: bool,
#[arg(long)]
all: bool,
#[arg(long)]
git_ref: Option<String>,
#[arg(long)]
focus: Option<String>,
#[arg(long)]
since: Option<String>,
#[arg(long)]
timing: bool,
#[arg(default_value = ".")]
path: PathBuf,
},
#[cfg(feature = "daemon")]
Serve {
#[arg(long, default_value = "3000")]
port: u16,
#[arg(long, default_value = "50k")]
tokens: String,
#[arg(long)]
verbose: bool,
#[arg(long)]
mcp: bool,
#[arg(default_value = ".")]
path: PathBuf,
},
#[cfg(feature = "daemon")]
Watch {
#[arg(long, default_value = "50k")]
tokens: String,
#[arg(long, default_value = "markdown")]
format: OutputFormat,
#[arg(long)]
verbose: bool,
#[arg(default_value = ".")]
path: PathBuf,
},
Trace {
#[arg(long, default_value = "50k")]
tokens: String,
#[arg(long)]
out: Option<PathBuf>,
#[arg(long, default_value = "markdown")]
format: OutputFormat,
#[arg(long)]
verbose: bool,
#[arg(long)]
all: bool,
#[arg(long)]
focus: Option<String>,
#[arg(long)]
timing: bool,
target: String,
#[arg(default_value = ".")]
path: PathBuf,
},
}
#[derive(Clone, Debug, clap::ValueEnum)]
pub enum OutputFormat {
Markdown,
Xml,
Json,
}
pub fn parse_token_count(s: &str) -> Result<usize, String> {
let s = s.trim().to_lowercase();
if let Some(prefix) = s.strip_suffix('k') {
prefix
.parse::<f64>()
.map(|n| (n * 1_000.0) as usize)
.map_err(|e| format!("invalid token count: {e}"))
} else if let Some(prefix) = s.strip_suffix('m') {
prefix
.parse::<f64>()
.map(|n| (n * 1_000_000.0) as usize)
.map_err(|e| format!("invalid token count: {e}"))
} else {
s.parse::<usize>()
.map_err(|e| format!("invalid token count: {e}"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_token_count_plain_number() {
assert_eq!(parse_token_count("50000").unwrap(), 50000);
}
#[test]
fn test_parse_token_count_k_suffix() {
assert_eq!(parse_token_count("50k").unwrap(), 50000);
assert_eq!(parse_token_count("50K").unwrap(), 50000);
assert_eq!(parse_token_count("100k").unwrap(), 100000);
}
#[test]
fn test_parse_token_count_m_suffix() {
assert_eq!(parse_token_count("1m").unwrap(), 1000000);
assert_eq!(parse_token_count("1M").unwrap(), 1000000);
}
#[test]
fn test_parse_token_count_fractional() {
assert_eq!(parse_token_count("1.5k").unwrap(), 1500);
assert_eq!(parse_token_count("0.5m").unwrap(), 500000);
}
#[test]
fn test_parse_token_count_invalid() {
assert!(parse_token_count("abc").is_err());
assert!(parse_token_count("").is_err());
assert!(parse_token_count("k").is_err());
assert!(parse_token_count("m").is_err()); assert!(parse_token_count("xyzm").is_err());
}
#[test]
fn test_focus_flag_parses_for_overview() {
let cli = Cli::try_parse_from([
"cxpak", "overview", "--tokens", "50k", "--focus", "src/auth",
])
.expect("should parse successfully");
match cli.command {
Commands::Overview { focus, .. } => {
assert_eq!(focus.as_deref(), Some("src/auth"));
}
_ => panic!("expected Overview command"),
}
}
#[test]
fn test_focus_flag_parses_for_diff() {
let cli = Cli::try_parse_from(["cxpak", "diff", "--tokens", "50k", "--focus", "src/api"])
.expect("should parse successfully");
match cli.command {
Commands::Diff { focus, .. } => {
assert_eq!(focus.as_deref(), Some("src/api"));
}
_ => panic!("expected Diff command"),
}
}
#[test]
fn test_focus_flag_parses_for_trace() {
let cli = Cli::try_parse_from([
"cxpak",
"trace",
"--tokens",
"50k",
"--focus",
"src/lib",
"my_function",
])
.expect("should parse successfully");
match cli.command {
Commands::Trace { focus, .. } => {
assert_eq!(focus.as_deref(), Some("src/lib"));
}
_ => panic!("expected Trace command"),
}
}
#[test]
fn test_focus_flag_is_optional() {
let cli = Cli::try_parse_from(["cxpak", "overview", "--tokens", "50k"])
.expect("should parse without --focus");
match cli.command {
Commands::Overview { focus, .. } => {
assert!(focus.is_none());
}
_ => panic!("expected Overview command"),
}
}
#[test]
fn test_timing_flag_parses_for_overview() {
let cli = Cli::try_parse_from(["cxpak", "overview", "--tokens", "50k", "--timing"])
.expect("should parse with --timing");
match cli.command {
Commands::Overview { timing, .. } => {
assert!(timing);
}
_ => panic!("expected Overview command"),
}
}
#[test]
fn test_timing_flag_defaults_to_false() {
let cli = Cli::try_parse_from(["cxpak", "overview", "--tokens", "50k"])
.expect("should parse without --timing");
match cli.command {
Commands::Overview { timing, .. } => {
assert!(!timing);
}
_ => panic!("expected Overview command"),
}
}
#[test]
fn test_timing_flag_parses_for_diff() {
let cli = Cli::try_parse_from(["cxpak", "diff", "--tokens", "50k", "--timing"])
.expect("should parse with --timing");
match cli.command {
Commands::Diff { timing, .. } => {
assert!(timing);
}
_ => panic!("expected Diff command"),
}
}
#[test]
fn test_timing_flag_parses_for_trace() {
let cli = Cli::try_parse_from([
"cxpak",
"trace",
"--tokens",
"50k",
"--timing",
"my_function",
])
.expect("should parse with --timing");
match cli.command {
Commands::Trace { timing, .. } => {
assert!(timing);
}
_ => panic!("expected Trace command"),
}
}
#[test]
fn test_overview_default_tokens() {
let cli =
Cli::try_parse_from(["cxpak", "overview"]).expect("should parse without --tokens");
match cli.command {
Commands::Overview { tokens, .. } => {
assert_eq!(tokens, "50k");
}
_ => panic!("expected Overview"),
}
}
#[test]
fn test_diff_default_tokens() {
let cli = Cli::try_parse_from(["cxpak", "diff"]).expect("should parse without --tokens");
match cli.command {
Commands::Diff { tokens, .. } => {
assert_eq!(tokens, "50k");
}
_ => panic!("expected Diff"),
}
}
#[test]
fn test_trace_default_tokens() {
let cli = Cli::try_parse_from(["cxpak", "trace", "my_symbol"])
.expect("should parse without --tokens");
match cli.command {
Commands::Trace { tokens, .. } => {
assert_eq!(tokens, "50k");
}
_ => panic!("expected Trace"),
}
}
#[test]
fn test_tokens_override_still_works() {
let cli = Cli::try_parse_from(["cxpak", "overview", "--tokens", "100k"])
.expect("should parse with explicit --tokens");
match cli.command {
Commands::Overview { tokens, .. } => {
assert_eq!(tokens, "100k");
}
_ => panic!("expected Overview"),
}
}
}