Skip to main content

rust_relations_explorer/cli/
mod.rs

1use crate::utils::project_root::effective_path_opt;
2use clap::{ArgAction, Parser, Subcommand, ValueEnum};
3use clap_complete::Shell;
4use serde::Deserialize;
5use std::path::PathBuf;
6
7#[derive(Debug, Parser)]
8#[command(
9    name = "rust-relations-explorer",
10    version,
11    about = "Rust Knowledge Graph System",
12    long_about = "Parse Rust projects into a knowledge graph and run queries. File discovery respects .gitignore and .ignore with parent traversal. Global git excludes are disabled for determinism. Use --no-ignore to bypass ignore rules.",
13    arg_required_else_help = true,
14    propagate_version = true
15)]
16pub struct Cli {
17    /// Increase output verbosity (-v, -vv, -vvv)
18    #[arg(short = 'v', long = "verbose", action = ArgAction::Count)]
19    pub verbose: u8,
20    /// Suppress non-essential output
21    #[arg(short = 'q', long, default_value_t = false)]
22    pub quiet: bool,
23    #[command(subcommand)]
24    pub command: Commands,
25}
26
27#[derive(Clone, Debug, Copy, ValueEnum, PartialEq, Eq)]
28pub enum OutputFormat {
29    Text,
30    Json,
31}
32
33#[derive(Clone, Debug, Copy, ValueEnum)]
34pub enum Direction {
35    Callers,
36    Callees,
37}
38
39#[derive(Clone, Debug, Copy, ValueEnum)]
40pub enum CentralityMetricArg {
41    In,
42    Out,
43    Total,
44}
45
46#[derive(Clone, Debug, Copy, ValueEnum, PartialEq, Eq)]
47pub enum OnOffArg {
48    On,
49    Off,
50}
51
52#[derive(Clone, Debug, Copy, ValueEnum, PartialEq, Eq)]
53pub enum DotThemeArg {
54    Light,
55    Dark,
56}
57
58#[derive(Clone, Debug, Copy, ValueEnum, PartialEq, Eq)]
59pub enum DotRankDirArg {
60    #[value(alias = "LR")]
61    LR,
62    #[value(alias = "TB")]
63    TB,
64}
65
66#[derive(Clone, Debug, Copy, ValueEnum, PartialEq, Eq)]
67pub enum DotSplinesArg {
68    Curved,
69    Ortho,
70    Polyline,
71}
72
73// --------------------
74// Config (TOML) schema
75// --------------------
76#[derive(Debug, Deserialize)]
77struct ConfigFile {
78    #[serde(default)]
79    dot: DotConfig,
80    #[serde(default)]
81    svg: SvgConfig,
82    #[serde(default)]
83    query: QueryConfig,
84}
85
86#[derive(Debug, Default, Deserialize)]
87struct DotConfig {
88    clusters: Option<bool>,
89    legend: Option<bool>,
90    theme: Option<String>,   // "light" | "dark"
91    rankdir: Option<String>, // "LR" | "TB"
92    splines: Option<String>, // "curved" | "ortho" | "polyline"
93    rounded: Option<bool>,
94}
95
96#[derive(Debug, Default, Deserialize)]
97struct SvgConfig {
98    interactive: Option<bool>,
99}
100
101#[derive(Debug, Default, Deserialize)]
102struct QueryConfig {
103    default_format: Option<String>, // "text" | "json"
104}
105
106fn load_config(path: &str) -> Option<ConfigFile> {
107    let content = std::fs::read_to_string(path).ok()?;
108    toml::from_str::<ConfigFile>(&content).ok()
109}
110
111fn on_off(b: bool) -> OnOffArg {
112    if b {
113        OnOffArg::On
114    } else {
115        OnOffArg::Off
116    }
117}
118fn parse_theme(s: &str) -> Option<DotThemeArg> {
119    match s.to_ascii_lowercase().as_str() {
120        "light" => Some(DotThemeArg::Light),
121        "dark" => Some(DotThemeArg::Dark),
122        _ => None,
123    }
124}
125fn parse_rankdir(s: &str) -> Option<DotRankDirArg> {
126    match s.to_ascii_uppercase().as_str() {
127        "LR" => Some(DotRankDirArg::LR),
128        "TB" => Some(DotRankDirArg::TB),
129        _ => None,
130    }
131}
132fn parse_splines(s: &str) -> Option<DotSplinesArg> {
133    match s.to_ascii_lowercase().as_str() {
134        "curved" => Some(DotSplinesArg::Curved),
135        "ortho" => Some(DotSplinesArg::Ortho),
136        "polyline" => Some(DotSplinesArg::Polyline),
137        _ => None,
138    }
139}
140fn parse_format(s: &str) -> Option<OutputFormat> {
141    match s.to_ascii_lowercase().as_str() {
142        "text" => Some(OutputFormat::Text),
143        "json" => Some(OutputFormat::Json),
144        _ => None,
145    }
146}
147
148#[derive(Clone, Debug, Copy, ValueEnum)]
149pub enum ItemKindArg {
150    Module,
151    Function,
152    Struct,
153    Enum,
154    Trait,
155    Impl,
156    Const,
157    Static,
158    Type,
159    Macro,
160}
161
162#[derive(Debug, Subcommand)]
163pub enum Commands {
164    /// Build the knowledge graph from a source directory
165    Build {
166        /// Path to the Rust project root (directory containing src/)
167        #[arg(short, long, env = "RRE_PATH")]
168        path: Option<PathBuf>,
169        /// Path to a TOML configuration file
170        #[arg(short = 'c', long)]
171        config: Option<String>,
172        /// Bypass ignore rules (.gitignore/.ignore) when discovering files
173        #[arg(
174            short='I', long,
175            visible_aliases=["no-gitignore","all","ni"],
176            default_value_t = false,
177            help = "Include files even if matched by .gitignore/.ignore. Global git excludes are always disabled for determinism."
178        )]
179        no_ignore: bool,
180        /// Ignore cache when building (do not reuse cached files)
181        #[arg(long, default_value_t = false)]
182        no_cache: bool,
183        /// Rebuild cache from scratch (clears previous cache)
184        #[arg(long, default_value_t = false)]
185        rebuild: bool,
186        /// Output JSON file path
187        #[arg(long)]
188        json: Option<String>,
189        /// Output DOT file path
190        #[arg(long)]
191        dot: Option<String>,
192        /// Output SVG file path
193        #[arg(long)]
194        svg: Option<String>,
195        /// DOT: enable/disable hierarchical clusters (default: on)
196        #[arg(long, value_enum, default_value_t = OnOffArg::On)]
197        dot_clusters: OnOffArg,
198        /// DOT: include legend (default: on)
199        #[arg(long, value_enum, default_value_t = OnOffArg::On)]
200        dot_legend: OnOffArg,
201        /// DOT: theme (light or dark)
202        #[arg(long, value_enum, default_value_t = DotThemeArg::Light)]
203        dot_theme: DotThemeArg,
204        /// DOT: rank direction (LR or TB)
205        #[arg(long, value_enum, default_value_t = DotRankDirArg::LR)]
206        dot_rankdir: DotRankDirArg,
207        /// DOT: edge splines style (curved, ortho, polyline)
208        #[arg(long, value_enum, default_value_t = DotSplinesArg::Curved)]
209        dot_splines: DotSplinesArg,
210        /// DOT: rounded node corners (on/off)
211        #[arg(long, value_enum, default_value_t = OnOffArg::On)]
212        dot_rounded: OnOffArg,
213        /// SVG: add interactive enhancements (on/off)
214        #[arg(long, value_enum, default_value_t = OnOffArg::On)]
215        svg_interactive: OnOffArg,
216        /// Save built graph to JSON file path
217        #[arg(long)]
218        save: Option<String>,
219    },
220    /// Run queries over the knowledge graph
221    Query {
222        #[command(subcommand)]
223        query: QueryCommands,
224    },
225    /// Generate shell completion scripts
226    Completions {
227        /// Target shell (bash, zsh, fish, powershell, elvish)
228        #[arg(value_enum)]
229        shell: Shell,
230    },
231}
232
233#[derive(Debug, Subcommand)]
234pub enum QueryCommands {
235    /// List files connected to the given file via relationships
236    ConnectedFiles {
237        /// Path to project root (directory containing src/)
238        #[arg(short, long, env = "RRE_PATH")]
239        path: Option<PathBuf>,
240        /// Path to a TOML configuration file
241        #[arg(short = 'c', long)]
242        config: Option<String>,
243        /// Bypass ignore rules (.gitignore/.ignore) when discovering files
244        #[arg(
245            short='I', long,
246            visible_aliases=["no-gitignore","all","ni"],
247            default_value_t = false,
248            help = "Include files even if matched by .gitignore/.ignore. Global git excludes are always disabled for determinism."
249        )]
250        no_ignore: bool,
251        /// Positional form of the file to analyze (absolute or relative)
252        #[arg(value_name = "FILE")]
253        file_pos: Option<String>,
254        /// The file to analyze (absolute or relative)
255        #[arg(long)]
256        file: Option<String>,
257        /// Optional path to a prebuilt graph JSON (skips rebuild)
258        #[arg(long, env = "RRE_GRAPH")]
259        graph: Option<String>,
260        /// Output format: text or json
261        #[arg(short='f', long, value_enum, default_value_t = OutputFormat::Text, env = "RRE_FORMAT")]
262        format: OutputFormat,
263        /// Pagination offset (number of cycles to skip)
264        #[arg(long, default_value_t = 0)]
265        offset: usize,
266        /// Pagination limit (max number of cycles to show)
267        #[arg(long)]
268        limit: Option<usize>,
269    },
270    /// Show a single item's definition and relations by ItemId
271    ItemInfo {
272        /// Path to project root (directory containing src/)
273        #[arg(short, long, env = "RRE_PATH")]
274        path: Option<PathBuf>,
275        /// Path to a TOML configuration file
276        #[arg(short = 'c', long)]
277        config: Option<String>,
278        /// Bypass ignore rules (.gitignore/.ignore) when discovering files
279        #[arg(short='I', long, visible_aliases=["no-gitignore","all","ni"], default_value_t = false)]
280        no_ignore: bool,
281        /// ItemId (e.g., fn:createIcons:6). Optional when --name is provided
282        #[arg(long, value_name = "ID")]
283        item_id: Option<String>,
284        /// Lookup by item name (e.g., createIcons). Use with optional --kind to disambiguate
285        #[arg(short = 'n', long, value_name = "NAME")]
286        name: Option<String>,
287        /// Optional kind to narrow name lookup (e.g., function, struct)
288        #[arg(short = 'k', long, value_enum)]
289        kind: Option<ItemKindArg>,
290        /// Optional path to a prebuilt graph JSON (skips rebuild)
291        #[arg(long, env = "RRE_GRAPH")]
292        graph: Option<String>,
293        /// Include code snippet of the item's definition
294        #[arg(long, default_value_t = true)]
295        show_code: bool,
296        /// Output format: text or json
297        #[arg(short='f', long, value_enum, default_value_t = OutputFormat::Text, env = "RRE_FORMAT")]
298        format: OutputFormat,
299    },
300    /// List files that call or are called by a given function name
301    FunctionUsage {
302        /// Path to project root (directory containing src/)
303        #[arg(short, long, env = "RRE_PATH")]
304        path: Option<PathBuf>,
305        /// Path to a TOML configuration file
306        #[arg(short = 'c', long)]
307        config: Option<String>,
308        /// Bypass ignore rules (.gitignore/.ignore) when discovering files
309        #[arg(short='I', long, visible_aliases=["no-gitignore","all","ni"], default_value_t = false)]
310        no_ignore: bool,
311        /// Function name to analyze
312        #[arg(long)]
313        function: String,
314        /// Direction: callers or callees
315        #[arg(long, value_enum, default_value_t = Direction::Callers)]
316        direction: Direction,
317        /// Optional path to a prebuilt graph JSON (skips rebuild)
318        #[arg(long, env = "RRE_GRAPH")]
319        graph: Option<String>,
320        /// Output format: text or json
321        #[arg(short='f', long, value_enum, default_value_t = OutputFormat::Text, env = "RRE_FORMAT")]
322        format: OutputFormat,
323        /// Pagination offset (number of items to skip)
324        #[arg(long, default_value_t = 0)]
325        offset: usize,
326        /// Pagination limit (max number of items to show)
327        #[arg(long)]
328        limit: Option<usize>,
329    },
330    /// Detect cycles between files
331    Cycles {
332        /// Path to project root (directory containing src/)
333        #[arg(short, long, env = "RRE_PATH")]
334        path: Option<PathBuf>,
335        /// Path to a TOML configuration file
336        #[arg(short = 'c', long)]
337        config: Option<String>,
338        /// Bypass ignore rules (.gitignore/.ignore) when discovering files
339        #[arg(short='I', long, visible_aliases=["no-gitignore","all","ni"], default_value_t = false)]
340        no_ignore: bool,
341        /// Optional path to a prebuilt graph JSON (skips rebuild)
342        #[arg(long, env = "RRE_GRAPH")]
343        graph: Option<String>,
344        /// Output format: text or json
345        #[arg(short='f', long, value_enum, default_value_t = OutputFormat::Text, env = "RRE_FORMAT")]
346        format: OutputFormat,
347        /// Pagination offset (number of cycles to skip)
348        #[arg(long, default_value_t = 0)]
349        offset: usize,
350        /// Pagination limit (max number of cycles to show)
351        #[arg(long)]
352        limit: Option<usize>,
353    },
354    /// Compute shortest path between two files
355    Path {
356        /// Path to project root (directory containing src/)
357        #[arg(short = 'p', long, env = "RRE_PATH")]
358        path: Option<PathBuf>,
359        /// Path to a TOML configuration file
360        #[arg(short = 'c', long)]
361        config: Option<String>,
362        /// Bypass ignore rules (.gitignore/.ignore) when discovering files
363        #[arg(short='I', long, visible_aliases=["no-gitignore","all","ni"], default_value_t = false)]
364        no_ignore: bool,
365        /// Source file path
366        #[arg(long)]
367        from: String,
368        /// Destination file path
369        #[arg(long)]
370        to: String,
371        /// Optional path to a prebuilt graph JSON (skips rebuild)
372        #[arg(long, env = "RRE_GRAPH")]
373        graph: Option<String>,
374        /// Output format: text or json
375        #[arg(short='f', long, value_enum, default_value_t = OutputFormat::Text, env = "RRE_FORMAT")]
376        format: OutputFormat,
377        /// Pagination offset (number of steps to skip)
378        #[arg(long, default_value_t = 0)]
379        offset: usize,
380        /// Pagination limit (max number of steps to show)
381        #[arg(long)]
382        limit: Option<usize>,
383    },
384    /// List top-N hub files by degree centrality
385    Hubs {
386        /// Path to project root (directory containing src/)
387        #[arg(short, long, env = "RRE_PATH")]
388        path: Option<PathBuf>,
389        /// Path to a TOML configuration file
390        #[arg(short = 'c', long)]
391        config: Option<String>,
392        /// Bypass ignore rules (.gitignore/.ignore) when discovering files
393        #[arg(short='I', long, visible_aliases=["no-gitignore","all","ni"], default_value_t = false)]
394        no_ignore: bool,
395        /// Optional path to a prebuilt graph JSON (skips rebuild)
396        #[arg(long, env = "RRE_GRAPH")]
397        graph: Option<String>,
398        /// Metric: in, out, total
399        #[arg(long, value_enum, default_value_t = CentralityMetricArg::Total)]
400        metric: CentralityMetricArg,
401        /// Top N results
402        #[arg(short = 't', long, default_value_t = 10)]
403        top: usize,
404        /// Output format: text or json
405        #[arg(short='f', long, value_enum, default_value_t = OutputFormat::Text, env = "RRE_FORMAT")]
406        format: OutputFormat,
407        /// Pagination offset (number of rows to skip)
408        #[arg(long, default_value_t = 0)]
409        offset: usize,
410        /// Pagination limit (max number of rows to show)
411        #[arg(long)]
412        limit: Option<usize>,
413    },
414    /// List top-N modules (directories) by degree centrality
415    ModuleCentrality {
416        /// Path to project root (directory containing src/)
417        #[arg(short, long, env = "RRE_PATH")]
418        path: Option<PathBuf>,
419        /// Path to a TOML configuration file
420        #[arg(short = 'c', long)]
421        config: Option<String>,
422        /// Bypass ignore rules (.gitignore/.ignore) when discovering files
423        #[arg(short='I', long, visible_aliases=["no-gitignore","all","ni"], default_value_t = false)]
424        no_ignore: bool,
425        /// Optional path to a prebuilt graph JSON (skips rebuild)
426        #[arg(long, env = "RRE_GRAPH")]
427        graph: Option<String>,
428        /// Metric: in, out, total
429        #[arg(long, value_enum, default_value_t = CentralityMetricArg::Total)]
430        metric: CentralityMetricArg,
431        /// Top N results
432        #[arg(short = 't', long, default_value_t = 10)]
433        top: usize,
434        /// Output format: text or json
435        #[arg(short='f', long, value_enum, default_value_t = OutputFormat::Text, env = "RRE_FORMAT")]
436        format: OutputFormat,
437        /// Pagination offset (number of rows to skip)
438        #[arg(long, default_value_t = 0)]
439        offset: usize,
440        /// Pagination limit (max number of rows to show)
441        #[arg(long)]
442        limit: Option<usize>,
443    },
444    /// List types implementing a trait
445    TraitImpls {
446        /// Path to project root (directory containing src/)
447        #[arg(short, long, env = "RRE_PATH")]
448        path: Option<PathBuf>,
449        /// Path to a TOML configuration file
450        #[arg(short = 'c', long)]
451        config: Option<String>,
452        /// Bypass ignore rules (.gitignore/.ignore) when discovering files
453        #[arg(short='I', long, visible_aliases=["no-gitignore","all","ni"], default_value_t = false)]
454        no_ignore: bool,
455        /// Trait name (e.g., Display)
456        #[arg(long, value_name = "NAME")]
457        r#trait: String,
458        /// Optional path to a prebuilt graph JSON (skips rebuild)
459        #[arg(long, env = "RRE_GRAPH")]
460        graph: Option<String>,
461        /// Output format: text or json
462        #[arg(short='f', long, value_enum, default_value_t = OutputFormat::Text, env = "RRE_FORMAT")]
463        format: OutputFormat,
464        /// Pagination offset (number of rows to skip)
465        #[arg(long, default_value_t = 0)]
466        offset: usize,
467        /// Pagination limit (max number of rows to show)
468        #[arg(long)]
469        limit: Option<usize>,
470    },
471    /// List items with no inbound usage edges (potentially dead code)
472    UnreferencedItems {
473        /// Path to project root (directory containing src/)
474        #[arg(short, long, env = "RRE_PATH")]
475        path: Option<PathBuf>,
476        /// Path to a TOML configuration file
477        #[arg(short = 'c', long)]
478        config: Option<String>,
479        /// Bypass ignore rules (.gitignore/.ignore) when discovering files
480        #[arg(short='I', long, visible_aliases=["no-gitignore","all","ni"], default_value_t = false)]
481        no_ignore: bool,
482        /// Include public items as well (by default public items are excluded)
483        #[arg(long, default_value_t = false)]
484        include_public: bool,
485        /// Regex to exclude paths (e.g., 'tests|benches|examples')
486        #[arg(long)]
487        exclude: Option<String>,
488        /// Optional path to a prebuilt graph JSON (skips rebuild)
489        #[arg(long, env = "RRE_GRAPH")]
490        graph: Option<String>,
491        /// Output format: text or json
492        #[arg(short='f', long, value_enum, default_value_t = OutputFormat::Text, env = "RRE_FORMAT")]
493        format: OutputFormat,
494        /// Pagination offset (number of rows to skip)
495        #[arg(long, default_value_t = 0)]
496        offset: usize,
497        /// Pagination limit (max number of rows to show)
498        #[arg(long)]
499        limit: Option<usize>,
500    },
501}
502
503#[must_use]
504pub fn parse() -> Cli {
505    let mut cli = Cli::parse();
506    match &mut cli.command {
507        Commands::Build {
508            path,
509            config,
510            no_ignore: _,
511            no_cache: _,
512            rebuild: _,
513            json: _,
514            dot: _,
515            svg: _,
516            dot_clusters,
517            dot_legend,
518            dot_theme,
519            dot_rankdir,
520            dot_splines,
521            dot_rounded,
522            svg_interactive,
523            save: _,
524        } => {
525            let p = effective_path_opt(path.as_deref());
526            *path = Some(p);
527            // Apply config if provided (only to defaulted values)
528            if let Some(cfg_path) = config.as_deref() {
529                if let Some(cfg) = load_config(cfg_path) {
530                    if let Some(b) = cfg.dot.clusters {
531                        if *dot_clusters == OnOffArg::On {
532                            *dot_clusters = on_off(b);
533                        }
534                    }
535                    if let Some(b) = cfg.dot.legend {
536                        if *dot_legend == OnOffArg::On {
537                            *dot_legend = on_off(b);
538                        }
539                    }
540                    if let Some(s) = cfg.dot.theme.as_deref().and_then(parse_theme) {
541                        if *dot_theme == DotThemeArg::Light {
542                            *dot_theme = s;
543                        }
544                    }
545                    if let Some(s) = cfg.dot.rankdir.as_deref().and_then(parse_rankdir) {
546                        if *dot_rankdir == DotRankDirArg::LR {
547                            *dot_rankdir = s;
548                        }
549                    }
550                    if let Some(s) = cfg.dot.splines.as_deref().and_then(parse_splines) {
551                        if *dot_splines == DotSplinesArg::Curved {
552                            *dot_splines = s;
553                        }
554                    }
555                    if let Some(b) = cfg.dot.rounded {
556                        if *dot_rounded == OnOffArg::On {
557                            *dot_rounded = on_off(b);
558                        }
559                    }
560                    if let Some(b) = cfg.svg.interactive {
561                        if *svg_interactive == OnOffArg::On {
562                            *svg_interactive = on_off(b);
563                        }
564                    }
565                }
566            }
567            if cli.verbose > 0 && !cli.quiet {
568                eprintln!("Using project root: {}", path.as_ref().unwrap().display());
569            }
570        }
571        Commands::Query { query } => match query {
572            QueryCommands::ConnectedFiles { path, file_pos, file, config, format, .. } => {
573                let p = effective_path_opt(path.as_deref());
574                *path = Some(p);
575                if let Some(cfg_path) = config.as_deref() {
576                    if let Some(cfg) = load_config(cfg_path) {
577                        if let Some(f) = cfg.query.default_format.as_deref().and_then(parse_format)
578                        {
579                            if *format == OutputFormat::Text {
580                                *format = f;
581                            }
582                        }
583                    }
584                }
585                if cli.verbose > 0 && !cli.quiet {
586                    eprintln!("Using project root: {}", path.as_ref().unwrap().display());
587                }
588                // Normalize positional <file> vs --file
589                let merged = if file.is_none() { file_pos.clone() } else { file.clone() };
590                if let Some(f) = merged {
591                    *file = Some(f);
592                }
593            }
594            QueryCommands::ItemInfo { path, config, format, .. } => {
595                let p = effective_path_opt(path.as_deref());
596                *path = Some(p);
597                if let Some(cfg_path) = config.as_deref() {
598                    if let Some(cfg) = load_config(cfg_path) {
599                        if let Some(f) = cfg.query.default_format.as_deref().and_then(parse_format)
600                        {
601                            if *format == OutputFormat::Text {
602                                *format = f;
603                            }
604                        }
605                    }
606                }
607                if cli.verbose > 0 && !cli.quiet {
608                    eprintln!("Using project root: {}", path.as_ref().unwrap().display());
609                }
610            }
611            QueryCommands::FunctionUsage { path, config, format, .. } => {
612                let p = effective_path_opt(path.as_deref());
613                *path = Some(p);
614                if let Some(cfg_path) = config.as_deref() {
615                    if let Some(cfg) = load_config(cfg_path) {
616                        if let Some(f) = cfg.query.default_format.as_deref().and_then(parse_format)
617                        {
618                            if *format == OutputFormat::Text {
619                                *format = f;
620                            }
621                        }
622                    }
623                }
624                if cli.verbose > 0 && !cli.quiet {
625                    eprintln!("Using project root: {}", path.as_ref().unwrap().display());
626                }
627            }
628            QueryCommands::Cycles { path, config, format, .. } => {
629                let p = effective_path_opt(path.as_deref());
630                *path = Some(p);
631                if let Some(cfg_path) = config.as_deref() {
632                    if let Some(cfg) = load_config(cfg_path) {
633                        if let Some(f) = cfg.query.default_format.as_deref().and_then(parse_format)
634                        {
635                            if *format == OutputFormat::Text {
636                                *format = f;
637                            }
638                        }
639                    }
640                }
641                if cli.verbose > 0 && !cli.quiet {
642                    eprintln!("Using project root: {}", path.as_ref().unwrap().display());
643                }
644            }
645            QueryCommands::Path { path, config, format, .. } => {
646                let p = effective_path_opt(path.as_deref());
647                *path = Some(p);
648                if let Some(cfg_path) = config.as_deref() {
649                    if let Some(cfg) = load_config(cfg_path) {
650                        if let Some(f) = cfg.query.default_format.as_deref().and_then(parse_format)
651                        {
652                            if *format == OutputFormat::Text {
653                                *format = f;
654                            }
655                        }
656                    }
657                }
658                if cli.verbose > 0 && !cli.quiet {
659                    eprintln!("Using project root: {}", path.as_ref().unwrap().display());
660                }
661            }
662            QueryCommands::Hubs { path, config, format, .. } => {
663                let p = effective_path_opt(path.as_deref());
664                *path = Some(p);
665                if let Some(cfg_path) = config.as_deref() {
666                    if let Some(cfg) = load_config(cfg_path) {
667                        if let Some(f) = cfg.query.default_format.as_deref().and_then(parse_format)
668                        {
669                            if *format == OutputFormat::Text {
670                                *format = f;
671                            }
672                        }
673                    }
674                }
675                if cli.verbose > 0 && !cli.quiet {
676                    eprintln!("Using project root: {}", path.as_ref().unwrap().display());
677                }
678            }
679            QueryCommands::ModuleCentrality { path, config, format, .. } => {
680                let p = effective_path_opt(path.as_deref());
681                *path = Some(p);
682                if let Some(cfg_path) = config.as_deref() {
683                    if let Some(cfg) = load_config(cfg_path) {
684                        if let Some(f) = cfg.query.default_format.as_deref().and_then(parse_format)
685                        {
686                            if *format == OutputFormat::Text {
687                                *format = f;
688                            }
689                        }
690                    }
691                }
692                if cli.verbose > 0 && !cli.quiet {
693                    eprintln!("Using project root: {}", path.as_ref().unwrap().display());
694                }
695            }
696            QueryCommands::TraitImpls { path, config, format, .. } => {
697                let p = effective_path_opt(path.as_deref());
698                *path = Some(p);
699                if let Some(cfg_path) = config.as_deref() {
700                    if let Some(cfg) = load_config(cfg_path) {
701                        if let Some(f) = cfg.query.default_format.as_deref().and_then(parse_format)
702                        {
703                            if *format == OutputFormat::Text {
704                                *format = f;
705                            }
706                        }
707                    }
708                }
709                if cli.verbose > 0 && !cli.quiet {
710                    eprintln!("Using project root: {}", path.as_ref().unwrap().display());
711                }
712            }
713            QueryCommands::UnreferencedItems { path, config, format, .. } => {
714                let p = effective_path_opt(path.as_deref());
715                *path = Some(p);
716                if let Some(cfg_path) = config.as_deref() {
717                    if let Some(cfg) = load_config(cfg_path) {
718                        if let Some(f) = cfg.query.default_format.as_deref().and_then(parse_format)
719                        {
720                            if *format == OutputFormat::Text {
721                                *format = f;
722                            }
723                        }
724                    }
725                }
726                if cli.verbose > 0 && !cli.quiet {
727                    eprintln!("Using project root: {}", path.as_ref().unwrap().display());
728                }
729            }
730        },
731        Commands::Completions { .. } => {
732            // No path normalization or config backfilling needed here
733        }
734    }
735    cli
736}