Skip to main content

git_cli/
doctor.rs

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}