#include #include #include #include #include #include using json = nlohmann::json; namespace { struct Args { std::string address = "localhost:9004"; std::string command; std::vector params; }; // ANSI colors constexpr auto C_RESET = "\033[0m"; constexpr auto C_BOLD = "\033[1m"; constexpr auto C_DIM = "\033[2m"; constexpr auto C_CYAN = "\033[36m"; constexpr auto C_GREEN = "\033[32m"; constexpr auto C_RED = "\033[31m"; constexpr auto C_YELLOW = "\033[33m"; void printJson(const json& j) { std::cout << j.dump(2) << "\n"; } void printError(const std::string& msg) { std::cerr << C_RED << "error: " << C_RESET << msg << "\n"; } void printUsage() { std::cout << C_BOLD << "smartbotic-db-cli" << C_RESET << " — admin tool for smartbotic-database\n\n" << C_BOLD << "Usage:" << C_RESET << "\n" << " smartbotic-db-cli [--address HOST:PORT] [args...]\n" << " smartbotic-db-cli [--address HOST:PORT] " << C_DIM << "# interactive mode" << C_RESET << "\n\n" << C_BOLD << "Commands:" << C_RESET << "\n" << " " << C_CYAN << "collections" << C_RESET << " List all collections\n" << " " << C_CYAN << "info" << C_RESET << " Collection info\n" << " " << C_CYAN << "find" << C_RESET << " List documents\n" << " " << C_CYAN << "get" << C_RESET << " Get a document\n" << " " << C_CYAN << "upsert" << C_RESET << " '' Insert or update\n" << " " << C_CYAN << "remove" << C_RESET << " Delete a document\n" << " " << C_CYAN << "count" << C_RESET << " Count documents\n" << " " << C_CYAN << "lock" << C_RESET << " Lock database (read-only)\n" << " " << C_CYAN << "unlock" << C_RESET << " Unlock database (accept writes)\n" << " " << C_CYAN << "status" << C_RESET << " Show read-only + recovery status\n" << " " << C_CYAN << "help" << C_RESET << " Show this help\n\n" << C_BOLD << "Options:" << C_RESET << "\n" << " --address HOST:PORT Database address (default: localhost:9004)\n"; } bool execCommand(smartbotic::database::Client& client, const std::string& cmd, const std::vector& params) { try { if (cmd == "collections") { auto collections = client.listCollections(); std::cout << C_BOLD << "Collections:" << C_RESET << "\n"; for (const auto& c : collections) { auto info = client.getCollectionInfo(c); int64_t count = 0; if (info) count = info->documentCount; std::cout << " " << C_CYAN << c << C_RESET << C_DIM << " (" << count << " docs)" << C_RESET << "\n"; } return true; } if (cmd == "info") { if (params.empty()) { printError("usage: info "); return false; } auto info = client.getCollectionInfo(params[0]); if (!info) { printError("collection not found: " + params[0]); return false; } std::cout << C_BOLD << params[0] << C_RESET << ":\n" << " documents: " << info->documentCount << "\n" << " size: " << info->sizeBytes << " bytes\n" << " encrypted: " << (info->encrypted ? "yes" : "no") << "\n" << " max_versions: " << info->maxVersions << "\n"; if (info->defaultTtlSeconds > 0) std::cout << " ttl: " << info->defaultTtlSeconds << "s\n"; return true; } if (cmd == "find") { if (params.empty()) { printError("usage: find [--limit N] [--exists FIELD]"); return false; } smartbotic::database::Client::QueryOptions opts; opts.limit = 100; for (size_t i = 1; i < params.size(); ++i) { if (params[i] == "--limit" && i + 1 < params.size()) { opts.limit = static_cast(std::stoul(params[++i])); } else if (params[i] == "--exists" && i + 1 < params.size()) { opts.filters.emplace_back(params[++i], smartbotic::database::Client::FilterOp::EXISTS, true); } } auto docs = client.find(params[0], opts); std::cout << C_DIM << "(" << docs.size() << " documents)" << C_RESET << "\n"; for (const auto& doc : docs) { auto id = doc.value("_id", ""); // Print compact summary line std::cout << C_GREEN << id << C_RESET; // Show a few key fields for (const auto& [k, v] : doc.items()) { if (k == "_id" || k == "_created_at" || k == "_updated_at") continue; auto val = v.dump(); if (val.size() > 60) val = val.substr(0, 57) + "..."; std::cout << " " << C_DIM << k << "=" << C_RESET << val; // Limit to 3 fields per line static int field_count = 0; if (++field_count >= 3) { field_count = 0; break; } } std::cout << "\n"; } return true; } if (cmd == "get") { if (params.size() < 2) { printError("usage: get "); return false; } auto doc = client.get(params[0], params[1]); if (!doc) { printError("not found: " + params[0] + "/" + params[1]); return false; } printJson(*doc); return true; } if (cmd == "upsert") { if (params.size() < 3) { printError("usage: upsert ''"); return false; } auto data = json::parse(params[2], nullptr, false); if (!data.is_object()) { printError("invalid JSON: " + params[2]); return false; } client.upsert(params[0], data, params[1]); std::cout << C_GREEN << "ok" << C_RESET << " " << params[0] << "/" << params[1] << "\n"; return true; } if (cmd == "remove" || cmd == "delete") { if (params.size() < 2) { printError("usage: remove "); return false; } client.remove(params[0], params[1]); std::cout << C_GREEN << "ok" << C_RESET << " removed " << params[0] << "/" << params[1] << "\n"; return true; } if (cmd == "count") { if (params.empty()) { printError("usage: count "); return false; } auto info = client.getCollectionInfo(params[0]); if (!info) { printError("collection not found: " + params[0]); return false; } std::cout << info->documentCount << "\n"; return true; } if (cmd == "lock") { if (client.setReadOnly(true)) { std::cout << C_GREEN << "ok" << C_RESET << " Database locked (read-only)\n"; return true; } printError("failed to lock database"); return false; } if (cmd == "unlock") { if (client.setReadOnly(false)) { std::cout << C_GREEN << "ok" << C_RESET << " Database unlocked (writes accepted)\n"; return true; } printError("failed to unlock database"); return false; } if (cmd == "status") { auto s = client.getReadOnlyStatus(); std::cout << "Read-only: " << (s.readOnly ? (std::string(C_RED) + "YES" + C_RESET) : "no") << "\n"; if (s.readOnly) { std::cout << "Reason: " << s.reason << "\n"; } std::cout << "Recovery outcome: " << s.recoveryOutcome << "\n"; if (!s.expectedSnapshot.empty()) { std::cout << "Expected snapshot: " << s.expectedSnapshot << "\n"; } if (!s.snapshotUsed.empty()) { std::cout << "Snapshot used: " << s.snapshotUsed << "\n"; } if (!s.failureReason.empty()) { std::cout << "Failure reason: " << s.failureReason << "\n"; } std::cout << "WAL replayed: " << s.walEntriesReplayed << " entries\n"; std::cout << "Snapshots tried: " << s.snapshotsAttempted << "\n"; return true; } if (cmd == "help" || cmd == "?") { printUsage(); return true; } printError("unknown command: " + cmd + " (try 'help')"); return false; } catch (const std::exception& e) { printError(e.what()); return false; } } // Tokenize a line, respecting single/double quotes and JSON braces std::vector tokenize(const std::string& line) { std::vector tokens; std::string current; int brace_depth = 0; char in_quote = 0; for (size_t i = 0; i < line.size(); ++i) { char c = line[i]; if (in_quote) { current += c; if (c == in_quote && (i == 0 || line[i-1] != '\\')) { in_quote = 0; // Strip surrounding quotes for simple string tokens if (brace_depth == 0 && current.size() >= 2 && (current.front() == '\'' || current.front() == '"') && current.front() == current.back()) { current = current.substr(1, current.size() - 2); } } continue; } if (c == '\'' || c == '"') { in_quote = c; current += c; continue; } if (c == '{') { brace_depth++; current += c; continue; } if (c == '}') { brace_depth--; current += c; if (brace_depth <= 0) { brace_depth = 0; tokens.push_back(current); current.clear(); } continue; } if (brace_depth > 0) { current += c; continue; } if (c == ' ' || c == '\t') { if (!current.empty()) { tokens.push_back(current); current.clear(); } continue; } current += c; } if (!current.empty()) tokens.push_back(current); return tokens; } } // anonymous namespace int main(int argc, char* argv[]) { Args args; // Parse flags std::vector positional; for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; if (arg == "--address" && i + 1 < argc) { args.address = argv[++i]; } else if (arg == "--help" || arg == "-h") { printUsage(); return 0; } else { positional.push_back(arg); } } if (!positional.empty()) { args.command = positional[0]; args.params.assign(positional.begin() + 1, positional.end()); } // Connect smartbotic::database::Client client({.address = args.address}); client.connect(); // Scriptable mode: single command if (!args.command.empty()) { return execCommand(client, args.command, args.params) ? 0 : 1; } // Interactive mode std::cout << C_BOLD << "smartbotic-db-cli" << C_RESET << " connected to " << C_CYAN << args.address << C_RESET << "\n" << C_DIM << "Type 'help' for commands, 'exit' to quit." << C_RESET << "\n"; std::string line; while (true) { std::cout << C_YELLOW << "> " << C_RESET; if (!std::getline(std::cin, line)) break; // Trim auto start = line.find_first_not_of(" \t"); if (start == std::string::npos) continue; line = line.substr(start); if (line == "exit" || line == "quit" || line == "q") break; if (line.empty()) continue; auto tokens = tokenize(line); if (tokens.empty()) continue; auto cmd = tokens[0]; std::vector params(tokens.begin() + 1, tokens.end()); execCommand(client, cmd, params); } return 0; }