use std::{
collections::HashMap,
fs,
path::PathBuf,
};
fn plugins_dir() -> PathBuf {
let base = dirs_next::config_dir()
.unwrap_or_else(|| PathBuf::from(".config"));
base.join("rok").join("plugins")
}
fn plugins_manifest() -> PathBuf {
plugins_dir().join("plugins.json")
}
fn load_plugins() -> HashMap<String, PluginEntry> {
let path = plugins_manifest();
if !path.exists() { return HashMap::new(); }
let content = fs::read_to_string(&path).unwrap_or_default();
serde_json::from_str(&content).unwrap_or_default()
}
fn save_plugins(plugins: &HashMap<String, PluginEntry>) -> anyhow::Result<()> {
let dir = plugins_dir();
fs::create_dir_all(&dir)?;
let json = serde_json::to_string_pretty(plugins)?;
fs::write(plugins_manifest(), json)?;
Ok(())
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct PluginEntry {
source: String,
version: Option<String>,
description: Option<String>,
installed_at: String,
}
pub fn install(name: &str, source: Option<&str>) -> anyhow::Result<()> {
let src = source.unwrap_or(name);
println!("Installing plugin: {name} from {src}");
let crate_name = if src.starts_with("rok-plugin-") || src.starts_with("http") {
src.to_string()
} else {
format!("rok-plugin-{src}")
};
let status = std::process::Command::new("cargo")
.args(["install", &crate_name])
.status()?;
if !status.success() {
anyhow::bail!("cargo install {crate_name} failed — ensure the crate exists on crates.io");
}
let mut plugins = load_plugins();
plugins.insert(name.to_string(), PluginEntry {
source: crate_name,
version: None,
description: None,
installed_at: chrono::Utc::now().to_rfc3339(),
});
save_plugins(&plugins)?;
println!(" ✓ Plugin '{name}' installed.");
println!(" Invoke with: rok plugin:{name} [args]");
Ok(())
}
pub fn list() -> anyhow::Result<()> {
let plugins = load_plugins();
if plugins.is_empty() {
println!("No plugins installed.");
println!("Install one with: rok plugin:install <name>");
} else {
println!("{:<30} {:<15} SOURCE", "NAME", "INSTALLED");
println!("{}", "─".repeat(70));
for (name, entry) in &plugins {
println!("{:<30} {:<15} {}", name, &entry.installed_at[..10], entry.source);
}
}
Ok(())
}
pub fn remove(name: &str) -> anyhow::Result<()> {
let mut plugins = load_plugins();
if let Some(entry) = plugins.remove(name) {
let status = std::process::Command::new("cargo")
.args(["uninstall", &entry.source])
.status()?;
if !status.success() {
println!(" ! cargo uninstall failed — removing from manifest only");
}
save_plugins(&plugins)?;
println!(" ✓ Plugin '{name}' removed.");
} else {
println!(" Plugin '{name}' is not installed.");
}
Ok(())
}