1pub mod callees;
2pub mod callers;
3pub mod clones;
4pub mod communities;
5pub mod dead_code;
6pub mod diff;
7pub mod eval;
8pub mod find;
9pub mod flows;
10pub mod helpers;
11pub mod impact;
12pub mod index;
13pub mod refs;
14pub mod risk;
15pub mod search;
16pub mod setup;
17pub mod setup_helpers;
18pub mod stats;
19pub mod stubs;
20pub mod watch;
21
22use clap::{ArgAction, Parser, Subcommand};
23
24#[derive(Parser)]
25#[command(
26 name = "tcg",
27 version,
28 about = "Index codebases into a queryable dependency graph"
29)]
30pub struct Cli {
31 #[arg(short, long, action = ArgAction::Count, global = true)]
33 pub verbose: u8,
34
35 #[arg(long, global = true)]
37 pub debug: bool,
38
39 #[arg(long, global = true)]
41 pub json: bool,
42
43 #[arg(long, global = true)]
45 pub table: bool,
46
47 #[command(subcommand)]
48 pub command: Commands,
49}
50
51#[derive(Subcommand)]
52pub enum Commands {
53 Index(IndexArgs),
55 Find(FindArgs),
57 Refs(RefsArgs),
59 Risk(RiskArgs),
61 Impact(ImpactArgs),
63 #[command(name = "dead-code")]
65 DeadCode(DeadCodeArgs),
66 Diff(DiffArgs),
68 Callers(CallersArgs),
70 Callees(CalleesArgs),
72 Search(SearchArgs),
74 Flows(FlowsArgs),
76 Clones(ClonesArgs),
78 Communities(CommunitiesArgs),
80 Stats,
82 Watch(WatchArgs),
84 Setup(SetupArgs),
86 Eval(EvalArgs),
88}
89
90#[derive(clap::Args)]
91pub struct IndexArgs {
92 #[arg(long)]
94 pub path: Option<std::path::PathBuf>,
95
96 #[arg(long)]
98 pub incremental: bool,
99
100 #[arg(long, value_delimiter = ',')]
102 pub files: Option<Vec<std::path::PathBuf>>,
103
104 #[arg(long)]
106 pub embed: bool,
107
108 #[arg(long, default_value = "all-MiniLM-L6-v2")]
110 pub embed_model: String,
111}
112
113#[derive(clap::Args)]
114pub struct FindArgs {
115 pub pattern: String,
117}
118
119#[derive(clap::Args)]
120pub struct RefsArgs {
121 pub qualified_name: String,
123}
124
125#[derive(clap::Args)]
126pub struct RiskArgs {
127 pub target: Option<String>,
129 #[arg(long)]
131 pub symbols: bool,
132 #[arg(long, default_value = "20")]
134 pub limit: usize,
135 #[arg(long, default_value = "0.0")]
137 pub min_score: f64,
138}
139
140#[derive(clap::Args)]
141pub struct ImpactArgs {
142 pub target: String,
144 #[arg(long, default_value = "3")]
146 pub depth: usize,
147 #[arg(long, default_value = "all")]
149 pub confidence: String,
150}
151
152#[derive(clap::Args)]
153pub struct DiffArgs {
154 #[arg(default_value = "HEAD")]
156 pub from: String,
157 pub to: Option<String>,
159 #[arg(long, default_value = "3")]
161 pub depth: usize,
162 #[arg(long, default_value = "all")]
164 pub confidence: String,
165}
166
167#[derive(clap::Args)]
168pub struct CallersArgs {
169 pub qualified_name: String,
171}
172
173#[derive(clap::Args)]
174pub struct CalleesArgs {
175 pub qualified_name: String,
177}
178
179#[derive(clap::Args)]
180pub struct WatchArgs {
181 #[arg(long)]
183 pub daemon: bool,
184
185 #[arg(long)]
187 pub status: bool,
188
189 #[arg(long)]
191 pub stop: bool,
192
193 #[arg(long, hide = true)]
195 pub daemon_internal: bool,
196
197 #[arg(long)]
199 pub path: Option<std::path::PathBuf>,
200}
201
202#[derive(clap::Args)]
203pub struct SearchArgs {
204 pub query: String,
206 #[arg(long, default_value = "20")]
208 pub limit: usize,
209 #[arg(long)]
211 pub semantic_only: bool,
212 #[arg(long)]
214 pub fts_only: bool,
215}
216
217#[derive(clap::Args)]
218pub struct EvalArgs {
219 #[arg(long, default_value = "all")]
221 pub suite: String,
222 #[arg(long)]
224 pub no_cache: bool,
225}
226
227#[derive(clap::Args)]
228pub struct FlowsArgs {
229 #[arg(long)]
231 pub symbol: Option<String>,
232 #[arg(long)]
234 pub rank: bool,
235 #[arg(long, default_value = "20")]
237 pub depth: usize,
238 #[arg(long, default_value = "20")]
240 pub limit: usize,
241}
242
243#[derive(clap::Args)]
244pub struct ClonesArgs {
245 #[arg(long, default_value = "0.7")]
247 pub threshold: f64,
248 #[arg(long, default_value = "5")]
250 pub min_lines: usize,
251 #[arg(long)]
253 pub cluster: Option<usize>,
254}
255
256#[derive(clap::Args)]
257pub struct CommunitiesArgs {
258 pub community_id: Option<usize>,
260 #[arg(long)]
262 pub resolution: Option<f64>,
263 #[arg(long)]
265 pub min_size: Option<usize>,
266 #[arg(long)]
268 pub seed: Option<u64>,
269 #[arg(long)]
271 pub symbol: Option<String>,
272 #[arg(long, default_value = "20")]
274 pub limit: usize,
275}
276
277#[derive(clap::Args)]
278pub struct SetupArgs {
279 pub platform: Option<String>,
281 #[arg(long)]
283 pub global: bool,
284 #[arg(long)]
286 pub check: bool,
287 #[arg(long)]
289 pub remove: bool,
290 #[arg(long, requires = "remove")]
292 pub clean: bool,
293 #[arg(long, requires = "remove")]
295 pub purge: bool,
296}
297
298#[derive(clap::Args)]
299pub struct DeadCodeArgs {
300 #[arg(long = "exclude-pattern")]
302 pub exclude_pattern: Vec<String>,
303 #[arg(long)]
305 pub include_tests: bool,
306 #[arg(long)]
308 pub kind: Vec<String>,
309 #[arg(long)]
311 pub limit: Option<usize>,
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn parse_index_command() {
320 let cli = Cli::parse_from(["code-graph", "index"]);
321 assert!(matches!(cli.command, Commands::Index(_)));
322 }
323
324 #[test]
325 fn parse_find_command() {
326 let cli = Cli::parse_from(["code-graph", "find", "Foo"]);
327 if let Commands::Find(args) = cli.command {
328 assert_eq!(args.pattern, "Foo");
329 } else {
330 panic!("expected Find command");
331 }
332 }
333
334 #[test]
335 fn parse_json_global_flag() {
336 let cli = Cli::parse_from(["code-graph", "--json", "stats"]);
337 assert!(cli.json);
338 }
339
340 #[test]
341 fn parse_verbose_flag() {
342 let cli = Cli::parse_from(["code-graph", "-vv", "stats"]);
343 assert_eq!(cli.verbose, 2);
344 }
345
346 #[test]
347 fn parse_clones_command() {
348 let cli = Cli::parse_from(["code-graph", "clones"]);
349 if let Commands::Clones(args) = cli.command {
350 assert!((args.threshold - 0.7).abs() < f64::EPSILON);
351 assert_eq!(args.min_lines, 5);
352 assert!(args.cluster.is_none());
353 } else {
354 panic!("expected Clones command");
355 }
356 }
357
358 #[test]
359 fn all_subcommands_parse() {
360 let commands = [
361 vec!["code-graph", "index"],
362 vec!["code-graph", "find", "X"],
363 vec!["code-graph", "refs", "a::b"],
364 vec!["code-graph", "impact", "a::b"],
365 vec!["code-graph", "diff"],
366 vec!["code-graph", "callers", "a::b"],
367 vec!["code-graph", "callees", "a::b"],
368 vec!["code-graph", "search", "foo"],
369 vec!["code-graph", "search", "foo", "--semantic-only"],
370 vec!["code-graph", "search", "foo", "--fts-only"],
371 vec!["code-graph", "index", "--embed"],
372 vec!["code-graph", "index", "--embed", "--embed-model", "custom"],
373 vec!["code-graph", "flows"],
374 vec!["code-graph", "flows", "--rank"],
375 vec!["code-graph", "flows", "--symbol", "foo::bar"],
376 vec!["code-graph", "flows", "--depth", "10", "--limit", "50"],
377 vec!["code-graph", "clones"],
378 vec!["code-graph", "clones", "--threshold", "0.8"],
379 vec!["code-graph", "clones", "--min-lines", "10"],
380 vec!["code-graph", "clones", "--cluster", "1"],
381 vec![
382 "code-graph",
383 "clones",
384 "--threshold",
385 "0.9",
386 "--min-lines",
387 "3",
388 "--cluster",
389 "2",
390 ],
391 vec!["code-graph", "risk"],
392 vec!["code-graph", "risk", "--symbols"],
393 vec!["code-graph", "risk", "--symbols", "--limit", "50"],
394 vec!["code-graph", "risk", "AuthService"],
395 vec!["code-graph", "risk", "--min-score", "0.5"],
396 vec!["code-graph", "stats"],
397 vec!["code-graph", "watch"],
398 vec!["code-graph", "watch", "--daemon"],
399 vec!["code-graph", "watch", "--status"],
400 vec!["code-graph", "watch", "--stop"],
401 vec!["code-graph", "setup", "claude"],
402 vec!["code-graph", "setup", "--check"],
403 vec!["code-graph", "setup", "--remove"],
404 vec!["code-graph", "setup", "--remove", "--clean"],
405 vec!["code-graph", "setup", "--remove", "--purge"],
406 vec!["code-graph", "eval"],
407 vec!["code-graph", "eval", "--suite", "search"],
408 vec!["code-graph", "eval", "--no-cache"],
409 vec!["code-graph", "communities"],
410 vec!["code-graph", "communities", "--resolution", "1.5"],
411 vec![
412 "code-graph",
413 "communities",
414 "--seed",
415 "42",
416 "--min-size",
417 "3",
418 ],
419 vec!["code-graph", "communities", "1"],
420 vec!["code-graph", "communities", "--symbol", "src/main.rs::main"],
421 vec!["code-graph", "dead-code"],
422 vec!["code-graph", "dead-code", "--include-tests"],
423 vec!["code-graph", "dead-code", "--exclude-pattern", "**/gen/**"],
424 vec![
425 "code-graph",
426 "dead-code",
427 "--kind",
428 "Function",
429 "--limit",
430 "10",
431 ],
432 ];
433 for args in &commands {
434 Cli::parse_from(args.iter());
435 }
436 }
437
438 #[test]
439 fn parse_dead_code_command() {
440 let cli = Cli::parse_from(["code-graph", "dead-code"]);
441 assert!(matches!(cli.command, Commands::DeadCode(_)));
442 }
443
444 #[test]
445 fn parse_dead_code_with_flags() {
446 let cli = Cli::parse_from([
447 "code-graph",
448 "dead-code",
449 "--include-tests",
450 "--exclude-pattern",
451 "**/generated/**",
452 "--kind",
453 "Function",
454 "--limit",
455 "50",
456 ]);
457 if let Commands::DeadCode(args) = cli.command {
458 assert!(args.include_tests);
459 assert_eq!(args.exclude_pattern, vec!["**/generated/**"]);
460 assert_eq!(args.kind, vec!["Function"]);
461 assert_eq!(args.limit, Some(50));
462 } else {
463 panic!("expected DeadCode command");
464 }
465 }
466
467 #[test]
468 fn parse_risk_command() {
469 let cli = Cli::parse_from(["code-graph", "risk"]);
470 assert!(matches!(cli.command, Commands::Risk(_)));
471 }
472
473 #[test]
474 fn parse_risk_symbols() {
475 let cli = Cli::parse_from(["code-graph", "risk", "--symbols", "--limit", "50"]);
476 if let Commands::Risk(args) = cli.command {
477 assert!(args.symbols);
478 assert_eq!(args.limit, 50);
479 } else {
480 panic!("expected Risk command");
481 }
482 }
483
484 #[test]
485 fn parse_risk_target() {
486 let cli = Cli::parse_from(["code-graph", "risk", "AuthService"]);
487 if let Commands::Risk(args) = cli.command {
488 assert_eq!(args.target.unwrap(), "AuthService");
489 } else {
490 panic!("expected Risk command");
491 }
492 }
493
494 #[test]
495 fn parse_risk_min_score() {
496 let cli = Cli::parse_from(["code-graph", "risk", "--min-score", "0.5"]);
497 if let Commands::Risk(args) = cli.command {
498 assert!((args.min_score - 0.5).abs() < f64::EPSILON);
499 } else {
500 panic!("expected Risk command");
501 }
502 }
503
504 #[test]
505 fn parse_search_with_semantic_only() {
506 let cli = Cli::parse_from(["code-graph", "search", "foo", "--semantic-only"]);
507 if let Commands::Search(args) = cli.command {
508 assert!(args.semantic_only);
509 assert!(!args.fts_only);
510 } else {
511 panic!("expected Search");
512 }
513 }
514
515 #[test]
516 fn parse_search_with_fts_only() {
517 let cli = Cli::parse_from(["code-graph", "search", "foo", "--fts-only"]);
518 if let Commands::Search(args) = cli.command {
519 assert!(args.fts_only);
520 assert!(!args.semantic_only);
521 } else {
522 panic!("expected Search");
523 }
524 }
525
526 #[test]
527 fn parse_index_with_embed() {
528 let cli = Cli::parse_from(["code-graph", "index", "--embed"]);
529 if let Commands::Index(args) = cli.command {
530 assert!(args.embed);
531 assert_eq!(args.embed_model, "all-MiniLM-L6-v2");
532 } else {
533 panic!("expected Index");
534 }
535 }
536
537 #[test]
538 fn parse_index_with_embed_model() {
539 let cli = Cli::parse_from([
540 "code-graph",
541 "index",
542 "--embed",
543 "--embed-model",
544 "custom-model",
545 ]);
546 if let Commands::Index(args) = cli.command {
547 assert!(args.embed);
548 assert_eq!(args.embed_model, "custom-model");
549 } else {
550 panic!("expected Index");
551 }
552 }
553
554 #[test]
555 fn stub_returns_not_implemented() {
556 let result = stubs::not_implemented("find");
557 assert!(result.is_err());
558 let msg = format!("{}", result.unwrap_err());
559 assert!(msg.contains("not yet implemented"));
560 }
561}