project_map_cli_rust/core/
orchestrator.rs1use std::path::Path;
2use std::collections::HashMap;
3use ignore::WalkBuilder;
4use crate::core::parser::CodeParser;
5use crate::core::graph::{ProjectGraph, NodeData, NodeType, EdgeType};
6use crate::core::utils::{path_to_fqn, resolve_import_path};
7use crate::error::Result;
8
9pub struct Orchestrator {
10 parser: CodeParser,
11 graph: ProjectGraph,
12}
13
14impl Orchestrator {
15 pub fn new() -> Self {
16 Self {
17 parser: CodeParser::new(),
18 graph: ProjectGraph::new(),
19 }
20 }
21
22 pub fn build_index(&mut self, root: &Path) -> Result<()> {
23 let mut outlines = Vec::new();
24 let mut fqn_to_node = HashMap::new();
25 let mut path_to_node = HashMap::new();
26
27 for result in WalkBuilder::new(root).build() {
30 let entry = match result {
31 Ok(e) => e,
32 Err(_) => continue,
33 };
34
35 let path = entry.path();
36 if entry.file_type().map(|ft| ft.is_file()).unwrap_or(false) {
37 let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("");
38 if extension == "py" || extension == "rs" || extension == "ts" || extension == "tsx" || extension == "kt" || extension == "sql" || extension == "vue" {
39 match self.parser.parse_file(path) {
40 Ok(outline) => {
41 let fqn = path_to_fqn(root, path);
42 let file_node = self.graph.add_node(NodeData {
43 path: outline.path.clone(),
44 name: fqn.clone(),
45 kind: "file".to_string(),
46 line: 0,
47 start_byte: 0,
48 end_byte: 0,
49 node_type: NodeType::File,
50 });
51 fqn_to_node.insert(fqn, file_node);
52 path_to_node.insert(outline.path.clone(), file_node);
53
54 for symbol in &outline.symbols {
55 let symbol_node = self.graph.add_node(NodeData {
56 path: outline.path.clone(),
57 name: symbol.name.clone(),
58 kind: symbol.kind.clone(),
59 line: symbol.line,
60 start_byte: symbol.start_byte,
61 end_byte: symbol.end_byte,
62 node_type: NodeType::Symbol,
63 });
64 self.graph.add_edge(file_node, symbol_node, EdgeType::Contains);
65 }
66 outlines.push(outline);
67 }
68 Err(e) => {
69 if !e.to_string().contains("valid UTF-8") {
71 eprintln!("Error parsing {}: {}", path.display(), e);
72 }
73 }
74 }
75 }
76 }
77 }
78
79 for outline in outlines {
81 if let Some(&from_node) = path_to_node.get(&outline.path) {
82 for imp in outline.imports {
83 if let Some(&to_node) = fqn_to_node.get(&imp) {
85 self.graph.add_edge(from_node, to_node, EdgeType::Imports);
86 } else {
87 let resolved_rel = resolve_import_path(&outline.path, &imp);
89
90 let mut found = false;
92 for ext in &["", ".ts", ".tsx", "/index.ts", "/index.tsx"] {
93 let candidate = format!("{}{}", resolved_rel, ext);
94 if let Some(&to_node) = path_to_node.get(&candidate) {
95 self.graph.add_edge(from_node, to_node, EdgeType::Imports);
96 found = true;
97 break;
98 }
99 }
100
101 if !found {
103 let matching_fqn = fqn_to_node.keys()
104 .find(|&k| k.ends_with(&imp));
105 if let Some(key) = matching_fqn {
106 let &to_node = fqn_to_node.get(key).unwrap();
107 self.graph.add_edge(from_node, to_node, EdgeType::Imports);
108 }
109 }
110 }
111 }
112 }
113 }
114
115 Ok(())
116 }
117
118 pub fn save_index(&self, path: &Path) -> Result<()> {
119 self.graph.save(path)
120 }
121
122 pub fn save_index_versioned(&self, base_dir: &Path) -> Result<()> {
123 use chrono::Local;
124 use std::fs;
125 use std::os::unix::fs::symlink;
126
127 let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string();
128 let backups_dir = base_dir.join("backups");
129 let current_backup_dir = backups_dir.join(×tamp);
130
131 fs::create_dir_all(¤t_backup_dir)?;
132
133 let index_path = current_backup_dir.join(".project-map.json");
134 self.graph.save(&index_path)?;
135
136 let latest_link = base_dir.join("latest");
137 if latest_link.exists() {
138 fs::remove_file(&latest_link).ok();
139 fs::remove_dir_all(&latest_link).ok();
140 }
141
142 #[cfg(unix)]
144 {
145 let rel_target = Path::new("backups").join(×tamp);
147 symlink(rel_target, &latest_link)?;
148 }
149
150 #[cfg(not(unix))]
152 {
153 fs::create_dir_all(&latest_link)?;
154 fs::copy(&index_path, latest_link.join(".project-map.json"))?;
155 }
156
157 Ok(())
158 }
159}