cortex_runtime/cli/
pathfind_cmd.rs1use crate::cli::output::{self, Styled};
4use crate::intelligence::cache::MapCache;
5use crate::map::types::PathConstraints;
6use anyhow::{bail, Result};
7
8pub async fn run(domain: &str, from: u32, to: u32) -> Result<()> {
10 let s = Styled::new();
11
12 let mut cache = MapCache::default_cache()?;
14 let map = match cache.load_map(domain)? {
15 Some(m) => m,
16 None => {
17 if output::is_json() {
18 output::print_json(&serde_json::json!({
19 "error": "no_map",
20 "message": format!("No cached map for '{domain}'"),
21 "hint": format!("Run: cortex map {domain}")
22 }));
23 return Ok(());
24 }
25 bail!("No map found for '{domain}'. Run 'cortex map {domain}' first.");
26 }
27 };
28
29 let max_node = map.nodes.len() as u32;
31 if from >= max_node {
32 bail!(
33 "Source node {from} doesn't exist in this map (max: {}).",
34 max_node - 1
35 );
36 }
37 if to >= max_node {
38 bail!(
39 "Target node {to} doesn't exist in this map (max: {}).",
40 max_node - 1
41 );
42 }
43
44 let constraints = PathConstraints::default();
45 let path = map.shortest_path(from, to, &constraints);
46
47 if output::is_json() {
48 match &path {
49 Some(p) => {
50 let nodes: Vec<serde_json::Value> = p
51 .nodes
52 .iter()
53 .map(|&idx| {
54 let url = map.urls.get(idx as usize).cloned().unwrap_or_default();
55 let node = &map.nodes[idx as usize];
56 serde_json::json!({
57 "index": idx,
58 "url": url,
59 "page_type": format!("{:?}", node.page_type),
60 })
61 })
62 .collect();
63 output::print_json(&serde_json::json!({
64 "found": true,
65 "hops": p.hops,
66 "total_weight": p.total_weight,
67 "nodes": nodes,
68 "required_actions": p.required_actions.len(),
69 }));
70 }
71 None => {
72 output::print_json(&serde_json::json!({
73 "found": false,
74 "from": from,
75 "to": to,
76 }));
77 }
78 }
79 return Ok(());
80 }
81
82 match path {
83 Some(path) => {
84 if !output::is_quiet() {
85 eprintln!(" Path from node {from} to node {to}:");
86 eprintln!(" Hops: {}", path.hops);
87 eprintln!(" Weight: {:.2}", path.total_weight);
88
89 if path.hops > 20 {
90 eprintln!();
91 eprintln!(
92 " {} Path has {} hops. This seems unusually long.",
93 s.warn_sym(),
94 path.hops
95 );
96 }
97
98 eprintln!();
99 eprintln!(" Route:");
100 for (i, &node_idx) in path.nodes.iter().enumerate() {
101 let url = map
102 .urls
103 .get(node_idx as usize)
104 .map(|s| s.as_str())
105 .unwrap_or("?");
106 let node = &map.nodes[node_idx as usize];
107 let arrow = if i < path.nodes.len() - 1 {
108 " \u{2192}"
109 } else {
110 " "
111 };
112 eprintln!(" [{node_idx:>5}] {:?} {url}{arrow}", node.page_type);
113 }
114
115 if !path.required_actions.is_empty() {
116 eprintln!();
117 eprintln!(" Required actions:");
118 for action in &path.required_actions {
119 eprintln!(
120 " At node {}: opcode ({:#04x}, {:#04x})",
121 action.at_node, action.opcode.category, action.opcode.action
122 );
123 }
124 }
125 }
126 }
127 None => {
128 if !output::is_quiet() {
129 eprintln!(" No path found from node {from} to node {to}.");
130 eprintln!(" Try relaxing constraints or checking that both nodes are connected.");
131 }
132 }
133 }
134
135 Ok(())
136}