vsix 1.0.2

A command-line utility that downloads and installs .vsix extensions into Visual Studio Code and Cursor
Documentation
use crate::domain::{DomainError, InstallationRepository};
use async_trait::async_trait;
use std::fs;
use std::path::{Path, PathBuf};
use zip::ZipArchive;

pub struct FileSystemRepository;

impl Default for FileSystemRepository {
    fn default() -> Self {
        Self::new()
    }
}

impl FileSystemRepository {
    #[must_use]
    pub fn new() -> Self {
        Self
    }

    fn extract_vsix(vsix_data: &[u8], target_dir: &Path) -> Result<(), DomainError> {
        let cursor = std::io::Cursor::new(vsix_data);
        let mut archive = ZipArchive::new(cursor)
            .map_err(|e| DomainError::InvalidExtensionFormat(e.to_string()))?;

        for i in 0..archive.len() {
            let mut file = archive
                .by_index(i)
                .map_err(|e| DomainError::IoError(std::io::Error::other(e)))?;

            let outpath = target_dir.join(file.name());

            if file.name().ends_with('/') {
                fs::create_dir_all(&outpath)?;
            } else {
                if let Some(parent) = outpath.parent() {
                    fs::create_dir_all(parent)?;
                }
                let mut outfile = fs::File::create(&outpath)?;
                std::io::copy(&mut file, &mut outfile)?;
            }
        }

        Ok(())
    }
}

#[async_trait]
impl InstallationRepository for FileSystemRepository {
    async fn install_vscode(
        &self,
        vsix_data: &[u8],
        extension_id: &str,
    ) -> Result<(), DomainError> {
        let extensions_dir = self.get_vscode_extensions_dir()?;
        let target_dir = extensions_dir.join(extension_id);

        if target_dir.exists() {
            fs::remove_dir_all(&target_dir)?;
        }

        fs::create_dir_all(&target_dir)?;
        Self::extract_vsix(vsix_data, &target_dir)?;

        Ok(())
    }

    async fn install_cursor(
        &self,
        vsix_data: &[u8],
        extension_id: &str,
    ) -> Result<(), DomainError> {
        let extensions_dir = self.get_cursor_extensions_dir()?;
        let target_dir = extensions_dir.join(extension_id);

        if target_dir.exists() {
            fs::remove_dir_all(&target_dir)?;
        }

        fs::create_dir_all(&target_dir)?;
        Self::extract_vsix(vsix_data, &target_dir)?;

        Ok(())
    }

    fn get_vscode_extensions_dir(&self) -> Result<PathBuf, DomainError> {
        let home = dirs::home_dir().ok_or_else(|| {
            DomainError::DirectoryNotFound("Home directory not found".to_string())
        })?;

        let extensions_dir = home.join(".vscode").join("extensions");

        if !extensions_dir.exists() {
            fs::create_dir_all(&extensions_dir)?;
        }

        Ok(extensions_dir)
    }

    fn get_cursor_extensions_dir(&self) -> Result<PathBuf, DomainError> {
        let home = dirs::home_dir().ok_or_else(|| {
            DomainError::DirectoryNotFound("Home directory not found".to_string())
        })?;

        let extensions_dir = if cfg!(target_os = "windows") {
            home.join("AppData")
                .join("Roaming")
                .join("Cursor")
                .join("User")
                .join("extensions")
        } else if cfg!(target_os = "macos") {
            home.join("Library")
                .join("Application Support")
                .join("Cursor")
                .join("User")
                .join("extensions")
        } else {
            home.join(".config")
                .join("Cursor")
                .join("User")
                .join("extensions")
        };

        if !extensions_dir.exists() {
            fs::create_dir_all(&extensions_dir)?;
        }

        Ok(extensions_dir)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_extensions_directory_paths() {
        let repo = FileSystemRepository::new();

        let vscode_dir = repo.get_vscode_extensions_dir();
        assert!(vscode_dir.is_ok());

        let cursor_dir = repo.get_cursor_extensions_dir();
        assert!(cursor_dir.is_ok());
    }
}