yana-rt 0.42.1

Yana AI Runtime — safety CLI for AI agents: scan, graph, vault, hunt, ci, map, fix, doctor
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use chrono::Utc;
use shell_words;

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Plugin {
    pub id:          String,
    pub name:        String,
    pub script:      String,
    pub description: String,
    pub enabled:     bool,
    pub added_at:    String,
}

fn plugins_path() -> PathBuf {
    let base = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
    base.join(".yana-ai").join("plugins.json")
}

fn load_plugins() -> Vec<Plugin> {
    let path = plugins_path();
    if !path.exists() { return vec![]; }
    serde_json::from_str(&fs::read_to_string(&path).unwrap_or_default()).unwrap_or_default()
}

fn save_plugins(plugins: &[Plugin]) {
    let path = plugins_path();
    if let Some(parent) = path.parent() { fs::create_dir_all(parent).ok(); }
    fs::write(&path, serde_json::to_string_pretty(plugins).expect("serialize failed"))
        .expect("write plugins failed");
}

pub fn cmd_plugin_list() {
    let plugins = load_plugins();
    if plugins.is_empty() {
        println!("No plugins registered.\nAdd: yana-rt plugin add <name> <script>");
        return;
    }
    println!("{:<10} {:<3} {:<20} {}", "ID", "ON", "NAME", "SCRIPT");
    println!("{}", "".repeat(62));
    for p in &plugins {
        let on = if p.enabled { "" } else { " " };
        println!("{:<10} {on}   {:<20} {}", &p.id[..8], p.name, p.script);
        if !p.description.is_empty() { println!("           {}", p.description); }
    }
}

pub fn cmd_plugin_add(name: String, script: String, description: String) {
    let mut plugins = load_plugins();
    if plugins.iter().any(|p| p.name == name) {
        eprintln!("error: plugin '{name}' already exists. Use 'plugin remove' first.");
        std::process::exit(1);
    }
    plugins.push(Plugin {
        id: Uuid::new_v4().to_string(), name: name.clone(), script: script.clone(),
        description, enabled: true,
        added_at: Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(),
    });
    save_plugins(&plugins);
    println!("✓ registered '{name}'\n  script: {script}");
}

pub fn cmd_plugin_remove(name: String) {
    let mut plugins = load_plugins();
    let before = plugins.len();
    plugins.retain(|p| p.name != name);
    if plugins.len() == before { eprintln!("error: plugin '{name}' not found"); std::process::exit(1); }
    save_plugins(&plugins);
    println!("✓ removed '{name}'");
}

pub fn cmd_plugin_toggle(name: String, enable: bool) {
    let mut plugins = load_plugins();
    match plugins.iter_mut().find(|p| p.name == name) {
        Some(p) => { p.enabled = enable; }
        None    => { eprintln!("error: plugin '{name}' not found"); std::process::exit(1); }
    }
    save_plugins(&plugins);
    println!("✓ plugin '{name}' {}", if enable { "enabled" } else { "disabled" });
}

pub fn cmd_plugin_run(name: String, input: Option<String>) {
    let plugins = load_plugins();
    let plugin = match plugins.iter().find(|p| p.name == name) {
        Some(p) => p,
        None    => { eprintln!("error: plugin '{name}' not found"); std::process::exit(1); }
    };
    if !plugin.enabled { eprintln!("error: plugin '{name}' is disabled"); std::process::exit(1); }
    println!("→ running '{name}': {}", plugin.script);
    let parts = match shell_words::split(&plugin.script) {
        Ok(p) if !p.is_empty() => p,
        Ok(_) => { eprintln!("error: empty script"); std::process::exit(1); }
        Err(e) => { eprintln!("error: invalid script: {e}"); std::process::exit(1); }
    };
    let mut proc = Command::new(&parts[0]);
    proc.args(&parts[1..]);
    if let Some(ref inp) = input { proc.env("YANA_PLUGIN_INPUT", inp); }
    let status = proc.status().expect("failed to spawn plugin");
    if !status.success() {
        eprintln!("plugin exited: {}", status.code().unwrap_or(-1));
        std::process::exit(1);
    }
    println!("✓ '{name}' done");
}