vtcode_core/tools/ast_grep_installer/
mod.rs1mod archive;
2mod release;
3mod state;
4
5use std::path::PathBuf;
6
7use crate::tools::ast_grep_binary::{
8 AST_GREP_NO_INSTALL_ENV, managed_ast_grep_bin_dir, missing_ast_grep_message,
9 resolve_ast_grep_binary_from_env_and_fs,
10};
11use crate::tools::editing::patch::resolve_ast_grep_binary_path;
12use anyhow::{Context, Result, bail};
13
14use self::archive::{ast_grep_version, install_archive};
15use self::release::{
16 current_platform_asset_spec, download_release_asset, fetch_latest_release, select_release_asset,
17 verify_checksum_if_available,
18};
19use self::state::{InstallLockGuard, InstallPaths, InstallationCache};
20
21#[derive(Debug, Clone, PartialEq)]
22pub enum AstGrepStatus {
23 Available {
24 version: String,
25 binary: PathBuf,
26 managed: bool,
27 },
28 NotFound,
29 Error {
30 reason: String,
31 },
32}
33
34#[derive(Debug, Clone)]
35pub struct AstGrepInstallOutcome {
36 pub version: String,
37 pub binary_path: PathBuf,
38 pub alias_path: Option<PathBuf>,
39 pub managed_bin_dir: PathBuf,
40 pub warning: Option<String>,
41}
42
43impl AstGrepStatus {
44 pub fn check() -> Self {
45 let Some(binary) = resolve_ast_grep_binary_from_env_and_fs() else {
46 return Self::NotFound;
47 };
48
49 match ast_grep_version(&binary) {
50 Ok(version) => Self::Available {
51 version,
52 managed: managed_ast_grep_bin_dir().is_ok_and(|directory| binary.starts_with(directory)),
53 binary,
54 },
55 Err(err) => Self::Error { reason: err.to_string() },
56 }
57 }
58
59 pub async fn install() -> Result<AstGrepInstallOutcome> {
60 let paths = InstallPaths::discover()?;
61 let _lock = InstallLockGuard::acquire(&paths)?;
62
63 if !InstallationCache::is_stale(&paths)
64 && let Ok(cache) = InstallationCache::load(&paths)
65 && cache.status == "failed"
66 && !cache.failure_reason.as_deref().is_some_and(should_retry_without_cooldown)
67 {
68 let reason = cache.failure_reason.as_deref().unwrap_or("unknown reason");
69 bail!("Previous ast-grep installation attempt failed ({reason}). Not retrying for 24 hours.");
70 }
71
72 let install_result: Result<AstGrepInstallOutcome> = async {
73 let client = reqwest::Client::builder()
74 .user_agent("vtcode-ast-grep-installer")
75 .build()
76 .context("Failed to create HTTP client")?;
77
78 let release = fetch_latest_release(&client).await?;
79 let platform = current_platform_asset_spec()?;
80 let selected_asset = select_release_asset(&release, &platform)?;
81 let archive_bytes = download_release_asset(&client, &selected_asset.asset).await?;
82 let warning = verify_checksum_if_available(&client, &release, &selected_asset, &archive_bytes).await?;
83
84 install_archive(&paths, &selected_asset.asset.name, &archive_bytes)?;
85 let version = ast_grep_version(&paths.binary_path).context("Installed ast-grep failed version check")?;
86 InstallationCache::mark_success(&paths, &selected_asset.tag_name);
87
88 Ok(AstGrepInstallOutcome {
89 version,
90 binary_path: paths.binary_path.clone(),
91 alias_path: paths.alias_path.clone(),
92 managed_bin_dir: paths.bin_dir.clone(),
93 warning,
94 })
95 }
96 .await;
97
98 match install_result {
99 Ok(outcome) => Ok(outcome),
100 Err(err) => {
101 InstallationCache::mark_failure(&paths, &err.to_string());
102 Err(err)
103 }
104 }
105 }
106
107 pub async fn resolve_or_install() -> std::result::Result<PathBuf, String> {
121 if let Ok(path) = resolve_ast_grep_binary_path() {
125 return Ok(path);
126 }
127
128 if std::env::var_os(AST_GREP_NO_INSTALL_ENV).is_some() {
131 return Err(missing_ast_grep_message("Auto-install disabled via VTCODE_AST_GREP_NO_INSTALL."));
132 }
133
134 if crate::tools::ast_grep_binary::is_binary_override_missing()
139 || crate::tools::editing::patch::is_binary_override_missing()
140 {
141 return Err(missing_ast_grep_message("Auto-install skipped: test override forces missing binary."));
142 }
143
144 match Self::install().await {
145 Ok(_) => match resolve_ast_grep_binary_from_env_and_fs() {
146 Some(path) => Ok(path),
147 None => {
148 Err(missing_ast_grep_message("Auto-install reported success but the binary could not be resolved."))
149 }
150 },
151 Err(err) => Err(format!(
152 "ast-grep is not available; auto-install failed: {err}. Run `{}` manually.",
153 crate::tools::ast_grep_binary::AST_GREP_INSTALL_COMMAND
154 )),
155 }
156 }
157}
158
159fn should_retry_without_cooldown(failure_reason: &str) -> bool {
160 failure_reason.contains("No ast-grep release asset matched the current platform")
161 || failure_reason.contains("Unsupported platform for VT Code-managed ast-grep install")
162}
163
164#[cfg(test)]
165mod tests {
166 use super::{AstGrepStatus, should_retry_without_cooldown};
167 use crate::tools::ast_grep_binary::AST_GREP_INSTALL_COMMAND;
168 use crate::tools::ast_grep_binary::set_ast_grep_binary_override_for_tests;
169 use serial_test::serial;
170 use vtcode_commons::env_lock;
171
172 #[test]
173 fn platform_mismatch_failures_do_not_enter_cooldown() {
174 assert!(should_retry_without_cooldown(
175 "No ast-grep release asset matched the current platform (aarch64-apple-darwin)"
176 ));
177 assert!(should_retry_without_cooldown("Unsupported platform for VT Code-managed ast-grep install"));
178 }
179
180 #[test]
181 fn unrelated_failures_still_use_cooldown() {
182 assert!(!should_retry_without_cooldown("Failed to fetch ast-grep release metadata"));
183 }
184
185 #[tokio::test]
186 #[serial]
187 async fn resolve_or_install_skips_install_when_test_override_missing() {
188 let _guard = set_ast_grep_binary_override_for_tests(None);
189 let err = AstGrepStatus::resolve_or_install()
190 .await
191 .expect_err("should not install in test mode");
192 let text = err.to_string();
193 assert!(text.contains("ast-grep"), "{text}");
194 assert!(text.contains("not available"), "{text}");
195 assert!(text.contains(AST_GREP_INSTALL_COMMAND), "{text}");
196 }
197
198 #[tokio::test]
199 #[serial]
200 async fn resolve_or_install_mentions_env_optout_when_set() {
201 let _override = set_ast_grep_binary_override_for_tests(None);
206 let env = env_lock::lock();
207 env.set_var("VTCODE_AST_GREP_NO_INSTALL", "1");
208 let err = AstGrepStatus::resolve_or_install()
209 .await
210 .expect_err("should not install when env opt-out is set");
211 env.remove_var("VTCODE_AST_GREP_NO_INSTALL");
212 assert!(err.contains("VTCODE_AST_GREP_NO_INSTALL"), "error should mention the env var: {err}");
213 }
214}