main.cpp 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720
  1. #include <smartbotic/database/client.hpp>
  2. #include <nlohmann/json.hpp>
  3. #include <openssl/bio.h>
  4. #include <openssl/bn.h>
  5. #include <openssl/err.h>
  6. #include <openssl/evp.h>
  7. #include <openssl/pem.h>
  8. #include <openssl/rand.h>
  9. #include <openssl/x509.h>
  10. #include <openssl/x509v3.h>
  11. #include <arpa/inet.h>
  12. #include <fcntl.h>
  13. #include <sys/random.h>
  14. #include <sys/stat.h>
  15. #include <unistd.h>
  16. #include <cerrno>
  17. #include <cstdio>
  18. #include <cstring>
  19. #include <fstream>
  20. #include <iostream>
  21. #include <sstream>
  22. #include <string>
  23. #include <vector>
  24. using json = nlohmann::json;
  25. namespace {
  26. struct Args {
  27. std::string address = "localhost:9004";
  28. std::string command;
  29. std::vector<std::string> params;
  30. };
  31. // ANSI colors
  32. constexpr auto C_RESET = "\033[0m";
  33. constexpr auto C_BOLD = "\033[1m";
  34. constexpr auto C_DIM = "\033[2m";
  35. constexpr auto C_CYAN = "\033[36m";
  36. constexpr auto C_GREEN = "\033[32m";
  37. constexpr auto C_RED = "\033[31m";
  38. constexpr auto C_YELLOW = "\033[33m";
  39. void printJson(const json& j) {
  40. std::cout << j.dump(2) << "\n";
  41. }
  42. void printError(const std::string& msg) {
  43. std::cerr << C_RED << "error: " << C_RESET << msg << "\n";
  44. }
  45. void printUsage() {
  46. std::cout << C_BOLD << "smartbotic-db-cli" << C_RESET << " — admin tool for smartbotic-database\n\n"
  47. << C_BOLD << "Usage:" << C_RESET << "\n"
  48. << " smartbotic-db-cli [--address HOST:PORT] <command> [args...]\n"
  49. << " smartbotic-db-cli [--address HOST:PORT] " << C_DIM << "# interactive mode" << C_RESET << "\n\n"
  50. << C_BOLD << "Commands:" << C_RESET << "\n"
  51. << " " << C_CYAN << "collections" << C_RESET << " List all collections\n"
  52. << " " << C_CYAN << "info" << C_RESET << " <collection> Collection info\n"
  53. << " " << C_CYAN << "find" << C_RESET << " <collection> List documents\n"
  54. << " " << C_CYAN << "get" << C_RESET << " <collection> <id> Get a document\n"
  55. << " " << C_CYAN << "upsert" << C_RESET << " <collection> <id> '<json>' Insert or update\n"
  56. << " " << C_CYAN << "remove" << C_RESET << " <collection> <id> Delete a document\n"
  57. << " " << C_CYAN << "count" << C_RESET << " <collection> Count documents\n"
  58. << " " << C_CYAN << "lock" << C_RESET << " Lock database (read-only)\n"
  59. << " " << C_CYAN << "unlock" << C_RESET << " Unlock database (accept writes)\n"
  60. << " " << C_CYAN << "status" << C_RESET << " Show read-only + recovery status\n"
  61. << " " << C_CYAN << "help" << C_RESET << " Show this help\n\n"
  62. << C_BOLD << "Offline helpers" << C_RESET << " " << C_DIM << "(no server connection required)" << C_RESET << ":\n"
  63. << " " << C_CYAN << "generate-auth-key" << C_RESET << " Emit a base64 32-byte API key\n"
  64. << " " << C_CYAN << "generate-tls-cert" << C_RESET << " --bind <addr> Generate a self-signed TLS cert + key\n"
  65. << " " << C_DIM << "[--out-cert PATH] [--out-key PATH] [--days N]" << C_RESET << "\n\n"
  66. << C_BOLD << "Options:" << C_RESET << "\n"
  67. << " --address HOST:PORT Database address (default: localhost:9004)\n";
  68. }
  69. // ---------------------------------------------------------------------------
  70. // v2.4 Stage F: offline helpers (no server connection required)
  71. // ---------------------------------------------------------------------------
  72. // Base64-encode raw bytes. Uses OpenSSL's EVP_EncodeBlock which emits the
  73. // standard base64 alphabet (no newlines) and pads with '='.
  74. std::string base64Encode(const unsigned char* data, size_t len) {
  75. if (len == 0) return {};
  76. // EVP_EncodeBlock writes ((len + 2) / 3) * 4 bytes + a NUL terminator.
  77. const size_t out_len = 4 * ((len + 2) / 3);
  78. std::string out(out_len, '\0');
  79. int written = EVP_EncodeBlock(
  80. reinterpret_cast<unsigned char*>(out.data()),
  81. data, static_cast<int>(len));
  82. if (written < 0) return {};
  83. out.resize(static_cast<size_t>(written));
  84. return out;
  85. }
  86. // Pull 32 cryptographically random bytes from getentropy() (preferred) or
  87. // /dev/urandom (fallback). Returns true on success.
  88. bool fillRandomBytes(unsigned char* buf, size_t len) {
  89. // getentropy() is limited to 256 bytes per call; our use-case is 32.
  90. if (len <= 256) {
  91. if (getentropy(buf, len) == 0) return true;
  92. }
  93. // Fallback: /dev/urandom.
  94. int fd = ::open("/dev/urandom", O_RDONLY | O_CLOEXEC);
  95. if (fd < 0) return false;
  96. size_t got = 0;
  97. while (got < len) {
  98. ssize_t n = ::read(fd, buf + got, len - got);
  99. if (n <= 0) {
  100. if (errno == EINTR) continue;
  101. ::close(fd);
  102. return false;
  103. }
  104. got += static_cast<size_t>(n);
  105. }
  106. ::close(fd);
  107. return true;
  108. }
  109. int cmdGenerateAuthKey() {
  110. unsigned char key[32];
  111. if (!fillRandomBytes(key, sizeof(key))) {
  112. std::fprintf(stderr, "error: failed to obtain entropy for key\n");
  113. return 1;
  114. }
  115. auto encoded = base64Encode(key, sizeof(key));
  116. if (encoded.empty()) {
  117. std::fprintf(stderr, "error: base64 encoding failed\n");
  118. return 1;
  119. }
  120. std::cout << encoded << "\n";
  121. return 0;
  122. }
  123. namespace {
  124. // Print the topmost OpenSSL error to stderr with a prefix.
  125. void printOpenSslError(const char* prefix) {
  126. unsigned long e = ERR_get_error();
  127. char buf[256] = {0};
  128. if (e != 0) {
  129. ERR_error_string_n(e, buf, sizeof(buf));
  130. std::fprintf(stderr, "error: %s: %s\n", prefix, buf);
  131. } else {
  132. std::fprintf(stderr, "error: %s\n", prefix);
  133. }
  134. }
  135. // Returns true if `s` parses as an IPv4 or IPv6 literal.
  136. bool looksLikeIp(const std::string& s) {
  137. unsigned char buf[16];
  138. if (inet_pton(AF_INET, s.c_str(), buf) == 1) return true;
  139. if (inet_pton(AF_INET6, s.c_str(), buf) == 1) return true;
  140. return false;
  141. }
  142. // Generate an RSA private key as an EVP_PKEY using the modern (3.x) API.
  143. EVP_PKEY* generateRsaKey(int bits) {
  144. EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr);
  145. if (!ctx) return nullptr;
  146. EVP_PKEY* pkey = nullptr;
  147. if (EVP_PKEY_keygen_init(ctx) <= 0) {
  148. EVP_PKEY_CTX_free(ctx);
  149. return nullptr;
  150. }
  151. if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits) <= 0) {
  152. EVP_PKEY_CTX_free(ctx);
  153. return nullptr;
  154. }
  155. if (EVP_PKEY_keygen(ctx, &pkey) <= 0) {
  156. EVP_PKEY_CTX_free(ctx);
  157. return nullptr;
  158. }
  159. EVP_PKEY_CTX_free(ctx);
  160. return pkey;
  161. }
  162. // Write a string to disk with the given file mode. Truncates existing files.
  163. bool writeFileWithMode(const std::string& path, const std::string& contents, mode_t mode) {
  164. int fd = ::open(path.c_str(),
  165. O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC,
  166. mode);
  167. if (fd < 0) {
  168. std::fprintf(stderr, "error: open %s: %s\n", path.c_str(), std::strerror(errno));
  169. return false;
  170. }
  171. // Re-assert mode in case the file pre-existed with a wider mode and
  172. // O_CREAT was a no-op (open(2) only applies the mode on create).
  173. if (fchmod(fd, mode) != 0) {
  174. std::fprintf(stderr, "error: fchmod %s: %s\n", path.c_str(), std::strerror(errno));
  175. ::close(fd);
  176. return false;
  177. }
  178. size_t off = 0;
  179. while (off < contents.size()) {
  180. ssize_t n = ::write(fd, contents.data() + off, contents.size() - off);
  181. if (n <= 0) {
  182. if (errno == EINTR) continue;
  183. std::fprintf(stderr, "error: write %s: %s\n", path.c_str(), std::strerror(errno));
  184. ::close(fd);
  185. return false;
  186. }
  187. off += static_cast<size_t>(n);
  188. }
  189. if (::close(fd) != 0) {
  190. std::fprintf(stderr, "error: close %s: %s\n", path.c_str(), std::strerror(errno));
  191. return false;
  192. }
  193. return true;
  194. }
  195. // Serialize an X509* to a PEM string.
  196. std::string pemEncodeCert(X509* cert) {
  197. BIO* bio = BIO_new(BIO_s_mem());
  198. if (!bio) return {};
  199. if (PEM_write_bio_X509(bio, cert) != 1) {
  200. BIO_free(bio);
  201. return {};
  202. }
  203. BUF_MEM* mem = nullptr;
  204. BIO_get_mem_ptr(bio, &mem);
  205. std::string out(mem->data, mem->length);
  206. BIO_free(bio);
  207. return out;
  208. }
  209. // Serialize an EVP_PKEY* to an unencrypted PEM string (PKCS#8 format via
  210. // PEM_write_bio_PrivateKey).
  211. std::string pemEncodeKey(EVP_PKEY* key) {
  212. BIO* bio = BIO_new(BIO_s_mem());
  213. if (!bio) return {};
  214. if (PEM_write_bio_PrivateKey(bio, key, nullptr, nullptr, 0, nullptr, nullptr) != 1) {
  215. BIO_free(bio);
  216. return {};
  217. }
  218. BUF_MEM* mem = nullptr;
  219. BIO_get_mem_ptr(bio, &mem);
  220. std::string out(mem->data, mem->length);
  221. BIO_free(bio);
  222. return out;
  223. }
  224. } // anonymous namespace
  225. int cmdGenerateTlsCert(const std::vector<std::string>& params) {
  226. std::string bind;
  227. std::string out_cert = "./server.pem";
  228. std::string out_key = "./server.key";
  229. int days = 3650;
  230. for (size_t i = 0; i < params.size(); ++i) {
  231. const auto& p = params[i];
  232. auto need = [&](const char* flag) -> const std::string* {
  233. if (i + 1 >= params.size()) {
  234. std::fprintf(stderr, "error: %s requires a value\n", flag);
  235. return nullptr;
  236. }
  237. return &params[++i];
  238. };
  239. if (p == "--bind") {
  240. auto* v = need("--bind"); if (!v) return 2; bind = *v;
  241. } else if (p == "--out-cert") {
  242. auto* v = need("--out-cert"); if (!v) return 2; out_cert = *v;
  243. } else if (p == "--out-key") {
  244. auto* v = need("--out-key"); if (!v) return 2; out_key = *v;
  245. } else if (p == "--days") {
  246. auto* v = need("--days"); if (!v) return 2;
  247. try { days = std::stoi(*v); }
  248. catch (...) {
  249. std::fprintf(stderr, "error: --days must be an integer\n");
  250. return 2;
  251. }
  252. if (days <= 0) {
  253. std::fprintf(stderr, "error: --days must be > 0\n");
  254. return 2;
  255. }
  256. } else {
  257. std::fprintf(stderr, "error: unknown argument: %s\n", p.c_str());
  258. return 2;
  259. }
  260. }
  261. if (bind.empty()) {
  262. std::fprintf(stderr,
  263. "usage: generate-tls-cert --bind <addr> "
  264. "[--out-cert PATH] [--out-key PATH] [--days N]\n");
  265. return 2;
  266. }
  267. // 1. RSA 4096-bit key.
  268. EVP_PKEY* pkey = generateRsaKey(4096);
  269. if (!pkey) {
  270. printOpenSslError("RSA key generation failed");
  271. return 1;
  272. }
  273. // 2. X.509 certificate.
  274. X509* cert = X509_new();
  275. if (!cert) {
  276. printOpenSslError("X509_new failed");
  277. EVP_PKEY_free(pkey);
  278. return 1;
  279. }
  280. // Version 3 (the integer field encodes v3 as 2).
  281. if (X509_set_version(cert, 2) != 1) {
  282. printOpenSslError("X509_set_version failed");
  283. X509_free(cert); EVP_PKEY_free(pkey);
  284. return 1;
  285. }
  286. // Random 64-bit serial number.
  287. {
  288. unsigned char serial_bytes[8];
  289. if (RAND_bytes(serial_bytes, sizeof(serial_bytes)) != 1) {
  290. printOpenSslError("RAND_bytes for serial failed");
  291. X509_free(cert); EVP_PKEY_free(pkey);
  292. return 1;
  293. }
  294. // Mask the top bit so the BIGNUM is positive.
  295. serial_bytes[0] &= 0x7F;
  296. BIGNUM* bn = BN_bin2bn(serial_bytes, sizeof(serial_bytes), nullptr);
  297. if (!bn) {
  298. printOpenSslError("BN_bin2bn failed");
  299. X509_free(cert); EVP_PKEY_free(pkey);
  300. return 1;
  301. }
  302. ASN1_INTEGER* ai = BN_to_ASN1_INTEGER(bn, nullptr);
  303. BN_free(bn);
  304. if (!ai) {
  305. printOpenSslError("BN_to_ASN1_INTEGER failed");
  306. X509_free(cert); EVP_PKEY_free(pkey);
  307. return 1;
  308. }
  309. if (X509_set_serialNumber(cert, ai) != 1) {
  310. printOpenSslError("X509_set_serialNumber failed");
  311. ASN1_INTEGER_free(ai);
  312. X509_free(cert); EVP_PKEY_free(pkey);
  313. return 1;
  314. }
  315. ASN1_INTEGER_free(ai);
  316. }
  317. // Validity period.
  318. if (!X509_gmtime_adj(X509_get_notBefore(cert), 0)) {
  319. printOpenSslError("X509_gmtime_adj(notBefore) failed");
  320. X509_free(cert); EVP_PKEY_free(pkey);
  321. return 1;
  322. }
  323. long seconds = static_cast<long>(days) * 24L * 60L * 60L;
  324. if (!X509_gmtime_adj(X509_get_notAfter(cert), seconds)) {
  325. printOpenSslError("X509_gmtime_adj(notAfter) failed");
  326. X509_free(cert); EVP_PKEY_free(pkey);
  327. return 1;
  328. }
  329. // Public key.
  330. if (X509_set_pubkey(cert, pkey) != 1) {
  331. printOpenSslError("X509_set_pubkey failed");
  332. X509_free(cert); EVP_PKEY_free(pkey);
  333. return 1;
  334. }
  335. // Subject + issuer name (self-signed, so identical).
  336. X509_NAME* name = X509_get_subject_name(cert);
  337. if (X509_NAME_add_entry_by_txt(
  338. name, "CN", MBSTRING_UTF8,
  339. reinterpret_cast<const unsigned char*>(bind.c_str()),
  340. -1, -1, 0) != 1) {
  341. printOpenSslError("X509_NAME_add_entry_by_txt(CN) failed");
  342. X509_free(cert); EVP_PKEY_free(pkey);
  343. return 1;
  344. }
  345. if (X509_set_issuer_name(cert, name) != 1) {
  346. printOpenSslError("X509_set_issuer_name failed");
  347. X509_free(cert); EVP_PKEY_free(pkey);
  348. return 1;
  349. }
  350. // SubjectAltName.
  351. {
  352. std::string san = "DNS:localhost,IP:127.0.0.1";
  353. if (bind != "localhost" && bind != "127.0.0.1") {
  354. san += ',';
  355. san += looksLikeIp(bind) ? "IP:" : "DNS:";
  356. san += bind;
  357. }
  358. X509_EXTENSION* ext = X509V3_EXT_conf_nid(
  359. nullptr, nullptr, NID_subject_alt_name, san.c_str());
  360. if (!ext) {
  361. printOpenSslError("X509V3_EXT_conf_nid(SAN) failed");
  362. X509_free(cert); EVP_PKEY_free(pkey);
  363. return 1;
  364. }
  365. if (X509_add_ext(cert, ext, -1) != 1) {
  366. printOpenSslError("X509_add_ext(SAN) failed");
  367. X509_EXTENSION_free(ext);
  368. X509_free(cert); EVP_PKEY_free(pkey);
  369. return 1;
  370. }
  371. X509_EXTENSION_free(ext);
  372. }
  373. // basicConstraints CA:FALSE — a server leaf cert, not a CA.
  374. {
  375. X509_EXTENSION* ext = X509V3_EXT_conf_nid(
  376. nullptr, nullptr, NID_basic_constraints, "critical,CA:FALSE");
  377. if (ext) {
  378. X509_add_ext(cert, ext, -1);
  379. X509_EXTENSION_free(ext);
  380. }
  381. }
  382. // Sign with SHA-256.
  383. if (X509_sign(cert, pkey, EVP_sha256()) == 0) {
  384. printOpenSslError("X509_sign failed");
  385. X509_free(cert); EVP_PKEY_free(pkey);
  386. return 1;
  387. }
  388. // Serialize.
  389. std::string cert_pem = pemEncodeCert(cert);
  390. std::string key_pem = pemEncodeKey(pkey);
  391. X509_free(cert);
  392. EVP_PKEY_free(pkey);
  393. if (cert_pem.empty() || key_pem.empty()) {
  394. std::fprintf(stderr, "error: PEM encoding failed\n");
  395. return 1;
  396. }
  397. // Write key first (0600), then cert (0644). If either fails, the other
  398. // may have been written — that's acceptable; the operator will see the
  399. // error and retry.
  400. if (!writeFileWithMode(out_key, key_pem, 0600)) return 1;
  401. if (!writeFileWithMode(out_cert, cert_pem, 0644)) return 1;
  402. std::cout << "Wrote cert: " << out_cert << "\n"
  403. << "Wrote key: " << out_key << "\n";
  404. return 0;
  405. }
  406. bool execCommand(smartbotic::database::Client& client,
  407. const std::string& cmd, const std::vector<std::string>& params) {
  408. try {
  409. if (cmd == "collections") {
  410. auto collections = client.listCollections();
  411. std::cout << C_BOLD << "Collections:" << C_RESET << "\n";
  412. for (const auto& c : collections) {
  413. auto info = client.getCollectionInfo(c);
  414. int64_t count = 0;
  415. if (info) count = info->documentCount;
  416. std::cout << " " << C_CYAN << c << C_RESET
  417. << C_DIM << " (" << count << " docs)" << C_RESET << "\n";
  418. }
  419. return true;
  420. }
  421. if (cmd == "info") {
  422. if (params.empty()) { printError("usage: info <collection>"); return false; }
  423. auto info = client.getCollectionInfo(params[0]);
  424. if (!info) { printError("collection not found: " + params[0]); return false; }
  425. std::cout << C_BOLD << params[0] << C_RESET << ":\n"
  426. << " documents: " << info->documentCount << "\n"
  427. << " size: " << info->sizeBytes << " bytes\n"
  428. << " encrypted: " << (info->encrypted ? "yes" : "no") << "\n"
  429. << " max_versions: " << info->maxVersions << "\n";
  430. if (info->defaultTtlSeconds > 0)
  431. std::cout << " ttl: " << info->defaultTtlSeconds << "s\n";
  432. return true;
  433. }
  434. if (cmd == "find") {
  435. if (params.empty()) { printError("usage: find <collection> [--limit N] [--exists FIELD]"); return false; }
  436. smartbotic::database::Client::QueryOptions opts;
  437. opts.limit = 100;
  438. for (size_t i = 1; i < params.size(); ++i) {
  439. if (params[i] == "--limit" && i + 1 < params.size()) {
  440. opts.limit = static_cast<uint32_t>(std::stoul(params[++i]));
  441. } else if (params[i] == "--exists" && i + 1 < params.size()) {
  442. opts.filters.emplace_back(params[++i], smartbotic::database::Client::FilterOp::EXISTS, true);
  443. }
  444. }
  445. auto docs = client.find(params[0], opts);
  446. std::cout << C_DIM << "(" << docs.size() << " documents)" << C_RESET << "\n";
  447. for (const auto& doc : docs) {
  448. auto id = doc.value("_id", "");
  449. // Print compact summary line
  450. std::cout << C_GREEN << id << C_RESET;
  451. // Show a few key fields
  452. for (const auto& [k, v] : doc.items()) {
  453. if (k == "_id" || k == "_created_at" || k == "_updated_at") continue;
  454. auto val = v.dump();
  455. if (val.size() > 60) val = val.substr(0, 57) + "...";
  456. std::cout << " " << C_DIM << k << "=" << C_RESET << val;
  457. // Limit to 3 fields per line
  458. static int field_count = 0;
  459. if (++field_count >= 3) { field_count = 0; break; }
  460. }
  461. std::cout << "\n";
  462. }
  463. return true;
  464. }
  465. if (cmd == "get") {
  466. if (params.size() < 2) { printError("usage: get <collection> <id>"); return false; }
  467. auto doc = client.get(params[0], params[1]);
  468. if (!doc) { printError("not found: " + params[0] + "/" + params[1]); return false; }
  469. printJson(*doc);
  470. return true;
  471. }
  472. if (cmd == "upsert") {
  473. if (params.size() < 3) { printError("usage: upsert <collection> <id> '<json>'"); return false; }
  474. auto data = json::parse(params[2], nullptr, false);
  475. if (!data.is_object()) { printError("invalid JSON: " + params[2]); return false; }
  476. client.upsert(params[0], data, params[1]);
  477. std::cout << C_GREEN << "ok" << C_RESET << " " << params[0] << "/" << params[1] << "\n";
  478. return true;
  479. }
  480. if (cmd == "remove" || cmd == "delete") {
  481. if (params.size() < 2) { printError("usage: remove <collection> <id>"); return false; }
  482. client.remove(params[0], params[1]);
  483. std::cout << C_GREEN << "ok" << C_RESET << " removed " << params[0] << "/" << params[1] << "\n";
  484. return true;
  485. }
  486. if (cmd == "count") {
  487. if (params.empty()) { printError("usage: count <collection>"); return false; }
  488. auto info = client.getCollectionInfo(params[0]);
  489. if (!info) { printError("collection not found: " + params[0]); return false; }
  490. std::cout << info->documentCount << "\n";
  491. return true;
  492. }
  493. if (cmd == "lock") {
  494. if (client.setReadOnly(true)) {
  495. std::cout << C_GREEN << "ok" << C_RESET << " Database locked (read-only)\n";
  496. return true;
  497. }
  498. printError("failed to lock database");
  499. return false;
  500. }
  501. if (cmd == "unlock") {
  502. if (client.setReadOnly(false)) {
  503. std::cout << C_GREEN << "ok" << C_RESET << " Database unlocked (writes accepted)\n";
  504. return true;
  505. }
  506. printError("failed to unlock database");
  507. return false;
  508. }
  509. if (cmd == "status") {
  510. auto s = client.getReadOnlyStatus();
  511. std::cout << "Read-only: " << (s.readOnly ? (std::string(C_RED) + "YES" + C_RESET) : "no") << "\n";
  512. if (s.readOnly) {
  513. std::cout << "Reason: " << s.reason << "\n";
  514. }
  515. std::cout << "Recovery outcome: " << s.recoveryOutcome << "\n";
  516. if (!s.expectedSnapshot.empty()) {
  517. std::cout << "Expected snapshot: " << s.expectedSnapshot << "\n";
  518. }
  519. if (!s.snapshotUsed.empty()) {
  520. std::cout << "Snapshot used: " << s.snapshotUsed << "\n";
  521. }
  522. if (!s.failureReason.empty()) {
  523. std::cout << "Failure reason: " << s.failureReason << "\n";
  524. }
  525. std::cout << "WAL replayed: " << s.walEntriesReplayed << " entries\n";
  526. std::cout << "Snapshots tried: " << s.snapshotsAttempted << "\n";
  527. return true;
  528. }
  529. if (cmd == "help" || cmd == "?") {
  530. printUsage();
  531. return true;
  532. }
  533. printError("unknown command: " + cmd + " (try 'help')");
  534. return false;
  535. } catch (const std::exception& e) {
  536. printError(e.what());
  537. return false;
  538. }
  539. }
  540. // Tokenize a line, respecting single/double quotes and JSON braces
  541. std::vector<std::string> tokenize(const std::string& line) {
  542. std::vector<std::string> tokens;
  543. std::string current;
  544. int brace_depth = 0;
  545. char in_quote = 0;
  546. for (size_t i = 0; i < line.size(); ++i) {
  547. char c = line[i];
  548. if (in_quote) {
  549. current += c;
  550. if (c == in_quote && (i == 0 || line[i-1] != '\\')) {
  551. in_quote = 0;
  552. // Strip surrounding quotes for simple string tokens
  553. if (brace_depth == 0 && current.size() >= 2
  554. && (current.front() == '\'' || current.front() == '"')
  555. && current.front() == current.back()) {
  556. current = current.substr(1, current.size() - 2);
  557. }
  558. }
  559. continue;
  560. }
  561. if (c == '\'' || c == '"') {
  562. in_quote = c;
  563. current += c;
  564. continue;
  565. }
  566. if (c == '{') { brace_depth++; current += c; continue; }
  567. if (c == '}') {
  568. brace_depth--;
  569. current += c;
  570. if (brace_depth <= 0) {
  571. brace_depth = 0;
  572. tokens.push_back(current);
  573. current.clear();
  574. }
  575. continue;
  576. }
  577. if (brace_depth > 0) { current += c; continue; }
  578. if (c == ' ' || c == '\t') {
  579. if (!current.empty()) {
  580. tokens.push_back(current);
  581. current.clear();
  582. }
  583. continue;
  584. }
  585. current += c;
  586. }
  587. if (!current.empty()) tokens.push_back(current);
  588. return tokens;
  589. }
  590. } // anonymous namespace
  591. int main(int argc, char* argv[]) {
  592. Args args;
  593. // Parse flags
  594. std::vector<std::string> positional;
  595. for (int i = 1; i < argc; ++i) {
  596. std::string arg = argv[i];
  597. if (arg == "--address" && i + 1 < argc) {
  598. args.address = argv[++i];
  599. } else if (arg == "--help" || arg == "-h") {
  600. printUsage();
  601. return 0;
  602. } else {
  603. positional.push_back(arg);
  604. }
  605. }
  606. if (!positional.empty()) {
  607. args.command = positional[0];
  608. args.params.assign(positional.begin() + 1, positional.end());
  609. }
  610. // Offline helpers — these don't need a running server, so dispatch
  611. // them before opening the gRPC channel.
  612. if (args.command == "generate-auth-key") {
  613. return cmdGenerateAuthKey();
  614. }
  615. if (args.command == "generate-tls-cert") {
  616. return cmdGenerateTlsCert(args.params);
  617. }
  618. // Connect
  619. smartbotic::database::Client client({.address = args.address});
  620. client.connect();
  621. // Scriptable mode: single command
  622. if (!args.command.empty()) {
  623. return execCommand(client, args.command, args.params) ? 0 : 1;
  624. }
  625. // Interactive mode
  626. std::cout << C_BOLD << "smartbotic-db-cli" << C_RESET
  627. << " connected to " << C_CYAN << args.address << C_RESET << "\n"
  628. << C_DIM << "Type 'help' for commands, 'exit' to quit." << C_RESET << "\n";
  629. std::string line;
  630. while (true) {
  631. std::cout << C_YELLOW << "> " << C_RESET;
  632. if (!std::getline(std::cin, line)) break;
  633. // Trim
  634. auto start = line.find_first_not_of(" \t");
  635. if (start == std::string::npos) continue;
  636. line = line.substr(start);
  637. if (line == "exit" || line == "quit" || line == "q") break;
  638. if (line.empty()) continue;
  639. auto tokens = tokenize(line);
  640. if (tokens.empty()) continue;
  641. auto cmd = tokens[0];
  642. std::vector<std::string> params(tokens.begin() + 1, tokens.end());
  643. execCommand(client, cmd, params);
  644. }
  645. return 0;
  646. }