Skip to main content

rust_relations_explorer/
app.rs

1use crate::cli::{Cli, Commands, ItemKindArg, OutputFormat, QueryCommands};
2use crate::graph::KnowledgeGraph;
3use crate::query::Query;
4use crate::visualization::{
5    DotGenerator, DotOptions, DotTheme, EdgeStyle, RankDir, SvgGenerator, SvgOptions,
6};
7use clap::CommandFactory;
8use clap_complete::generate;
9use std::fs;
10use std::io;
11
12/// Run the CLI logic in-process.
13///
14/// Returns an exit code (0 = success).
15///
16/// # Panics
17/// May panic during JSON serialization if graph serialization fails when producing
18/// `--json` output in the build command.
19#[must_use]
20#[allow(clippy::too_many_lines)]
21pub fn run_cli(cli: Cli) -> i32 {
22    match cli.command {
23        Commands::Completions { shell } => {
24            let mut cmd = crate::cli::Cli::command();
25            let bin_name = env!("CARGO_PKG_NAME");
26            let mut out = io::stdout();
27            generate(shell, &mut cmd, bin_name, &mut out);
28            0
29        }
30        Commands::Build {
31            path,
32            config,
33            no_ignore,
34            no_cache,
35            rebuild,
36            json,
37            dot,
38            svg,
39            dot_clusters,
40            dot_legend,
41            dot_theme,
42            dot_rankdir,
43            dot_splines,
44            dot_rounded,
45            svg_interactive,
46            save,
47        } => {
48            // Determine cache mode
49            let mode = if rebuild {
50                crate::utils::cache::CacheMode::Rebuild
51            } else if no_cache {
52                crate::utils::cache::CacheMode::Ignore
53            } else {
54                crate::utils::cache::CacheMode::Use
55            };
56
57            let build_path = path.as_ref().unwrap().as_path();
58            if matches!(mode, crate::utils::cache::CacheMode::Rebuild) {
59                crate::utils::cache::clear_cache(build_path);
60            }
61
62            let graph = match KnowledgeGraph::build_from_directory_with_cache_opts(
63                build_path, mode, no_ignore,
64            ) {
65                Ok(g) => g,
66                Err(e) => {
67                    eprintln!("Build failed: {e}");
68                    return 1;
69                }
70            };
71
72            // Optionally write JSON output
73            if let Some(json_path) = json {
74                let serialized =
75                    serde_json::to_string_pretty(&graph).expect("serialize graph to JSON");
76                if let Err(e) = fs::write(&json_path, serialized) {
77                    eprintln!("Failed to write JSON output {json_path}: {e}");
78                }
79            }
80
81            // DOT options from flags and optional config overrides
82            let mut clusters = matches!(dot_clusters, crate::cli::OnOffArg::On);
83            let mut legend = matches!(dot_legend, crate::cli::OnOffArg::On);
84            let mut theme = match dot_theme {
85                crate::cli::DotThemeArg::Dark => DotTheme::Dark,
86                crate::cli::DotThemeArg::Light => DotTheme::Light,
87            };
88            let mut rankdir = match dot_rankdir {
89                crate::cli::DotRankDirArg::TB => RankDir::TB,
90                crate::cli::DotRankDirArg::LR => RankDir::LR,
91            };
92            let mut splines = match dot_splines {
93                crate::cli::DotSplinesArg::Ortho => EdgeStyle::Ortho,
94                crate::cli::DotSplinesArg::Polyline => EdgeStyle::Polyline,
95                crate::cli::DotSplinesArg::Curved => EdgeStyle::Curved,
96            };
97            let mut rounded = matches!(dot_rounded, crate::cli::OnOffArg::On);
98            if let Some(cfg_path) = config.as_ref() {
99                if let Some(cfg) =
100                    crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
101                {
102                    if let Some(dot) = cfg.dot {
103                        if let Some(v) = dot.clusters {
104                            clusters = v;
105                        }
106                        if let Some(v) = dot.legend {
107                            legend = v;
108                        }
109                        if let Some(v) = dot.theme {
110                            theme = if v == "dark" { DotTheme::Dark } else { DotTheme::Light };
111                        }
112                        if let Some(v) = dot.rankdir {
113                            rankdir = if v == "TB" { RankDir::TB } else { RankDir::LR };
114                        }
115                        if let Some(v) = dot.splines {
116                            splines = match v.as_str() {
117                                "ortho" => EdgeStyle::Ortho,
118                                "polyline" => EdgeStyle::Polyline,
119                                _ => EdgeStyle::Curved,
120                            };
121                        }
122                        if let Some(v) = dot.rounded {
123                            rounded = v;
124                        }
125                    }
126                }
127            }
128            let dot_opts = DotOptions { clusters, legend, theme, rankdir, splines, rounded };
129
130            if let Some(dot_path) = dot {
131                match DotGenerator::new().generate_dot_with_options(&graph, dot_opts) {
132                    Ok(content) => {
133                        if let Err(e) = fs::write(&dot_path, content) {
134                            eprintln!("Failed to write DOT output {dot_path}: {e}");
135                        }
136                    }
137                    Err(e) => eprintln!("Visualization error: {e}"),
138                }
139            }
140
141            if let Some(svg_path) = svg {
142                let mut interactive = matches!(svg_interactive, crate::cli::OnOffArg::On);
143                if let Some(cfg_path) = config.as_ref() {
144                    if let Some(cfg) =
145                        crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
146                    {
147                        if let Some(svg) = cfg.svg {
148                            if let Some(v) = svg.interactive {
149                                interactive = v;
150                            }
151                        }
152                    }
153                }
154                let svg_opts = SvgOptions { dot: dot_opts, interactive };
155                match SvgGenerator::new().generate_svg_with_options(&graph, svg_opts) {
156                    Ok(content) => {
157                        if let Err(e) = fs::write(&svg_path, content) {
158                            eprintln!("Failed to write SVG output {svg_path}: {e}");
159                        }
160                    }
161                    Err(e) => eprintln!("Visualization error: {e}"),
162                }
163            }
164
165            if let Some(save_path) = save {
166                if let Err(e) = KnowledgeGraph::save_json(&graph, std::path::Path::new(&save_path))
167                {
168                    eprintln!("Failed to save graph JSON {save_path}: {e}");
169                }
170            }
171
172            if !cli.quiet {
173                println!("Build completed for path: {}", build_path.display());
174            }
175            0
176        }
177        Commands::Query { query } => match query {
178            QueryCommands::ConnectedFiles {
179                path,
180                config,
181                no_ignore,
182                file,
183                graph: graph_path,
184                format,
185                offset,
186                limit,
187                file_pos: _,
188            } => {
189                let graph = if let Some(p) = graph_path {
190                    match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
191                        Ok(g) => g,
192                        Err(e) => {
193                            eprintln!("Load graph failed: {e}");
194                            return 1;
195                        }
196                    }
197                } else {
198                    let res = match KnowledgeGraph::build_from_directory_opts(
199                        path.as_ref().unwrap().as_path(),
200                        no_ignore,
201                    ) {
202                        Ok(g) => g,
203                        Err(e) => {
204                            eprintln!("Build failed: {e}");
205                            return 1;
206                        }
207                    };
208                    res
209                };
210                let file = match file.as_ref() {
211                    Some(f) => f,
212                    None => {
213                        eprintln!("Missing file argument. Provide <file> or --file <path>.");
214                        return 2;
215                    }
216                };
217                let q = crate::query::ConnectedFilesQuery::new(file);
218                let results = q.run(&graph);
219                let fmt = if let Some(cfg_path) = config.as_ref() {
220                    if let Some(cfg) =
221                        crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
222                    {
223                        match cfg.query.and_then(|q| q.default_format).as_deref() {
224                            Some("json") => OutputFormat::Json,
225                            Some("text") => OutputFormat::Text,
226                            _ => format,
227                        }
228                    } else {
229                        format
230                    }
231                } else {
232                    format
233                };
234                let start = offset.min(results.len());
235                let end = match limit {
236                    Some(l) => (start + l).min(results.len()),
237                    None => results.len(),
238                };
239                let page = &results[start..end];
240                if matches!(fmt, OutputFormat::Json) {
241                    let out: Vec<String> = page.iter().map(|p| p.display().to_string()).collect();
242                    match serde_json::to_string_pretty(&out) {
243                        Ok(s) => println!("{s}"),
244                        Err(e) => {
245                            eprintln!("JSON encode error: {e}");
246                            return 1;
247                        }
248                    }
249                } else {
250                    let rows: Vec<Vec<String>> = page
251                        .iter()
252                        .enumerate()
253                        .map(|(i, p)| vec![format!("{}", start + i + 1), p.display().to_string()])
254                        .collect();
255                    let table = crate::utils::table::render(&["#", "Path"], &rows);
256                    println!("{table}");
257                }
258                0
259            }
260            QueryCommands::FunctionUsage {
261                path,
262                config,
263                no_ignore,
264                function,
265                direction,
266                graph: graph_path,
267                format,
268                offset,
269                limit,
270            } => {
271                let graph = if let Some(p) = graph_path {
272                    match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
273                        Ok(g) => g,
274                        Err(e) => {
275                            eprintln!("Load graph failed: {e}");
276                            return 1;
277                        }
278                    }
279                } else {
280                    if no_ignore {
281                        std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
282                    }
283                    let res = match KnowledgeGraph::build_from_directory(
284                        path.as_ref().unwrap().as_path(),
285                    ) {
286                        Ok(g) => g,
287                        Err(e) => {
288                            eprintln!("Build failed: {e}");
289                            if no_ignore {
290                                std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
291                            }
292                            return 1;
293                        }
294                    };
295                    if no_ignore {
296                        std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
297                    }
298                    res
299                };
300                let dir = match direction {
301                    crate::cli::Direction::Callees => crate::query::UsageDirection::Callees,
302                    crate::cli::Direction::Callers => crate::query::UsageDirection::Callers,
303                };
304                let q = crate::query::FunctionUsageQuery { function, direction: dir };
305                let results = q.run(&graph);
306                let fmt = if let Some(cfg_path) = config.as_ref() {
307                    if let Some(cfg) =
308                        crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
309                    {
310                        match cfg.query.and_then(|q| q.default_format).as_deref() {
311                            Some("json") => OutputFormat::Json,
312                            Some("text") => OutputFormat::Text,
313                            _ => format,
314                        }
315                    } else {
316                        format
317                    }
318                } else {
319                    format
320                };
321                let start = offset.min(results.len());
322                let end = match limit {
323                    Some(l) => (start + l).min(results.len()),
324                    None => results.len(),
325                };
326                let page = &results[start..end];
327                if matches!(fmt, OutputFormat::Json) {
328                    let out: Vec<String> = page.iter().map(|p| p.display().to_string()).collect();
329                    match serde_json::to_string_pretty(&out) {
330                        Ok(s) => println!("{s}"),
331                        Err(e) => {
332                            eprintln!("JSON encode error: {e}");
333                            return 1;
334                        }
335                    }
336                } else {
337                    for p in page {
338                        println!("{}", p.display());
339                    }
340                }
341                0
342            }
343            QueryCommands::Cycles {
344                path,
345                config,
346                no_ignore,
347                graph: graph_path,
348                format,
349                offset,
350                limit,
351            } => {
352                let graph = if let Some(p) = graph_path {
353                    match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
354                        Ok(g) => g,
355                        Err(e) => {
356                            eprintln!("Load graph failed: {e}");
357                            return 1;
358                        }
359                    }
360                } else {
361                    if no_ignore {
362                        std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
363                    }
364                    let res = match KnowledgeGraph::build_from_directory(
365                        path.as_ref().unwrap().as_path(),
366                    ) {
367                        Ok(g) => g,
368                        Err(e) => {
369                            eprintln!("Build failed: {e}");
370                            if no_ignore {
371                                std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
372                            }
373                            return 1;
374                        }
375                    };
376                    if no_ignore {
377                        std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
378                    }
379                    res
380                };
381                let q = crate::query::CycleDetectionQuery::new();
382                let cycles = q.run(&graph);
383                let fmt = if let Some(cfg_path) = config.as_ref() {
384                    if let Some(cfg) =
385                        crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
386                    {
387                        match cfg.query.and_then(|q| q.default_format).as_deref() {
388                            Some("json") => OutputFormat::Json,
389                            Some("text") => OutputFormat::Text,
390                            _ => format,
391                        }
392                    } else {
393                        format
394                    }
395                } else {
396                    format
397                };
398                let start = offset.min(cycles.len());
399                let end = match limit {
400                    Some(l) => (start + l).min(cycles.len()),
401                    None => cycles.len(),
402                };
403                let page = &cycles[start..end];
404                if matches!(fmt, OutputFormat::Json) {
405                    let out: Vec<Vec<String>> = page
406                        .iter()
407                        .map(|cyc| cyc.iter().map(|p| p.display().to_string()).collect())
408                        .collect();
409                    match serde_json::to_string_pretty(&out) {
410                        Ok(s) => println!("{s}"),
411                        Err(e) => {
412                            eprintln!("JSON encode error: {e}");
413                            return 1;
414                        }
415                    }
416                } else {
417                    for cyc in page {
418                        let parts: Vec<String> =
419                            cyc.iter().map(|p| p.display().to_string()).collect();
420                        println!("{}", parts.join(" -> "));
421                    }
422                }
423                0
424            }
425            QueryCommands::Path {
426                path,
427                config,
428                no_ignore,
429                from,
430                to,
431                graph: graph_path,
432                format,
433                offset,
434                limit,
435            } => {
436                let graph = if let Some(p) = graph_path {
437                    match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
438                        Ok(g) => g,
439                        Err(e) => {
440                            eprintln!("Load graph failed: {e}");
441                            return 1;
442                        }
443                    }
444                } else {
445                    if no_ignore {
446                        std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
447                    }
448                    let res = match KnowledgeGraph::build_from_directory(
449                        path.as_ref().unwrap().as_path(),
450                    ) {
451                        Ok(g) => g,
452                        Err(e) => {
453                            eprintln!("Build failed: {e}");
454                            if no_ignore {
455                                std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
456                            }
457                            return 1;
458                        }
459                    };
460                    if no_ignore {
461                        std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
462                    }
463                    res
464                };
465                let q = crate::query::ShortestPathQuery::new(&from, &to);
466                let results = q.run(&graph);
467                let fmt = if let Some(cfg_path) = config.as_ref() {
468                    if let Some(cfg) =
469                        crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
470                    {
471                        match cfg.query.and_then(|q| q.default_format).as_deref() {
472                            Some("json") => OutputFormat::Json,
473                            Some("text") => OutputFormat::Text,
474                            _ => format,
475                        }
476                    } else {
477                        format
478                    }
479                } else {
480                    format
481                };
482                if matches!(fmt, OutputFormat::Json) {
483                    let start = offset.min(results.len());
484                    let end = match limit {
485                        Some(l) => (start + l).min(results.len()),
486                        None => results.len(),
487                    };
488                    let page = &results[start..end];
489                    let out: Vec<String> = page.iter().map(|p| p.display().to_string()).collect();
490                    match serde_json::to_string_pretty(&out) {
491                        Ok(s) => println!("{s}"),
492                        Err(e) => {
493                            eprintln!("JSON encode error: {e}");
494                            return 1;
495                        }
496                    }
497                } else if results.is_empty() {
498                    println!("<no path>");
499                } else {
500                    let start = offset.min(results.len());
501                    let end = match limit {
502                        Some(l) => (start + l).min(results.len()),
503                        None => results.len(),
504                    };
505                    let page = &results[start..end];
506                    let rows: Vec<Vec<String>> = page
507                        .iter()
508                        .enumerate()
509                        .map(|(i, p)| vec![format!("{}", start + i + 1), p.display().to_string()])
510                        .collect();
511                    let table = crate::utils::table::render(&["Step", "Path"], &rows);
512                    println!("{table}");
513                }
514                0
515            }
516            QueryCommands::Hubs {
517                path,
518                config,
519                no_ignore,
520                graph: graph_path,
521                metric,
522                top,
523                format,
524                offset,
525                limit,
526            } => {
527                use crate::query::{CentralityMetric, HubsQuery};
528                let graph = if let Some(p) = graph_path {
529                    match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
530                        Ok(g) => g,
531                        Err(e) => {
532                            eprintln!("Load graph failed: {e}");
533                            return 1;
534                        }
535                    }
536                } else {
537                    if no_ignore {
538                        std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
539                    }
540                    let res = match KnowledgeGraph::build_from_directory(
541                        path.as_ref().unwrap().as_path(),
542                    ) {
543                        Ok(g) => g,
544                        Err(e) => {
545                            eprintln!("Build failed: {e}");
546                            if no_ignore {
547                                std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
548                            }
549                            return 1;
550                        }
551                    };
552                    if no_ignore {
553                        std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
554                    }
555                    res
556                };
557                let m = match metric {
558                    crate::cli::CentralityMetricArg::In => CentralityMetric::In,
559                    crate::cli::CentralityMetricArg::Out => CentralityMetric::Out,
560                    crate::cli::CentralityMetricArg::Total => CentralityMetric::Total,
561                };
562                let q = HubsQuery::new(m, top);
563                let rows = q.run(&graph);
564                let fmt = if let Some(cfg_path) = config.as_ref() {
565                    if let Some(cfg) =
566                        crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
567                    {
568                        match cfg.query.and_then(|q| q.default_format).as_deref() {
569                            Some("json") => OutputFormat::Json,
570                            Some("text") => OutputFormat::Text,
571                            _ => format,
572                        }
573                    } else {
574                        format
575                    }
576                } else {
577                    format
578                };
579                let start = offset.min(rows.len());
580                let end = match limit {
581                    Some(l) => (start + l).min(rows.len()),
582                    None => rows.len(),
583                };
584                let page = &rows[start..end];
585                if matches!(fmt, OutputFormat::Json) {
586                    #[derive(serde::Serialize)]
587                    struct HubRow {
588                        path: String,
589                        indegree: usize,
590                        outdegree: usize,
591                    }
592                    let out: Vec<HubRow> = page
593                        .iter()
594                        .map(|(p, i, o)| HubRow {
595                            path: p.display().to_string(),
596                            indegree: *i,
597                            outdegree: *o,
598                        })
599                        .collect();
600                    match serde_json::to_string_pretty(&out) {
601                        Ok(s) => println!("{s}"),
602                        Err(e) => {
603                            eprintln!("JSON encode error: {e}");
604                            return 1;
605                        }
606                    }
607                } else {
608                    let body: Vec<Vec<String>> = if cli.verbose == 0 {
609                        page.iter()
610                            .map(|(p, i, o)| vec![p.display().to_string(), (i + o).to_string()])
611                            .collect()
612                    } else {
613                        page.iter()
614                            .map(|(p, i, o)| {
615                                vec![
616                                    p.display().to_string(),
617                                    i.to_string(),
618                                    o.to_string(),
619                                    (i + o).to_string(),
620                                ]
621                            })
622                            .collect()
623                    };
624                    let headers: &[&str] = if cli.verbose == 0 {
625                        &["Path", "Total"]
626                    } else {
627                        &["Path", "In", "Out", "Total"]
628                    };
629                    let table = crate::utils::table::render(headers, &body);
630                    println!("{table}");
631                }
632                0
633            }
634            QueryCommands::ModuleCentrality {
635                path,
636                config,
637                no_ignore,
638                graph: graph_path,
639                metric,
640                top,
641                format,
642                offset,
643                limit,
644            } => {
645                use crate::query::{CentralityMetric, ModuleCentralityQuery};
646                let graph = if let Some(p) = graph_path {
647                    match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
648                        Ok(g) => g,
649                        Err(e) => {
650                            eprintln!("Load graph failed: {e}");
651                            return 1;
652                        }
653                    }
654                } else {
655                    if no_ignore {
656                        std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
657                    }
658                    let res = match KnowledgeGraph::build_from_directory(
659                        path.as_ref().unwrap().as_path(),
660                    ) {
661                        Ok(g) => g,
662                        Err(e) => {
663                            eprintln!("Build failed: {e}");
664                            if no_ignore {
665                                std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
666                            }
667                            return 1;
668                        }
669                    };
670                    if no_ignore {
671                        std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
672                    }
673                    res
674                };
675                let m = match metric {
676                    crate::cli::CentralityMetricArg::In => CentralityMetric::In,
677                    crate::cli::CentralityMetricArg::Out => CentralityMetric::Out,
678                    crate::cli::CentralityMetricArg::Total => CentralityMetric::Total,
679                };
680                let q = ModuleCentralityQuery::new(m, top);
681                let rows = q.run(&graph);
682                let fmt = if let Some(cfg_path) = config.as_ref() {
683                    if let Some(cfg) =
684                        crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
685                    {
686                        match cfg.query.and_then(|q| q.default_format).as_deref() {
687                            Some("json") => OutputFormat::Json,
688                            Some("text") => OutputFormat::Text,
689                            _ => format,
690                        }
691                    } else {
692                        format
693                    }
694                } else {
695                    format
696                };
697                let start = offset.min(rows.len());
698                let end = match limit {
699                    Some(l) => (start + l).min(rows.len()),
700                    None => rows.len(),
701                };
702                let page = &rows[start..end];
703                if matches!(fmt, OutputFormat::Json) {
704                    #[derive(serde::Serialize)]
705                    struct Row {
706                        module: String,
707                        indegree: usize,
708                        outdegree: usize,
709                    }
710                    let out: Vec<Row> = page
711                        .iter()
712                        .map(|(p, i, o)| Row {
713                            module: p.display().to_string(),
714                            indegree: *i,
715                            outdegree: *o,
716                        })
717                        .collect();
718                    match serde_json::to_string_pretty(&out) {
719                        Ok(s) => println!("{s}"),
720                        Err(e) => {
721                            eprintln!("JSON encode error: {e}");
722                            return 1;
723                        }
724                    }
725                } else {
726                    let body: Vec<Vec<String>> = if cli.verbose == 0 {
727                        page.iter()
728                            .map(|(p, i, o)| vec![p.display().to_string(), (i + o).to_string()])
729                            .collect()
730                    } else {
731                        page.iter()
732                            .map(|(p, i, o)| {
733                                vec![
734                                    p.display().to_string(),
735                                    i.to_string(),
736                                    o.to_string(),
737                                    (i + o).to_string(),
738                                ]
739                            })
740                            .collect()
741                    };
742                    let headers: &[&str] = if cli.verbose == 0 {
743                        &["Module", "Total"]
744                    } else {
745                        &["Module", "In", "Out", "Total"]
746                    };
747                    let table = crate::utils::table::render(headers, &body);
748                    println!("{table}");
749                }
750                0
751            }
752            QueryCommands::TraitImpls {
753                path,
754                config,
755                no_ignore,
756                r#trait,
757                graph: graph_path,
758                format,
759                offset,
760                limit,
761            } => {
762                use crate::query::TraitImplsQuery;
763                let graph = if let Some(p) = graph_path {
764                    match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
765                        Ok(g) => g,
766                        Err(e) => {
767                            eprintln!("Load graph failed: {e}");
768                            return 1;
769                        }
770                    }
771                } else {
772                    if no_ignore {
773                        std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
774                    }
775                    let res = match KnowledgeGraph::build_from_directory(
776                        path.as_ref().unwrap().as_path(),
777                    ) {
778                        Ok(g) => g,
779                        Err(e) => {
780                            eprintln!("Build failed: {e}");
781                            if no_ignore {
782                                std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
783                            }
784                            return 1;
785                        }
786                    };
787                    if no_ignore {
788                        std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
789                    }
790                    res
791                };
792                let q = TraitImplsQuery::new(&r#trait);
793                let rows = q.run(&graph);
794                let fmt = if let Some(cfg_path) = config.as_ref() {
795                    if let Some(cfg) =
796                        crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
797                    {
798                        match cfg.query.and_then(|q| q.default_format).as_deref() {
799                            Some("json") => OutputFormat::Json,
800                            Some("text") => OutputFormat::Text,
801                            _ => format,
802                        }
803                    } else {
804                        format
805                    }
806                } else {
807                    format
808                };
809                let start = offset.min(rows.len());
810                let end = match limit {
811                    Some(l) => (start + l).min(rows.len()),
812                    None => rows.len(),
813                };
814                let page = &rows[start..end];
815                if matches!(fmt, OutputFormat::Json) {
816                    #[derive(serde::Serialize)]
817                    struct Row {
818                        path: String,
819                        r#type: String,
820                    }
821                    let out: Vec<Row> = page
822                        .iter()
823                        .map(|(p, t)| Row { path: p.display().to_string(), r#type: t.to_string() })
824                        .collect();
825                    match serde_json::to_string_pretty(&out) {
826                        Ok(s) => println!("{s}"),
827                        Err(e) => {
828                            eprintln!("JSON encode error: {e}");
829                            return 1;
830                        }
831                    }
832                } else if rows.is_empty() {
833                    println!("<no implementations found>");
834                } else {
835                    let body: Vec<Vec<String>> = if cli.verbose == 0 {
836                        page.iter().map(|(p, _t)| vec![p.display().to_string()]).collect()
837                    } else {
838                        page.iter().map(|(p, t)| vec![p.display().to_string(), t.clone()]).collect()
839                    };
840                    let headers: &[&str] =
841                        if cli.verbose == 0 { &["Path"] } else { &["Path", "Type"] };
842                    let table = crate::utils::table::render(headers, &body);
843                    println!("{table}");
844                }
845                0
846            }
847            QueryCommands::UnreferencedItems {
848                path,
849                config,
850                no_ignore,
851                include_public,
852                exclude,
853                graph: graph_path,
854                format,
855                offset,
856                limit,
857            } => {
858                use crate::query::UnreferencedItemsQuery;
859                let graph = if let Some(p) = graph_path {
860                    match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
861                        Ok(g) => g,
862                        Err(e) => {
863                            eprintln!("Load graph failed: {e}");
864                            return 1;
865                        }
866                    }
867                } else {
868                    if no_ignore {
869                        std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
870                    }
871                    let res = match KnowledgeGraph::build_from_directory(
872                        path.as_ref().unwrap().as_path(),
873                    ) {
874                        Ok(g) => g,
875                        Err(e) => {
876                            eprintln!("Build failed: {e}");
877                            if no_ignore {
878                                std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
879                            }
880                            return 1;
881                        }
882                    };
883                    if no_ignore {
884                        std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
885                    }
886                    res
887                };
888                let exclude_re = if let Some(pat) = exclude.as_ref() {
889                    match regex::Regex::new(pat) {
890                        Ok(r) => Some(r),
891                        Err(e) => {
892                            eprintln!("Invalid --exclude regex: {e}");
893                            return 1;
894                        }
895                    }
896                } else {
897                    None
898                };
899                let q = UnreferencedItemsQuery::new(include_public, exclude_re);
900                let rows = q.run(&graph);
901                let fmt = if let Some(cfg_path) = config.as_ref() {
902                    if let Some(cfg) =
903                        crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
904                    {
905                        match cfg.query.and_then(|q| q.default_format).as_deref() {
906                            Some("json") => OutputFormat::Json,
907                            Some("text") => OutputFormat::Text,
908                            _ => format,
909                        }
910                    } else {
911                        format
912                    }
913                } else {
914                    format
915                };
916                let start = offset.min(rows.len());
917                let end = match limit {
918                    Some(l) => (start + l).min(rows.len()),
919                    None => rows.len(),
920                };
921                let page = &rows[start..end];
922                if matches!(fmt, OutputFormat::Json) {
923                    #[derive(serde::Serialize)]
924                    struct Row {
925                        path: String,
926                        id: String,
927                        name: String,
928                        kind: String,
929                        visibility: String,
930                    }
931                    let out: Vec<Row> = page
932                        .iter()
933                        .map(|(p, id, name, kind, vis)| Row {
934                            path: p.display().to_string(),
935                            id: id.clone(),
936                            name: name.clone(),
937                            kind: kind.clone(),
938                            visibility: vis.clone(),
939                        })
940                        .collect();
941                    match serde_json::to_string_pretty(&out) {
942                        Ok(s) => println!("{s}"),
943                        Err(e) => {
944                            eprintln!("JSON encode error: {e}");
945                            return 1;
946                        }
947                    }
948                } else if rows.is_empty() {
949                    println!("<no unreferenced items>");
950                } else {
951                    let body: Vec<Vec<String>> = if cli.verbose == 0 {
952                        page.iter()
953                            .map(|(p, _id, name, _kind, _vis)| {
954                                vec![p.display().to_string(), name.clone()]
955                            })
956                            .collect()
957                    } else {
958                        page.iter()
959                            .map(|(p, id, name, kind, vis)| {
960                                vec![
961                                    p.display().to_string(),
962                                    id.clone(),
963                                    name.clone(),
964                                    kind.clone(),
965                                    vis.clone(),
966                                ]
967                            })
968                            .collect()
969                    };
970                    let headers: &[&str] = if cli.verbose == 0 {
971                        &["Path", "Name"]
972                    } else {
973                        &["Path", "ItemId", "Name", "Kind", "Vis"]
974                    };
975                    let table = crate::utils::table::render(headers, &body);
976                    println!("{table}");
977                }
978                0
979            }
980            QueryCommands::ItemInfo {
981                path,
982                config,
983                no_ignore,
984                item_id,
985                name,
986                kind,
987                graph: graph_path,
988                show_code,
989                format,
990            } => {
991                use crate::query::ItemInfoQuery;
992                let graph = if let Some(p) = graph_path {
993                    match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
994                        Ok(g) => g,
995                        Err(e) => {
996                            eprintln!("Load graph failed: {e}");
997                            return 1;
998                        }
999                    }
1000                } else {
1001                    if no_ignore {
1002                        std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
1003                    }
1004                    let res = match KnowledgeGraph::build_from_directory(
1005                        path.as_ref().unwrap().as_path(),
1006                    ) {
1007                        Ok(g) => g,
1008                        Err(e) => {
1009                            eprintln!("Build failed: {e}");
1010                            if no_ignore {
1011                                std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
1012                            }
1013                            return 1;
1014                        }
1015                    };
1016                    if no_ignore {
1017                        std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
1018                    }
1019                    res
1020                };
1021                // Determine target ItemId: prefer explicit --item-id, else resolve by --name/--kind
1022                let id = if let Some(raw_id) = item_id {
1023                    crate::graph::ItemId(raw_id)
1024                } else if let Some(nm) = name {
1025                    use crate::graph::{resolver::Resolver, ItemId, ItemType};
1026                    use std::path::Path;
1027
1028                    let resolver = Resolver::new(&graph);
1029                    let mut ids: Vec<ItemId> = resolver.find_by_name(&nm);
1030                    if let Some(k) = kind {
1031                        ids.retain(|id| {
1032                            matches!(
1033                                (k, resolver.item_kind(id)),
1034                                (ItemKindArg::Module, Some(ItemType::Module { .. }))
1035                                    | (ItemKindArg::Function, Some(ItemType::Function { .. }))
1036                                    | (ItemKindArg::Struct, Some(ItemType::Struct { .. }))
1037                                    | (ItemKindArg::Enum, Some(ItemType::Enum { .. }))
1038                                    | (ItemKindArg::Trait, Some(ItemType::Trait { .. }))
1039                                    | (ItemKindArg::Impl, Some(ItemType::Impl { .. }))
1040                                    | (ItemKindArg::Const, Some(ItemType::Const))
1041                                    | (ItemKindArg::Static, Some(ItemType::Static { .. }))
1042                                    | (ItemKindArg::Type, Some(ItemType::Type))
1043                                    | (ItemKindArg::Macro, Some(ItemType::Macro))
1044                            )
1045                        });
1046                    }
1047                    if ids.is_empty() {
1048                        eprintln!("No item found with name '{nm}'.");
1049                        if let Some(k) = kind {
1050                            eprintln!("Hint: try a different --kind (current: {:?})", k);
1051                        }
1052                        return 1;
1053                    }
1054
1055                    // Build candidate tuples (id, kind_str, path)
1056                    let mut candidates: Vec<(ItemId, String, std::path::PathBuf)> =
1057                        Vec::with_capacity(ids.len());
1058                    for id in ids.into_iter() {
1059                        let kind_s = match resolver.item_kind(&id) {
1060                            Some(ItemType::Module { .. }) => "module",
1061                            Some(ItemType::Function { .. }) => "function",
1062                            Some(ItemType::Struct { .. }) => "struct",
1063                            Some(ItemType::Enum { .. }) => "enum",
1064                            Some(ItemType::Trait { .. }) => "trait",
1065                            Some(ItemType::Impl { .. }) => "impl",
1066                            Some(ItemType::Const) => "const",
1067                            Some(ItemType::Static { .. }) => "static",
1068                            Some(ItemType::Type) => "type",
1069                            Some(ItemType::Macro) => "macro",
1070                            None => "?",
1071                        };
1072                        if let Some(p) = resolver.item_path(&id) {
1073                            candidates.push((id, kind_s.to_string(), p.clone()));
1074                        }
1075                    }
1076                    if candidates.is_empty() {
1077                        eprintln!("No item found with name '{nm}'.");
1078                        return 1;
1079                    }
1080
1081                    // Prefer matches in current crate src/ if path is known
1082                    let root_src: Option<std::path::PathBuf> = path
1083                        .as_ref()
1084                        .map(|pb| pb.join("src"))
1085                        .or_else(|| std::env::current_dir().ok().map(|d| d.join("src")));
1086
1087                    // Rank: (in_root_src desc, shallower depth asc, path lex asc)
1088                    let mut ranked = candidates;
1089                    if let Some(root_src_path) = root_src.as_ref() {
1090                        let root_src_canon = root_src_path;
1091                        ranked.sort_by(|a, b| {
1092                            let a_in = a.2.starts_with(root_src_canon);
1093                            let b_in = b.2.starts_with(root_src_canon);
1094                            let in_cmp = b_in.cmp(&a_in); // true first
1095                            if in_cmp != std::cmp::Ordering::Equal {
1096                                return in_cmp;
1097                            }
1098                            let depth = |p: &Path| -> usize {
1099                                let comps: Vec<_> = p.components().collect();
1100                                let mut seen_src = false;
1101                                let mut c = 0usize;
1102                                for comp in comps {
1103                                    if let std::path::Component::Normal(os) = comp {
1104                                        if os.to_str() == Some("src") {
1105                                            seen_src = true;
1106                                            continue;
1107                                        }
1108                                        if seen_src {
1109                                            c += 1;
1110                                        }
1111                                    }
1112                                }
1113                                c
1114                            };
1115                            let a_d = depth(&a.2);
1116                            let b_d = depth(&b.2);
1117                            let d_cmp = a_d.cmp(&b_d);
1118                            if d_cmp != std::cmp::Ordering::Equal {
1119                                return d_cmp;
1120                            }
1121                            a.2.cmp(&b.2)
1122                        });
1123                    } else {
1124                        ranked.sort_by(|a, b| a.2.cmp(&b.2));
1125                    }
1126
1127                    // If still multiple and top two tie in rank dimensions, present ambiguity
1128                    let top = &ranked[0];
1129                    let same_rank = ranked
1130                        .iter()
1131                        .take_while(|cand| {
1132                            let a = cand;
1133                            let b = top;
1134                            let a_in = if let Some(r) = root_src.as_ref() {
1135                                a.2.starts_with(r)
1136                            } else {
1137                                false
1138                            };
1139                            let b_in = if let Some(r) = root_src.as_ref() {
1140                                b.2.starts_with(r)
1141                            } else {
1142                                false
1143                            };
1144                            let depth = |p: &Path| -> usize {
1145                                let comps: Vec<_> = p.components().collect();
1146                                let mut seen_src = false;
1147                                let mut c = 0usize;
1148                                for comp in comps {
1149                                    if let std::path::Component::Normal(os) = comp {
1150                                        if os.to_str() == Some("src") {
1151                                            seen_src = true;
1152                                            continue;
1153                                        }
1154                                        if seen_src {
1155                                            c += 1;
1156                                        }
1157                                    }
1158                                }
1159                                c
1160                            };
1161                            a_in == b_in && depth(&a.2) == depth(&b.2)
1162                        })
1163                        .count();
1164                    if ranked.len() > 1 && same_rank > 1 {
1165                        eprintln!("Ambiguous name '{nm}'. Top matches:");
1166                        for (cid, ck, cp) in ranked.iter().take(10) {
1167                            eprintln!("- id={}  kind={}  path={}", cid.0, ck, cp.display());
1168                        }
1169                        eprintln!("Disambiguate by providing --item-id or add --kind.");
1170                        return 1;
1171                    }
1172                    top.0.clone()
1173                } else {
1174                    eprintln!("Missing --item-id or --name for item-info.");
1175                    return 1;
1176                };
1177                let q = ItemInfoQuery::new(id, show_code);
1178                let result = q.run(&graph);
1179                let fmt = if let Some(cfg_path) = config.as_ref() {
1180                    if let Some(cfg) =
1181                        crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
1182                    {
1183                        match cfg.query.and_then(|q| q.default_format).as_deref() {
1184                            Some("json") => OutputFormat::Json,
1185                            Some("text") => OutputFormat::Text,
1186                            _ => format,
1187                        }
1188                    } else {
1189                        format
1190                    }
1191                } else {
1192                    format
1193                };
1194                if matches!(fmt, OutputFormat::Json) {
1195                    // Trim heavy fields when not verbose: drop code and relation contexts
1196                    let result = if cli.verbose == 0 {
1197                        result.map(|mut info| {
1198                            info.code = None;
1199                            for r in &mut info.inbound {
1200                                r.context.clear();
1201                            }
1202                            for r in &mut info.outbound {
1203                                r.context.clear();
1204                            }
1205                            info
1206                        })
1207                    } else {
1208                        result
1209                    };
1210                    match serde_json::to_string_pretty(&result) {
1211                        Ok(s) => println!("{s}"),
1212                        Err(e) => {
1213                            eprintln!("JSON encode error: {e}");
1214                            return 1;
1215                        }
1216                    }
1217                } else {
1218                    match result {
1219                        None => println!("<item not found>"),
1220                        Some(info) => {
1221                            println!("Item: {}", info.name);
1222                            println!("Id: {}", info.id);
1223                            println!("Kind: {}", info.kind);
1224                            println!("Vis: {}", info.visibility);
1225                            println!(
1226                                "Location: {}:{}-{}",
1227                                info.path, info.line_start, info.line_end
1228                            );
1229                            if cli.verbose == 0 {
1230                                let callers: String = if info.inbound.is_empty() {
1231                                    "<none>".to_string()
1232                                } else {
1233                                    info.inbound
1234                                        .iter()
1235                                        .map(|r| r.id.clone())
1236                                        .collect::<Vec<_>>()
1237                                        .join(", ")
1238                                };
1239                                let callees: String = if info.outbound.is_empty() {
1240                                    "<none>".to_string()
1241                                } else {
1242                                    info.outbound
1243                                        .iter()
1244                                        .map(|r| r.id.clone())
1245                                        .collect::<Vec<_>>()
1246                                        .join(", ")
1247                                };
1248                                println!("\nCallers: {}", callers);
1249                                println!("Callees: {}", callees);
1250                            } else {
1251                                if show_code {
1252                                    if let Some(code) = info.code.as_deref() {
1253                                        println!("\n--- code ---\n{}\n--- end code ---", code);
1254                                    }
1255                                }
1256                                if info.inbound.is_empty() {
1257                                    println!("\nCallers: <none>");
1258                                } else {
1259                                    println!("\nCallers:");
1260                                    for r in info.inbound {
1261                                        println!(
1262                                            "- [{}] {} ({}) @ {} :: {}",
1263                                            r.relation, r.name, r.id, r.path, r.context
1264                                        );
1265                                    }
1266                                }
1267                                if info.outbound.is_empty() {
1268                                    println!("\nCallees: <none>");
1269                                } else {
1270                                    println!("\nCallees:");
1271                                    for r in info.outbound {
1272                                        println!(
1273                                            "- [{}] {} ({}) @ {} :: {}",
1274                                            r.relation, r.name, r.id, r.path, r.context
1275                                        );
1276                                    }
1277                                }
1278                            }
1279                        }
1280                    }
1281                }
1282                0
1283            }
1284        },
1285    }
1286}