// BEX C++ CLI Tool — Pure C ABI + Callback Architecture // // Full-featured CLI demonstrating the pure C FFI with async callback pattern. // No bridge crate dependency — just bex_engine.h and the Rust static/shared library. // // Features: // - Plugin management: install, uninstall, list, info, enable, disable // - API key management: set-key, get-key, delete-key, list-keys // - Media browsing: home, search, info, servers, stream // - Async operations use std::promise/future for blocking wait // // Build with CMake: // mkdir build && cd build // cmake .. -DCMAKE_BUILD_TYPE=Release // make -j$(nproc) #include #include #include #include #include #include #include #include #include #include #include "bex_engine.h" // ── Callback handler for async operations ────────────────────────────── /// The C-callback triggered by Rust's Tokio thread when an async task finishes. /// It casts user_data back to a std::promise and fulfills it. extern "C" void on_bex_result(void* user_data, uint64_t req_id, bool success, const uint8_t* payload, size_t len) { auto* promise = static_cast*>(user_data); if (success) { std::string result(reinterpret_cast(payload), len); promise->set_value(std::move(result)); } else { std::string error_msg(reinterpret_cast(payload), len); promise->set_exception(std::make_exception_ptr(std::runtime_error(error_msg))); } } /// Submit an async request and block until the result arrives. /// Uses std::promise/future to bridge the async callback to sync CLI. std::string await_request(BexEngine* engine, uint64_t request_id, std::promise& promise, uint32_t timeout_ms = 30000) { auto future = promise.get_future(); auto status = future.wait_for(std::chrono::milliseconds(timeout_ms)); if (status == std::future_status::timeout) { bex_cancel_request(engine, request_id); throw std::runtime_error("Request timed out after " + std::to_string(timeout_ms) + "ms"); } return future.get(); } // ── Pretty-print JSON ────────────────────────────────────────────────── void print_json(const std::string& json_str) { bool already_pretty = json_str.find('\n') != std::string::npos; if (already_pretty) { std::cout << json_str << std::endl; return; } int indent = 0; bool in_string = false; bool escape = false; for (size_t i = 0; i < json_str.size(); i++) { char c = json_str[i]; if (escape) { std::cout << c; escape = false; continue; } if (c == '\\' && in_string) { std::cout << c; escape = true; continue; } if (c == '"') { in_string = !in_string; std::cout << c; continue; } if (in_string) { std::cout << c; continue; } switch (c) { case '{': case '[': std::cout << c << "\n"; indent += 2; std::cout << std::string(indent, ' '); break; case '}': case ']': std::cout << "\n"; indent -= 2; std::cout << std::string(indent, ' ') << c; break; case ',': std::cout << c << "\n" << std::string(indent, ' '); break; case ':': std::cout << c << " "; break; case ' ': case '\n': case '\r': case '\t': break; default: std::cout << c; } } std::cout << std::endl; } /// Decode capability bits into a string std::string capabilities_str(uint32_t caps) { std::string result; if (caps & (1 << 0)) result += "HOME "; if (caps & (1 << 1)) result += "CATEGORY "; if (caps & (1 << 2)) result += "SEARCH "; if (caps & (1 << 3)) result += "INFO "; if (caps & (1 << 4)) result += "SERVERS "; if (caps & (1 << 5)) result += "STREAM "; if (caps & (1 << 6)) result += "SUBTITLES "; if (caps & (1 << 7)) result += "ARTICLES "; if (!result.empty()) result.pop_back(); return result; } /// RAII wrapper for BexPluginInfoList struct PluginListGuard { BexPluginInfoList list; explicit PluginListGuard(BexPluginInfoList l) : list(l) {} ~PluginListGuard() { bex_plugin_info_list_free(list); } BexPluginInfo* begin() { return list.items; } BexPluginInfo* end() { return list.items + list.count; } size_t size() const { return list.count; } }; /// RAII wrapper for C strings returned by the engine struct CStrGuard { char* ptr; explicit CStrGuard(char* p) : ptr(p) {} ~CStrGuard() { if (ptr) bex_string_free(ptr); } std::string str() const { return ptr ? std::string(ptr) : std::string(); } }; // ── Usage ────────────────────────────────────────────────────────────── void print_usage(const char* prog) { std::cerr << "BEX C++ CLI v4.0 — WASM Plugin Engine (Pure C ABI + Callbacks)\n" << "\n" << "Usage:\n" << " " << prog << " install Install a .bex plugin package\n" << " " << prog << " uninstall Uninstall a plugin by ID\n" << " " << prog << " list List installed plugins\n" << " " << prog << " info-plugin Show detailed plugin information\n" << " " << prog << " enable Enable a plugin\n" << " " << prog << " disable Disable a plugin\n" << "\n" << " API Key / Secret Management:\n" << " " << prog << " set-key Set an API key for a plugin\n" << " " << prog << " get-key Get an API key value\n" << " " << prog << " delete-key Delete an API key\n" << " " << prog << " list-keys List all keys for a plugin\n" << "\n" << " Media Browsing (async with callbacks):\n" << " " << prog << " home Get home sections\n" << " " << prog << " search Search media\n" << " " << prog << " info Get media info\n" << " " << prog << " servers Get servers (id is self-describing)\n" << " " << prog << " stream Resolve stream from server JSON\n" << "\n" << " Debug:\n" << " " << prog << " stats Show engine stats\n" << "\n" << "Design: IDs are self-describing. The plugin knows how to parse its own IDs.\n" << "Example: bexcli servers com.gogoanime 'one-piece$ep=1$sub=1$dub=0'\n" << std::endl; } // ── Main ─────────────────────────────────────────────────────────────── int main(int argc, char* argv[]) { if (argc < 2) { print_usage(argv[0]); return 1; } std::string data_dir = std::string(getenv("HOME") ? getenv("HOME") : ".") + "/.bex-data"; if (getenv("BEX_DATA_DIR")) { data_dir = getenv("BEX_DATA_DIR"); } std::string cmd = argv[1]; // Create the engine BexEngine* engine = bex_engine_new(data_dir.c_str()); if (!engine) { std::cerr << "Error: Failed to create BexEngine" << std::endl; return 1; } try { // ── Plugin Management ────────────────────────────────────── if (cmd == "install" && argc >= 3) { int rc = bex_engine_install(engine, argv[2]); if (rc != 0) { CStrGuard err(bex_engine_last_error(engine)); throw std::runtime_error("Install failed: " + err.str()); } std::cout << "Plugin installed from: " << argv[2] << std::endl; } else if (cmd == "uninstall" && argc >= 3) { int rc = bex_engine_uninstall(engine, argv[2]); if (rc != 0) { CStrGuard err(bex_engine_last_error(engine)); throw std::runtime_error("Uninstall failed: " + err.str()); } std::cout << "Plugin uninstalled: " << argv[2] << std::endl; } else if (cmd == "list") { PluginListGuard list(bex_engine_list_plugins(engine)); if (list.size() == 0) { std::cout << "No plugins installed." << std::endl; } else { std::cout << std::left << std::setw(40) << "ID" << std::setw(20) << "NAME" << std::setw(10) << "VERSION" << std::setw(10) << "STATUS" << "CAPABILITIES" << std::endl; std::cout << std::string(100, '-') << std::endl; for (size_t i = 0; i < list.size(); i++) { auto& p = list.list.items[i]; std::cout << std::left << std::setw(40) << (p.id ? p.id : "") << std::setw(20) << (p.name ? p.name : "") << std::setw(10) << (p.version ? p.version : "") << std::setw(10) << (p.enabled ? "enabled" : "disabled") << capabilities_str(p.capabilities) << std::endl; } } } else if (cmd == "info-plugin" && argc >= 3) { BexPluginInfo info; int rc = bex_engine_plugin_info(engine, argv[2], &info); if (rc != 0) { CStrGuard err(bex_engine_last_error(engine)); throw std::runtime_error("Plugin info failed: " + err.str()); } std::cout << "ID: " << (info.id ? info.id : "") << std::endl; std::cout << "Name: " << (info.name ? info.name : "") << std::endl; std::cout << "Version: " << (info.version ? info.version : "") << std::endl; std::cout << "Enabled: " << (info.enabled ? "yes" : "no") << std::endl; std::cout << "Capabilities: " << capabilities_str(info.capabilities) << std::endl; // Show API keys for this plugin CStrGuard keys(bex_engine_secret_keys(engine, argv[2])); if (keys.ptr && strlen(keys.ptr) > 0) { std::cout << "API Keys: " << keys.str() << std::endl; } else { std::cout << "API Keys: (none)" << std::endl; } bex_plugin_info_free(info); } else if (cmd == "enable" && argc >= 3) { int rc = bex_engine_enable(engine, argv[2]); if (rc != 0) { CStrGuard err(bex_engine_last_error(engine)); throw std::runtime_error("Enable failed: " + err.str()); } std::cout << "Enabled: " << argv[2] << std::endl; } else if (cmd == "disable" && argc >= 3) { int rc = bex_engine_disable(engine, argv[2]); if (rc != 0) { CStrGuard err(bex_engine_last_error(engine)); throw std::runtime_error("Disable failed: " + err.str()); } std::cout << "Disabled: " << argv[2] << std::endl; } // ── API Key / Secret Management ─────────────────────────── else if (cmd == "set-key" && argc >= 5) { int rc = bex_engine_secret_set(engine, argv[2], argv[3], argv[4]); if (rc != 0) { CStrGuard err(bex_engine_last_error(engine)); throw std::runtime_error("Set key failed: " + err.str()); } std::cout << "Key '" << argv[3] << "' set for plugin '" << argv[2] << "'" << std::endl; } else if (cmd == "get-key" && argc >= 4) { char buf[4096]; size_t buf_len = sizeof(buf); int rc = bex_engine_secret_get(engine, argv[2], argv[3], buf, &buf_len); if (rc == 0) { std::cout << buf << std::endl; } else { std::cout << "Key '" << argv[3] << "' not found for plugin '" << argv[2] << "'" << std::endl; } } else if (cmd == "delete-key" && argc >= 4) { int rc = bex_engine_secret_delete(engine, argv[2], argv[3]); if (rc == 0) { std::cout << "Key '" << argv[3] << "' deleted from plugin '" << argv[2] << "'" << std::endl; } else if (rc == 1) { std::cout << "Key '" << argv[3] << "' not found for plugin '" << argv[2] << "'" << std::endl; } else { CStrGuard err(bex_engine_last_error(engine)); throw std::runtime_error("Delete key failed: " + err.str()); } } else if (cmd == "list-keys" && argc >= 3) { CStrGuard keys(bex_engine_secret_keys(engine, argv[2])); if (keys.ptr && strlen(keys.ptr) > 0) { std::cout << "Keys for plugin '" << argv[2] << "': " << keys.str() << std::endl; } else { std::cout << "No keys found for plugin '" << argv[2] << "'" << std::endl; } } // ── Media Browsing (async with callbacks) ──────────────── else if (cmd == "home" && argc >= 3) { std::promise promise; uint64_t req_id = bex_submit_home(engine, argv[2], on_bex_result, &promise); std::string result = await_request(engine, req_id, promise); print_json(result); } else if (cmd == "search" && argc >= 4) { std::promise promise; uint64_t req_id = bex_submit_search(engine, argv[2], argv[3], on_bex_result, &promise); std::string result = await_request(engine, req_id, promise); print_json(result); } else if (cmd == "info" && argc >= 4) { std::promise promise; uint64_t req_id = bex_submit_info(engine, argv[2], argv[3], on_bex_result, &promise); std::string result = await_request(engine, req_id, promise); print_json(result); } else if (cmd == "servers" && argc >= 4) { // The ID is self-describing — the plugin knows how to parse its own IDs. // Example: bexcli servers com.gogoanime 'one-piece$ep=1$sub=1$dub=0' std::promise promise; uint64_t req_id = bex_submit_servers(engine, argv[2], argv[3], on_bex_result, &promise); std::string result = await_request(engine, req_id, promise); print_json(result); } else if (cmd == "stream" && argc >= 4) { std::promise promise; uint64_t req_id = bex_submit_stream(engine, argv[2], argv[3], on_bex_result, &promise); std::string result = await_request(engine, req_id, promise); print_json(result); } // ── Debug ───────────────────────────────────────────────── else if (cmd == "stats") { CStrGuard stats(bex_engine_stats(engine)); if (stats.ptr) { print_json(stats.str()); } else { std::cout << "Unable to get stats" << std::endl; } } else { print_usage(argv[0]); bex_engine_free(engine); return 1; } bex_engine_free(engine); } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; bex_engine_free(engine); return 1; } return 0; }