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 managed_ast_grep_bin_dir, resolve_ast_grep_binary_from_env_and_fs,
9};
10use anyhow::{Context, Result, bail};
11
12use self::archive::{ast_grep_version, install_archive};
13use self::release::{
14 current_platform_asset_spec, download_release_asset, fetch_latest_release,
15 select_release_asset, verify_checksum_if_available,
16};
17use self::state::{InstallLockGuard, InstallPaths, InstallationCache};
18
19#[derive(Debug, Clone, PartialEq)]
20pub enum AstGrepStatus {
21 Available {
22 version: String,
23 binary: PathBuf,
24 managed: bool,
25 },
26 NotFound,
27 Error {
28 reason: String,
29 },
30}
31
32#[derive(Debug, Clone)]
33pub struct AstGrepInstallOutcome {
34 pub version: String,
35 pub binary_path: PathBuf,
36 pub alias_path: Option<PathBuf>,
37 pub managed_bin_dir: PathBuf,
38 pub warning: Option<String>,
39}
40
41impl AstGrepStatus {
42 pub fn check() -> Self {
43 let Some(binary) = resolve_ast_grep_binary_from_env_and_fs() else {
44 return Self::NotFound;
45 };
46
47 match ast_grep_version(&binary) {
48 Ok(version) => Self::Available {
49 version,
50 managed: binary.starts_with(managed_ast_grep_bin_dir()),
51 binary,
52 },
53 Err(err) => Self::Error {
54 reason: err.to_string(),
55 },
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
67 .failure_reason
68 .as_deref()
69 .is_some_and(should_retry_without_cooldown)
70 {
71 let reason = cache.failure_reason.as_deref().unwrap_or("unknown reason");
72 bail!(
73 "Previous ast-grep installation attempt failed ({}). Not retrying for 24 hours.",
74 reason
75 );
76 }
77
78 let install_result: Result<AstGrepInstallOutcome> = async {
79 let client = reqwest::Client::builder()
80 .user_agent("vtcode-ast-grep-installer")
81 .build()
82 .context("Failed to create HTTP client")?;
83
84 let release = fetch_latest_release(&client).await?;
85 let platform = current_platform_asset_spec()?;
86 let selected_asset = select_release_asset(&release, &platform)?;
87 let archive_bytes = download_release_asset(&client, &selected_asset.asset).await?;
88 let warning =
89 verify_checksum_if_available(&client, &release, &selected_asset, &archive_bytes)
90 .await?;
91
92 install_archive(&paths, &selected_asset.asset.name, &archive_bytes)?;
93 let version = ast_grep_version(&paths.binary_path)
94 .context("Installed ast-grep failed version check")?;
95 InstallationCache::mark_success(&paths, &selected_asset.tag_name);
96
97 Ok(AstGrepInstallOutcome {
98 version,
99 binary_path: paths.binary_path.clone(),
100 alias_path: paths.alias_path.clone(),
101 managed_bin_dir: paths.bin_dir.clone(),
102 warning,
103 })
104 }
105 .await;
106
107 match install_result {
108 Ok(outcome) => Ok(outcome),
109 Err(err) => {
110 InstallationCache::mark_failure(&paths, &err.to_string());
111 Err(err)
112 }
113 }
114 }
115}
116
117fn should_retry_without_cooldown(failure_reason: &str) -> bool {
118 failure_reason.contains("No ast-grep release asset matched the current platform")
119 || failure_reason.contains("Unsupported platform for VT Code-managed ast-grep install")
120}
121
122#[cfg(test)]
123mod tests {
124 use super::should_retry_without_cooldown;
125
126 #[test]
127 fn platform_mismatch_failures_do_not_enter_cooldown() {
128 assert!(should_retry_without_cooldown(
129 "No ast-grep release asset matched the current platform (aarch64-apple-darwin)"
130 ));
131 assert!(should_retry_without_cooldown(
132 "Unsupported platform for VT Code-managed ast-grep install"
133 ));
134 }
135
136 #[test]
137 fn unrelated_failures_still_use_cooldown() {
138 assert!(!should_retry_without_cooldown(
139 "Failed to fetch ast-grep release metadata"
140 ));
141 }
142}