Skip to main content

the_code_graph_cli/commands/
mod.rs

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    /// Increase verbosity (-v info, -vv debug)
32    #[arg(short, long, action = ArgAction::Count, global = true)]
33    pub verbose: u8,
34
35    /// Enable debug logging
36    #[arg(long, global = true)]
37    pub debug: bool,
38
39    /// Output as JSON
40    #[arg(long, global = true)]
41    pub json: bool,
42
43    /// Output as table
44    #[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 the current project
54    Index(IndexArgs),
55    /// Find a symbol by name or pattern
56    Find(FindArgs),
57    /// Show references to a symbol
58    Refs(RefsArgs),
59    /// Analyze risk scores across the codebase
60    Risk(RiskArgs),
61    /// Analyze blast radius of changes
62    Impact(ImpactArgs),
63    /// Detect unused symbols in the codebase
64    #[command(name = "dead-code")]
65    DeadCode(DeadCodeArgs),
66    /// Show symbols affected by git diff
67    Diff(DiffArgs),
68    /// Show callers of a symbol
69    Callers(CallersArgs),
70    /// Show callees of a symbol
71    Callees(CalleesArgs),
72    /// Full-text search across symbols
73    Search(SearchArgs),
74    /// Analyze execution flows and criticality
75    Flows(FlowsArgs),
76    /// Detect code clones across the codebase
77    Clones(ClonesArgs),
78    /// Detect communities of tightly-coupled symbols
79    Communities(CommunitiesArgs),
80    /// Show graph statistics
81    Stats,
82    /// Watch for file changes and re-index
83    Watch(WatchArgs),
84    /// Set up agent integration hooks
85    Setup(SetupArgs),
86    /// Run evaluation suite
87    Eval(EvalArgs),
88}
89
90#[derive(clap::Args)]
91pub struct IndexArgs {
92    /// Path to the project root (defaults to auto-detect)
93    #[arg(long)]
94    pub path: Option<std::path::PathBuf>,
95
96    /// Incremental update (only re-index changed files)
97    #[arg(long)]
98    pub incremental: bool,
99
100    /// Specific files to re-index (implies --incremental)
101    #[arg(long, value_delimiter = ',')]
102    pub files: Option<Vec<std::path::PathBuf>>,
103
104    /// Generate embeddings for all symbols (enables semantic search)
105    #[arg(long)]
106    pub embed: bool,
107
108    /// ONNX model name for embeddings
109    #[arg(long, default_value = "all-MiniLM-L6-v2")]
110    pub embed_model: String,
111}
112
113#[derive(clap::Args)]
114pub struct FindArgs {
115    /// Symbol name or pattern to search for
116    pub pattern: String,
117}
118
119#[derive(clap::Args)]
120pub struct RefsArgs {
121    /// Qualified name of the symbol
122    pub qualified_name: String,
123}
124
125#[derive(clap::Args)]
126pub struct RiskArgs {
127    /// Specific symbol or file to analyze
128    pub target: Option<String>,
129    /// Show symbol-level risk instead of file-level
130    #[arg(long)]
131    pub symbols: bool,
132    /// Maximum number of results to display
133    #[arg(long, default_value = "20")]
134    pub limit: usize,
135    /// Minimum risk score to display
136    #[arg(long, default_value = "0.0")]
137    pub min_score: f64,
138}
139
140#[derive(clap::Args)]
141pub struct ImpactArgs {
142    /// Symbol name, qualified name, or file path to analyze
143    pub target: String,
144    /// Maximum traversal depth
145    #[arg(long, default_value = "3")]
146    pub depth: usize,
147    /// Minimum confidence level (high, medium, low, all)
148    #[arg(long, default_value = "all")]
149    pub confidence: String,
150}
151
152#[derive(clap::Args)]
153pub struct DiffArgs {
154    /// Git ref to compare from (default: HEAD)
155    #[arg(default_value = "HEAD")]
156    pub from: String,
157    /// Git ref to compare to (default: working tree)
158    pub to: Option<String>,
159    /// Maximum traversal depth
160    #[arg(long, default_value = "3")]
161    pub depth: usize,
162    /// Minimum confidence level (high, medium, low, all)
163    #[arg(long, default_value = "all")]
164    pub confidence: String,
165}
166
167#[derive(clap::Args)]
168pub struct CallersArgs {
169    /// Qualified name of the symbol
170    pub qualified_name: String,
171}
172
173#[derive(clap::Args)]
174pub struct CalleesArgs {
175    /// Qualified name of the symbol
176    pub qualified_name: String,
177}
178
179#[derive(clap::Args)]
180pub struct WatchArgs {
181    /// Run as background daemon
182    #[arg(long)]
183    pub daemon: bool,
184
185    /// Show daemon status
186    #[arg(long)]
187    pub status: bool,
188
189    /// Stop running daemon
190    #[arg(long)]
191    pub stop: bool,
192
193    /// Internal flag: marks this process as the daemon child
194    #[arg(long, hide = true)]
195    pub daemon_internal: bool,
196
197    /// Path to the project root (defaults to auto-detect)
198    #[arg(long)]
199    pub path: Option<std::path::PathBuf>,
200}
201
202#[derive(clap::Args)]
203pub struct SearchArgs {
204    /// Search query
205    pub query: String,
206    /// Maximum results to return
207    #[arg(long, default_value = "20")]
208    pub limit: usize,
209    /// Use only vector similarity (skip FTS5)
210    #[arg(long)]
211    pub semantic_only: bool,
212    /// Use only FTS5 BM25 (skip vectors)
213    #[arg(long)]
214    pub fts_only: bool,
215}
216
217#[derive(clap::Args)]
218pub struct EvalArgs {
219    /// Which suite to run: search, impact, or all
220    #[arg(long, default_value = "all")]
221    pub suite: String,
222    /// Force re-clone of eval repos (ignore cache)
223    #[arg(long)]
224    pub no_cache: bool,
225}
226
227#[derive(clap::Args)]
228pub struct FlowsArgs {
229    /// Filter flows through a specific symbol
230    #[arg(long)]
231    pub symbol: Option<String>,
232    /// Show criticality ranking instead of flows
233    #[arg(long)]
234    pub rank: bool,
235    /// Maximum flow depth
236    #[arg(long, default_value = "20")]
237    pub depth: usize,
238    /// Maximum number of results to display
239    #[arg(long, default_value = "20")]
240    pub limit: usize,
241}
242
243#[derive(clap::Args)]
244pub struct ClonesArgs {
245    /// Similarity threshold (0.0-1.0)
246    #[arg(long, default_value = "0.7")]
247    pub threshold: f64,
248    /// Minimum symbol body lines
249    #[arg(long, default_value = "5")]
250    pub min_lines: usize,
251    /// Show detailed members of a specific cluster
252    #[arg(long)]
253    pub cluster: Option<usize>,
254}
255
256#[derive(clap::Args)]
257pub struct CommunitiesArgs {
258    /// Show details for a specific community
259    pub community_id: Option<usize>,
260    /// Modularity resolution parameter
261    #[arg(long)]
262    pub resolution: Option<f64>,
263    /// Minimum community size to display
264    #[arg(long)]
265    pub min_size: Option<usize>,
266    /// Random seed for reproducibility
267    #[arg(long)]
268    pub seed: Option<u64>,
269    /// Show which community a symbol belongs to
270    #[arg(long)]
271    pub symbol: Option<String>,
272    /// Maximum communities to display
273    #[arg(long, default_value = "20")]
274    pub limit: usize,
275}
276
277#[derive(clap::Args)]
278pub struct SetupArgs {
279    /// Target platform (currently: "claude")
280    pub platform: Option<String>,
281    /// Install to ~/.claude/settings.json instead of .claude/settings.json
282    #[arg(long)]
283    pub global: bool,
284    /// Check hook installation status
285    #[arg(long)]
286    pub check: bool,
287    /// Remove all code-graph hooks
288    #[arg(long)]
289    pub remove: bool,
290    /// Also remove .code-graph/ from .gitignore (requires --remove)
291    #[arg(long, requires = "remove")]
292    pub clean: bool,
293    /// Also delete .code-graph/ directory entirely (requires --remove)
294    #[arg(long, requires = "remove")]
295    pub purge: bool,
296}
297
298#[derive(clap::Args)]
299pub struct DeadCodeArgs {
300    /// Additional exclusion patterns (repeatable)
301    #[arg(long = "exclude-pattern")]
302    pub exclude_pattern: Vec<String>,
303    /// Include test functions as dead code candidates
304    #[arg(long)]
305    pub include_tests: bool,
306    /// Filter to specific symbol kinds (repeatable)
307    #[arg(long)]
308    pub kind: Vec<String>,
309    /// Maximum results to display
310    #[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}