Skip to main content

vtcode_core/tools/ast_grep_installer/
mod.rs

1mod 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}