pluginengine01 / cpp-cli /bexcli.cpp
krystv's picture
Upload 107 files
3374e90 verified
// 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 <iostream>
#include <string>
#include <vector>
#include <cstring>
#include <cstdlib>
#include <future>
#include <memory>
#include <sstream>
#include <iomanip>
#include <stdexcept>
#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<std::promise<std::string>*>(user_data);
if (success) {
std::string result(reinterpret_cast<const char*>(payload), len);
promise->set_value(std::move(result));
} else {
std::string error_msg(reinterpret_cast<const char*>(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<std::string>& 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 <path> Install a .bex plugin package\n"
<< " " << prog << " uninstall <id> Uninstall a plugin by ID\n"
<< " " << prog << " list List installed plugins\n"
<< " " << prog << " info-plugin <id> Show detailed plugin information\n"
<< " " << prog << " enable <id> Enable a plugin\n"
<< " " << prog << " disable <id> Disable a plugin\n"
<< "\n"
<< " API Key / Secret Management:\n"
<< " " << prog << " set-key <plugin_id> <key> <val> Set an API key for a plugin\n"
<< " " << prog << " get-key <plugin_id> <key> Get an API key value\n"
<< " " << prog << " delete-key <plugin_id> <key> Delete an API key\n"
<< " " << prog << " list-keys <plugin_id> List all keys for a plugin\n"
<< "\n"
<< " Media Browsing (async with callbacks):\n"
<< " " << prog << " home <plugin_id> Get home sections\n"
<< " " << prog << " search <plugin_id> <query> Search media\n"
<< " " << prog << " info <plugin_id> <id> Get media info\n"
<< " " << prog << " servers <plugin_id> <id> Get servers (id is self-describing)\n"
<< " " << prog << " stream <plugin_id> <json> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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;
}