pub mod commands;
use std::io::IsTerminal;
use std::path::PathBuf;
use std::process;
use clap::{Parser, Subcommand};
use tracing_subscriber::EnvFilter;
pub(crate) const RED: &str = "\x1b[31m";
pub(crate) const GREEN: &str = "\x1b[32m";
pub(crate) const YELLOW: &str = "\x1b[33m";
pub(crate) const BLUE: &str = "\x1b[34m";
pub(crate) const MAGENTA: &str = "\x1b[35m";
pub(crate) const CYAN: &str = "\x1b[36m";
pub(crate) const WHITE: &str = "\x1b[97m";
pub(crate) const DIM: &str = "\x1b[2m";
pub(crate) const BOLD: &str = "\x1b[1m";
pub(crate) const RESET: &str = "\x1b[0m";
pub(crate) fn use_colors() -> bool {
std::env::var("NO_COLOR").is_err()
&& std::env::var("CI").is_err()
&& std::io::stderr().is_terminal()
}
#[allow(dead_code)]
pub(crate) struct C {
pub enabled: bool,
}
#[allow(dead_code)]
impl C {
pub fn new() -> Self {
Self {
enabled: use_colors(),
}
}
pub fn red(&self, s: &str) -> String {
if self.enabled {
format!("{RED}{s}{RESET}")
} else {
s.to_string()
}
}
pub fn green(&self, s: &str) -> String {
if self.enabled {
format!("{GREEN}{s}{RESET}")
} else {
s.to_string()
}
}
pub fn yellow(&self, s: &str) -> String {
if self.enabled {
format!("{YELLOW}{s}{RESET}")
} else {
s.to_string()
}
}
pub fn cyan(&self, s: &str) -> String {
if self.enabled {
format!("{CYAN}{s}{RESET}")
} else {
s.to_string()
}
}
pub fn magenta(&self, s: &str) -> String {
if self.enabled {
format!("{MAGENTA}{s}{RESET}")
} else {
s.to_string()
}
}
pub fn white(&self, s: &str) -> String {
if self.enabled {
format!("{WHITE}{s}{RESET}")
} else {
s.to_string()
}
}
pub fn dim(&self, s: &str) -> String {
if self.enabled {
format!("{DIM}{s}{RESET}")
} else {
s.to_string()
}
}
pub fn bold(&self, s: &str) -> String {
if self.enabled {
format!("{BOLD}{s}{RESET}")
} else {
s.to_string()
}
}
}
fn banner_string(use_colors: bool) -> String {
let (g, c, bl, m, y, re, w, d, b, r) = if use_colors {
(
GREEN, CYAN, BLUE, MAGENTA, YELLOW, RED, WHITE, DIM, BOLD, RESET,
)
} else {
("", "", "", "", "", "", "", "", "", "")
};
let version = env!("CARGO_PKG_VERSION");
let fossil = [
format!(" {g}{b}███████╗{r} {c}{b}██████╗{r} {bl}{b}███████╗{r}{m}{b}███████╗{r}{y}{b}██╗{r}{re}{b}██╗{r}"),
format!(" {g}{b}██{r}{g}╔════╝{r}{c}{b}██{r}{c}╔═══{r}{c}{b}██{r}{c}╗{r}{bl}{b}██{r}{bl}╔════╝{r}{m}{b}██{r}{m}╔════╝{r}{y}{b}██{r}{y}║{r}{re}{b}██{r}{re}║{r}"),
format!(" {g}{b}█████╗{r} {c}{b}██{r}{c}║ {r}{c}{b}██{r}{c}║{r}{bl}{b}███████╗{r}{m}{b}███████╗{r}{y}{b}██{r}{y}║{r}{re}{b}██{r}{re}║{r}"),
format!(" {g}{b}██{r}{g}╔══╝{r} {c}{b}██{r}{c}║ {r}{c}{b}██{r}{c}║{r}{bl}╚════{r}{bl}{b}██{r}{bl}║{r}{m}╚════{r}{m}{b}██{r}{m}║{r}{y}{b}██{r}{y}║{r}{re}{b}██{r}{re}║{r}"),
format!(" {g}{b}██{r}{g}║{r} {c}╚{r}{c}{b}██████{r}{c}╔╝{r}{bl}{b}███████{r}{bl}║{r}{m}{b}███████{r}{m}║{r}{y}{b}██{r}{y}║{r}{re}{b}███████{r}{re}╗{r}"),
format!(" {d}╚═╝ ╚═════╝ ╚══════╝╚══════╝╚═╝╚══════╝{r}"),
];
let pad = [5, 5, 5, 5, 0, 0]; let gap = 6;
let bone = [
format!("{d}() (){r}"),
format!("{d} \\ /{r}"),
format!("{d} | |{r}"),
format!("{d} | |{r}"),
format!("{d} / \\{r}"),
format!("{d}() (){r}"),
];
let mut out = String::from("\n");
for i in 0..6 {
out.push_str(&fossil[i]);
for _ in 0..(pad[i] + gap) {
out.push(' ');
}
out.push_str(&bone[i]);
out.push('\n');
}
out.push_str(&format!(
" {d}~~ ~~ ~ ~~ ~ ~~ ~ ~~ ~ ~~ ~ ~~ ~ ~~ ~ ~~ ~ ~~{r}\n"
));
out.push_str(&format!(
"{w}{b} Dig up dead code. Unearth clones. Expose scaffolding.{r}\n"
));
out.push_str(&format!(
"{d} ────────────────────────────────────────────────────────────────{r}\n"
));
out.push_str(&format!("{d} Version:{r} {y}{version}{r} {d}Languages:{r} {w}15{r} {d}Analyses:{r} {w}dead code · clones · scaffolding · temp files{r}\n"));
out.push_str(&format!(
"{d} ────────────────────────────────────────────────────────────────{r}\n"
));
out
}
pub fn print_banner() {
let colors = use_colors();
eprint!("{}", banner_string(colors));
}
const HELP_BANNER: &str = r#"
███████╗ ██████╗ ███████╗███████╗██╗██╗ () ()
██╔════╝██╔═══██╗██╔════╝██╔════╝██║██║ \ /
█████╗ ██║ ██║███████╗███████╗██║██║ | |
██╔══╝ ██║ ██║╚════██║╚════██║██║██║ | |
██║ ╚██████╔╝███████║███████║██║███████╗ / \
╚═╝ ╚═════╝ ╚══════╝╚══════╝╚═╝╚══════╝ () ()
~~ ~~ ~ ~~ ~ ~~ ~ ~~ ~ ~~ ~ ~~ ~ ~~ ~ ~~ ~ ~~
Dig up dead code. Unearth clones. Expose scaffolding.
"#;
#[derive(Parser)]
#[command(name = "fossil-mcp", version, about, long_about = None)]
#[command(propagate_version = true)]
#[command(before_help = HELP_BANNER)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(long, global = true, default_value = "text")]
format: String,
#[arg(short, long, global = true)]
output: Option<PathBuf>,
#[arg(short, long, global = true)]
quiet: bool,
#[arg(short, long, global = true)]
verbose: bool,
#[arg(short, long, global = true)]
config: Option<PathBuf>,
}
#[derive(Subcommand)]
enum Commands {
#[command(name = "dead-code")]
DeadCode {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long)]
include_tests: bool,
#[arg(long, default_value = "low")]
min_confidence: String,
#[arg(long, default_value = "0")]
min_lines: usize,
},
Clones {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, default_value = "6")]
min_lines: usize,
#[arg(long, default_value = "0.8")]
similarity: f64,
#[arg(long, default_value = "type1,type2,type3")]
types: String,
},
Scan {
#[arg(default_value = ".")]
path: PathBuf,
},
Rules {
#[command(subcommand)]
action: RulesAction,
},
Update {
#[arg(long)]
check: bool,
},
}
#[derive(Subcommand)]
enum RulesAction {
List,
Validate {
path: PathBuf,
},
}
pub fn run() {
let cli = Cli::parse();
if !cli.quiet {
print_banner();
}
let filter = if cli.verbose {
EnvFilter::new("debug")
} else {
EnvFilter::new("error")
};
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_target(false)
.with_writer(std::io::stderr)
.init();
let mut config = if let Some(ref config_path) = cli.config {
match crate::config::FossilConfig::load(config_path) {
Ok(c) => c,
Err(e) => {
eprintln!("Error loading config: {e}");
process::exit(1);
}
}
} else {
crate::config::FossilConfig::discover(&std::env::current_dir().unwrap_or_default())
};
config.apply_env_overrides();
let result = match cli.command {
Commands::DeadCode {
path,
include_tests,
min_confidence,
min_lines,
} => commands::dead_code::run(
&path,
include_tests,
&min_confidence,
min_lines,
&cli.format,
cli.quiet,
),
Commands::Clones {
path,
min_lines,
similarity,
types,
} => commands::clones::run(&path, min_lines, similarity, &types, &cli.format, cli.quiet),
Commands::Scan { path } => commands::scan::run(&path, &config, &cli.format, cli.quiet),
Commands::Rules { action } => match action {
RulesAction::List => commands::rules::list(),
RulesAction::Validate { path } => commands::rules::validate(&path),
},
Commands::Update { check } => commands::update::run(check),
};
match result {
Ok(output) => {
if let Some(ref output_path) = cli.output {
if let Err(e) = std::fs::write(output_path, &output) {
eprintln!("Error writing output: {e}");
process::exit(1);
}
} else if !output.is_empty() {
print!("{output}");
}
}
Err(e) => {
eprintln!("Error: {e}");
process::exit(1);
}
}
}