main.cpp 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147
  1. #include <smartbotic/database/client.hpp>
  2. #include <nlohmann/json.hpp>
  3. #include "storage/subdb_placement.hpp"
  4. #include <openssl/bio.h>
  5. #include <openssl/bn.h>
  6. #include <openssl/err.h>
  7. #include <openssl/evp.h>
  8. #include <openssl/pem.h>
  9. #include <openssl/rand.h>
  10. #include <openssl/x509.h>
  11. #include <openssl/x509v3.h>
  12. #include <arpa/inet.h>
  13. #include <fcntl.h>
  14. #include <sys/random.h>
  15. #include <sys/stat.h>
  16. #include <unistd.h>
  17. #include <cerrno>
  18. #include <cstdio>
  19. #include <cstring>
  20. #include <fstream>
  21. #include <iostream>
  22. #include <sstream>
  23. #include <string>
  24. #include <vector>
  25. using json = nlohmann::json;
  26. namespace {
  27. struct Args {
  28. std::string address = "localhost:9004";
  29. // v2.11.0 T6b — needed to exercise relation management against a
  30. // non-default project from the CLI. Empty means the client's own
  31. // "default" (unchanged behaviour for every existing command).
  32. std::string project;
  33. std::string command;
  34. std::vector<std::string> params;
  35. };
  36. // ANSI colors
  37. constexpr auto C_RESET = "\033[0m";
  38. constexpr auto C_BOLD = "\033[1m";
  39. constexpr auto C_DIM = "\033[2m";
  40. constexpr auto C_CYAN = "\033[36m";
  41. constexpr auto C_GREEN = "\033[32m";
  42. constexpr auto C_RED = "\033[31m";
  43. constexpr auto C_YELLOW = "\033[33m";
  44. void printJson(const json& j) {
  45. std::cout << j.dump(2) << "\n";
  46. }
  47. void printError(const std::string& msg) {
  48. std::cerr << C_RED << "error: " << C_RESET << msg << "\n";
  49. }
  50. void printUsage() {
  51. std::cout << C_BOLD << "smartbotic-db-cli" << C_RESET << " — admin tool for smartbotic-database\n\n"
  52. << C_BOLD << "Usage:" << C_RESET << "\n"
  53. << " smartbotic-db-cli [--address HOST:PORT] <command> [args...]\n"
  54. << " smartbotic-db-cli [--address HOST:PORT] " << C_DIM << "# interactive mode" << C_RESET << "\n\n"
  55. << C_BOLD << "Commands:" << C_RESET << "\n"
  56. << " " << C_CYAN << "collections" << C_RESET << " List all collections\n"
  57. << " " << C_CYAN << "info" << C_RESET << " <collection> Collection info\n"
  58. << " " << C_CYAN << "find" << C_RESET << " <collection> List documents\n"
  59. << " " << C_CYAN << "get" << C_RESET << " <collection> <id> Get a document\n"
  60. << " " << C_CYAN << "upsert" << C_RESET << " <collection> <id> '<json>' Insert or update\n"
  61. << " " << C_CYAN << "remove" << C_RESET << " <collection> <id> Delete a document\n"
  62. << " " << C_CYAN << "count" << C_RESET << " <collection> Count documents\n"
  63. << " " << C_CYAN << "lock" << C_RESET << " Lock database (read-only)\n"
  64. << " " << C_CYAN << "unlock" << C_RESET << " Unlock database (accept writes)\n"
  65. << " " << C_CYAN << "status" << C_RESET << " Show read-only + recovery status\n"
  66. << C_BOLD << " Access policy (v2.7.0+)" << C_RESET << "\n"
  67. << " " << C_CYAN << "security" << C_RESET << " [project] Show whether a project enforces policy\n"
  68. << " " << C_CYAN << "indexes" << C_RESET << " <collection>"
  69. << " list secondary indexes\n"
  70. << " " << C_CYAN << "index-create" << C_RESET << " <collection> <field>"
  71. << " declare an index and backfill it\n"
  72. << " " << C_CYAN << "index-drop" << C_RESET << " <collection> <field>"
  73. << " remove an index\n"
  74. << " " << C_CYAN << "index-values" << C_RESET << " <coll> <field> [n] [asc|desc]"
  75. << " distinct values + counts\n"
  76. << C_BOLD << " Relations / referential integrity (v2.11.0+)" << C_RESET << "\n"
  77. << " " << C_CYAN << "relations" << C_RESET
  78. << " List declared relations\n"
  79. << " " << C_CYAN << "relation" << C_RESET << " <name>"
  80. << " Show one relation's declaration\n"
  81. << " " << C_CYAN << "relation-create" << C_RESET
  82. << " <name> <child> <child_field> <parent> [on_delete] [validate_on_write]\n"
  83. << " " << C_CYAN << "relation-drop" << C_RESET << " <name>\n"
  84. << " " << C_CYAN << "configure-relations" << C_RESET
  85. << " <collection> <on|off> Enable/disable enforcement for a collection\n"
  86. << " " << C_CYAN << "security-set" << C_RESET << " <project> <on|off> [enforce|audit]\n"
  87. << " " << C_CYAN << "policies" << C_RESET << " [project] List principals with a policy\n"
  88. << " " << C_CYAN << "policy" << C_RESET << " <project> <principal> Show one policy\n"
  89. << " " << C_CYAN << "policy-set" << C_RESET << " <project> <principal> '<json>'\n"
  90. << " " << C_CYAN << "policy-rm" << C_RESET << " <project> <principal>\n"
  91. << " " << C_CYAN << "help" << C_RESET << " Show this help\n\n"
  92. << C_BOLD << "Offline helpers" << C_RESET << " " << C_DIM << "(no server connection required)" << C_RESET << ":\n"
  93. << " " << C_CYAN << "generate-auth-key" << C_RESET << " Emit a base64 32-byte API key\n"
  94. << " " << C_CYAN << "generate-tls-cert" << C_RESET << " --bind <addr> Generate a self-signed TLS cert + key\n"
  95. << " " << C_DIM << "[--out-cert PATH] [--out-key PATH] [--days N]" << C_RESET << "\n"
  96. << " " << C_CYAN << "verify-subdbs" << C_RESET << " --env <path> Report documents filed under the wrong collection\n"
  97. << " " << C_DIM << "[--project NAME] read-only; safe against a running server" << C_RESET << "\n"
  98. << " " << C_CYAN << "reconcile-subdbs" << C_RESET << " --env <path> Repair misfiled documents (dry run by default)\n"
  99. << " " << C_DIM << "[--project NAME] [--apply] STOP THE SERVICE and back up before --apply" << C_RESET << "\n\n"
  100. << C_BOLD << "Options:" << C_RESET << "\n"
  101. << " --address HOST:PORT Database address (default: localhost:9004)\n"
  102. << " --project NAME Operate as this project namespace (default: default)\n";
  103. }
  104. // ---------------------------------------------------------------------------
  105. // v2.4 Stage F: offline helpers (no server connection required)
  106. // ---------------------------------------------------------------------------
  107. // Base64-encode raw bytes. Uses OpenSSL's EVP_EncodeBlock which emits the
  108. // standard base64 alphabet (no newlines) and pads with '='.
  109. std::string base64Encode(const unsigned char* data, size_t len) {
  110. if (len == 0) return {};
  111. // EVP_EncodeBlock writes ((len + 2) / 3) * 4 bytes + a NUL terminator.
  112. const size_t out_len = 4 * ((len + 2) / 3);
  113. std::string out(out_len, '\0');
  114. int written = EVP_EncodeBlock(
  115. reinterpret_cast<unsigned char*>(out.data()),
  116. data, static_cast<int>(len));
  117. if (written < 0) return {};
  118. out.resize(static_cast<size_t>(written));
  119. return out;
  120. }
  121. // Pull 32 cryptographically random bytes from getentropy() (preferred) or
  122. // /dev/urandom (fallback). Returns true on success.
  123. bool fillRandomBytes(unsigned char* buf, size_t len) {
  124. // getentropy() is limited to 256 bytes per call; our use-case is 32.
  125. if (len <= 256) {
  126. if (getentropy(buf, len) == 0) return true;
  127. }
  128. // Fallback: /dev/urandom.
  129. int fd = ::open("/dev/urandom", O_RDONLY | O_CLOEXEC);
  130. if (fd < 0) return false;
  131. size_t got = 0;
  132. while (got < len) {
  133. ssize_t n = ::read(fd, buf + got, len - got);
  134. if (n <= 0) {
  135. if (errno == EINTR) continue;
  136. ::close(fd);
  137. return false;
  138. }
  139. got += static_cast<size_t>(n);
  140. }
  141. ::close(fd);
  142. return true;
  143. }
  144. int cmdGenerateAuthKey() {
  145. unsigned char key[32];
  146. if (!fillRandomBytes(key, sizeof(key))) {
  147. std::fprintf(stderr, "error: failed to obtain entropy for key\n");
  148. return 1;
  149. }
  150. auto encoded = base64Encode(key, sizeof(key));
  151. if (encoded.empty()) {
  152. std::fprintf(stderr, "error: base64 encoding failed\n");
  153. return 1;
  154. }
  155. std::cout << encoded << "\n";
  156. return 0;
  157. }
  158. namespace {
  159. // Print the topmost OpenSSL error to stderr with a prefix.
  160. void printOpenSslError(const char* prefix) {
  161. unsigned long e = ERR_get_error();
  162. char buf[256] = {0};
  163. if (e != 0) {
  164. ERR_error_string_n(e, buf, sizeof(buf));
  165. std::fprintf(stderr, "error: %s: %s\n", prefix, buf);
  166. } else {
  167. std::fprintf(stderr, "error: %s\n", prefix);
  168. }
  169. }
  170. // Returns true if `s` parses as an IPv4 or IPv6 literal.
  171. bool looksLikeIp(const std::string& s) {
  172. unsigned char buf[16];
  173. if (inet_pton(AF_INET, s.c_str(), buf) == 1) return true;
  174. if (inet_pton(AF_INET6, s.c_str(), buf) == 1) return true;
  175. return false;
  176. }
  177. // Generate an RSA private key as an EVP_PKEY using the modern (3.x) API.
  178. EVP_PKEY* generateRsaKey(int bits) {
  179. EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr);
  180. if (!ctx) return nullptr;
  181. EVP_PKEY* pkey = nullptr;
  182. if (EVP_PKEY_keygen_init(ctx) <= 0) {
  183. EVP_PKEY_CTX_free(ctx);
  184. return nullptr;
  185. }
  186. if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits) <= 0) {
  187. EVP_PKEY_CTX_free(ctx);
  188. return nullptr;
  189. }
  190. if (EVP_PKEY_keygen(ctx, &pkey) <= 0) {
  191. EVP_PKEY_CTX_free(ctx);
  192. return nullptr;
  193. }
  194. EVP_PKEY_CTX_free(ctx);
  195. return pkey;
  196. }
  197. // Write a string to disk with the given file mode. Truncates existing files.
  198. bool writeFileWithMode(const std::string& path, const std::string& contents, mode_t mode) {
  199. int fd = ::open(path.c_str(),
  200. O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC,
  201. mode);
  202. if (fd < 0) {
  203. std::fprintf(stderr, "error: open %s: %s\n", path.c_str(), std::strerror(errno));
  204. return false;
  205. }
  206. // Re-assert mode in case the file pre-existed with a wider mode and
  207. // O_CREAT was a no-op (open(2) only applies the mode on create).
  208. if (fchmod(fd, mode) != 0) {
  209. std::fprintf(stderr, "error: fchmod %s: %s\n", path.c_str(), std::strerror(errno));
  210. ::close(fd);
  211. return false;
  212. }
  213. size_t off = 0;
  214. while (off < contents.size()) {
  215. ssize_t n = ::write(fd, contents.data() + off, contents.size() - off);
  216. if (n <= 0) {
  217. if (errno == EINTR) continue;
  218. std::fprintf(stderr, "error: write %s: %s\n", path.c_str(), std::strerror(errno));
  219. ::close(fd);
  220. return false;
  221. }
  222. off += static_cast<size_t>(n);
  223. }
  224. if (::close(fd) != 0) {
  225. std::fprintf(stderr, "error: close %s: %s\n", path.c_str(), std::strerror(errno));
  226. return false;
  227. }
  228. return true;
  229. }
  230. // Serialize an X509* to a PEM string.
  231. std::string pemEncodeCert(X509* cert) {
  232. BIO* bio = BIO_new(BIO_s_mem());
  233. if (!bio) return {};
  234. if (PEM_write_bio_X509(bio, cert) != 1) {
  235. BIO_free(bio);
  236. return {};
  237. }
  238. BUF_MEM* mem = nullptr;
  239. BIO_get_mem_ptr(bio, &mem);
  240. std::string out(mem->data, mem->length);
  241. BIO_free(bio);
  242. return out;
  243. }
  244. // Serialize an EVP_PKEY* to an unencrypted PEM string (PKCS#8 format via
  245. // PEM_write_bio_PrivateKey).
  246. std::string pemEncodeKey(EVP_PKEY* key) {
  247. BIO* bio = BIO_new(BIO_s_mem());
  248. if (!bio) return {};
  249. if (PEM_write_bio_PrivateKey(bio, key, nullptr, nullptr, 0, nullptr, nullptr) != 1) {
  250. BIO_free(bio);
  251. return {};
  252. }
  253. BUF_MEM* mem = nullptr;
  254. BIO_get_mem_ptr(bio, &mem);
  255. std::string out(mem->data, mem->length);
  256. BIO_free(bio);
  257. return out;
  258. }
  259. } // anonymous namespace
  260. int cmdGenerateTlsCert(const std::vector<std::string>& params) {
  261. std::string bind;
  262. std::string out_cert = "./server.pem";
  263. std::string out_key = "./server.key";
  264. int days = 3650;
  265. for (size_t i = 0; i < params.size(); ++i) {
  266. const auto& p = params[i];
  267. auto need = [&](const char* flag) -> const std::string* {
  268. if (i + 1 >= params.size()) {
  269. std::fprintf(stderr, "error: %s requires a value\n", flag);
  270. return nullptr;
  271. }
  272. return &params[++i];
  273. };
  274. if (p == "--bind") {
  275. auto* v = need("--bind"); if (!v) return 2; bind = *v;
  276. } else if (p == "--out-cert") {
  277. auto* v = need("--out-cert"); if (!v) return 2; out_cert = *v;
  278. } else if (p == "--out-key") {
  279. auto* v = need("--out-key"); if (!v) return 2; out_key = *v;
  280. } else if (p == "--days") {
  281. auto* v = need("--days"); if (!v) return 2;
  282. try { days = std::stoi(*v); }
  283. catch (...) {
  284. std::fprintf(stderr, "error: --days must be an integer\n");
  285. return 2;
  286. }
  287. if (days <= 0) {
  288. std::fprintf(stderr, "error: --days must be > 0\n");
  289. return 2;
  290. }
  291. } else {
  292. std::fprintf(stderr, "error: unknown argument: %s\n", p.c_str());
  293. return 2;
  294. }
  295. }
  296. if (bind.empty()) {
  297. std::fprintf(stderr,
  298. "usage: generate-tls-cert --bind <addr> "
  299. "[--out-cert PATH] [--out-key PATH] [--days N]\n");
  300. return 2;
  301. }
  302. // 1. RSA 4096-bit key.
  303. EVP_PKEY* pkey = generateRsaKey(4096);
  304. if (!pkey) {
  305. printOpenSslError("RSA key generation failed");
  306. return 1;
  307. }
  308. // 2. X.509 certificate.
  309. X509* cert = X509_new();
  310. if (!cert) {
  311. printOpenSslError("X509_new failed");
  312. EVP_PKEY_free(pkey);
  313. return 1;
  314. }
  315. // Version 3 (the integer field encodes v3 as 2).
  316. if (X509_set_version(cert, 2) != 1) {
  317. printOpenSslError("X509_set_version failed");
  318. X509_free(cert); EVP_PKEY_free(pkey);
  319. return 1;
  320. }
  321. // Random 64-bit serial number.
  322. {
  323. unsigned char serial_bytes[8];
  324. if (RAND_bytes(serial_bytes, sizeof(serial_bytes)) != 1) {
  325. printOpenSslError("RAND_bytes for serial failed");
  326. X509_free(cert); EVP_PKEY_free(pkey);
  327. return 1;
  328. }
  329. // Mask the top bit so the BIGNUM is positive.
  330. serial_bytes[0] &= 0x7F;
  331. BIGNUM* bn = BN_bin2bn(serial_bytes, sizeof(serial_bytes), nullptr);
  332. if (!bn) {
  333. printOpenSslError("BN_bin2bn failed");
  334. X509_free(cert); EVP_PKEY_free(pkey);
  335. return 1;
  336. }
  337. ASN1_INTEGER* ai = BN_to_ASN1_INTEGER(bn, nullptr);
  338. BN_free(bn);
  339. if (!ai) {
  340. printOpenSslError("BN_to_ASN1_INTEGER failed");
  341. X509_free(cert); EVP_PKEY_free(pkey);
  342. return 1;
  343. }
  344. if (X509_set_serialNumber(cert, ai) != 1) {
  345. printOpenSslError("X509_set_serialNumber failed");
  346. ASN1_INTEGER_free(ai);
  347. X509_free(cert); EVP_PKEY_free(pkey);
  348. return 1;
  349. }
  350. ASN1_INTEGER_free(ai);
  351. }
  352. // Validity period.
  353. if (!X509_gmtime_adj(X509_get_notBefore(cert), 0)) {
  354. printOpenSslError("X509_gmtime_adj(notBefore) failed");
  355. X509_free(cert); EVP_PKEY_free(pkey);
  356. return 1;
  357. }
  358. long seconds = static_cast<long>(days) * 24L * 60L * 60L;
  359. if (!X509_gmtime_adj(X509_get_notAfter(cert), seconds)) {
  360. printOpenSslError("X509_gmtime_adj(notAfter) failed");
  361. X509_free(cert); EVP_PKEY_free(pkey);
  362. return 1;
  363. }
  364. // Public key.
  365. if (X509_set_pubkey(cert, pkey) != 1) {
  366. printOpenSslError("X509_set_pubkey failed");
  367. X509_free(cert); EVP_PKEY_free(pkey);
  368. return 1;
  369. }
  370. // Subject + issuer name (self-signed, so identical).
  371. X509_NAME* name = X509_get_subject_name(cert);
  372. if (X509_NAME_add_entry_by_txt(
  373. name, "CN", MBSTRING_UTF8,
  374. reinterpret_cast<const unsigned char*>(bind.c_str()),
  375. -1, -1, 0) != 1) {
  376. printOpenSslError("X509_NAME_add_entry_by_txt(CN) failed");
  377. X509_free(cert); EVP_PKEY_free(pkey);
  378. return 1;
  379. }
  380. if (X509_set_issuer_name(cert, name) != 1) {
  381. printOpenSslError("X509_set_issuer_name failed");
  382. X509_free(cert); EVP_PKEY_free(pkey);
  383. return 1;
  384. }
  385. // SubjectAltName.
  386. {
  387. std::string san = "DNS:localhost,IP:127.0.0.1";
  388. if (bind != "localhost" && bind != "127.0.0.1") {
  389. san += ',';
  390. san += looksLikeIp(bind) ? "IP:" : "DNS:";
  391. san += bind;
  392. }
  393. X509_EXTENSION* ext = X509V3_EXT_conf_nid(
  394. nullptr, nullptr, NID_subject_alt_name, san.c_str());
  395. if (!ext) {
  396. printOpenSslError("X509V3_EXT_conf_nid(SAN) failed");
  397. X509_free(cert); EVP_PKEY_free(pkey);
  398. return 1;
  399. }
  400. if (X509_add_ext(cert, ext, -1) != 1) {
  401. printOpenSslError("X509_add_ext(SAN) failed");
  402. X509_EXTENSION_free(ext);
  403. X509_free(cert); EVP_PKEY_free(pkey);
  404. return 1;
  405. }
  406. X509_EXTENSION_free(ext);
  407. }
  408. // basicConstraints CA:FALSE — a server leaf cert, not a CA.
  409. {
  410. X509_EXTENSION* ext = X509V3_EXT_conf_nid(
  411. nullptr, nullptr, NID_basic_constraints, "critical,CA:FALSE");
  412. if (ext) {
  413. X509_add_ext(cert, ext, -1);
  414. X509_EXTENSION_free(ext);
  415. }
  416. }
  417. // Sign with SHA-256.
  418. if (X509_sign(cert, pkey, EVP_sha256()) == 0) {
  419. printOpenSslError("X509_sign failed");
  420. X509_free(cert); EVP_PKEY_free(pkey);
  421. return 1;
  422. }
  423. // Serialize.
  424. std::string cert_pem = pemEncodeCert(cert);
  425. std::string key_pem = pemEncodeKey(pkey);
  426. X509_free(cert);
  427. EVP_PKEY_free(pkey);
  428. if (cert_pem.empty() || key_pem.empty()) {
  429. std::fprintf(stderr, "error: PEM encoding failed\n");
  430. return 1;
  431. }
  432. // Write key first (0600), then cert (0644). If either fails, the other
  433. // may have been written — that's acceptable; the operator will see the
  434. // error and retry.
  435. if (!writeFileWithMode(out_key, key_pem, 0600)) return 1;
  436. if (!writeFileWithMode(out_cert, cert_pem, 0644)) return 1;
  437. std::cout << "Wrote cert: " << out_cert << "\n"
  438. << "Wrote key: " << out_key << "\n";
  439. return 0;
  440. }
  441. bool execCommand(smartbotic::database::Client& client,
  442. const std::string& cmd, const std::vector<std::string>& params) {
  443. try {
  444. if (cmd == "collections") {
  445. auto collections = client.listCollections();
  446. std::cout << C_BOLD << "Collections:" << C_RESET << "\n";
  447. for (const auto& c : collections) {
  448. auto info = client.getCollectionInfo(c);
  449. int64_t count = 0;
  450. if (info) count = info->documentCount;
  451. std::cout << " " << C_CYAN << c << C_RESET
  452. << C_DIM << " (" << count << " docs)" << C_RESET << "\n";
  453. }
  454. return true;
  455. }
  456. if (cmd == "info") {
  457. if (params.empty()) { printError("usage: info <collection>"); return false; }
  458. auto info = client.getCollectionInfo(params[0]);
  459. if (!info) { printError("collection not found: " + params[0]); return false; }
  460. std::cout << C_BOLD << params[0] << C_RESET << ":\n"
  461. << " documents: " << info->documentCount << "\n"
  462. << " size: " << info->sizeBytes << " bytes\n"
  463. << " encrypted: " << (info->encrypted ? "yes" : "no") << "\n"
  464. << " max_versions: " << info->maxVersions << "\n";
  465. if (info->defaultTtlSeconds > 0)
  466. std::cout << " ttl: " << info->defaultTtlSeconds << "s\n";
  467. return true;
  468. }
  469. if (cmd == "find") {
  470. if (params.empty()) { printError("usage: find <collection> [--limit N] [--exists FIELD]"); return false; }
  471. smartbotic::database::Client::QueryOptions opts;
  472. opts.limit = 100;
  473. for (size_t i = 1; i < params.size(); ++i) {
  474. if (params[i] == "--limit" && i + 1 < params.size()) {
  475. opts.limit = static_cast<uint32_t>(std::stoul(params[++i]));
  476. } else if (params[i] == "--exists" && i + 1 < params.size()) {
  477. opts.filters.emplace_back(params[++i], smartbotic::database::Client::FilterOp::EXISTS, true);
  478. }
  479. }
  480. auto docs = client.find(params[0], opts);
  481. std::cout << C_DIM << "(" << docs.size() << " documents)" << C_RESET << "\n";
  482. for (const auto& doc : docs) {
  483. auto id = doc.value("_id", "");
  484. // Print compact summary line
  485. std::cout << C_GREEN << id << C_RESET;
  486. // Show a few key fields
  487. for (const auto& [k, v] : doc.items()) {
  488. if (k == "_id" || k == "_created_at" || k == "_updated_at") continue;
  489. auto val = v.dump();
  490. if (val.size() > 60) val = val.substr(0, 57) + "...";
  491. std::cout << " " << C_DIM << k << "=" << C_RESET << val;
  492. // Limit to 3 fields per line
  493. static int field_count = 0;
  494. if (++field_count >= 3) { field_count = 0; break; }
  495. }
  496. std::cout << "\n";
  497. }
  498. return true;
  499. }
  500. if (cmd == "get") {
  501. if (params.size() < 2) { printError("usage: get <collection> <id>"); return false; }
  502. auto doc = client.get(params[0], params[1]);
  503. if (!doc) { printError("not found: " + params[0] + "/" + params[1]); return false; }
  504. printJson(*doc);
  505. return true;
  506. }
  507. if (cmd == "upsert") {
  508. if (params.size() < 3) { printError("usage: upsert <collection> <id> '<json>'"); return false; }
  509. auto data = json::parse(params[2], nullptr, false);
  510. if (!data.is_object()) { printError("invalid JSON: " + params[2]); return false; }
  511. client.upsert(params[0], data, params[1]);
  512. std::cout << C_GREEN << "ok" << C_RESET << " " << params[0] << "/" << params[1] << "\n";
  513. return true;
  514. }
  515. if (cmd == "remove" || cmd == "delete") {
  516. if (params.size() < 2) { printError("usage: remove <collection> <id>"); return false; }
  517. // v2.11.0 T6b — the return value used to be discarded and "ok
  518. // removed" printed unconditionally. That was merely sloppy
  519. // before referential integrity: now a delete can be REFUSED (a
  520. // relation with on_delete=restrict/no_action still has children
  521. // referencing it), and the server reports that as a real error,
  522. // not deleted=false. Report the actual outcome, and surface the
  523. // server's message on refusal so an operator learns what
  524. // blocked them rather than being told it worked.
  525. std::string err;
  526. bool deleted = client.remove(params[0], params[1], err);
  527. if (!err.empty()) {
  528. printError("remove " + params[0] + "/" + params[1] + ": " + err);
  529. return false;
  530. }
  531. if (!deleted) {
  532. std::cout << C_YELLOW << "not found" << C_RESET << " "
  533. << params[0] << "/" << params[1] << "\n";
  534. return true;
  535. }
  536. std::cout << C_GREEN << "ok" << C_RESET << " removed " << params[0] << "/" << params[1] << "\n";
  537. return true;
  538. }
  539. if (cmd == "count") {
  540. if (params.empty()) { printError("usage: count <collection>"); return false; }
  541. auto info = client.getCollectionInfo(params[0]);
  542. if (!info) { printError("collection not found: " + params[0]); return false; }
  543. std::cout << info->documentCount << "\n";
  544. return true;
  545. }
  546. // ===== v2.9.0 secondary indexes =====
  547. //
  548. // Declaration is explicit because an index costs write throughput and is
  549. // not always a win: on a low-cardinality field the planner will decline to
  550. // use it, since reading the index and then fetching most of the collection
  551. // by id loses to scanning. `indexes` reports distinct values precisely so
  552. // an operator can see that coming.
  553. if (cmd == "indexes") {
  554. if (params.empty()) { printError("usage: indexes <collection>"); return false; }
  555. auto list = client.listIndexes(params[0]);
  556. if (list.empty()) {
  557. std::cout << "no indexes on " << params[0] << "\n";
  558. return true;
  559. }
  560. std::cout << C_BOLD << "field distinct entries"
  561. << C_RESET << "\n";
  562. for (const auto& i : list) {
  563. std::cout << " " << i.field
  564. << std::string(i.field.size() < 29 ? 29 - i.field.size() : 1, ' ')
  565. << i.distinctValues
  566. << std::string(std::to_string(i.distinctValues).size() < 13
  567. ? 13 - std::to_string(i.distinctValues).size()
  568. : 1, ' ')
  569. << i.entries << "\n";
  570. }
  571. return true;
  572. }
  573. if (cmd == "index-values") {
  574. if (params.size() < 2) {
  575. printError("usage: index-values <collection> <field> [limit] [asc|desc]");
  576. return false;
  577. }
  578. const uint32_t limit = params.size() > 2 ? std::stoul(params[2]) : 20;
  579. const bool asc = params.size() > 3 ? (params[3] != "desc") : true;
  580. auto vals = client.indexValues(params[0], params[1], limit, asc);
  581. if (vals.empty()) {
  582. std::cout << "no values (is " << params[1] << " indexed?)\n";
  583. return true;
  584. }
  585. std::cout << C_BOLD << "rows value" << C_RESET << "\n";
  586. for (const auto& v : vals) {
  587. const std::string c = std::to_string(v.count);
  588. std::cout << " " << c
  589. << std::string(c.size() < 9 ? 9 - c.size() : 1, ' ')
  590. << v.value.dump() << "\n";
  591. }
  592. return true;
  593. }
  594. if (cmd == "index-create") {
  595. if (params.size() < 2) {
  596. printError("usage: index-create <collection> <field>");
  597. return false;
  598. }
  599. uint64_t rows = 0;
  600. if (!client.createIndex(params[0], params[1], rows)) {
  601. printError("could not create the index (see the service log)");
  602. return false;
  603. }
  604. std::cout << "indexed " << rows << " existing row(s) on "
  605. << params[0] << "#" << params[1] << "\n";
  606. return true;
  607. }
  608. if (cmd == "index-drop") {
  609. if (params.size() < 2) {
  610. printError("usage: index-drop <collection> <field>");
  611. return false;
  612. }
  613. if (!client.dropIndex(params[0], params[1])) {
  614. printError("could not drop the index (see the service log)");
  615. return false;
  616. }
  617. std::cout << "dropped " << params[0] << "#" << params[1] << "\n";
  618. return true;
  619. }
  620. // ===== v2.11.0 T6b — relations (referential integrity) =====
  621. //
  622. // Declaration is admin-only: `_relations` is a system collection and a
  623. // relation names another collection's schema, which is not ordinary
  624. // per-collection write access. Follows the `indexes` output shape.
  625. if (cmd == "relations") {
  626. auto list = client.listRelations();
  627. if (list.empty()) {
  628. std::cout << "no relations declared\n";
  629. return true;
  630. }
  631. std::cout << C_BOLD << "name child.field -> parent on_delete enforced-at-write"
  632. << C_RESET << "\n";
  633. for (const auto& r : list) {
  634. std::cout << " " << C_CYAN << r.name << C_RESET
  635. << std::string(r.name.size() < 19 ? 19 - r.name.size() : 1, ' ')
  636. << r.child << "." << r.childField << " -> " << r.parent
  637. << " " << r.onDelete
  638. << (r.validateOnWrite ? " (validate_on_write)" : "") << "\n";
  639. }
  640. return true;
  641. }
  642. if (cmd == "relation") {
  643. if (params.empty()) { printError("usage: relation <name>"); return false; }
  644. auto r = client.getRelationInfo(params[0]);
  645. if (!r) { printError("relation not found: " + params[0]); return false; }
  646. std::cout << C_BOLD << r->name << C_RESET << ":\n"
  647. << " child: " << r->child << "\n"
  648. << " child_field: " << r->childField << "\n"
  649. << " parent: " << r->parent << "\n"
  650. << " on_delete: " << r->onDelete << "\n"
  651. << " validate_on_write: " << (r->validateOnWrite ? "yes" : "no") << "\n";
  652. return true;
  653. }
  654. if (cmd == "relation-create") {
  655. if (params.size() < 4) {
  656. printError("usage: relation-create <name> <child> <child_field> <parent> "
  657. "[on_delete] [validate_on_write]\n"
  658. " on_delete: restrict (default) | cascade | set_null | no_action\n"
  659. " note: cascade/set_null are accepted and persisted but currently "
  660. "behave as permit");
  661. return false;
  662. }
  663. const std::string onDelete = params.size() > 4 ? params[4] : "restrict";
  664. const bool validateOnWrite = params.size() > 5
  665. && (params[5] == "true" || params[5] == "1" || params[5] == "yes");
  666. if (!client.createRelation(params[0], params[1], params[2], params[3],
  667. onDelete, validateOnWrite)) {
  668. printError("could not create the relation (see the service log)");
  669. return false;
  670. }
  671. std::cout << "declared relation " << params[0] << " (" << params[1] << "."
  672. << params[2] << " -> " << params[3] << ", on_delete=" << onDelete << ")\n";
  673. return true;
  674. }
  675. if (cmd == "relation-drop") {
  676. if (params.empty()) { printError("usage: relation-drop <name>"); return false; }
  677. if (!client.dropRelation(params[0])) {
  678. printError("could not drop the relation (see the service log)");
  679. return false;
  680. }
  681. std::cout << "dropped relation " << params[0] << "\n";
  682. return true;
  683. }
  684. // v2.11.0 T8 — the only reachable path to relations_enforced besides
  685. // grpcurl. A partial update: touches only this one knob.
  686. if (cmd == "configure-relations") {
  687. if (params.size() < 2) {
  688. printError("usage: configure-relations <collection> <on|off>");
  689. return false;
  690. }
  691. if (params[1] != "on" && params[1] != "off") {
  692. printError("expected 'on' or 'off', got: " + params[1]);
  693. return false;
  694. }
  695. const bool enforced = params[1] == "on";
  696. if (!client.setRelationsEnforced(params[0], enforced)) {
  697. printError("could not update relations_enforced (see the service log)");
  698. return false;
  699. }
  700. std::cout << C_GREEN << "ok" << C_RESET << " " << params[0]
  701. << " relations_enforced=" << (enforced ? "on" : "off") << "\n";
  702. return true;
  703. }
  704. // ===== v2.7.0 access policy =====
  705. //
  706. // Policy lives in the `_policies` collection and is managed through the
  707. // ordinary document API, which the server intercepts so edits refresh
  708. // its cache and the `__security__` record goes through the lockout
  709. // guards. These commands are ergonomics over that, not a second
  710. // mechanism - which is why there is no policy RPC to keep in step.
  711. if (cmd == "policies") {
  712. const std::string project = params.empty() ? "default" : params[0];
  713. auto rows = client.find("_policies", smartbotic::database::Client::QueryOptions{.limit = 1000});
  714. std::cout << C_BOLD << "policies in project '" << project << "'" << C_RESET << "\n";
  715. size_t shown = 0;
  716. for (const auto& r : rows) {
  717. const std::string id = r.value("_id", "");
  718. if (id.rfind(project + ":", 0) != 0) continue;
  719. const std::string tail = id.substr(project.size() + 1);
  720. if (tail == "__security__") continue;
  721. std::cout << " " << C_CYAN << tail << C_RESET
  722. << (r.value("admin", false) ? " (admin)" : "") << "\n";
  723. ++shown;
  724. }
  725. if (shown == 0) std::cout << C_DIM << " (none)" << C_RESET << "\n";
  726. return true;
  727. }
  728. if (cmd == "policy") {
  729. if (params.size() < 2) {
  730. printError("usage: policy <project> <principal>");
  731. return false;
  732. }
  733. auto doc = client.get("_policies", params[0] + ":" + params[1]);
  734. if (!doc) { printError("no policy for " + params[0] + ":" + params[1]); return false; }
  735. printJson(*doc);
  736. return true;
  737. }
  738. if (cmd == "policy-set") {
  739. if (params.size() < 3) {
  740. printError("usage: policy-set <project> <principal> '<json>'\n"
  741. " e.g. policy-set acme svc "
  742. "'{\"collections\":{\"users\":{\"read\":true,\"mask\":[\"ssn\"]}}}'\n"
  743. " admin: policy-set acme ops '{\"admin\":true}'");
  744. return false;
  745. }
  746. try {
  747. auto body = json::parse(params[2]);
  748. client.upsert("_policies", body, params[0] + ":" + params[1]);
  749. std::cout << C_GREEN << "ok" << C_RESET << " policy set for "
  750. << params[0] << ":" << params[1] << "\n";
  751. return true;
  752. } catch (const std::exception& e) {
  753. printError(e.what());
  754. return false;
  755. }
  756. }
  757. if (cmd == "policy-rm") {
  758. if (params.size() < 2) { printError("usage: policy-rm <project> <principal>"); return false; }
  759. try {
  760. bool ok = client.remove("_policies", params[0] + ":" + params[1]);
  761. std::cout << (ok ? "removed\n" : "not found\n");
  762. return ok;
  763. } catch (const std::exception& e) {
  764. // The server refuses to remove the last admin of a secured
  765. // project - that refusal is the lockout guard, not an error to
  766. // work around.
  767. printError(e.what());
  768. return false;
  769. }
  770. }
  771. if (cmd == "security") {
  772. const std::string project = params.empty() ? "default" : params[0];
  773. auto doc = client.get("_policies", project + ":__security__");
  774. if (!doc) {
  775. std::cout << "project '" << project << "': security "
  776. << C_GREEN << "disabled" << C_RESET
  777. << C_DIM << " (default - all access allowed)" << C_RESET << "\n";
  778. return true;
  779. }
  780. const bool on = doc->value("enabled", false);
  781. const std::string mode = doc->value("mode", "enforce");
  782. std::cout << "project '" << project << "': security "
  783. << (on ? (mode == "audit" ? C_YELLOW : C_RED) : C_GREEN)
  784. << (on ? (mode == "audit" ? "AUDIT" : "ENFORCED") : "disabled")
  785. << C_RESET << "\n";
  786. if (on && mode == "audit") {
  787. std::cout << C_DIM << " audit mode logs what it would deny and "
  788. "allows the request - watch the service log, then "
  789. "switch to enforce." << C_RESET << "\n";
  790. }
  791. return true;
  792. }
  793. if (cmd == "security-set") {
  794. if (params.size() < 2) {
  795. printError("usage: security-set <project> <on|off> [enforce|audit]\n"
  796. " Enabling is REFUSED unless some policy in the project has "
  797. "admin=true.\n"
  798. " Switch a live project on with 'audit' first.");
  799. return false;
  800. }
  801. const bool on = params[1] == "on" || params[1] == "true";
  802. const std::string mode = params.size() > 2 ? params[2] : "enforce";
  803. if (mode != "enforce" && mode != "audit") {
  804. printError("mode must be 'enforce' or 'audit'");
  805. return false;
  806. }
  807. try {
  808. json body;
  809. body["enabled"] = on;
  810. body["mode"] = mode;
  811. client.upsert("_policies", body, params[0] + ":__security__");
  812. std::cout << C_GREEN << "ok" << C_RESET << " project '" << params[0]
  813. << "' security " << (on ? mode : "disabled") << "\n";
  814. return true;
  815. } catch (const std::exception& e) {
  816. printError(e.what());
  817. return false;
  818. }
  819. }
  820. if (cmd == "lock") {
  821. if (client.setReadOnly(true)) {
  822. std::cout << C_GREEN << "ok" << C_RESET << " Database locked (read-only)\n";
  823. return true;
  824. }
  825. printError("failed to lock database");
  826. return false;
  827. }
  828. if (cmd == "unlock") {
  829. if (client.setReadOnly(false)) {
  830. std::cout << C_GREEN << "ok" << C_RESET << " Database unlocked (writes accepted)\n";
  831. return true;
  832. }
  833. printError("failed to unlock database");
  834. return false;
  835. }
  836. if (cmd == "status") {
  837. auto s = client.getReadOnlyStatus();
  838. std::cout << "Read-only: " << (s.readOnly ? (std::string(C_RED) + "YES" + C_RESET) : "no") << "\n";
  839. if (s.readOnly) {
  840. std::cout << "Reason: " << s.reason << "\n";
  841. }
  842. std::cout << "Recovery outcome: " << s.recoveryOutcome << "\n";
  843. if (!s.expectedSnapshot.empty()) {
  844. std::cout << "Expected snapshot: " << s.expectedSnapshot << "\n";
  845. }
  846. if (!s.snapshotUsed.empty()) {
  847. std::cout << "Snapshot used: " << s.snapshotUsed << "\n";
  848. }
  849. if (!s.failureReason.empty()) {
  850. std::cout << "Failure reason: " << s.failureReason << "\n";
  851. }
  852. std::cout << "WAL replayed: " << s.walEntriesReplayed << " entries\n";
  853. std::cout << "Snapshots tried: " << s.snapshotsAttempted << "\n";
  854. return true;
  855. }
  856. if (cmd == "help" || cmd == "?") {
  857. printUsage();
  858. return true;
  859. }
  860. printError("unknown command: " + cmd + " (try 'help')");
  861. return false;
  862. } catch (const std::exception& e) {
  863. printError(e.what());
  864. return false;
  865. }
  866. }
  867. // Tokenize a line, respecting single/double quotes and JSON braces
  868. std::vector<std::string> tokenize(const std::string& line) {
  869. std::vector<std::string> tokens;
  870. std::string current;
  871. int brace_depth = 0;
  872. char in_quote = 0;
  873. for (size_t i = 0; i < line.size(); ++i) {
  874. char c = line[i];
  875. if (in_quote) {
  876. current += c;
  877. if (c == in_quote && (i == 0 || line[i-1] != '\\')) {
  878. in_quote = 0;
  879. // Strip surrounding quotes for simple string tokens
  880. if (brace_depth == 0 && current.size() >= 2
  881. && (current.front() == '\'' || current.front() == '"')
  882. && current.front() == current.back()) {
  883. current = current.substr(1, current.size() - 2);
  884. }
  885. }
  886. continue;
  887. }
  888. if (c == '\'' || c == '"') {
  889. in_quote = c;
  890. current += c;
  891. continue;
  892. }
  893. if (c == '{') { brace_depth++; current += c; continue; }
  894. if (c == '}') {
  895. brace_depth--;
  896. current += c;
  897. if (brace_depth <= 0) {
  898. brace_depth = 0;
  899. tokens.push_back(current);
  900. current.clear();
  901. }
  902. continue;
  903. }
  904. if (brace_depth > 0) { current += c; continue; }
  905. if (c == ' ' || c == '\t') {
  906. if (!current.empty()) {
  907. tokens.push_back(current);
  908. current.clear();
  909. }
  910. continue;
  911. }
  912. current += c;
  913. }
  914. if (!current.empty()) tokens.push_back(current);
  915. return tokens;
  916. }
  917. // v2.4.4 — offline sub-db placement audit / repair.
  918. //
  919. // `verify-subdbs` is read-only and safe against a running server.
  920. // `reconcile-subdbs --apply` rewrites document placement and REQUIRES the
  921. // service to be stopped: it holds an LMDB write txn over the whole env.
  922. int cmdSubdbs(const std::string& command, const std::vector<std::string>& params) {
  923. std::string env_path, project;
  924. bool apply = false;
  925. bool stamp = false;
  926. for (size_t i = 0; i < params.size(); ++i) {
  927. const std::string& p = params[i];
  928. if (p == "--env" && i + 1 < params.size()) {
  929. env_path = params[++i];
  930. } else if (p == "--project" && i + 1 < params.size()) {
  931. project = params[++i];
  932. } else if (p == "--apply") {
  933. apply = true;
  934. } else if (p == "--stamp-identity") {
  935. stamp = true;
  936. } else {
  937. printError("unknown argument: " + p);
  938. return 2;
  939. }
  940. }
  941. if (env_path.empty()) {
  942. printError("--env <path> is required (e.g. "
  943. "/var/lib/smartbotic-database/projects/default/env)");
  944. return 2;
  945. }
  946. if (command == "verify-subdbs" && apply) {
  947. printError("--apply is not valid for verify-subdbs; use reconcile-subdbs");
  948. return 2;
  949. }
  950. try {
  951. auto report = smartbotic::db::storage::audit(env_path, project);
  952. smartbotic::db::storage::print_audit(report);
  953. if (command == "verify-subdbs") {
  954. return report.misplaced.empty() ? 0 : 1;
  955. }
  956. if (!apply) {
  957. std::cout << "\n" << C_YELLOW << "Dry run." << C_RESET
  958. << " Re-run with --apply to perform the repair.\n"
  959. << C_DIM
  960. << "Stop the service first, and take a backup: the repair "
  961. "rewrites document placement in place.\n"
  962. << C_RESET;
  963. return report.misplaced.empty() ? 0 : 1;
  964. }
  965. if (stamp) {
  966. std::cout << "\n" << C_YELLOW << "--stamp-identity:" << C_RESET
  967. << " writing sentinels. This REQUIRES the server binary to be"
  968. " v2.4.4 or newer;\n earlier builds do not skip the sentinel"
  969. " and will fail on scan.\n";
  970. }
  971. std::cout << "\nApplying...\n";
  972. auto res = smartbotic::db::storage::repair(env_path, report, stamp);
  973. std::cout << C_GREEN << "done." << C_RESET
  974. << " moved=" << res.moved
  975. << " quarantined=" << res.quarantined
  976. << " stamped=" << res.stamped << "\n";
  977. for (const auto& e : res.errors) {
  978. std::cerr << C_RED << " warn: " << C_RESET << e << "\n";
  979. }
  980. return res.errors.empty() ? 0 : 1;
  981. } catch (const std::exception& e) {
  982. printError(e.what());
  983. return 1;
  984. }
  985. }
  986. } // anonymous namespace
  987. int main(int argc, char* argv[]) {
  988. Args args;
  989. // Parse flags
  990. std::vector<std::string> positional;
  991. for (int i = 1; i < argc; ++i) {
  992. std::string arg = argv[i];
  993. if (arg == "--address" && i + 1 < argc) {
  994. args.address = argv[++i];
  995. } else if (arg == "--project" && i + 1 < argc) {
  996. args.project = argv[++i];
  997. } else if (arg == "--help" || arg == "-h") {
  998. printUsage();
  999. return 0;
  1000. } else {
  1001. positional.push_back(arg);
  1002. }
  1003. }
  1004. if (!positional.empty()) {
  1005. args.command = positional[0];
  1006. args.params.assign(positional.begin() + 1, positional.end());
  1007. }
  1008. // Offline helpers — these don't need a running server, so dispatch
  1009. // them before opening the gRPC channel.
  1010. if (args.command == "generate-auth-key") {
  1011. return cmdGenerateAuthKey();
  1012. }
  1013. if (args.command == "generate-tls-cert") {
  1014. return cmdGenerateTlsCert(args.params);
  1015. }
  1016. if (args.command == "verify-subdbs" || args.command == "reconcile-subdbs") {
  1017. return cmdSubdbs(args.command, args.params);
  1018. }
  1019. // Connect
  1020. smartbotic::database::Client::Config clientCfg{.address = args.address};
  1021. if (!args.project.empty()) clientCfg.project = args.project;
  1022. smartbotic::database::Client client(clientCfg);
  1023. client.connect();
  1024. // Scriptable mode: single command
  1025. if (!args.command.empty()) {
  1026. return execCommand(client, args.command, args.params) ? 0 : 1;
  1027. }
  1028. // Interactive mode
  1029. std::cout << C_BOLD << "smartbotic-db-cli" << C_RESET
  1030. << " connected to " << C_CYAN << args.address << C_RESET << "\n"
  1031. << C_DIM << "Type 'help' for commands, 'exit' to quit." << C_RESET << "\n";
  1032. std::string line;
  1033. while (true) {
  1034. std::cout << C_YELLOW << "> " << C_RESET;
  1035. if (!std::getline(std::cin, line)) break;
  1036. // Trim
  1037. auto start = line.find_first_not_of(" \t");
  1038. if (start == std::string::npos) continue;
  1039. line = line.substr(start);
  1040. if (line == "exit" || line == "quit" || line == "q") break;
  1041. if (line.empty()) continue;
  1042. auto tokens = tokenize(line);
  1043. if (tokens.empty()) continue;
  1044. auto cmd = tokens[0];
  1045. std::vector<std::string> params(tokens.begin() + 1, tokens.end());
  1046. execCommand(client, cmd, params);
  1047. }
  1048. return 0;
  1049. }