use std::io::Read;
use std::path::PathBuf;
use std::sync::OnceLock;
use clap::error::ErrorKind;
use clap::{Parser, ValueEnum};
use clap_complete::Shell;
use crate::graph::RiskTier;
fn long_version() -> &'static str {
static VERSION: OnceLock<String> = OnceLock::new();
VERSION.get_or_init(|| {
#[allow(unused_mut)]
let mut languages = vec!["javascript/typescript"];
#[cfg(feature = "python")]
languages.push("python");
#[cfg(feature = "rust")]
languages.push("rust");
#[cfg(feature = "vue")]
languages.push("vue");
#[cfg(feature = "svelte")]
languages.push("svelte");
format!(
"{}\nlanguages: {}",
env!("CARGO_PKG_VERSION"),
languages.join(", ")
)
})
}
#[derive(Debug, Clone, Parser)]
#[command(name = "blast-radius")]
#[command(
version,
long_version = long_version(),
about = "Estimate the transitive blast radius of frontend code changes"
)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
#[arg(long, global = true, default_value = ".")]
pub repo_root: PathBuf,
#[arg(long, global = true, value_enum, default_value_t = OutputFormat::Tree)]
pub format: OutputFormat,
#[arg(long, global = true)]
pub output: Option<PathBuf>,
#[arg(long, short = 'v', global = true, default_value_t = false)]
pub verbose: bool,
#[arg(long, short = 'q', global = true, default_value_t = false)]
pub quiet: bool,
#[arg(long, global = true, value_enum, default_value_t = ColorChoice::Auto)]
pub color: ColorChoice,
#[arg(long, global = true, default_value_t = false)]
pub explain_unresolved: bool,
#[arg(long, global = true)]
pub fail_threshold: Option<usize>,
#[arg(long, global = true, value_enum)]
pub fail_on_risk: Option<RiskTier>,
}
#[derive(Debug, Clone, Parser)]
pub enum Command {
Export { file: PathBuf, export_name: String },
File { file: PathBuf },
Files {
#[arg(required = true, num_args = 1..)]
files: Vec<PathBuf>,
},
Completions {
#[arg(value_enum)]
shell: Shell,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum OutputFormat {
Tree,
Json,
Mermaid,
Dot,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum ColorChoice {
Auto,
Always,
Never,
}
impl Cli {
pub fn parse_args() -> Self {
match Self::try_parse() {
Ok(cli) => cli,
Err(error) => {
let code = match error.kind() {
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => 0,
_ => 64,
};
let _ = error.print();
std::process::exit(code);
}
}
}
pub fn expand_stdin_file_list(&mut self) -> anyhow::Result<()> {
let Command::Files { files } = &mut self.command else {
return Ok(());
};
if !files.iter().any(|file| file.as_os_str() == "-") {
return Ok(());
}
let mut buffer = String::new();
std::io::stdin()
.read_to_string(&mut buffer)
.map_err(|error| anyhow::anyhow!("failed to read file list from stdin: {error}"))?;
let stdin_files: Vec<PathBuf> = buffer
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(PathBuf::from)
.collect();
let mut expanded = Vec::with_capacity(files.len() + stdin_files.len());
for file in files.drain(..) {
if file.as_os_str() == "-" {
expanded.extend(stdin_files.iter().cloned());
} else {
expanded.push(file);
}
}
if expanded.is_empty() {
anyhow::bail!("no files provided: stdin file list was empty");
}
*files = expanded;
Ok(())
}
}