1use colored::Colorize;
2use std::process::Command;
3
4pub struct Check {
5 pub name: &'static str,
6 pub ok: bool,
7 pub detail: String,
8 pub hint: Option<&'static str>,
9}
10
11pub fn gh_on_path() -> bool {
12 Command::new("gh")
13 .arg("--version")
14 .output()
15 .map(|o| o.status.success())
16 .unwrap_or(false)
17}
18
19pub fn check_git() -> Check {
20 match Command::new("git").arg("--version").output() {
21 Ok(o) if o.status.success() => {
22 let version = String::from_utf8_lossy(&o.stdout).trim().to_string();
23 Check {
24 name: "git",
25 ok: true,
26 detail: version,
27 hint: None,
28 }
29 }
30 Ok(o) => Check {
31 name: "git",
32 ok: false,
33 detail: String::from_utf8_lossy(&o.stderr).trim().to_string(),
34 hint: Some("Install git: https://git-scm.com/downloads"),
35 },
36 Err(e) => Check {
37 name: "git",
38 ok: false,
39 detail: e.to_string(),
40 hint: Some("Install git: https://git-scm.com/downloads"),
41 },
42 }
43}
44
45pub fn check_gh() -> Check {
46 match Command::new("gh").arg("--version").output() {
47 Ok(o) if o.status.success() => {
48 let version = String::from_utf8_lossy(&o.stdout)
49 .lines()
50 .next()
51 .unwrap_or("gh")
52 .to_string();
53 Check {
54 name: "gh",
55 ok: true,
56 detail: version,
57 hint: None,
58 }
59 }
60 Ok(o) => Check {
61 name: "gh",
62 ok: false,
63 detail: String::from_utf8_lossy(&o.stderr).trim().to_string(),
64 hint: Some("Install GitHub CLI: https://cli.github.com — then run `gh auth login`"),
65 },
66 Err(_) => Check {
67 name: "gh",
68 ok: false,
69 detail: "not found on PATH".to_string(),
70 hint: Some("Install GitHub CLI: brew install gh — then run `gh auth login`"),
71 },
72 }
73}
74
75pub async fn check_ollama(endpoint: &str) -> Check {
76 let url = format!("{endpoint}/api/tags");
77 let client = match reqwest::Client::builder()
78 .timeout(std::time::Duration::from_secs(3))
79 .build()
80 {
81 Ok(c) => c,
82 Err(e) => {
83 return Check {
84 name: "ollama",
85 ok: false,
86 detail: e.to_string(),
87 hint: Some("Start Ollama: ollama serve"),
88 };
89 }
90 };
91
92 match client.get(&url).send().await {
93 Ok(resp) if resp.status().is_success() => Check {
94 name: "ollama",
95 ok: true,
96 detail: format!("reachable at {endpoint}"),
97 hint: None,
98 },
99 Ok(resp) => Check {
100 name: "ollama",
101 ok: false,
102 detail: format!("returned HTTP {}", resp.status()),
103 hint: Some("Start Ollama: ollama serve"),
104 },
105 Err(e) => Check {
106 name: "ollama",
107 ok: false,
108 detail: format!("not reachable at {endpoint}: {e}"),
109 hint: Some("Start Ollama: ollama serve"),
110 },
111 }
112}
113
114pub fn gh_pr_list_error() -> Option<String> {
115 if !gh_on_path() {
116 return Some(
117 "GitHub CLI (gh) not found — PR context unavailable. Install: https://cli.github.com"
118 .to_string(),
119 );
120 }
121
122 let output = Command::new("gh")
123 .args([
124 "pr",
125 "list",
126 "--state",
127 "open",
128 "--limit",
129 "1",
130 "--json",
131 "number",
132 ])
133 .output()
134 .ok()?;
135
136 if output.status.success() {
137 return None;
138 }
139
140 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
141 if stderr.contains("auth") || stderr.contains("login") {
142 Some(format!(
143 "gh not authenticated — PR context unavailable. Run: gh auth login ({stderr})"
144 ))
145 } else if stderr.contains("not a git repository") {
146 None
147 } else {
148 Some(format!("gh pr list failed — PR context unavailable: {stderr}"))
149 }
150}
151
152pub async fn run(endpoint: &str) -> bool {
153 println!("{}", "git-cli doctor".bold().underline());
154 println!();
155
156 let checks = [
157 check_git(),
158 check_gh(),
159 check_ollama(endpoint).await,
160 ];
161
162 let mut all_ok = true;
163 for check in &checks {
164 let icon = if check.ok { "✓".green() } else { "✗".red() };
165 println!(" {} {} — {}", icon, check.name.bold(), check.detail);
166 if let Some(hint) = check.hint {
167 println!(" {} {}", "hint:".dimmed(), hint.dimmed());
168 }
169 if !check.ok {
170 all_ok = false;
171 }
172 }
173
174 println!();
175 if all_ok {
176 println!("{}", "All checks passed.".green().bold());
177 } else {
178 println!("{}", "Some checks failed.".red().bold());
179 }
180
181 all_ok
182}