use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
use crate::terminal::ColorChoice;
#[derive(Debug, Parser)]
#[command(
name = "smackdebt",
version,
about = "Less spaghetti. Help your agent write code you can follow.",
after_help = "Examples:\n smackdebt Find the trouble spots\n smackdebt src/auth Look closer\n smackdebt diff main Check what changed\n smackdebt init Set up your agents\n smackdebt gate Check the baseline"
)]
pub(crate) struct Cli {
#[command(subcommand)]
pub(crate) command: Option<Command>,
pub(crate) path: Option<PathBuf>,
#[command(flatten)]
pub(crate) common: Common,
}
#[derive(Debug, Subcommand)]
pub(crate) enum Command {
Init(crate::init::InitArgs),
#[command(name = "__hook", hide = true)]
Hook(crate::agent_hook::HookArgs),
#[command(
after_help = "Examples:\n smackdebt diff main\n smackdebt diff main src/auth\n smackdebt diff --json"
)]
Diff(DiffArgs),
#[command(
after_help = "Examples:\n smackdebt gate\n smackdebt gate --json\n smackdebt gate --update Accept the current debt as your baseline"
)]
Gate(GateArgs),
}
#[derive(Debug, Args)]
pub(crate) struct DiffArgs {
pub(crate) reference: Option<String>,
pub(crate) path: Option<PathBuf>,
#[command(flatten)]
pub(crate) common: Common,
}
#[derive(Debug, Args)]
pub(crate) struct GateArgs {
pub(crate) path: Option<PathBuf>,
#[arg(long)]
pub(crate) baseline: Option<PathBuf>,
#[arg(long, conflicts_with = "json")]
pub(crate) update: bool,
#[arg(long, conflicts_with = "color")]
pub(crate) json: bool,
#[arg(long, value_parser = parse_jobs)]
pub(crate) jobs: Option<usize>,
}
#[derive(Clone, Debug, Args)]
pub(crate) struct Common {
#[arg(long, conflicts_with = "color", help_heading = "Output")]
pub(crate) json: bool,
#[arg(long, value_parser = parse_jobs)]
pub(crate) jobs: Option<usize>,
#[arg(long, value_parser = parse_days)]
pub(crate) history: Option<u32>,
#[arg(long, help_heading = "Output")]
pub(crate) all: bool,
#[arg(long, value_parser = parse_top, conflicts_with_all = ["json", "all"], help_heading = "Output")]
pub(crate) top: Option<usize>,
#[arg(long, value_enum, global = true, help_heading = "Output")]
pub(crate) color: Option<ColorChoice>,
}
pub(crate) fn parse_days(value: &str) -> Result<u32, String> {
let days = value
.strip_suffix('d')
.ok_or_else(|| "use days, for example 90d".to_owned())?
.parse::<u32>()
.map_err(|_| "use a whole number of days".to_owned())?;
if days == 0 {
return Err("use at least one day".to_owned());
}
Ok(days)
}
fn parse_top(value: &str) -> Result<usize, String> {
let top = value
.parse::<usize>()
.map_err(|_| "use a whole number greater than zero".to_owned())?;
if top == 0 {
return Err("use a whole number greater than zero".to_owned());
}
Ok(top)
}
fn parse_jobs(value: &str) -> Result<usize, String> {
let jobs = value
.parse::<usize>()
.map_err(|_| "use a whole number greater than zero".to_owned())?;
if jobs == 0 {
return Err("use a whole number greater than zero".to_owned());
}
Ok(jobs)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_history_days() {
assert_eq!(parse_days("180d"), Ok(180));
assert!(parse_days("0d").is_err());
assert!(parse_days("90").is_err());
}
#[test]
fn rejects_zero_jobs() {
assert!(Cli::try_parse_from(["smackdebt", "--jobs", "0"]).is_err());
}
#[test]
fn all_and_json_are_rejected_by_the_command_not_by_usage_text() {
let cli = Cli::try_parse_from(["smackdebt", "--json", "--all"]).unwrap();
assert!(cli.common.json && cli.common.all);
}
#[test]
fn explicit_color_conflicts_with_json() {
assert!(Cli::try_parse_from(["smackdebt", "--json", "--color", "always"]).is_err());
}
#[test]
fn rejects_a_zero_finding_limit() {
assert!(Cli::try_parse_from(["smackdebt", "--top", "0"]).is_err());
let cli = Cli::try_parse_from(["smackdebt", "--top", "5"]).unwrap();
assert_eq!(cli.common.top, Some(5));
}
#[test]
fn a_baseline_update_conflicts_with_gate_json() {
assert!(Cli::try_parse_from(["smackdebt", "gate", "--update", "--json"]).is_err());
let cli = Cli::try_parse_from(["smackdebt", "gate", "--update"]).unwrap();
let Some(Command::Gate(args)) = cli.command else {
panic!("expected the gate subcommand");
};
assert!(args.update && !args.json);
}
#[test]
fn a_finding_limit_conflicts_with_json_and_with_all() {
assert!(Cli::try_parse_from(["smackdebt", "--top", "5", "--json"]).is_err());
assert!(Cli::try_parse_from(["smackdebt", "--top", "5", "--all"]).is_err());
}
}