1use std::collections::HashMap;
7use std::process::Command;
8
9#[derive(Debug, Clone, serde::Serialize)]
11pub struct DiagnosticEntry {
12 pub category: String,
14 pub key: String,
16 pub value: String,
18}
19
20#[derive(Debug, Clone, serde::Serialize)]
22pub struct DiagnosticReport {
23 pub entries: Vec<DiagnosticEntry>,
25 pub timestamp: chrono::DateTime<chrono::Utc>,
27 pub version: String,
29}
30
31impl DiagnosticReport {
32 pub fn entries_for_category(&self, category: &str) -> Vec<&DiagnosticEntry> {
34 self.entries
35 .iter()
36 .filter(|e| e.category == category)
37 .collect()
38 }
39
40 pub fn get(&self, category: &str, key: &str) -> Option<&str> {
42 self.entries
43 .iter()
44 .find(|e| e.category == category && e.key == key)
45 .map(|e| e.value.as_str())
46 }
47
48 pub fn to_map(&self) -> HashMap<String, String> {
50 self.entries
51 .iter()
52 .map(|e| (format!("{}.{}", e.category, e.key), e.value.clone()))
53 .collect()
54 }
55}
56
57fn add_entry(report: &mut Vec<DiagnosticEntry>, category: &str, key: &str, value: String) {
59 report.push(DiagnosticEntry {
60 category: category.to_string(),
61 key: key.to_string(),
62 value,
63 });
64}
65
66pub fn collect_os_info(entries: &mut Vec<DiagnosticEntry>) {
68 add_entry(entries, "os", "family", std::env::consts::OS.to_string());
69 add_entry(entries, "os", "arch", std::env::consts::ARCH.to_string());
70
71 if let Ok(version) = std::fs::read_to_string("/proc/version") {
73 let version = version.trim();
74 add_entry(entries, "os", "kernel", version.to_string());
75 }
76
77 if let Ok(lsb_release) = std::fs::read_to_string("/etc/os-release") {
79 for line in lsb_release.lines() {
80 if let Some(value) = line.strip_prefix("PRETTY_NAME=") {
81 let value = value.trim_matches('"').to_string();
82 add_entry(entries, "os", "distribution", value);
83 break;
84 }
85 }
86 }
87
88 let hostname = std::env::var("HOSTNAME")
90 .or_else(|_| std::env::var("HOST"))
91 .unwrap_or_else(|_| "unknown".to_string());
92 add_entry(entries, "os", "hostname", hostname.trim().to_string());
93}
94
95pub fn collect_shell_info(entries: &mut Vec<DiagnosticEntry>) {
97 if let Ok(shell) = std::env::var("SHELL") {
98 add_entry(entries, "shell", "path", shell.clone());
99
100 let shell_name = std::path::Path::new(&shell)
102 .file_name()
103 .and_then(|n| n.to_str())
104 .unwrap_or("unknown");
105
106 let version_output = Command::new(&shell)
107 .args(["--version"])
108 .output()
109 .ok()
110 .and_then(|o| String::from_utf8(o.stdout).ok())
111 .map(|s| s.trim().to_string())
112 .unwrap_or_else(|| "unknown".to_string());
113
114 add_entry(entries, "shell", "name", shell_name.to_string());
115 add_entry(entries, "shell", "version", version_output);
116 }
117
118 if let Ok(term) = std::env::var("TERM") {
120 add_entry(entries, "shell", "terminal", term);
121 }
122}
123
124pub fn collect_tool_versions(entries: &mut Vec<DiagnosticEntry>) {
126 let tools = vec![
127 ("git", &["git", "--version"]),
128 ("node", &["node", "--version"]),
129 ("npm", &["npm", "--version"]),
130 ("cargo", &["cargo", "--version"]),
131 ("rustc", &["rustc", "--version"]),
132 ("python", &["python3", "--version"]),
133 ("go", &["go", "version"]),
134 ];
135
136 for (name, cmd) in tools {
137 let output = Command::new(cmd[0])
138 .args(&cmd[1..])
139 .output()
140 .ok()
141 .and_then(|o| String::from_utf8(o.stdout).ok())
142 .map(|s| s.trim().to_string())
143 .unwrap_or_else(|| "not found".to_string());
144
145 add_entry(entries, "tools", name, output);
146 }
147}
148
149pub fn collect_env_info(entries: &mut Vec<DiagnosticEntry>) {
151 let env_vars = vec![
152 "HOME",
153 "USER",
154 "PATH",
155 "LD_LIBRARY_PATH",
156 "DYLD_LIBRARY_PATH",
157 "PI_OFFLINE",
158 "PI_VERBOSE",
159 ];
160
161 for var in env_vars {
162 if let Ok(value) = std::env::var(var) {
163 let value = if value.len() > 200 {
165 format!("{}... (truncated)", &value[..200])
166 } else {
167 value
168 };
169 add_entry(entries, "env", var, value);
170 }
171 }
172}
173
174pub fn collect_build_info(entries: &mut Vec<DiagnosticEntry>) {
176 add_entry(
177 entries,
178 "build",
179 "version",
180 env!("CARGO_PKG_VERSION").to_string(),
181 );
182
183 if let Some(git_sha) = option_env!("VERGEN_GIT_SHA") {
184 add_entry(entries, "build", "git_sha", git_sha.to_string());
185 }
186
187 if let Some(build_ts) = option_env!("VERGEN_BUILD_TIMESTAMP") {
188 add_entry(entries, "build", "build_timestamp", build_ts.to_string());
189 }
190
191 add_entry(
192 entries,
193 "build",
194 "features",
195 std::env::var("CARGO_CFG_DEBUG").ok().map(|_| "debug").unwrap_or("release").to_string(),
196 );
197}
198
199pub fn collect_path_info(entries: &mut Vec<DiagnosticEntry>) {
201 if let Some(home) = dirs::home_dir() {
202 add_entry(entries, "paths", "home", home.to_string_lossy().to_string());
203 }
204
205 if let Some(config) = dirs::config_dir() {
206 add_entry(
207 entries,
208 "paths",
209 "config",
210 config.join("oxi").to_string_lossy().to_string(),
211 );
212 }
213
214 if let Some(data) = dirs::data_dir() {
215 add_entry(
216 entries,
217 "paths",
218 "data",
219 data.join("oxi").to_string_lossy().to_string(),
220 );
221 }
222
223 if let Some(cache) = dirs::cache_dir() {
224 add_entry(
225 entries,
226 "paths",
227 "cache",
228 cache.join("oxi").to_string_lossy().to_string(),
229 );
230 }
231
232 if let Ok(cwd) = std::env::current_dir() {
233 add_entry(entries, "paths", "cwd", cwd.to_string_lossy().to_string());
234 }
235}
236
237pub fn generate_diagnostic_report() -> DiagnosticReport {
239 let mut entries = Vec::new();
240
241 collect_os_info(&mut entries);
242 collect_shell_info(&mut entries);
243 collect_tool_versions(&mut entries);
244 collect_env_info(&mut entries);
245 collect_build_info(&mut entries);
246 collect_path_info(&mut entries);
247
248 DiagnosticReport {
249 entries,
250 timestamp: chrono::Utc::now(),
251 version: env!("CARGO_PKG_VERSION").to_string(),
252 }
253}
254
255pub fn format_diagnostic_report(report: &DiagnosticReport) -> String {
257 let mut output = String::new();
258
259 output.push_str(&format!("oxi Diagnostic Report - {}\n", report.timestamp));
260 output.push_str(&format!("Version: {}\n", report.version));
261 output.push_str(&"=".repeat(60));
262 output.push('\n');
263
264 let mut current_category = String::new();
265 for entry in &report.entries {
266 if entry.category != current_category {
267 current_category = entry.category.clone();
268 output.push('\n');
269 output.push_str(&format!("[{}]\n", current_category.to_uppercase()));
270 }
271 output.push_str(&format!(" {}: {}\n", entry.key, entry.value));
272 }
273
274 output
275}
276
277pub fn diagnostic_report_json(report: &DiagnosticReport) -> String {
279 serde_json::to_string_pretty(report).unwrap_or_else(|_| "{}".to_string())
280}
281
282pub fn check_common_issues() -> Vec<String> {
284 let mut issues = Vec::new();
285
286 if std::env::var("TERM").is_err() && std::env::var("COLORTERM").is_err() {
289 }
291
292 if !crate::bash_executor::command_exists("git") {
294 issues.push("git is not installed".to_string());
295 }
296
297 if std::env::var("SHELL").is_err() {
299 issues.push("SHELL environment variable not set".to_string());
300 }
301
302 if dirs::home_dir().is_none() {
304 issues.push("Could not determine home directory".to_string());
305 }
306
307 issues
308}
309
310pub fn run_diagnostics() -> String {
312 let report = generate_diagnostic_report();
313 let mut output = format_diagnostic_report(&report);
314
315 let issues = check_common_issues();
316 if !issues.is_empty() {
317 output.push('\n');
318 output.push_str(&"=".repeat(60));
319 output.push('\n');
320 output.push_str("Potential Issues:\n");
321 for issue in issues {
322 output.push_str(&format!(" - {}\n", issue));
323 }
324 }
325
326 output
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn test_generate_diagnostic_report() {
335 let report = generate_diagnostic_report();
336 assert!(!report.entries.is_empty());
337 assert!(!report.version.is_empty());
338 }
339
340 #[test]
341 fn test_diagnostic_entry_lookup() {
342 let report = generate_diagnostic_report();
343
344 let os_family = report.get("os", "family");
346 assert!(os_family.is_some());
347
348 let version = report.get("build", "version");
350 assert!(version.is_some());
351 }
352
353 #[test]
354 fn test_entries_for_category() {
355 let report = generate_diagnostic_report();
356
357 let os_entries = report.entries_for_category("os");
358 assert!(!os_entries.is_empty());
359 }
360
361 #[test]
362 fn test_to_map() {
363 let report = generate_diagnostic_report();
364 let map = report.to_map();
365 assert!(!map.is_empty());
366 assert!(map.contains_key("os.family"));
367 }
368
369 #[test]
370 fn test_format_report() {
371 let report = generate_diagnostic_report();
372 let formatted = format_diagnostic_report(&report);
373 assert!(!formatted.is_empty());
374 assert!(formatted.contains("oxi Diagnostic Report"));
375 }
376
377 #[test]
378 fn test_json_export() {
379 let report = generate_diagnostic_report();
380 let json = diagnostic_report_json(&report);
381 assert!(json.starts_with("{"));
382 assert!(json.ends_with("}"));
383 }
384
385 #[test]
386 fn test_check_common_issues() {
387 let issues = check_common_issues();
388 assert!(issues.len() >= 0);
390 }
391
392 #[test]
393 fn test_run_diagnostics() {
394 let output = run_diagnostics();
395 assert!(output.contains("Diagnostic Report"));
396 assert!(output.contains("Version:"));
397 }
398
399 #[test]
400 fn test_collect_shell_info() {
401 let mut entries = Vec::new();
402 collect_shell_info(&mut entries);
403 assert!(!entries.is_empty() || entries.is_empty()); }
406
407 #[test]
408 fn test_collect_tool_versions() {
409 let mut entries = Vec::new();
410 collect_tool_versions(&mut entries);
411 assert!(!entries.is_empty());
413 }
414
415 #[test]
416 fn test_diagnostic_report_timestamp() {
417 let report = generate_diagnostic_report();
418 let now = chrono::Utc::now();
420 let diff = now.signed_duration_since(report.timestamp);
421 assert!(diff.num_seconds() < 60);
422 }
423}