1use serde_json::Value;
4use std::fs;
5use std::path::Path;
6
7pub fn detect_project_context(root_path: &Path) -> Option<String> {
9 if let Some(context) = read_cargo_toml(root_path) {
13 return Some(context);
14 }
15
16 if let Some(context) = read_package_json(root_path) {
18 return Some(context);
19 }
20
21 if let Some(context) = read_pyproject_toml(root_path) {
23 return Some(context);
24 }
25
26 if let Some(context) = read_go_mod(root_path) {
28 return Some(context);
29 }
30
31 if let Some(context) = read_git_description(root_path) {
33 return Some(context);
34 }
35
36 if let Some(context) = read_readme(root_path) {
38 return Some(context);
39 }
40
41 None
42}
43
44fn read_cargo_toml(root_path: &Path) -> Option<String> {
45 let cargo_path = root_path.join("Cargo.toml");
46 if !cargo_path.exists() {
47 return None;
48 }
49
50 let content = fs::read_to_string(&cargo_path).ok()?;
51 let toml: toml::Value = toml::from_str(&content).ok()?;
52
53 let package = toml.get("package")?;
54 let name = package.get("name")?.as_str()?;
55 let desc = package.get("description")?.as_str()?;
56
57 Some(format!("Rust: {} - {}", name, truncate_string(desc, 80)))
58}
59
60fn read_package_json(root_path: &Path) -> Option<String> {
61 let package_path = root_path.join("package.json");
62 if !package_path.exists() {
63 return None;
64 }
65
66 let content = fs::read_to_string(&package_path).ok()?;
67 let json: Value = serde_json::from_str(&content).ok()?;
68
69 let name = json.get("name")?.as_str()?;
70 let desc = json
71 .get("description")?
72 .as_str()
73 .unwrap_or("No description");
74
75 Some(format!("Node: {} - {}", name, truncate_string(desc, 80)))
76}
77
78fn read_pyproject_toml(root_path: &Path) -> Option<String> {
79 let pyproject_path = root_path.join("pyproject.toml");
80 if !pyproject_path.exists() {
81 return None;
82 }
83
84 let content = fs::read_to_string(&pyproject_path).ok()?;
85 let toml: toml::Value = toml::from_str(&content).ok()?;
86
87 if let Some(project) = toml.get("project") {
89 let name = project.get("name")?.as_str()?;
90 let desc = project
91 .get("description")?
92 .as_str()
93 .unwrap_or("No description");
94 return Some(format!("Python: {} - {}", name, truncate_string(desc, 80)));
95 } else if let Some(tool) = toml.get("tool") {
96 if let Some(poetry) = tool.get("poetry") {
97 let name = poetry.get("name")?.as_str()?;
98 let desc = poetry
99 .get("description")?
100 .as_str()
101 .unwrap_or("No description");
102 return Some(format!("Python: {} - {}", name, truncate_string(desc, 80)));
103 }
104 }
105
106 None
107}
108
109fn read_go_mod(root_path: &Path) -> Option<String> {
110 let go_mod_path = root_path.join("go.mod");
111 if !go_mod_path.exists() {
112 return None;
113 }
114
115 let content = fs::read_to_string(&go_mod_path).ok()?;
116 let first_line = content.lines().next()?;
117
118 if first_line.starts_with("module ") {
119 let module_name = first_line.strip_prefix("module ")?.trim();
120 return Some(format!("Go: {}", module_name));
121 }
122
123 None
124}
125
126fn read_git_description(root_path: &Path) -> Option<String> {
127 let git_desc_path = root_path.join(".git/description");
128 if !git_desc_path.exists() {
129 return None;
130 }
131
132 let content = fs::read_to_string(&git_desc_path).ok()?;
133 let desc = content.trim();
134
135 if desc.contains("Unnamed repository") {
137 return None;
138 }
139
140 Some(format!("Git: {}", truncate_string(desc, 80)))
141}
142
143fn read_readme(root_path: &Path) -> Option<String> {
144 let readme_names = [
146 "README.md",
147 "README.MD",
148 "readme.md",
149 "README",
150 "README.txt",
151 ];
152
153 for name in &readme_names {
154 let readme_path = root_path.join(name);
155 if readme_path.exists() {
156 let content = fs::read_to_string(&readme_path).ok()?;
157
158 for line in content.lines() {
160 let trimmed = line.trim();
161 if !trimmed.is_empty() && !trimmed.starts_with('#') {
163 return Some(truncate_string(trimmed, 100));
164 }
165 }
166 }
167 }
168
169 None
170}
171
172fn truncate_string(s: &str, max_len: usize) -> String {
173 if s.chars().count() <= max_len {
174 s.to_string()
175 } else {
176 let truncated: String = s.chars().take(max_len.saturating_sub(3)).collect();
177 format!("{}...", truncated)
178 }
179}