codewhale-tui 0.8.66

Terminal UI for open-source and open-weight coding models
use std::collections::HashMap;
use std::path::{Path, PathBuf};

use serde::Deserialize;

use crate::mcp::McpServerConfig;

#[derive(Debug, Clone, Deserialize)]
pub struct PluginManifest {
    pub plugin: PluginMeta,
    pub skills: Option<PluginSkills>,
    #[serde(default)]
    pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
    pub when: Option<PluginWhen>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct PluginMeta {
    pub name: String,
    pub description: Option<String>,
    pub version: Option<String>,
    pub author: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct PluginSkills {
    pub path: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct PluginWhen {
    pub os: Option<Vec<String>>,
    pub binaries: Option<Vec<String>>,
}

#[derive(Debug, Clone)]
pub struct LoadedPlugin {
    pub manifest: PluginManifest,
    pub base_path: PathBuf,
    pub enabled: bool,
}

impl PluginManifest {
    pub fn from_path(path: &Path) -> Result<Self, String> {
        let content = std::fs::read_to_string(path)
            .map_err(|e| format!("failed to read plugin.toml: {}", e))?;
        toml::from_str(&content).map_err(|e| format!("failed to parse plugin.toml: {}", e))
    }

    pub fn check_when(&self) -> bool {
        if let Some(when) = &self.when {
            if let Some(os_list) = &when.os {
                let os = std::env::consts::OS;
                if !os_list.iter().any(|o| o.eq_ignore_ascii_case(os)) {
                    return false;
                }
            }
            if let Some(binaries) = &when.binaries {
                for binary in binaries {
                    if !Self::has_binary(binary) {
                        return false;
                    }
                }
            }
        }
        true
    }

    fn has_binary(name: &str) -> bool {
        let paths = std::env::var_os("PATH").unwrap_or_default();
        for path in std::env::split_paths(&paths) {
            let candidate = path.join(name);
            if candidate.exists() {
                return true;
            }
            #[cfg(windows)]
            {
                let candidate_exe = candidate.with_extension("exe");
                if candidate_exe.exists() {
                    return true;
                }
            }
        }
        false
    }
}