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).into_owned();
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 { reason: err.to_string() }
63                }
64            }
65        }
66    }
67
68    /// Attempt to install ripgrep for the current platform.
69    /// Uses smart installer detection to try available tools first.
70    pub fn install() -> Result<()> {
71        debug_log("Installation attempt started");
72
73        if matches!(Self::check(), RipgrepStatus::Available { .. }) {
74            debug_log("ripgrep already available; skipping installation");
75            return Ok(());
76        }
77
78        if std::env::var("VTCODE_RIPGREP_NO_INSTALL").is_ok() {
79            debug_log("Auto-install disabled via VTCODE_RIPGREP_NO_INSTALL");
80            return Err(anyhow!("Ripgrep auto-installation disabled via VTCODE_RIPGREP_NO_INSTALL"));
81        }
82
83        if InstallLockGuard::is_install_in_progress() {
84            debug_log("Another installation is already in progress, skipping");
85            return Err(anyhow!("Ripgrep installation already in progress"));
86        }
87
88        let _lock = InstallLockGuard::acquire()?;
89        debug_log("Installation lock acquired");
90
91        if !InstallationCache::is_stale()
92            && let Ok(cache) = InstallationCache::load()
93            && cache.status == "failed"
94        {
95            let reason = cache.failure_reason.as_deref().unwrap_or("unknown reason");
96            debug_log(&format!("Cache shows previous failure: {reason}"));
97            return Err(anyhow!("Previous installation attempt failed ({reason}). Not retrying for 24 hours."));
98        }
99
100        let result = install_with_smart_detection();
101
102        match result {
103            Ok(()) => match Self::check() {
104                RipgrepStatus::Available { .. } => {
105                    debug_log("Installation verified successfully");
106                    InstallationCache::mark_success("auto-detected");
107                    Ok(())
108                }
109                status => {
110                    let msg = format!("Installation verification failed: {status:?}");
111                    debug_log(&msg);
112                    InstallationCache::mark_failed("auto-detected", &msg);
113                    Err(anyhow!(msg))
114                }
115            },
116            Err(err) => {
117                let msg = err.to_string();
118                debug_log(&format!("Installation failed: {msg}"));
119                InstallationCache::mark_failed("all-methods", &msg);
120                Err(anyhow!(msg))
121            }
122        }
123    }
124
125    /// Check if ripgrep is available, installing it if the caller explicitly requests that flow.
126    pub fn ensure_available() -> Result<Self> {
127        match Self::check() {
128            status @ RipgrepStatus::Available { .. } => Ok(status),
129            RipgrepStatus::NotFound => {
130                tracing::warn!("ripgrep not found, run `{RIPGREP_INSTALL_COMMAND}` to install");
131                Self::install()?;
132                Ok(Self::check())
133            }
134            status => Ok(status),
135        }
136    }
137}
138
139pub(super) fn debug_log(message: &str) {
140    if std::env::var("VTCODE_DEBUG_RIPGREP").is_ok() {
141        tracing::debug!(message, "ripgrep");
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn test_ripgrep_status_check() {
151        let status = RipgrepStatus::check();
152        tracing::debug!(?status, "Ripgrep status");
153    }
154}