vs-core 0.0.2

Core orchestration services for the vs version manager.
Documentation
//! Application orchestration for the `vs` CLI.

mod app;
mod error;
mod models;
mod plugin_source;
mod registry_source;
mod service;

pub use app::App;
pub use error::CoreError;
pub use models::{
    CurrentTool, InstalledVersion, MigrateSummary, PluginInfo, SelfUpgradeSummary, UninstallResult,
    UseScope, VersionInfo,
};

#[cfg(test)]
mod tests {
    #[cfg(feature = "lua")]
    use std::error::Error;
    #[cfg(feature = "lua")]
    use std::fs;

    #[cfg(feature = "lua")]
    use tempfile::TempDir;
    #[cfg(feature = "lua")]
    use vs_config::{AppConfig, HomeLayout, RegistryConfig, write_app_config};
    #[cfg(feature = "lua")]
    use vs_plugin_api::PluginBackendKind;

    #[cfg(feature = "lua")]
    use crate::{App, UseScope};

    #[cfg(feature = "lua")]
    #[test]
    fn use_tool_should_write_project_config() -> Result<(), Box<dyn Error>> {
        let temp_dir = TempDir::new()?;
        let home = temp_dir.path().join("home");
        let cwd = temp_dir.path().join("project");
        fs::create_dir_all(&cwd)?;
        let app = App::new(
            HomeLayout {
                active_home: home,
                migration_candidates: Vec::new(),
            },
            cwd.clone(),
            Some(String::from("session")),
        )?;

        let source = temp_dir.path().join("nodejs-lua");
        write_lua_fixture(&source);
        app.add_plugin(
            Some("nodejs"),
            Some(source.display().to_string()),
            Some(PluginBackendKind::Lua),
            None,
        )?;
        app.install_plugin_version("nodejs", Some("20.11.1"))?;

        app.use_tool("nodejs", "20.11.1", UseScope::Project, false)?;

        let config = fs::read_to_string(cwd.join(".vs.toml"))?;
        assert!(config.contains("nodejs = \"20.11.1\""));
        Ok(())
    }

    #[cfg(feature = "lua")]
    #[test]
    fn available_plugins_should_bootstrap_registry_when_source_is_configured()
    -> Result<(), Box<dyn Error>> {
        let temp_dir = TempDir::new()?;
        let home = temp_dir.path().join("home");
        let cwd = temp_dir.path().join("project");
        fs::create_dir_all(&cwd)?;
        let registry_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("../../fixtures/registry/index.json");

        write_app_config(
            &home,
            &AppConfig {
                proxy: Default::default(),
                storage: Default::default(),
                registry: RegistryConfig {
                    address: registry_path.display().to_string(),
                },
                legacy_version_file: Default::default(),
                cache: Default::default(),
            },
        )?;

        let app = App::new(
            HomeLayout {
                active_home: home,
                migration_candidates: Vec::new(),
            },
            cwd,
            Some(String::from("session")),
        )?;

        let entries = app.available_plugins()?;
        assert!(!entries.is_empty());
        assert!(entries.iter().any(|entry| entry.name == "nodejs"));
        Ok(())
    }

    #[cfg(feature = "lua")]
    #[test]
    fn available_plugins_should_fallback_to_cached_registry_when_refresh_fails()
    -> Result<(), Box<dyn Error>> {
        let temp_dir = TempDir::new()?;
        let home = temp_dir.path().join("home");
        let cwd = temp_dir.path().join("project");
        fs::create_dir_all(&cwd)?;
        let registry_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("../../fixtures/registry/index.json");

        write_app_config(
            &home,
            &AppConfig {
                proxy: Default::default(),
                storage: Default::default(),
                registry: RegistryConfig {
                    address: registry_path.display().to_string(),
                },
                legacy_version_file: Default::default(),
                cache: Default::default(),
            },
        )?;

        let app = App::new(
            HomeLayout {
                active_home: home.clone(),
                migration_candidates: Vec::new(),
            },
            cwd.clone(),
            Some(String::from("session")),
        )?;
        assert!(!app.available_plugins()?.is_empty());

        write_app_config(
            &home,
            &AppConfig {
                proxy: Default::default(),
                storage: Default::default(),
                registry: RegistryConfig {
                    address: temp_dir
                        .path()
                        .join("missing/index.json")
                        .display()
                        .to_string(),
                },
                legacy_version_file: Default::default(),
                cache: Default::default(),
            },
        )?;

        let fallback = App::new(
            HomeLayout {
                active_home: home,
                migration_candidates: Vec::new(),
            },
            cwd,
            Some(String::from("session")),
        )?;
        let entries = fallback.available_plugins()?;
        assert!(entries.iter().any(|entry| entry.name == "nodejs"));
        Ok(())
    }

    #[cfg(feature = "lua")]
    #[test]
    fn add_plugin_should_fallback_to_cached_registry_when_refresh_fails()
    -> Result<(), Box<dyn Error>> {
        let temp_dir = TempDir::new()?;
        let home = temp_dir.path().join("home");
        let cwd = temp_dir.path().join("project");
        fs::create_dir_all(&cwd)?;
        let registry_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("../../fixtures/registry/index.json");

        write_app_config(
            &home,
            &AppConfig {
                proxy: Default::default(),
                storage: Default::default(),
                registry: RegistryConfig {
                    address: registry_path.display().to_string(),
                },
                legacy_version_file: Default::default(),
                cache: Default::default(),
            },
        )?;

        let app = App::new(
            HomeLayout {
                active_home: home.clone(),
                migration_candidates: Vec::new(),
            },
            cwd.clone(),
            Some(String::from("session")),
        )?;
        assert!(!app.available_plugins()?.is_empty());

        write_app_config(
            &home,
            &AppConfig {
                proxy: Default::default(),
                storage: Default::default(),
                registry: RegistryConfig {
                    address: temp_dir
                        .path()
                        .join("missing/index.json")
                        .display()
                        .to_string(),
                },
                legacy_version_file: Default::default(),
                cache: Default::default(),
            },
        )?;

        let fallback = App::new(
            HomeLayout {
                active_home: home,
                migration_candidates: Vec::new(),
            },
            cwd,
            Some(String::from("session")),
        )?;
        let entry = fallback.add_plugin(Some("nodejs"), None, None, None)?;
        assert_eq!(entry.name, "nodejs");
        Ok(())
    }

    #[cfg(feature = "lua")]
    fn write_lua_fixture(root: &std::path::Path) {
        if let Err(error) = fs::create_dir_all(root.join("hooks")) {
            panic!("failed to create hooks directory: {error}");
        }
        if let Err(error) = fs::create_dir_all(root.join("packages/20.11.1/bin")) {
            panic!("failed to create package directory: {error}");
        }
        fs::write(
            root.join("metadata.lua"),
            "PLUGIN = {}\nPLUGIN.name = 'nodejs'\nPLUGIN.version = '0.1.0'\nPLUGIN.legacyFilenames = { '.nvmrc' }\n",
        )
        .unwrap_or_else(|error| panic!("failed to write metadata fixture: {error}"));
        fs::write(
            root.join("hooks/available.lua"),
            "function PLUGIN:Available(ctx)\n  return { { version = '20.11.1' } }\nend\n",
        )
        .unwrap_or_else(|error| panic!("failed to write available fixture: {error}"));
        fs::write(
            root.join("hooks/pre_install.lua"),
            "function PLUGIN:PreInstall(ctx)\n  return { version = '20.11.1', url = 'packages/20.11.1' }\nend\n",
        )
        .unwrap_or_else(|error| panic!("failed to write pre_install fixture: {error}"));
        fs::write(
            root.join("hooks/env_keys.lua"),
            "function PLUGIN:EnvKeys(ctx)\n  return { { key = 'NODEJS_HOME', value = ctx.path }, { key = 'PATH', value = ctx.path .. '/bin' } }\nend\n",
        )
        .unwrap_or_else(|error| panic!("failed to write env_keys fixture: {error}"));
    }
}