main.cpp 48 KB

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