Skip to main content

vtcode_core/tools/ripgrep_installer/
mod.rs

1//! Ripgrep availability detection and explicit installation management.
2//!
3//! This module handles detecting if ripgrep is available and installing it on
4//! demand through `vtcode dependencies install ripgrep`.
5
6mod platform;
7mod state;
8
9use crate::tools::ripgrep_binary::RIPGREP_INSTALL_COMMAND;
10use anyhow::{Result, anyhow};
11use std::process::Command;
12
13use self::platform::install_with_smart_detection;
14use self::state::{InstallLockGuard, InstallationCache};
15
16/// Result of ripgrep availability check
17#[derive(Debug, Clone, PartialEq)]
18pub enum RipgrepStatus {
19    /// Ripgrep is available and working
20    Available { version: String },
21    /// Ripgrep is not installed
22    NotFound,
23    /// Ripgrep exists but returned an error
24    Error { reason: String },
25}
26
27impl RipgrepStatus {
28    /// Check if ripgrep is currently available
29    pub fn check() -> Self {
30        debug_log("Checking ripgrep availability...");
31        match Command::new("rg").arg("--version").output() {
32            Ok(output) => {
33                if output.status.success() {
34                    let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
35                    if version.is_empty() {
36                        debug_log("ripgrep found but returned empty version");
37                        RipgrepStatus::Error {
38                            reason: "rg --version returned empty output".to_string(),
39                        }
40                    } else {
41                        debug_log(&format!("ripgrep available: {}", version));
42                        RipgrepStatus::Available { version }
43                    }
44                } else {
45                    let stderr = String::from_utf8_lossy(&output.stderr).to_string();
46                    debug_log(&format!("ripgrep check failed: {}", stderr));
47                    RipgrepStatus::Error {
48                        reason: if stderr.is_empty() {
49                            "rg exited with error".to_string()
50                        } else {
51                            stderr
52                        },
53                    }
54                }
55            }
56            Err(err) => {
57                if err.kind() == std::io::ErrorKind::NotFound {
58                    debug_log("ripgrep not found in PATH");
59                    RipgrepStatus::NotFound
60                } else {
61                    debug_log(&format!("ripgrep check error: {}", err));
62                    RipgrepStatus::Error {
63                        reason: err.to_string(),
64                    }
65                }
66            }
67        }
68    }
69
70    /// Attempt to install ripgrep for the current platform.
71    /// Uses smart installer detection to try available tools first.
72    pub fn install() -> Result<()> {
73        debug_log("Installation attempt started");
74
75        if matches!(Self::check(), RipgrepStatus::Available { .. }) {
76            debug_log("ripgrep already available; skipping installation");
77            return Ok(());
78        }
79
80        if std::env::var("VTCODE_RIPGREP_NO_INSTALL").is_ok() {
81            debug_log("Auto-install disabled via VTCODE_RIPGREP_NO_INSTALL");
82            return Err(anyhow!(
83                "Ripgrep auto-installation disabled via VTCODE_RIPGREP_NO_INSTALL"
84            ));
85        }
86
87        if InstallLockGuard::is_install_in_progress() {
88            debug_log("Another installation is already in progress, skipping");
89            return Err(anyhow!("Ripgrep installation already in progress"));
90        }
91
92        let _lock = InstallLockGuard::acquire()?;
93        debug_log("Installation lock acquired");
94
95        if !InstallationCache::is_stale()
96            && let Ok(cache) = InstallationCache::load()
97            && cache.status == "failed"
98        {
99            let reason = cache.failure_reason.as_deref().unwrap_or("unknown reason");
100            debug_log(&format!("Cache shows previous failure: {}", reason));
101            return Err(anyhow!(
102                "Previous installation attempt failed ({}). Not retrying for 24 hours.",
103                reason
104            ));
105        }
106
107        let result = install_with_smart_detection();
108
109        match result {
110            Ok(()) => match Self::check() {
111                RipgrepStatus::Available { .. } => {
112                    debug_log("Installation verified successfully");
113                    InstallationCache::mark_success("auto-detected");
114                    Ok(())
115                }
116                status => {
117                    let msg = format!("Installation verification failed: {:?}", status);
118                    debug_log(&msg);
119                    InstallationCache::mark_failed("auto-detected", &msg);
120                    Err(anyhow!(msg))
121                }
122            },
123            Err(err) => {
124                let msg = err.to_string();
125                debug_log(&format!("Installation failed: {}", msg));
126                InstallationCache::mark_failed("all-methods", &msg);
127                Err(anyhow!(msg))
128            }
129        }
130    }
131
132    /// Check if ripgrep is available, installing it if the caller explicitly requests that flow.
133    pub fn ensure_available() -> Result<Self> {
134        match Self::check() {
135            status @ RipgrepStatus::Available { .. } => Ok(status),
136            RipgrepStatus::NotFound => {
137                tracing::warn!("ripgrep not found, run `{RIPGREP_INSTALL_COMMAND}` to install");
138                Self::install()?;
139                Ok(Self::check())
140            }
141            status => Ok(status),
142        }
143    }
144}
145
146pub(super) fn debug_log(message: &str) {
147    if std::env::var("VTCODE_DEBUG_RIPGREP").is_ok() {
148        tracing::debug!(message, "ripgrep");
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn test_ripgrep_status_check() {
158        let status = RipgrepStatus::check();
159        tracing::debug!(?status, "Ripgrep status");
160    }
161}