pub mod callees;
pub mod callers;
pub mod clones;
pub mod communities;
pub mod dead_code;
pub mod diff;
pub mod eval;
pub mod find;
pub mod flows;
pub mod helpers;
pub mod impact;
pub mod index;
pub mod refs;
pub mod risk;
pub mod search;
pub mod setup;
pub mod setup_helpers;
pub mod stats;
pub mod stubs;
pub mod watch;
use clap::{ArgAction, Parser, Subcommand};
#[derive(Parser)]
#[command(
name = "tcg",
version,
about = "Index codebases into a queryable dependency graph"
)]
pub struct Cli {
#[arg(short, long, action = ArgAction::Count, global = true)]
pub verbose: u8,
#[arg(long, global = true)]
pub debug: bool,
#[arg(long, global = true)]
pub json: bool,
#[arg(long, global = true)]
pub table: bool,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
Index(IndexArgs),
Find(FindArgs),
Refs(RefsArgs),
Risk(RiskArgs),
Impact(ImpactArgs),
#[command(name = "dead-code")]
DeadCode(DeadCodeArgs),
Diff(DiffArgs),
Callers(CallersArgs),
Callees(CalleesArgs),
Search(SearchArgs),
Flows(FlowsArgs),
Clones(ClonesArgs),
Communities(CommunitiesArgs),
Stats,
Watch(WatchArgs),
Setup(SetupArgs),
Eval(EvalArgs),
}
#[derive(clap::Args)]
pub struct IndexArgs {
#[arg(long)]
pub path: Option<std::path::PathBuf>,
#[arg(long)]
pub incremental: bool,
#[arg(long, value_delimiter = ',')]
pub files: Option<Vec<std::path::PathBuf>>,
#[arg(long)]
pub embed: bool,
#[arg(long, default_value = "all-MiniLM-L6-v2")]
pub embed_model: String,
}
#[derive(clap::Args)]
pub struct FindArgs {
pub pattern: String,
}
#[derive(clap::Args)]
pub struct RefsArgs {
pub qualified_name: String,
}
#[derive(clap::Args)]
pub struct RiskArgs {
pub target: Option<String>,
#[arg(long)]
pub symbols: bool,
#[arg(long, default_value = "20")]
pub limit: usize,
#[arg(long, default_value = "0.0")]
pub min_score: f64,
}
#[derive(clap::Args)]
pub struct ImpactArgs {
pub target: String,
#[arg(long, default_value = "3")]
pub depth: usize,
#[arg(long, default_value = "all")]
pub confidence: String,
}
#[derive(clap::Args)]
pub struct DiffArgs {
#[arg(default_value = "HEAD")]
pub from: String,
pub to: Option<String>,
#[arg(long, default_value = "3")]
pub depth: usize,
#[arg(long, default_value = "all")]
pub confidence: String,
}
#[derive(clap::Args)]
pub struct CallersArgs {
pub qualified_name: String,
}
#[derive(clap::Args)]
pub struct CalleesArgs {
pub qualified_name: String,
}
#[derive(clap::Args)]
pub struct WatchArgs {
#[arg(long)]
pub daemon: bool,
#[arg(long)]
pub status: bool,
#[arg(long)]
pub stop: bool,
#[arg(long, hide = true)]
pub daemon_internal: bool,
#[arg(long)]
pub path: Option<std::path::PathBuf>,
}
#[derive(clap::Args)]
pub struct SearchArgs {
pub query: String,
#[arg(long, default_value = "20")]
pub limit: usize,
#[arg(long)]
pub semantic_only: bool,
#[arg(long)]
pub fts_only: bool,
}
#[derive(clap::Args)]
pub struct EvalArgs {
#[arg(long, default_value = "all")]
pub suite: String,
#[arg(long)]
pub no_cache: bool,
}
#[derive(clap::Args)]
pub struct FlowsArgs {
#[arg(long)]
pub symbol: Option<String>,
#[arg(long)]
pub rank: bool,
#[arg(long, default_value = "20")]
pub depth: usize,
#[arg(long, default_value = "20")]
pub limit: usize,
}
#[derive(clap::Args)]
pub struct ClonesArgs {
#[arg(long, default_value = "0.7")]
pub threshold: f64,
#[arg(long, default_value = "5")]
pub min_lines: usize,
#[arg(long)]
pub cluster: Option<usize>,
}
#[derive(clap::Args)]
pub struct CommunitiesArgs {
pub community_id: Option<usize>,
#[arg(long)]
pub resolution: Option<f64>,
#[arg(long)]
pub min_size: Option<usize>,
#[arg(long)]
pub seed: Option<u64>,
#[arg(long)]
pub symbol: Option<String>,
#[arg(long, default_value = "20")]
pub limit: usize,
}
#[derive(clap::Args)]
pub struct SetupArgs {
pub platform: Option<String>,
#[arg(long)]
pub global: bool,
#[arg(long)]
pub check: bool,
#[arg(long)]
pub remove: bool,
#[arg(long, requires = "remove")]
pub clean: bool,
#[arg(long, requires = "remove")]
pub purge: bool,
}
#[derive(clap::Args)]
pub struct DeadCodeArgs {
#[arg(long = "exclude-pattern")]
pub exclude_pattern: Vec<String>,
#[arg(long)]
pub include_tests: bool,
#[arg(long)]
pub kind: Vec<String>,
#[arg(long)]
pub limit: Option<usize>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_index_command() {
let cli = Cli::parse_from(["code-graph", "index"]);
assert!(matches!(cli.command, Commands::Index(_)));
}
#[test]
fn parse_find_command() {
let cli = Cli::parse_from(["code-graph", "find", "Foo"]);
if let Commands::Find(args) = cli.command {
assert_eq!(args.pattern, "Foo");
} else {
panic!("expected Find command");
}
}
#[test]
fn parse_json_global_flag() {
let cli = Cli::parse_from(["code-graph", "--json", "stats"]);
assert!(cli.json);
}
#[test]
fn parse_verbose_flag() {
let cli = Cli::parse_from(["code-graph", "-vv", "stats"]);
assert_eq!(cli.verbose, 2);
}
#[test]
fn parse_clones_command() {
let cli = Cli::parse_from(["code-graph", "clones"]);
if let Commands::Clones(args) = cli.command {
assert!((args.threshold - 0.7).abs() < f64::EPSILON);
assert_eq!(args.min_lines, 5);
assert!(args.cluster.is_none());
} else {
panic!("expected Clones command");
}
}
#[test]
fn all_subcommands_parse() {
let commands = [
vec!["code-graph", "index"],
vec!["code-graph", "find", "X"],
vec!["code-graph", "refs", "a::b"],
vec!["code-graph", "impact", "a::b"],
vec!["code-graph", "diff"],
vec!["code-graph", "callers", "a::b"],
vec!["code-graph", "callees", "a::b"],
vec!["code-graph", "search", "foo"],
vec!["code-graph", "search", "foo", "--semantic-only"],
vec!["code-graph", "search", "foo", "--fts-only"],
vec!["code-graph", "index", "--embed"],
vec!["code-graph", "index", "--embed", "--embed-model", "custom"],
vec!["code-graph", "flows"],
vec!["code-graph", "flows", "--rank"],
vec!["code-graph", "flows", "--symbol", "foo::bar"],
vec!["code-graph", "flows", "--depth", "10", "--limit", "50"],
vec!["code-graph", "clones"],
vec!["code-graph", "clones", "--threshold", "0.8"],
vec!["code-graph", "clones", "--min-lines", "10"],
vec!["code-graph", "clones", "--cluster", "1"],
vec![
"code-graph",
"clones",
"--threshold",
"0.9",
"--min-lines",
"3",
"--cluster",
"2",
],
vec!["code-graph", "risk"],
vec!["code-graph", "risk", "--symbols"],
vec!["code-graph", "risk", "--symbols", "--limit", "50"],
vec!["code-graph", "risk", "AuthService"],
vec!["code-graph", "risk", "--min-score", "0.5"],
vec!["code-graph", "stats"],
vec!["code-graph", "watch"],
vec!["code-graph", "watch", "--daemon"],
vec!["code-graph", "watch", "--status"],
vec!["code-graph", "watch", "--stop"],
vec!["code-graph", "setup", "claude"],
vec!["code-graph", "setup", "--check"],
vec!["code-graph", "setup", "--remove"],
vec!["code-graph", "setup", "--remove", "--clean"],
vec!["code-graph", "setup", "--remove", "--purge"],
vec!["code-graph", "eval"],
vec!["code-graph", "eval", "--suite", "search"],
vec!["code-graph", "eval", "--no-cache"],
vec!["code-graph", "communities"],
vec!["code-graph", "communities", "--resolution", "1.5"],
vec![
"code-graph",
"communities",
"--seed",
"42",
"--min-size",
"3",
],
vec!["code-graph", "communities", "1"],
vec!["code-graph", "communities", "--symbol", "src/main.rs::main"],
vec!["code-graph", "dead-code"],
vec!["code-graph", "dead-code", "--include-tests"],
vec!["code-graph", "dead-code", "--exclude-pattern", "**/gen/**"],
vec![
"code-graph",
"dead-code",
"--kind",
"Function",
"--limit",
"10",
],
];
for args in &commands {
Cli::parse_from(args.iter());
}
}
#[test]
fn parse_dead_code_command() {
let cli = Cli::parse_from(["code-graph", "dead-code"]);
assert!(matches!(cli.command, Commands::DeadCode(_)));
}
#[test]
fn parse_dead_code_with_flags() {
let cli = Cli::parse_from([
"code-graph",
"dead-code",
"--include-tests",
"--exclude-pattern",
"**/generated/**",
"--kind",
"Function",
"--limit",
"50",
]);
if let Commands::DeadCode(args) = cli.command {
assert!(args.include_tests);
assert_eq!(args.exclude_pattern, vec!["**/generated/**"]);
assert_eq!(args.kind, vec!["Function"]);
assert_eq!(args.limit, Some(50));
} else {
panic!("expected DeadCode command");
}
}
#[test]
fn parse_risk_command() {
let cli = Cli::parse_from(["code-graph", "risk"]);
assert!(matches!(cli.command, Commands::Risk(_)));
}
#[test]
fn parse_risk_symbols() {
let cli = Cli::parse_from(["code-graph", "risk", "--symbols", "--limit", "50"]);
if let Commands::Risk(args) = cli.command {
assert!(args.symbols);
assert_eq!(args.limit, 50);
} else {
panic!("expected Risk command");
}
}
#[test]
fn parse_risk_target() {
let cli = Cli::parse_from(["code-graph", "risk", "AuthService"]);
if let Commands::Risk(args) = cli.command {
assert_eq!(args.target.unwrap(), "AuthService");
} else {
panic!("expected Risk command");
}
}
#[test]
fn parse_risk_min_score() {
let cli = Cli::parse_from(["code-graph", "risk", "--min-score", "0.5"]);
if let Commands::Risk(args) = cli.command {
assert!((args.min_score - 0.5).abs() < f64::EPSILON);
} else {
panic!("expected Risk command");
}
}
#[test]
fn parse_search_with_semantic_only() {
let cli = Cli::parse_from(["code-graph", "search", "foo", "--semantic-only"]);
if let Commands::Search(args) = cli.command {
assert!(args.semantic_only);
assert!(!args.fts_only);
} else {
panic!("expected Search");
}
}
#[test]
fn parse_search_with_fts_only() {
let cli = Cli::parse_from(["code-graph", "search", "foo", "--fts-only"]);
if let Commands::Search(args) = cli.command {
assert!(args.fts_only);
assert!(!args.semantic_only);
} else {
panic!("expected Search");
}
}
#[test]
fn parse_index_with_embed() {
let cli = Cli::parse_from(["code-graph", "index", "--embed"]);
if let Commands::Index(args) = cli.command {
assert!(args.embed);
assert_eq!(args.embed_model, "all-MiniLM-L6-v2");
} else {
panic!("expected Index");
}
}
#[test]
fn parse_index_with_embed_model() {
let cli = Cli::parse_from([
"code-graph",
"index",
"--embed",
"--embed-model",
"custom-model",
]);
if let Commands::Index(args) = cli.command {
assert!(args.embed);
assert_eq!(args.embed_model, "custom-model");
} else {
panic!("expected Index");
}
}
#[test]
fn stub_returns_not_implemented() {
let result = stubs::not_implemented("find");
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("not yet implemented"));
}
}