virtuoso-cli 0.3.18

CLI tool to control Cadence Virtuoso from anywhere, locally or remotely
Documentation
use chrono::Utc;
use serde::{Deserialize, Serialize};
use std::io::Write;
use std::path::PathBuf;
use std::sync::{Mutex, OnceLock};

static CMD_LOCK: OnceLock<Mutex<()>> = OnceLock::new();

fn cmd_lock() -> std::sync::MutexGuard<'static, ()> {
    CMD_LOCK
        .get_or_init(|| Mutex::new(()))
        .lock()
        .expect("cmd history lock poisoned")
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SkillEntry {
    pub ts: String,
    pub skill: String,
    pub ok: bool,
    pub output: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CmdEntry {
    pub ts: String,
    pub session: Option<String>,
    pub cmd: Vec<String>,
    pub exit_code: i32,
}

pub fn history_dir() -> PathBuf {
    dirs::home_dir()
        .unwrap_or_else(|| PathBuf::from("/tmp"))
        .join(".cache/virtuoso_bridge/history")
}

fn write_jsonl_line(path: &std::path::Path, line: &str) {
    if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
        let _ = writeln!(f, "{line}");
    }
}

pub fn append_skill(session_id: &str, skill: &str, ok: bool, output: &str) {
    let dir = history_dir();
    let _ = std::fs::create_dir_all(&dir);
    let entry = SkillEntry {
        ts: Utc::now().to_rfc3339(),
        skill: skill.to_string(),
        ok,
        output: output.chars().take(512).collect(),
    };
    if let Ok(line) = serde_json::to_string(&entry) {
        write_jsonl_line(&dir.join(format!("{session_id}.jsonl")), &line);
    }
}

pub fn append_cmd(args: &[String], session: Option<&str>, exit_code: i32) {
    let _guard = cmd_lock();
    let dir = history_dir();
    let _ = std::fs::create_dir_all(&dir);
    let entry = CmdEntry {
        ts: Utc::now().to_rfc3339(),
        session: session.map(String::from),
        cmd: args.to_vec(),
        exit_code,
    };
    if let Ok(line) = serde_json::to_string(&entry) {
        write_jsonl_line(&dir.join("cmd.jsonl"), &line);
    }
}

fn tail<T>(mut v: Vec<T>, limit: usize) -> Vec<T> {
    if limit > 0 && v.len() > limit {
        v.drain(..v.len() - limit);
    }
    v
}

pub fn load_skill(session_id: &str, limit: usize) -> Vec<SkillEntry> {
    let path = history_dir().join(format!("{session_id}.jsonl"));
    let entries: Vec<SkillEntry> = std::fs::read_to_string(path)
        .unwrap_or_default()
        .lines()
        .filter_map(|line| serde_json::from_str(line).ok())
        .collect();
    tail(entries, limit)
}

pub fn load_cmd(session_filter: Option<&str>, limit: usize) -> Vec<CmdEntry> {
    let _guard = cmd_lock();
    let path = history_dir().join("cmd.jsonl");
    let entries: Vec<CmdEntry> = std::fs::read_to_string(path)
        .unwrap_or_default()
        .lines()
        .filter_map(|line| serde_json::from_str(line).ok())
        .filter(|e: &CmdEntry| {
            session_filter.map_or(true, |id| e.session.as_deref() == Some(id))
        })
        .collect();
    tail(entries, limit)
}