krystv's picture
Upload 107 files
3374e90 verified
use bex_core::{Engine, EngineConfig};
use bex_types::Manifest;
use clap::{Parser, Subcommand};
use std::io::Read;
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "bex", about = "BEX Plugin Engine CLI")]
struct Cli {
#[arg(long, default_value = "./bex-data")]
data_dir: PathBuf,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Install { path: PathBuf },
Uninstall { id: String },
List,
Inspect { path: PathBuf },
Pack { manifest: PathBuf, wasm: PathBuf, output: PathBuf },
Home { plugin_id: String },
Search { plugin_id: String, query: String },
Info { plugin_id: String, id: String },
/// Get servers for an episode. The ID is self-describing — the plugin knows
/// how to parse its own IDs (e.g. "slug$ep=5$sub=1$dub=0").
Servers { plugin_id: String, id: String },
Stream { plugin_id: String, server_json: String },
Enable { id: String },
Disable { id: String },
/// Show detailed info about an installed plugin
PluginInfo { id: String },
/// Set an API key / secret for a plugin
SetKey { plugin_id: String, key: String, value: String },
/// Get an API key / secret value for a plugin
GetKey { plugin_id: String, key: String },
/// Delete an API key / secret for a plugin
DeleteKey { plugin_id: String, key: String },
/// List all API keys / secrets for a plugin
ListKeys { plugin_id: String },
Stats,
}
fn main() -> anyhow::Result<()> {
// Initialize logger
tracing_subscriber::fmt::init();
let cli = Cli::parse();
let config = EngineConfig {
data_dir: cli.data_dir.clone(),
..Default::default()
};
let engine = Engine::new(config)?;
match cli.command {
Commands::Install { path } => {
let info = engine.install_plugin(&path)?;
println!("Installed: {} ({}) v{}", info.name, info.id, info.version);
let caps = bex_types::Capabilities::from_bits(info.capabilities).unwrap_or(bex_types::Capabilities::empty());
println!("Capabilities: {:?}", caps);
}
Commands::Uninstall { id } => {
engine.uninstall_plugin(&id)?;
println!("Uninstalled: {}", id);
}
Commands::List => {
let plugins = engine.list_plugins();
if plugins.is_empty() {
println!("No plugins installed.");
return Ok(());
}
println!("{:<40} {:<20} {:<10} {}", "ID", "NAME", "VERSION", "ENABLED");
for p in plugins {
println!("{:<40} {:<20} {:<10} {}", p.id, p.name, p.version, p.enabled);
}
}
Commands::Inspect { path } => {
let data = std::fs::read(&path)?;
let manifest = bex_pkg::read_manifest(&data)?;
println!("ID: {}", manifest.id);
println!("Name: {}", manifest.name);
println!("Version: {}", manifest.version);
println!("ABI: {}", manifest.abi);
println!("Capabilities: {:?}", manifest.capabilities());
}
Commands::Pack { manifest, wasm, output } => {
let yaml_str = std::fs::read_to_string(&manifest)?;
let m: Manifest = serde_yaml::from_str(&yaml_str)?;
let wasm_bytes = std::fs::read(&wasm)?;
let packed = bex_pkg::pack(&m, &wasm_bytes)?;
std::fs::write(&output, packed)?;
println!(
"Packed to {} ({} bytes)",
output.display(),
std::fs::metadata(&output)?.len()
);
}
Commands::Home { plugin_id } => {
let result = engine.call_get_home_json(&plugin_id)?;
println_pretty(&result);
}
Commands::Search { plugin_id, query } => {
let result = engine.call_search_json(&plugin_id, &query)?;
println_pretty(&result);
}
Commands::Info { plugin_id, id } => {
let result = engine.call_get_info_json(&plugin_id, &id)?;
println_pretty(&result);
}
Commands::Servers { plugin_id, id } => {
// The ID is self-describing — the plugin knows how to parse its own IDs.
// No separate episode_id parameter needed.
let result = engine.call_get_servers_json(&plugin_id, &id)?;
println_pretty(&result);
}
Commands::Stream { plugin_id, server_json } => {
let server_json = if server_json == "-" {
let mut input = String::new();
std::io::stdin().read_to_string(&mut input)?;
input
} else {
server_json
};
let result = engine.call_resolve_stream_json(&plugin_id, &server_json)?;
println_pretty(&result);
}
Commands::Enable { id } => {
engine.enable_plugin(&id)?;
println!("Enabled: {}", id);
}
Commands::Disable { id } => {
engine.disable_plugin(&id)?;
println!("Disabled: {}", id);
}
Commands::PluginInfo { id } => {
match engine.get_plugin_info(&id) {
Some(info) => {
println!("ID: {}", info.id);
println!("Name: {}", info.name);
println!("Version: {}", info.version);
println!("Enabled: {}", info.enabled);
let caps = bex_types::Capabilities::from_bits(info.capabilities)
.unwrap_or(bex_types::Capabilities::empty());
println!("Capabilities: {:?}", caps);
}
None => println!("Plugin not found: {}", id),
}
}
Commands::SetKey { plugin_id, key, value } => {
engine.secret_set(&plugin_id, &key, &value)?;
println!("Key '{}' set for plugin '{}'", key, plugin_id);
}
Commands::GetKey { plugin_id, key } => {
match engine.secret_get(&plugin_id, &key)? {
Some(val) => println!("{}", val),
None => println!("Key '{}' not found for plugin '{}'", key, plugin_id),
}
}
Commands::DeleteKey { plugin_id, key } => {
let existed = engine.secret_remove(&plugin_id, &key)?;
if existed {
println!("Key '{}' deleted from plugin '{}'", key, plugin_id);
} else {
println!("Key '{}' not found for plugin '{}'", key, plugin_id);
}
}
Commands::ListKeys { plugin_id } => {
let keys = engine.secret_keys(&plugin_id)?;
if keys.is_empty() {
println!("No keys found for plugin '{}'", plugin_id);
} else {
println!("Keys for plugin '{}':", plugin_id);
for k in keys {
println!(" {}", k);
}
}
}
Commands::Stats => {
let stats = engine.stats();
println!("Uptime: {}ms", stats.uptime_ms);
println!("Total plugins: {}", stats.total_plugins);
println!("Enabled plugins: {}", stats.enabled_plugins);
}
}
Ok(())
}
fn println_pretty(json: &str) {
match serde_json::from_str::<serde_json::Value>(json) {
Ok(val) => println!("{}", serde_json::to_string_pretty(&val).unwrap()),
Err(_) => println!("{}", json),
}
}