vtcode 0.99.1

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use anyhow::{Context, Result};
use semver::Version;
use std::time::Duration;

use super::Updater;
use super::types::{UpdateInfo, VersionInfo};

pub(super) const REPO_OWNER: &str = "vinhnx";
pub(super) const REPO_NAME: &str = "vtcode";
const REPO_SLUG: &str = "vinhnx/vtcode";

pub(super) fn release_url(version: &Version) -> String {
    format!("https://github.com/{REPO_SLUG}/releases/tag/v{version}")
}

pub(super) async fn fetch_latest_release(updater: &Updater) -> Result<Option<UpdateInfo>> {
    let latest = fetch_latest_release_info().await?;
    if latest.version > updater.current_version {
        Ok(Some(latest))
    } else {
        Ok(None)
    }
}

pub(super) async fn fetch_latest_release_info() -> Result<UpdateInfo> {
    let url = format!("https://api.github.com/repos/{REPO_SLUG}/releases/latest");

    let client = reqwest::Client::builder()
        .user_agent("vtcode-updater")
        .build()
        .context("Failed to create HTTP client")?;

    let response = client
        .get(&url)
        .timeout(Duration::from_secs(8))
        .send()
        .await
        .context("Failed to fetch latest release from GitHub")?
        .error_for_status()
        .context("GitHub API returned non-success status")?;

    let json = response
        .json::<serde_json::Value>()
        .await
        .context("Failed to parse GitHub API response")?;

    let tag_name = json
        .get("tag_name")
        .and_then(|v| v.as_str())
        .context("Missing tag_name in GitHub response")?;

    let version_str = tag_name.trim_start_matches('v');
    let version = Version::parse(version_str)
        .with_context(|| format!("Invalid version in GitHub release: {}", tag_name))?;

    Ok(UpdateInfo {
        version,
        release_notes: json
            .get("body")
            .and_then(|v| v.as_str())
            .unwrap_or("See release notes on GitHub")
            .to_string(),
    })
}

pub(super) async fn list_versions(limit: usize) -> Result<Vec<VersionInfo>> {
    let url = format!(
        "https://api.github.com/repos/{REPO_SLUG}/releases?per_page={}",
        limit
    );

    let client = reqwest::Client::builder()
        .user_agent("vtcode-updater")
        .build()
        .context("Failed to create HTTP client")?;

    let response = client
        .get(&url)
        .timeout(Duration::from_secs(8))
        .send()
        .await
        .context("Failed to fetch releases from GitHub")?
        .error_for_status()
        .context("GitHub API returned non-success status")?;

    let json = response
        .json::<serde_json::Value>()
        .await
        .context("Failed to parse GitHub API response")?;

    let versions = json
        .as_array()
        .context("Expected array of releases")?
        .iter()
        .filter_map(|release| {
            let tag_name = release.get("tag_name")?.as_str()?;
            let version_str = tag_name.trim_start_matches('v');
            let version = Version::parse(version_str).ok()?;
            let is_prerelease = release
                .get("prerelease")
                .and_then(|v| v.as_bool())
                .unwrap_or(false);
            let published_at = release
                .get("published_at")
                .and_then(|v| v.as_str())
                .map(|s| s.to_string());

            Some(VersionInfo {
                version,
                tag: tag_name.to_string(),
                is_prerelease,
                published_at,
            })
        })
        .collect();

    Ok(versions)
}