rust_relations_explorer/
app.rs

1use crate::cli::{Cli, Commands, QueryCommands};
2use crate::graph::KnowledgeGraph;
3use crate::query::Query;
4use crate::visualization::{
5    DotGenerator, DotOptions, DotTheme, EdgeStyle, RankDir, SvgGenerator, SvgOptions,
6};
7use std::fs;
8
9/// Run the CLI logic in-process.
10///
11/// Returns an exit code (0 = success).
12///
13/// # Panics
14/// May panic during JSON serialization if graph serialization fails when producing
15/// `--json` output in the build command.
16#[must_use]
17#[allow(clippy::too_many_lines)]
18pub fn run_cli(cli: Cli) -> i32 {
19    match cli.command {
20        Commands::Build {
21            path,
22            config,
23            no_ignore,
24            no_cache,
25            rebuild,
26            json,
27            dot,
28            svg,
29            dot_clusters,
30            dot_legend,
31            dot_theme,
32            dot_rankdir,
33            dot_splines,
34            dot_rounded,
35            svg_interactive,
36            save,
37        } => {
38            // Determine cache mode
39            let mode = if rebuild {
40                crate::utils::cache::CacheMode::Rebuild
41            } else if no_cache {
42                crate::utils::cache::CacheMode::Ignore
43            } else {
44                crate::utils::cache::CacheMode::Use
45            };
46
47            let build_path = std::path::Path::new(&path);
48            if matches!(mode, crate::utils::cache::CacheMode::Rebuild) {
49                crate::utils::cache::clear_cache(build_path);
50            }
51
52            let graph = match KnowledgeGraph::build_from_directory_with_cache_opts(
53                build_path, mode, no_ignore,
54            ) {
55                Ok(g) => g,
56                Err(e) => {
57                    eprintln!("Build failed: {e}");
58                    return 1;
59                }
60            };
61
62            // Optionally write JSON output
63            if let Some(json_path) = json {
64                let serialized =
65                    serde_json::to_string_pretty(&graph).expect("serialize graph to JSON");
66                if let Err(e) = fs::write(&json_path, serialized) {
67                    eprintln!("Failed to write JSON output {json_path}: {e}");
68                }
69            }
70
71            // DOT options from flags and optional config overrides
72            let mut clusters = matches!(dot_clusters.as_str(), "on");
73            let mut legend = matches!(dot_legend.as_str(), "on");
74            let mut theme = match dot_theme.as_str() {
75                "dark" => DotTheme::Dark,
76                _ => DotTheme::Light,
77            };
78            let mut rankdir = match dot_rankdir.as_str() {
79                "TB" => RankDir::TB,
80                _ => RankDir::LR,
81            };
82            let mut splines = match dot_splines.as_str() {
83                "ortho" => EdgeStyle::Ortho,
84                "polyline" => EdgeStyle::Polyline,
85                _ => EdgeStyle::Curved,
86            };
87            let mut rounded = matches!(dot_rounded.as_str(), "on");
88            if let Some(cfg_path) = config.as_ref() {
89                if let Some(cfg) =
90                    crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
91                {
92                    if let Some(dot) = cfg.dot {
93                        if let Some(v) = dot.clusters {
94                            clusters = v;
95                        }
96                        if let Some(v) = dot.legend {
97                            legend = v;
98                        }
99                        if let Some(v) = dot.theme {
100                            theme = if v == "dark" { DotTheme::Dark } else { DotTheme::Light };
101                        }
102                        if let Some(v) = dot.rankdir {
103                            rankdir = if v == "TB" { RankDir::TB } else { RankDir::LR };
104                        }
105                        if let Some(v) = dot.splines {
106                            splines = match v.as_str() {
107                                "ortho" => EdgeStyle::Ortho,
108                                "polyline" => EdgeStyle::Polyline,
109                                _ => EdgeStyle::Curved,
110                            };
111                        }
112                        if let Some(v) = dot.rounded {
113                            rounded = v;
114                        }
115                    }
116                }
117            }
118            let dot_opts = DotOptions { clusters, legend, theme, rankdir, splines, rounded };
119
120            if let Some(dot_path) = dot {
121                match DotGenerator::new().generate_dot_with_options(&graph, dot_opts) {
122                    Ok(content) => {
123                        if let Err(e) = fs::write(&dot_path, content) {
124                            eprintln!("Failed to write DOT output {dot_path}: {e}");
125                        }
126                    }
127                    Err(e) => eprintln!("Visualization error: {e}"),
128                }
129            }
130
131            if let Some(svg_path) = svg {
132                let mut interactive = matches!(svg_interactive.as_str(), "on");
133                if let Some(cfg_path) = config.as_ref() {
134                    if let Some(cfg) =
135                        crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
136                    {
137                        if let Some(svg) = cfg.svg {
138                            if let Some(v) = svg.interactive {
139                                interactive = v;
140                            }
141                        }
142                    }
143                }
144                let svg_opts = SvgOptions { dot: dot_opts, interactive };
145                match SvgGenerator::new().generate_svg_with_options(&graph, svg_opts) {
146                    Ok(content) => {
147                        if let Err(e) = fs::write(&svg_path, content) {
148                            eprintln!("Failed to write SVG output {svg_path}: {e}");
149                        }
150                    }
151                    Err(e) => eprintln!("Visualization error: {e}"),
152                }
153            }
154
155            if let Some(save_path) = save {
156                if let Err(e) = KnowledgeGraph::save_json(&graph, std::path::Path::new(&save_path))
157                {
158                    eprintln!("Failed to save graph JSON {save_path}: {e}");
159                }
160            }
161
162            println!("Build completed for path: {path}");
163            0
164        }
165        Commands::Query { query } => match query {
166            QueryCommands::ConnectedFiles {
167                path,
168                config,
169                no_ignore,
170                file,
171                graph: graph_path,
172                format,
173            } => {
174                let graph = if let Some(p) = graph_path {
175                    match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
176                        Ok(g) => g,
177                        Err(e) => {
178                            eprintln!("Load graph failed: {e}");
179                            return 1;
180                        }
181                    }
182                } else {
183                    let res = match KnowledgeGraph::build_from_directory_opts(
184                        std::path::Path::new(&path),
185                        no_ignore,
186                    ) {
187                        Ok(g) => g,
188                        Err(e) => {
189                            eprintln!("Build failed: {e}");
190                            return 1;
191                        }
192                    };
193                    res
194                };
195                let q = crate::query::ConnectedFilesQuery::new(&file);
196                let results = q.run(&graph);
197                let fmt = if let Some(cfg_path) = config.as_ref() {
198                    if let Some(cfg) =
199                        crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
200                    {
201                        cfg.query.and_then(|q| q.default_format).unwrap_or(format.clone())
202                    } else {
203                        format.clone()
204                    }
205                } else {
206                    format.clone()
207                };
208                if fmt == "json" {
209                    let out: Vec<String> =
210                        results.into_iter().map(|p| p.display().to_string()).collect();
211                    match serde_json::to_string_pretty(&out) {
212                        Ok(s) => println!("{s}"),
213                        Err(e) => {
214                            eprintln!("JSON encode error: {e}");
215                            return 1;
216                        }
217                    }
218                } else {
219                    let rows: Vec<Vec<String>> = results
220                        .into_iter()
221                        .enumerate()
222                        .map(|(i, p)| vec![format!("{}", i + 1), p.display().to_string()])
223                        .collect();
224                    let table = crate::utils::table::render(&["#", "Path"], &rows);
225                    println!("{table}");
226                }
227                0
228            }
229            QueryCommands::FunctionUsage {
230                path,
231                config,
232                no_ignore,
233                function,
234                direction,
235                graph: graph_path,
236                format,
237            } => {
238                let graph = if let Some(p) = graph_path {
239                    match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
240                        Ok(g) => g,
241                        Err(e) => {
242                            eprintln!("Load graph failed: {e}");
243                            return 1;
244                        }
245                    }
246                } else {
247                    if no_ignore {
248                        std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
249                    }
250                    let res =
251                        match KnowledgeGraph::build_from_directory(std::path::Path::new(&path)) {
252                            Ok(g) => g,
253                            Err(e) => {
254                                eprintln!("Build failed: {e}");
255                                if no_ignore {
256                                    std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
257                                }
258                                return 1;
259                            }
260                        };
261                    if no_ignore {
262                        std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
263                    }
264                    res
265                };
266                let dir = match direction.as_str() {
267                    "callees" => crate::query::UsageDirection::Callees,
268                    _ => crate::query::UsageDirection::Callers,
269                };
270                let q = crate::query::FunctionUsageQuery { function, direction: dir };
271                let results = q.run(&graph);
272                let fmt = if let Some(cfg_path) = config.as_ref() {
273                    if let Some(cfg) =
274                        crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
275                    {
276                        cfg.query.and_then(|q| q.default_format).unwrap_or(format.clone())
277                    } else {
278                        format.clone()
279                    }
280                } else {
281                    format.clone()
282                };
283                if fmt == "json" {
284                    let out: Vec<String> =
285                        results.into_iter().map(|p| p.display().to_string()).collect();
286                    match serde_json::to_string_pretty(&out) {
287                        Ok(s) => println!("{s}"),
288                        Err(e) => {
289                            eprintln!("JSON encode error: {e}");
290                            return 1;
291                        }
292                    }
293                } else {
294                    for p in results {
295                        println!("{}", p.display());
296                    }
297                }
298                0
299            }
300            QueryCommands::Cycles { path, config, no_ignore, graph: graph_path, format } => {
301                let graph = if let Some(p) = graph_path {
302                    match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
303                        Ok(g) => g,
304                        Err(e) => {
305                            eprintln!("Load graph failed: {e}");
306                            return 1;
307                        }
308                    }
309                } else {
310                    if no_ignore {
311                        std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
312                    }
313                    let res =
314                        match KnowledgeGraph::build_from_directory(std::path::Path::new(&path)) {
315                            Ok(g) => g,
316                            Err(e) => {
317                                eprintln!("Build failed: {e}");
318                                if no_ignore {
319                                    std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
320                                }
321                                return 1;
322                            }
323                        };
324                    if no_ignore {
325                        std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
326                    }
327                    res
328                };
329                let q = crate::query::CycleDetectionQuery::new();
330                let cycles = q.run(&graph);
331                let fmt = if let Some(cfg_path) = config.as_ref() {
332                    if let Some(cfg) =
333                        crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
334                    {
335                        cfg.query.and_then(|q| q.default_format).unwrap_or(format.clone())
336                    } else {
337                        format.clone()
338                    }
339                } else {
340                    format.clone()
341                };
342                if fmt == "json" {
343                    let out: Vec<Vec<String>> = cycles
344                        .into_iter()
345                        .map(|cyc| cyc.into_iter().map(|p| p.display().to_string()).collect())
346                        .collect();
347                    match serde_json::to_string_pretty(&out) {
348                        Ok(s) => println!("{s}"),
349                        Err(e) => {
350                            eprintln!("JSON encode error: {e}");
351                            return 1;
352                        }
353                    }
354                } else {
355                    for cyc in cycles {
356                        let parts: Vec<String> =
357                            cyc.iter().map(|p| p.display().to_string()).collect();
358                        println!("{}", parts.join(" -> "));
359                    }
360                }
361                0
362            }
363            QueryCommands::Path {
364                path,
365                config,
366                no_ignore,
367                from,
368                to,
369                graph: graph_path,
370                format,
371            } => {
372                let graph = if let Some(p) = graph_path {
373                    match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
374                        Ok(g) => g,
375                        Err(e) => {
376                            eprintln!("Load graph failed: {e}");
377                            return 1;
378                        }
379                    }
380                } else {
381                    if no_ignore {
382                        std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
383                    }
384                    let res =
385                        match KnowledgeGraph::build_from_directory(std::path::Path::new(&path)) {
386                            Ok(g) => g,
387                            Err(e) => {
388                                eprintln!("Build failed: {e}");
389                                if no_ignore {
390                                    std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
391                                }
392                                return 1;
393                            }
394                        };
395                    if no_ignore {
396                        std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
397                    }
398                    res
399                };
400                let q = crate::query::ShortestPathQuery::new(&from, &to);
401                let results = q.run(&graph);
402                let fmt = if let Some(cfg_path) = config.as_ref() {
403                    if let Some(cfg) =
404                        crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
405                    {
406                        cfg.query.and_then(|q| q.default_format).unwrap_or(format.clone())
407                    } else {
408                        format.clone()
409                    }
410                } else {
411                    format.clone()
412                };
413                if fmt == "json" {
414                    let out: Vec<String> =
415                        results.into_iter().map(|p| p.display().to_string()).collect();
416                    match serde_json::to_string_pretty(&out) {
417                        Ok(s) => println!("{s}"),
418                        Err(e) => {
419                            eprintln!("JSON encode error: {e}");
420                            return 1;
421                        }
422                    }
423                } else if results.is_empty() {
424                    println!("<no path>");
425                } else {
426                    let rows: Vec<Vec<String>> = results
427                        .into_iter()
428                        .enumerate()
429                        .map(|(i, p)| vec![format!("{}", i + 1), p.display().to_string()])
430                        .collect();
431                    let table = crate::utils::table::render(&["Step", "Path"], &rows);
432                    println!("{table}");
433                }
434                0
435            }
436            QueryCommands::Hubs {
437                path,
438                config,
439                no_ignore,
440                graph: graph_path,
441                metric,
442                top,
443                format,
444            } => {
445                use crate::query::{CentralityMetric, HubsQuery};
446                let graph = if let Some(p) = graph_path {
447                    match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
448                        Ok(g) => g,
449                        Err(e) => {
450                            eprintln!("Load graph failed: {e}");
451                            return 1;
452                        }
453                    }
454                } else {
455                    if no_ignore {
456                        std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
457                    }
458                    let res =
459                        match KnowledgeGraph::build_from_directory(std::path::Path::new(&path)) {
460                            Ok(g) => g,
461                            Err(e) => {
462                                eprintln!("Build failed: {e}");
463                                if no_ignore {
464                                    std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
465                                }
466                                return 1;
467                            }
468                        };
469                    if no_ignore {
470                        std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
471                    }
472                    res
473                };
474                let m = match metric.as_str() {
475                    "in" => CentralityMetric::In,
476                    "out" => CentralityMetric::Out,
477                    _ => CentralityMetric::Total,
478                };
479                let q = HubsQuery::new(m, top);
480                let rows = q.run(&graph);
481                let fmt = if let Some(cfg_path) = config.as_ref() {
482                    if let Some(cfg) =
483                        crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
484                    {
485                        cfg.query.and_then(|q| q.default_format).unwrap_or(format.clone())
486                    } else {
487                        format.clone()
488                    }
489                } else {
490                    format.clone()
491                };
492                if fmt == "json" {
493                    #[derive(serde::Serialize)]
494                    struct HubRow {
495                        path: String,
496                        indegree: usize,
497                        outdegree: usize,
498                    }
499                    let out: Vec<HubRow> = rows
500                        .into_iter()
501                        .map(|(p, i, o)| HubRow {
502                            path: p.display().to_string(),
503                            indegree: i,
504                            outdegree: o,
505                        })
506                        .collect();
507                    match serde_json::to_string_pretty(&out) {
508                        Ok(s) => println!("{s}"),
509                        Err(e) => {
510                            eprintln!("JSON encode error: {e}");
511                            return 1;
512                        }
513                    }
514                } else {
515                    let body: Vec<Vec<String>> = rows
516                        .into_iter()
517                        .map(|(p, i, o)| {
518                            vec![
519                                p.display().to_string(),
520                                i.to_string(),
521                                o.to_string(),
522                                (i + o).to_string(),
523                            ]
524                        })
525                        .collect();
526                    let table = crate::utils::table::render(&["Path", "In", "Out", "Total"], &body);
527                    println!("{table}");
528                }
529                0
530            }
531            QueryCommands::ModuleCentrality {
532                path,
533                config,
534                no_ignore,
535                graph: graph_path,
536                metric,
537                top,
538                format,
539            } => {
540                use crate::query::{CentralityMetric, ModuleCentralityQuery};
541                let graph = if let Some(p) = graph_path {
542                    match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
543                        Ok(g) => g,
544                        Err(e) => {
545                            eprintln!("Load graph failed: {e}");
546                            return 1;
547                        }
548                    }
549                } else {
550                    if no_ignore {
551                        std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
552                    }
553                    let res =
554                        match KnowledgeGraph::build_from_directory(std::path::Path::new(&path)) {
555                            Ok(g) => g,
556                            Err(e) => {
557                                eprintln!("Build failed: {e}");
558                                if no_ignore {
559                                    std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
560                                }
561                                return 1;
562                            }
563                        };
564                    if no_ignore {
565                        std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
566                    }
567                    res
568                };
569                let m = match metric.as_str() {
570                    "in" => CentralityMetric::In,
571                    "out" => CentralityMetric::Out,
572                    _ => CentralityMetric::Total,
573                };
574                let q = ModuleCentralityQuery::new(m, top);
575                let rows = q.run(&graph);
576                let fmt = if let Some(cfg_path) = config.as_ref() {
577                    if let Some(cfg) =
578                        crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
579                    {
580                        cfg.query.and_then(|q| q.default_format).unwrap_or(format.clone())
581                    } else {
582                        format.clone()
583                    }
584                } else {
585                    format.clone()
586                };
587                if fmt == "json" {
588                    #[derive(serde::Serialize)]
589                    struct Row {
590                        module: String,
591                        indegree: usize,
592                        outdegree: usize,
593                    }
594                    let out: Vec<Row> = rows
595                        .into_iter()
596                        .map(|(p, i, o)| Row {
597                            module: p.display().to_string(),
598                            indegree: i,
599                            outdegree: o,
600                        })
601                        .collect();
602                    match serde_json::to_string_pretty(&out) {
603                        Ok(s) => println!("{s}"),
604                        Err(e) => {
605                            eprintln!("JSON encode error: {e}");
606                            return 1;
607                        }
608                    }
609                } else {
610                    let body: Vec<Vec<String>> = rows
611                        .into_iter()
612                        .map(|(p, i, o)| {
613                            vec![
614                                p.display().to_string(),
615                                i.to_string(),
616                                o.to_string(),
617                                (i + o).to_string(),
618                            ]
619                        })
620                        .collect();
621                    let table =
622                        crate::utils::table::render(&["Module", "In", "Out", "Total"], &body);
623                    println!("{table}");
624                }
625                0
626            }
627            QueryCommands::TraitImpls {
628                path,
629                config,
630                no_ignore,
631                r#trait,
632                graph: graph_path,
633                format,
634            } => {
635                use crate::query::TraitImplsQuery;
636                let graph = if let Some(p) = graph_path {
637                    match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
638                        Ok(g) => g,
639                        Err(e) => {
640                            eprintln!("Load graph failed: {e}");
641                            return 1;
642                        }
643                    }
644                } else {
645                    if no_ignore {
646                        std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
647                    }
648                    let res =
649                        match KnowledgeGraph::build_from_directory(std::path::Path::new(&path)) {
650                            Ok(g) => g,
651                            Err(e) => {
652                                eprintln!("Build failed: {e}");
653                                if no_ignore {
654                                    std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
655                                }
656                                return 1;
657                            }
658                        };
659                    if no_ignore {
660                        std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
661                    }
662                    res
663                };
664                let q = TraitImplsQuery::new(&r#trait);
665                let rows = q.run(&graph);
666                let fmt = if let Some(cfg_path) = config.as_ref() {
667                    if let Some(cfg) =
668                        crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
669                    {
670                        cfg.query.and_then(|q| q.default_format).unwrap_or(format.clone())
671                    } else {
672                        format.clone()
673                    }
674                } else {
675                    format.clone()
676                };
677                if fmt == "json" {
678                    #[derive(serde::Serialize)]
679                    struct Row {
680                        path: String,
681                        r#type: String,
682                    }
683                    let out: Vec<Row> = rows
684                        .into_iter()
685                        .map(|(p, t)| Row { path: p.display().to_string(), r#type: t })
686                        .collect();
687                    match serde_json::to_string_pretty(&out) {
688                        Ok(s) => println!("{s}"),
689                        Err(e) => {
690                            eprintln!("JSON encode error: {e}");
691                            return 1;
692                        }
693                    }
694                } else if rows.is_empty() {
695                    println!("<no implementations found>");
696                } else {
697                    let body: Vec<Vec<String>> =
698                        rows.into_iter().map(|(p, t)| vec![p.display().to_string(), t]).collect();
699                    let table = crate::utils::table::render(&["Path", "Type"], &body);
700                    println!("{table}");
701                }
702                0
703            }
704            QueryCommands::UnreferencedItems {
705                path,
706                config,
707                no_ignore,
708                include_public,
709                exclude,
710                graph: graph_path,
711                format,
712            } => {
713                use crate::query::UnreferencedItemsQuery;
714                let graph = if let Some(p) = graph_path {
715                    match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
716                        Ok(g) => g,
717                        Err(e) => {
718                            eprintln!("Load graph failed: {e}");
719                            return 1;
720                        }
721                    }
722                } else {
723                    if no_ignore {
724                        std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
725                    }
726                    let res =
727                        match KnowledgeGraph::build_from_directory(std::path::Path::new(&path)) {
728                            Ok(g) => g,
729                            Err(e) => {
730                                eprintln!("Build failed: {e}");
731                                if no_ignore {
732                                    std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
733                                }
734                                return 1;
735                            }
736                        };
737                    if no_ignore {
738                        std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
739                    }
740                    res
741                };
742                let exclude_re = if let Some(pat) = exclude.as_ref() {
743                    match regex::Regex::new(pat) {
744                        Ok(r) => Some(r),
745                        Err(e) => {
746                            eprintln!("Invalid --exclude regex: {e}");
747                            return 1;
748                        }
749                    }
750                } else {
751                    None
752                };
753                let q = UnreferencedItemsQuery::new(include_public, exclude_re);
754                let rows = q.run(&graph);
755                let fmt = if let Some(cfg_path) = config.as_ref() {
756                    if let Some(cfg) =
757                        crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
758                    {
759                        cfg.query.and_then(|q| q.default_format).unwrap_or(format.clone())
760                    } else {
761                        format.clone()
762                    }
763                } else {
764                    format.clone()
765                };
766                if fmt == "json" {
767                    #[derive(serde::Serialize)]
768                    struct Row {
769                        path: String,
770                        id: String,
771                        name: String,
772                        kind: String,
773                        visibility: String,
774                    }
775                    let out: Vec<Row> = rows
776                        .into_iter()
777                        .map(|(p, id, name, kind, vis)| Row {
778                            path: p.display().to_string(),
779                            id,
780                            name,
781                            kind,
782                            visibility: vis,
783                        })
784                        .collect();
785                    match serde_json::to_string_pretty(&out) {
786                        Ok(s) => println!("{s}"),
787                        Err(e) => {
788                            eprintln!("JSON encode error: {e}");
789                            return 1;
790                        }
791                    }
792                } else if rows.is_empty() {
793                    println!("<no unreferenced items>");
794                } else {
795                    let body: Vec<Vec<String>> = rows
796                        .into_iter()
797                        .map(|(p, id, name, kind, vis)| {
798                            vec![p.display().to_string(), id, name, kind, vis]
799                        })
800                        .collect();
801                    let table = crate::utils::table::render(
802                        &["Path", "ItemId", "Name", "Kind", "Vis"],
803                        &body,
804                    );
805                    println!("{table}");
806                }
807                0
808            }
809            QueryCommands::ItemInfo {
810                path,
811                config,
812                no_ignore,
813                item_id,
814                graph: graph_path,
815                show_code,
816                format,
817            } => {
818                use crate::query::ItemInfoQuery;
819                let graph = if let Some(p) = graph_path {
820                    match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
821                        Ok(g) => g,
822                        Err(e) => {
823                            eprintln!("Load graph failed: {e}");
824                            return 1;
825                        }
826                    }
827                } else {
828                    if no_ignore {
829                        std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
830                    }
831                    let res =
832                        match KnowledgeGraph::build_from_directory(std::path::Path::new(&path)) {
833                            Ok(g) => g,
834                            Err(e) => {
835                                eprintln!("Build failed: {e}");
836                                if no_ignore {
837                                    std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
838                                }
839                                return 1;
840                            }
841                        };
842                    if no_ignore {
843                        std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
844                    }
845                    res
846                };
847                let id = crate::graph::ItemId(item_id);
848                let q = ItemInfoQuery::new(id, show_code);
849                let result = q.run(&graph);
850                let fmt = if let Some(cfg_path) = config.as_ref() {
851                    if let Some(cfg) =
852                        crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
853                    {
854                        cfg.query.and_then(|q| q.default_format).unwrap_or(format.clone())
855                    } else {
856                        format.clone()
857                    }
858                } else {
859                    format.clone()
860                };
861                if fmt == "json" {
862                    match serde_json::to_string_pretty(&result) {
863                        Ok(s) => println!("{s}"),
864                        Err(e) => {
865                            eprintln!("JSON encode error: {e}");
866                            return 1;
867                        }
868                    }
869                } else {
870                    match result {
871                        None => println!("<item not found>"),
872                        Some(info) => {
873                            println!("Item: {}", info.name);
874                            println!("Id: {}", info.id);
875                            println!("Kind: {}", info.kind);
876                            println!("Vis: {}", info.visibility);
877                            println!(
878                                "Location: {}:{}-{}",
879                                info.path, info.line_start, info.line_end
880                            );
881                            if show_code {
882                                if let Some(code) = info.code.as_deref() {
883                                    println!("\n--- code ---\n{}\n--- end code ---", code);
884                                }
885                            }
886                            if info.inbound.is_empty() {
887                                println!("\nInbound: <none>");
888                            } else {
889                                println!("\nInbound:");
890                                for r in info.inbound {
891                                    println!(
892                                        "- [{}] {} ({}) @ {} :: {}",
893                                        r.relation, r.name, r.id, r.path, r.context
894                                    );
895                                }
896                            }
897                            if info.outbound.is_empty() {
898                                println!("\nOutbound: <none>");
899                            } else {
900                                println!("\nOutbound:");
901                                for r in info.outbound {
902                                    println!(
903                                        "- [{}] {} ({}) @ {} :: {}",
904                                        r.relation, r.name, r.id, r.path, r.context
905                                    );
906                                }
907                            }
908                        }
909                    }
910                }
911                0
912            }
913        },
914    }
915}