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::(json) { Ok(val) => println!("{}", serde_json::to_string_pretty(&val).unwrap()), Err(_) => println!("{}", json), } }