ai-agent 0.13.4

Idiomatic agent sdk inspired by the claude code source leak
Documentation
//! Plugin loader - cache-only loading functions
//!
//! Ported from ~/claudecode/openclaudecode/src/utils/plugins/marketplaceManager.ts
//!
//! This module provides cache-only loading functions for marketplaces and plugins.
//! These functions are used for startup paths that should never block on network.

use std::fs;
use std::path::PathBuf;

use super::types::{KnownMarketplacesFile, PluginMarketplace, PluginMarketplaceEntry};
use crate::utils::config::get_global_config_path;

/// Get the path to the known marketplaces config file
fn get_known_marketplaces_file() -> PathBuf {
    get_global_config_path().join("known_marketplaces.json")
}

/// Read a cached marketplace from disk
async fn read_cached_marketplace(install_location: &str) -> Option<PluginMarketplace> {
    let marketplace_path = PathBuf::from(install_location)
        .join(".ai-plugin")
        .join("marketplace.json");

    if !marketplace_path.exists() {
        return None;
    }

    match fs::read_to_string(&marketplace_path) {
        Ok(content) => match serde_json::from_str::<PluginMarketplace>(&content) {
            Ok(marketplace) => Some(marketplace),
            Err(e) => {
                eprintln!(
                    "Failed to parse marketplace at {}: {}",
                    marketplace_path.display(),
                    e
                );
                None
            }
        },
        Err(e) => {
            eprintln!(
                "Failed to read marketplace at {}: {}",
                marketplace_path.display(),
                e
            );
            None
        }
    }
}

/// Load known marketplaces config from cache
async fn load_known_marketplaces_config() -> Option<KnownMarketplacesFile> {
    let config_file = get_known_marketplaces_file();

    if !config_file.exists() {
        return None;
    }

    match fs::read_to_string(&config_file) {
        Ok(content) => match serde_json::from_str::<KnownMarketplacesFile>(&content) {
            Ok(config) => Some(config),
            Err(e) => {
                eprintln!("Failed to parse known marketplaces: {}", e);
                None
            }
        },
        Err(e) => {
            eprintln!("Failed to read known marketplaces file: {}", e);
            None
        }
    }
}

/// Parse plugin identifier into name and marketplace
///
/// # Arguments
/// * `plugin_id` - Plugin ID in format "name@marketplace"
///
/// # Returns
/// Tuple of (name, marketplace) or (None, None) if invalid
pub fn parse_plugin_identifier(plugin_id: &str) -> (Option<String>, Option<String>) {
    if let Some(at_pos) = plugin_id.rfind('@') {
        let name = plugin_id[..at_pos].to_string();
        let marketplace = plugin_id[at_pos + 1..].to_string();
        if !name.is_empty() && !marketplace.is_empty() {
            return (Some(name), Some(marketplace));
        }
    }
    (None, None)
}

/// Get a marketplace by name from cache only (no network)
///
/// Use this for startup paths that should never block on network.
///
/// # Arguments
/// * `name` - Marketplace name
///
/// # Returns
/// The marketplace or null if not found/cache missing
pub async fn get_marketplace_cache_only(name: &str) -> Option<PluginMarketplace> {
    let config_file = get_known_marketplaces_file();

    if !config_file.exists() {
        return None;
    }

    match fs::read_to_string(&config_file) {
        Ok(content) => {
            match serde_json::from_str::<KnownMarketplacesFile>(&content) {
                Ok(config) => {
                    if let Some(entry) = config.get(name) {
                        // Try to read the marketplace from the install location
                        if let Some(marketplace) =
                            read_cached_marketplace(&entry.install_location).await
                        {
                            return Some(marketplace);
                        }
                    }
                    None
                }
                Err(e) => {
                    eprintln!("Failed to parse known marketplaces config: {}", e);
                    None
                }
            }
        }
        Err(_) => None,
    }
}

/// Get a plugin by ID from cache only (no network)
///
/// # Arguments
/// * `plugin_id` - Plugin ID in format "name@marketplace"
///
/// # Returns
/// The plugin entry and marketplace install location, or null if not found/cache missing
pub async fn get_plugin_by_id_cache_only(
    plugin_id: &str,
) -> Option<(PluginMarketplaceEntry, String)> {
    let (plugin_name, marketplace_name) = parse_plugin_identifier(plugin_id);
    let plugin_name = plugin_name?;
    let marketplace_name = marketplace_name?;

    let config_file = get_known_marketplaces_file();

    if !config_file.exists() {
        return None;
    }

    match fs::read_to_string(&config_file) {
        Ok(content) => {
            match serde_json::from_str::<KnownMarketplacesFile>(&content) {
                Ok(config) => {
                    // Get marketplace config
                    let marketplace_config = config.get(&marketplace_name)?;

                    // Get the marketplace itself
                    let marketplace = get_marketplace_cache_only(&marketplace_name).await?;

                    // Find the plugin in the marketplace
                    marketplace
                        .plugins
                        .into_iter()
                        .find(|p| p.name == plugin_name)
                        .map(|entry| (entry, marketplace_config.install_location.clone()))
                }
                Err(_) => None,
            }
        }
        Err(_) => None,
    }
}

/// Get all known marketplace names
pub async fn get_known_marketplace_names() -> Vec<String> {
    match load_known_marketplaces_config().await {
        Some(config) => config.keys().cloned().collect(),
        None => vec![],
    }
}

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

    #[test]
    fn test_parse_plugin_identifier_basic() {
        let (name, marketplace) = parse_plugin_identifier("my-plugin@my-marketplace");
        assert_eq!(name, Some("my-plugin".to_string()));
        assert_eq!(marketplace, Some("my-marketplace".to_string()));
    }

    #[test]
    fn test_parse_plugin_identifier_invalid() {
        let (name, marketplace) = parse_plugin_identifier("invalid");
        assert_eq!(name, None);
        assert_eq!(marketplace, None);
    }

    #[test]
    fn test_parse_plugin_identifier_empty_marketplace() {
        let (name, marketplace) = parse_plugin_identifier("my-plugin@");
        assert_eq!(name, None);
        assert_eq!(marketplace, None);
    }
}