1use super::types::FlagSchema;
6
7#[derive(Debug, Clone)]
13pub struct Flag {
14 pub short: Option<char>,
15 pub long: String,
16 pub description: String,
17 pub default: Option<String>,
18 pub arg: Option<String>,
19}
20
21impl Flag {
22 pub fn new(long: &str, desc: &str) -> Self {
23 Self {
24 short: None,
25 long: long.to_string(),
26 description: desc.to_string(),
27 default: None,
28 arg: None,
29 }
30 }
31
32 pub fn with_short(mut self, short: char) -> Self {
33 self.short = Some(short);
34 self
35 }
36
37 pub fn with_default(mut self, default: &str) -> Self {
38 self.default = Some(default.to_string());
39 self
40 }
41
42 pub fn with_arg(mut self, arg: &str) -> Self {
43 self.arg = Some(arg.to_string());
44 self
45 }
46}
47
48#[derive(Debug, Clone)]
50pub struct Route {
51 pub verb: &'static str,
52 pub summary: &'static str,
53 pub usage: &'static str,
54}
55
56pub struct CommandDef {
62 pub name: &'static str,
63 pub summary: &'static str,
64 pub usage: &'static str,
65 pub flags: Vec<FlagSchema>,
66}
67
68pub fn all_commands() -> Vec<CommandDef> {
70 vec![
71 CommandDef {
72 name: "server",
73 summary: "Start the database server (router/HTTP/gRPC/wire)",
74 usage: "red server [--grpc] [--http] [--grpc-bind 127.0.0.1:55055] [--http-bind 127.0.0.1:5000] [--wire-bind 127.0.0.1:5050] [--path ./data/reddb.rdb]",
75 flags: server_flags(),
76 },
77 CommandDef {
78 name: "service",
79 summary: "Install or inspect a systemd service",
80 usage: "red service <install|print-unit> [--binary /usr/local/bin/red] [--grpc-bind 0.0.0.0:55055] [--http-bind 0.0.0.0:5000] [--path /var/lib/reddb/data.rdb]",
81 flags: service_flags(),
82 },
83 CommandDef {
84 name: "query",
85 summary: "Execute a query against the database",
86 usage: "red query \"SELECT * FROM users WHERE age > $1\" -p 21",
87 flags: query_flags(),
88 },
89 CommandDef {
90 name: "insert",
91 summary: "Insert an entity into a collection",
92 usage: "red insert users '{\"name\": \"Alice\", \"age\": 30}'",
93 flags: insert_flags(),
94 },
95 CommandDef {
96 name: "get",
97 summary: "Get an entity by ID from a collection",
98 usage: "red get users abc123",
99 flags: get_flags(),
100 },
101 CommandDef {
102 name: "delete",
103 summary: "Delete an entity by ID from a collection",
104 usage: "red delete users abc123",
105 flags: delete_flags(),
106 },
107 CommandDef {
108 name: "health",
109 summary: "Run a health check against the server",
110 usage: "red health [--bind 127.0.0.1:5050] [--grpc|--http]",
111 flags: health_flags(),
112 },
113 CommandDef {
114 name: "tick",
115 summary: "Run maintenance/reclaim tick operations",
116 usage: "red tick [--bind 127.0.0.1:5000] [--operations maintenance,retention,checkpoint] [--dry-run]",
117 flags: tick_flags(),
118 },
119 CommandDef {
120 name: "migrate-from-redis",
121 summary: "Validate Redis to Blob Cache migration readiness; dual-write uses the documented application-owned helper pattern",
122 usage: "red migrate-from-redis --dry-run --redis-url redis://127.0.0.1:6379/0 [--path ./data/reddb.rdb]",
123 flags: migrate_from_redis_flags(),
124 },
125 CommandDef {
126 name: "salvage",
127 summary: "Extract verified data from a damaged embedded store into a fresh .rdb",
128 usage: "red salvage --source damaged.rdb --destination recovered.rdb [--json]",
129 flags: salvage_flags(),
130 },
131 CommandDef {
132 name: "replica",
133 summary: "Start as a read replica connected to a primary",
134 usage: "red replica --primary-addr http://primary:55055 [--grpc] [--http] [--grpc-bind 127.0.0.1:55055] [--http-bind 127.0.0.1:5000] [--path ./data/reddb.rdb]",
135 flags: replica_flags(),
136 },
137 CommandDef {
138 name: "status",
139 summary: "Show replication status",
140 usage: "red status [--bind 0.0.0.0:6380]",
141 flags: status_flags(),
142 },
143 CommandDef {
144 name: "inspect",
145 summary: "Inspect on-disk database state (catalog snapshot)",
146 usage: "red inspect catalog --path <FILE> [--at <SEQ>] [--json]",
147 flags: inspect_flags(),
148 },
149 CommandDef {
150 name: "mcp",
151 summary: "Start MCP server for AI agent integration",
152 usage: "red mcp [--path /data | --url <URI>] [--token <token>]",
153 flags: mcp_flags(),
154 },
155 CommandDef {
156 name: "auth",
157 summary: "Manage authentication (users, tokens, roles)",
158 usage: "red auth <subcommand>",
159 flags: auth_flags(),
160 },
161 CommandDef {
162 name: "connect",
163 summary: "Connect to a remote RedDB server (interactive REPL)",
164 usage: "red connect [--token <token>] [--query <sql>] <addr>",
165 flags: connect_flags(),
166 },
167 CommandDef {
168 name: "dump",
169 summary: "Export one or all collections as JSONL for backup/migration",
170 usage: "red dump [--path file] [--collection NAME] [-o FILE]",
171 flags: dump_flags(),
172 },
173 CommandDef {
174 name: "restore",
175 summary: "Import a previously dumped JSONL file into the database",
176 usage: "red restore [--path file] -i FILE [--collection NAME]",
177 flags: restore_flags(),
178 },
179 CommandDef {
180 name: "pitr-list",
181 summary: "List available point-in-time restore points from a snapshot archive",
182 usage: "red pitr-list --snapshot-prefix DIR --wal-prefix DIR",
183 flags: pitr_list_flags(),
184 },
185 CommandDef {
186 name: "pitr-restore",
187 summary: "Restore a database to a specific point in time from snapshots + WAL archive",
188 usage: "red pitr-restore --target-time UNIX_MS --dest PATH --snapshot-prefix DIR --wal-prefix DIR",
189 flags: pitr_restore_flags(),
190 },
191 CommandDef {
192 name: "doctor",
193 summary: "Health-check a running server against operator thresholds (PLAN.md Phase 5.5)",
194 usage: "red doctor [--bind 127.0.0.1:5000] [--token <admin>] [--json] [--backup-age-warn-secs 600] [--backup-age-crit-secs 3600] [--wal-lag-warn 1000] [--wal-lag-crit 10000]",
195 flags: doctor_flags(),
196 },
197 CommandDef {
198 name: "bootstrap",
199 summary: "One-shot first-admin bootstrap for headless containers / K8s Jobs",
200 usage: "red bootstrap --path PATH --vault [--username USER] [--password-stdin] [--print-certificate] [--json]",
201 flags: bootstrap_flags(),
202 },
203 CommandDef {
204 name: "version",
205 summary: "Show RedDB version information",
206 usage: "red version",
207 flags: vec![],
208 },
209 CommandDef {
210 name: "vcs",
211 summary: "Version-control operations (Git for Data)",
212 usage: "red vcs <commit|branch|branches|tag|tags|checkout|merge|log|status|lca|resolve> [args] [flags]",
213 flags: vcs_flags(),
214 },
215 CommandDef {
216 name: "ui",
217 summary: "Open a graphical UI against a local .rdb or a remote red:///reds:// instance over a RedWire-over-WS bridge",
218 usage: "red ui file://./data.rdb | red ui red://host:port [--token TOKEN] [--ui-dir DIR] [--port N] [--tls-ca PEM] [--no-browser]",
219 flags: ui_flags(),
220 },
221 ]
222}
223
224pub fn main_help_text() -> String {
226 let mut out = String::with_capacity(1024);
227
228 out.push_str("reddb -- unified multi-model database engine\n");
229 out.push('\n');
230 out.push_str("Usage: red <command> [args] [flags]\n");
231 out.push('\n');
232
233 out.push_str("Commands:\n");
234 for cmd in all_commands() {
235 out.push_str(&format!(" {:<14} {}\n", cmd.name, cmd.summary));
236 }
237 out.push_str(&format!(" {:<14} {}\n", "help", "Show help for a command"));
238 out.push('\n');
239
240 out.push_str("Global flags:\n");
241 out.push_str(&format!(" {:<24} {}\n", "-h, --help", "Show help"));
242 out.push_str(&format!(" {:<24} {}\n", "-j, --json", "Force JSON output"));
243 out.push_str(&format!(
244 " {:<24} {}\n",
245 "-o, --output FORMAT", "Output format [text|json|yaml]"
246 ));
247 out.push_str(&format!(" {:<24} {}\n", "-v, --verbose", "Verbose output"));
248 out.push_str(&format!(
249 " {:<24} {}\n",
250 " --no-color", "Disable colors"
251 ));
252 out.push_str(&format!(" {:<24} {}\n", " --version", "Show version"));
253 out.push('\n');
254
255 out.push_str("Examples:\n");
256 out.push_str(" red server --path ./data/reddb.rdb\n");
257 out.push_str(" red server --grpc-bind 127.0.0.1:55055 --http-bind 127.0.0.1:5000 --path ./data/reddb.rdb\n");
258 out.push_str(" red server --wire-bind 127.0.0.1:5050 --path ./data/reddb.rdb\n");
259 out.push_str(" sudo red service install --binary /usr/local/bin/red --grpc-bind 0.0.0.0:55055 --http-bind 0.0.0.0:5000 --path /var/lib/reddb/data.rdb\n");
260 out.push_str(" red replica --primary-addr http://primary:55055 --path ./data/replica.rdb\n");
261 out.push_str(" red query \"SELECT * FROM users\"\n");
262 out.push_str(" red insert users '{\"name\": \"Alice\"}'\n");
263 out.push_str(" red get users abc123\n");
264 out.push_str(" red health\n");
265 out.push_str(
266 " red tick --bind 127.0.0.1:5000 --operations maintenance,retention,checkpoint\n",
267 );
268 out.push_str(" red auth create-user alice --password secret --role admin\n");
269 out.push_str(" red auth create-api-key alice --name \"ci-token\" --role write\n");
270 out.push_str(" red auth list-users\n");
271 out.push_str(" red auth login alice --password secret\n");
272 out.push_str(" red connect 127.0.0.1:5050\n");
273 out.push_str(" red connect --query \"SELECT * FROM users\" 127.0.0.1:5050\n");
274 out.push('\n');
275
276 out.push_str("Run 'red <command> --help' for more information on a command.\n");
277 out
278}
279
280pub fn command_help_text(name: &str) -> Option<String> {
282 let cmds = all_commands();
283 let cmd = cmds.iter().find(|c| c.name == name)?;
284
285 let mut out = String::with_capacity(512);
286
287 out.push_str(&format!("red {} -- {}\n", cmd.name, cmd.summary));
288 out.push('\n');
289 out.push_str(&format!("Usage: {}\n", cmd.usage));
290 out.push('\n');
291
292 if !cmd.flags.is_empty() {
293 out.push_str("Flags:\n");
294 for flag in &cmd.flags {
295 let short_part = match flag.short {
296 Some(ch) => format!("-{}, ", ch),
297 None => " ".to_string(),
298 };
299 let value_part = if flag.expects_value {
300 format!(" <{}>", flag.long.to_uppercase())
301 } else {
302 String::new()
303 };
304 let label = format!("{}--{}{}", short_part, flag.long, value_part);
305 let padding = if label.len() < 24 {
306 24 - label.len()
307 } else {
308 2
309 };
310 let default_text = match &flag.default {
311 Some(d) => format!(" (default: {})", d),
312 None => String::new(),
313 };
314 out.push_str(&format!(
315 " {}{}{}{}\n",
316 label,
317 " ".repeat(padding),
318 flag.description,
319 default_text,
320 ));
321 }
322 out.push('\n');
323 }
324
325 Some(out)
326}
327
328fn server_flags() -> Vec<FlagSchema> {
333 vec![
334 FlagSchema::new("path")
335 .with_short('d')
336 .with_description("Persistent database file path (omit for in-memory)")
337 .with_default("./data/reddb.rdb"),
338 FlagSchema::new("bind").with_short('b').with_description(
339 "Bind address (host:port) for the routed front-door or legacy single-transport mode",
340 ),
341 FlagSchema::boolean("grpc").with_description("Enable the gRPC API"),
342 FlagSchema::boolean("http").with_description("Serve the HTTP API"),
343 FlagSchema::new("grpc-bind").with_description("Explicit gRPC bind address (host:port)"),
344 FlagSchema::new("http-bind").with_description("Explicit HTTP bind address (host:port)"),
345 FlagSchema::new("wire-bind")
346 .with_description("Explicit wire bind address (host:port or unix:///path/to/socket)"),
347 FlagSchema::new("wire-tls-bind")
348 .with_description("Explicit wire TLS bind address (host:port)"),
349 FlagSchema::new("wire-tls-cert")
350 .with_description("Path to TLS certificate PEM for wire TLS"),
351 FlagSchema::new("wire-tls-key")
352 .with_description("Path to TLS private key PEM for wire TLS"),
353 FlagSchema::new("pg-bind").with_description(
354 "PostgreSQL wire protocol bind address (enables psql / JDBC / DBeaver clients)",
355 ),
356 FlagSchema::new("role")
357 .with_short('r')
358 .with_description("Replication role")
359 .with_choices(&["standalone", "primary", "replica"])
360 .with_default("standalone"),
361 FlagSchema::new("primary-addr").with_description("Primary gRPC address for replica mode"),
362 FlagSchema::boolean("read-only").with_description("Open the database in read-only mode"),
363 FlagSchema::boolean("no-create-if-missing")
364 .with_description("Fail instead of creating the database file"),
365 FlagSchema::boolean("auth").with_description("Enable authentication for this boot"),
366 FlagSchema::boolean("require-auth")
367 .with_description("Reject anonymous requests; implies --auth"),
368 FlagSchema::new("vault")
369 .with_description("Enable encrypted auth vault (reserved pages in main .rdb file)")
370 .with_default("false"),
371 FlagSchema::boolean("no-auth").with_description(
372 "Hard-disable auth: anonymous access, ignores REDDB_USERNAME/PASSWORD/vault, \
373 prints a startup warning. Local-dev shortcut — NEVER use in production.",
374 ),
375 FlagSchema::boolean("dev")
376 .with_description("Alias for --no-auth (local development convenience)."),
377 FlagSchema::new("bootstrap-preset")
378 .with_description(
379 "First-boot preset. With --vault on a fresh --path, red server \
380 self-bootstraps the paged vault in place (no separate `red bootstrap`) \
381 then applies the preset and serves; a re-boot against the existing \
382 vault just serves (idempotent — no re-bootstrap, no new certificate).",
383 )
384 .with_choices(&["simple", "production", "regulated", "cloud"]),
385 FlagSchema::new("bootstrap-manifest").with_description(
386 "Path to manifest-driven first boot JSON. Applied once on a fresh database \
387 before serving; later boots observe bootstrap state and skip re-applying \
388 the manifest (idempotent).",
389 ),
390 FlagSchema::new("bootstrap-admin")
391 .with_description("First admin username for production/cloud bootstrap"),
392 FlagSchema::new("bootstrap-admin-password").with_description(
393 "First admin password (DEV ONLY; prefer --bootstrap-admin-password-file)",
394 ),
395 FlagSchema::new("bootstrap-admin-password-file")
396 .with_description("File containing first admin password"),
397 FlagSchema::new("cloud-head-admin")
398 .with_description("Cloud preset head/platform admin username"),
399 FlagSchema::new("cloud-head-admin-password").with_description(
400 "Cloud preset head/platform admin password (DEV ONLY; prefer file flag)",
401 ),
402 FlagSchema::new("cloud-head-admin-password-file")
403 .with_description("File containing cloud head admin password"),
404 FlagSchema::new("customer-admin").with_description("Cloud preset customer admin username"),
405 FlagSchema::new("customer-admin-password")
406 .with_description("Cloud preset customer admin password (DEV ONLY; prefer file flag)"),
407 FlagSchema::new("customer-admin-password-file")
408 .with_description("File containing cloud customer admin password"),
409 FlagSchema::new("bootstrap-cert-out").with_description(
410 "Write the first-boot unseal certificate to this file (e.g. /run/reddb/cert.pem) \
411 in addition to stderr, so a distroless init can read it back for the next boot's \
412 REDDB_CERTIFICATE_FILE unseal. Written only on the bootstrap-creating boot; a \
413 re-boot against the existing vault does not rewrite it.",
414 ),
415 FlagSchema::new("log-dir").with_description(
416 "Directory for rotating log files (defaults to the parent of --path / ./logs)",
417 ),
418 FlagSchema::new("log-level")
419 .with_description(
420 "Log level filter — trace / debug / info / warn / error, or a RUST_LOG expression",
421 )
422 .with_default("info"),
423 FlagSchema::new("log-format")
424 .with_description("Log output format")
425 .with_choices(&["pretty", "json"])
426 .with_default("pretty"),
427 FlagSchema::new("log-keep-days")
428 .with_description("Number of rotated log files to keep")
429 .with_default("14"),
430 FlagSchema::boolean("no-log-file")
431 .with_description("Disable rotating file logs (stderr only)"),
432 FlagSchema::new("http-max-handlers").with_description(
433 "Max concurrent HTTP handler threads (env: REDDB_HTTP_MAX_HANDLERS; \
434 red_config: red.http.max_handlers; default: (2 x num_cpus).clamp(8, 256))",
435 ),
436 FlagSchema::new("http-handler-timeout-ms")
437 .with_description(
438 "Per-handler total-time budget in ms (env: REDDB_HTTP_HANDLER_TIMEOUT_MS; \
439 red_config: red.http.handler_timeout_ms)",
440 )
441 .with_default("30000"),
442 FlagSchema::new("http-retry-after-secs")
443 .with_description(
444 "Retry-After seconds on limiter 503 (env: REDDB_HTTP_RETRY_AFTER_SECS; \
445 red_config: red.http.retry_after_secs; clamped to [1, 30])",
446 )
447 .with_default("5"),
448 FlagSchema::new("http-max-inflight-per-principal").with_description(
449 "Max concurrent in-flight HTTP requests per principal; over-cap requests \
450 get a structured 429 (env: REDDB_HTTP_MAX_INFLIGHT_PER_PRINCIPAL; \
451 red_config: red.http.max_inflight_per_principal; 0 disables; default: 64)",
452 ),
453 ]
454}
455
456fn replica_flags() -> Vec<FlagSchema> {
457 vec![
458 FlagSchema::new("primary-addr")
459 .with_short('p')
460 .with_description("Primary gRPC address (e.g. http://primary:55055)"),
461 FlagSchema::new("path")
462 .with_short('d')
463 .with_description("Local replica database file path")
464 .with_default("./data/reddb.rdb"),
465 FlagSchema::new("bind").with_short('b').with_description(
466 "Bind address (host:port) for the routed front-door or legacy single-transport mode",
467 ),
468 FlagSchema::boolean("grpc").with_description("Enable the gRPC API"),
469 FlagSchema::boolean("http").with_description("Serve the HTTP API"),
470 FlagSchema::new("grpc-bind").with_description("Explicit gRPC bind address (host:port)"),
471 FlagSchema::new("http-bind").with_description("Explicit HTTP bind address (host:port)"),
472 FlagSchema::new("wire-bind")
473 .with_description("Explicit wire bind address (host:port or unix:///path/to/socket)"),
474 FlagSchema::boolean("auth").with_description("Enable authentication for this boot"),
475 FlagSchema::boolean("require-auth")
476 .with_description("Reject anonymous requests; implies --auth"),
477 FlagSchema::new("vault")
478 .with_description("Enable encrypted auth vault (reserved pages in main .rdb file)")
479 .with_default("false"),
480 FlagSchema::boolean("no-auth")
481 .with_description("Hard-disable auth: anonymous access, ignores vault"),
482 ]
483}
484
485fn ui_flags() -> Vec<FlagSchema> {
486 vec![
487 FlagSchema::boolean("server")
488 .with_description("Force the browser-served bridge path (skip the desktop deep link)"),
489 FlagSchema::boolean("desktop").with_description(
490 "Force the desktop app via the redui:// deep link (no browser fallback)",
491 ),
492 FlagSchema::new("ui-dir").with_description(
493 "Directory to serve the UI bundle from (defaults to the built-in fixture)",
494 ),
495 FlagSchema::new("port")
496 .with_description("Loopback port for the bridge (0 / omit picks an ephemeral port)"),
497 FlagSchema::new("tls-ca").with_description(
498 "PEM CA bundle to trust for a reds:// target (on top of system roots)",
499 ),
500 FlagSchema::new("token").with_short('t').with_description(
501 "Bearer token (session/API key). Held by red and injected into the \
502 RedWire handshake — the UI never sees it (env: RED_UI_TOKEN)",
503 ),
504 FlagSchema::boolean("no-browser").with_description(
505 "Do not open the default browser (also honoured via RED_UI_NO_BROWSER)",
506 ),
507 ]
508}
509
510fn vcs_flags() -> Vec<FlagSchema> {
511 vec![
512 FlagSchema::new("path")
513 .with_short('d')
514 .with_description("Persistent database file path (omit for in-memory)"),
515 FlagSchema::new("connection")
516 .with_short('c')
517 .with_description("Connection id for workset scoping")
518 .with_default("1"),
519 FlagSchema::new("branch").with_description("Branch name (for log/checkout/merge)"),
520 FlagSchema::new("from").with_description("Source ref or commit (branch create / merge)"),
521 FlagSchema::new("to").with_description("Upper bound for log range"),
522 FlagSchema::new("author")
523 .with_description("Commit author name")
524 .with_default("reddb"),
525 FlagSchema::new("email")
526 .with_description("Commit author email")
527 .with_default("reddb@localhost"),
528 FlagSchema::new("message")
529 .with_short('m')
530 .with_description("Commit message"),
531 FlagSchema::new("limit")
532 .with_description("Max log entries")
533 .with_default("20"),
534 FlagSchema::boolean("ff-only").with_description("Merge only if fast-forward"),
535 FlagSchema::boolean("no-ff").with_description("Always create a merge commit"),
536 ]
537}
538
539fn service_flags() -> Vec<FlagSchema> {
540 vec![
541 FlagSchema::new("binary")
542 .with_description("Path to the red binary")
543 .with_default("/usr/local/bin/red"),
544 FlagSchema::new("service-name")
545 .with_description("systemd unit name")
546 .with_default("reddb"),
547 FlagSchema::new("user")
548 .with_description("Service user")
549 .with_default("reddb"),
550 FlagSchema::new("group")
551 .with_description("Service group")
552 .with_default("reddb"),
553 FlagSchema::new("path")
554 .with_short('d')
555 .with_description("Persistent database file path")
556 .with_default(reddb_file::DEFAULT_SERVICE_DATABASE_PATH),
557 FlagSchema::new("bind").with_short('b').with_description(
558 "Bind address (host:port) for the routed front-door or legacy single-transport mode",
559 ),
560 FlagSchema::boolean("grpc").with_description("Enable the gRPC API in the service"),
561 FlagSchema::boolean("http").with_description("Install an HTTP service"),
562 FlagSchema::new("grpc-bind").with_description("Explicit gRPC bind address (host:port)"),
563 FlagSchema::new("http-bind").with_description("Explicit HTTP bind address (host:port)"),
564 ]
565}
566
567fn query_flags() -> Vec<FlagSchema> {
568 vec![
569 FlagSchema::new("bind")
570 .with_short('b')
571 .with_description("Server address")
572 .with_default("0.0.0.0:6380"),
573 FlagSchema::new("path").with_description("Open a local .rdb file in embedded mode"),
574 FlagSchema::new("param")
575 .with_short('p')
576 .with_description("Positional parameter for $1, $2, ... (repeatable)"),
577 FlagSchema::new("param-type").with_description("Type override for the preceding --param"),
578 FlagSchema::boolean("dry-run")
579 .with_description("Preview the statement via EXPLAIN without executing it"),
580 ]
581}
582
583fn insert_flags() -> Vec<FlagSchema> {
584 vec![FlagSchema::new("bind")
585 .with_short('b')
586 .with_description("Server address")
587 .with_default("0.0.0.0:6380")]
588}
589
590fn get_flags() -> Vec<FlagSchema> {
591 vec![FlagSchema::new("bind")
592 .with_short('b')
593 .with_description("Server address")
594 .with_default("0.0.0.0:6380")]
595}
596
597fn delete_flags() -> Vec<FlagSchema> {
598 vec![FlagSchema::new("bind")
599 .with_short('b')
600 .with_description("Server address")
601 .with_default("0.0.0.0:6380")]
602}
603
604fn health_flags() -> Vec<FlagSchema> {
605 vec![
606 FlagSchema::new("bind")
607 .with_short('b')
608 .with_description("Server address; defaults by transport"),
609 FlagSchema::boolean("grpc").with_description("Probe a gRPC listener (default transport)"),
610 FlagSchema::boolean("http").with_description("Probe an HTTP listener"),
611 ]
612}
613
614fn bootstrap_flags() -> Vec<FlagSchema> {
615 vec![
616 FlagSchema::new("path")
617 .with_short('d')
618 .with_description("Persistent database file path"),
619 FlagSchema::boolean("vault")
620 .with_description("Required: seal credentials in the encrypted vault"),
621 FlagSchema::new("username")
622 .with_short('u')
623 .with_description("Admin username (defaults to REDDB_USERNAME)"),
624 FlagSchema::new("password")
625 .with_description("Admin password (DEV ONLY; prefer --password-stdin)"),
626 FlagSchema::boolean("password-stdin")
627 .with_description("Read the admin password from stdin (one line)"),
628 FlagSchema::boolean("print-certificate")
629 .with_description("Print only the certificate to stdout"),
630 ]
631}
632
633fn doctor_flags() -> Vec<FlagSchema> {
634 vec![
635 FlagSchema::new("bind")
636 .with_description("HTTP address of the server to probe")
637 .with_default("127.0.0.1:5000"),
638 FlagSchema::new("token")
639 .with_description("Admin bearer token; defaults to RED_ADMIN_TOKEN env"),
640 FlagSchema::boolean("json")
641 .with_description("Emit a single JSON object instead of human text"),
642 FlagSchema::new("backup-age-warn-secs")
643 .with_description("Warn when last successful backup is older than N seconds")
644 .with_default("600"),
645 FlagSchema::new("backup-age-crit-secs")
646 .with_description("Critical when last successful backup is older than N seconds")
647 .with_default("3600"),
648 FlagSchema::new("wal-lag-warn")
649 .with_description("Warn when WAL archive lag exceeds N records")
650 .with_default("1000"),
651 FlagSchema::new("wal-lag-crit")
652 .with_description("Critical when WAL archive lag exceeds N records")
653 .with_default("10000"),
654 ]
655}
656
657fn dump_flags() -> Vec<FlagSchema> {
658 vec![
659 FlagSchema::new("path")
660 .with_description("Local database file to dump from")
661 .with_default("./data/reddb.rdb"),
662 FlagSchema::new("collection")
663 .with_short('c')
664 .with_description("Single collection to dump (omit for all)"),
665 FlagSchema::new("output")
666 .with_short('o')
667 .with_description("Destination file (defaults to stdout)"),
668 ]
669}
670
671fn restore_flags() -> Vec<FlagSchema> {
672 vec![
673 FlagSchema::new("path")
674 .with_description("Local database file to restore into")
675 .with_default("./data/reddb.rdb"),
676 FlagSchema::new("input")
677 .with_short('i')
678 .with_description("Dump file to read (required)"),
679 FlagSchema::new("collection")
680 .with_short('c')
681 .with_description("Override target collection name"),
682 ]
683}
684
685fn pitr_list_flags() -> Vec<FlagSchema> {
686 vec![
687 FlagSchema::new("snapshot-prefix")
688 .with_description("Directory (or remote prefix) holding .snapshot files"),
689 FlagSchema::new("wal-prefix")
690 .with_description("Directory (or remote prefix) holding archived WAL segments"),
691 ]
692}
693
694fn pitr_restore_flags() -> Vec<FlagSchema> {
695 vec![
696 FlagSchema::new("target-time")
697 .with_description("Recovery target — UNIX ms (0 = latest available)"),
698 FlagSchema::new("dest")
699 .with_description("Destination database file path for the restored DB"),
700 FlagSchema::new("snapshot-prefix")
701 .with_description("Directory (or remote prefix) holding .snapshot files"),
702 FlagSchema::new("wal-prefix")
703 .with_description("Directory (or remote prefix) holding archived WAL segments"),
704 ]
705}
706
707fn tick_flags() -> Vec<FlagSchema> {
708 vec![
709 FlagSchema::new("bind")
710 .with_short('b')
711 .with_description("Server HTTP bind address")
712 .with_default("127.0.0.1:5000"),
713 FlagSchema::new("operations")
714 .with_description("Comma-separated operations: maintenance,retention,checkpoint"),
715 FlagSchema::boolean("dry-run")
716 .with_description("Validate operations without applying changes"),
717 ]
718}
719
720fn migrate_from_redis_flags() -> Vec<FlagSchema> {
721 vec![
722 FlagSchema::boolean("dry-run")
723 .with_description("Validate Redis and RedDB connectivity without cache writes"),
724 FlagSchema::new("redis-url")
725 .with_description("Redis URL to validate, for example redis://127.0.0.1:6379/0"),
726 FlagSchema::new("path")
727 .with_short('d')
728 .with_description("Local RedDB .rdb file to open for connectivity validation"),
729 FlagSchema::new("phase")
730 .with_description("Migration phase: dry-run | dual-write")
731 .with_default("dry-run"),
732 FlagSchema::new("namespace")
733 .with_description("Blob Cache namespace recorded in dry-run output")
734 .with_default("redis-migration"),
735 ]
736}
737
738fn salvage_flags() -> Vec<FlagSchema> {
739 vec![
740 FlagSchema::new("source").with_description("Damaged source .rdb file to read"),
741 FlagSchema::new("destination").with_description("Fresh destination .rdb file to create"),
742 ]
743}
744
745fn status_flags() -> Vec<FlagSchema> {
746 vec![FlagSchema::new("bind")
747 .with_short('b')
748 .with_description("Server address")
749 .with_default("0.0.0.0:6380")]
750}
751
752fn inspect_flags() -> Vec<FlagSchema> {
753 vec![
754 FlagSchema::new("path")
755 .with_short('d')
756 .with_description("Path to the on-disk database file"),
757 FlagSchema::new("at")
758 .with_description("Catalog at snapshot sequence (requires metadata journal)"),
759 ]
760}
761
762fn mcp_flags() -> Vec<FlagSchema> {
763 vec![
764 FlagSchema::new("path")
765 .with_short('d')
766 .with_description("Data directory path (omit for in-memory)")
767 .with_default(""),
768 FlagSchema::new("url")
769 .with_description("Remote or embedded MCP connection URI; overrides REDDB_MCP_URI"),
770 FlagSchema::new("token")
771 .with_description("Bearer token fallback when --url has no userinfo"),
772 ]
773}
774
775fn connect_flags() -> Vec<FlagSchema> {
776 vec![
777 FlagSchema::new("token")
778 .with_short('t')
779 .with_description("Auth token (session or API key)"),
780 FlagSchema::new("query")
781 .with_short('q')
782 .with_description("Execute a single query and exit"),
783 FlagSchema::new("user")
784 .with_short('u')
785 .with_description("Username for login"),
786 FlagSchema::new("password")
787 .with_short('p')
788 .with_description("Password for login"),
789 ]
790}
791
792fn auth_flags() -> Vec<FlagSchema> {
793 vec![
794 FlagSchema::new("bind")
795 .with_short('b')
796 .with_description("Server address")
797 .with_default("0.0.0.0:6380"),
798 FlagSchema::new("password")
799 .with_short('p')
800 .with_description("User password"),
801 FlagSchema::new("role")
802 .with_short('r')
803 .with_description("User role")
804 .with_choices(&["read", "write", "admin"]),
805 FlagSchema::new("name")
806 .with_short('n')
807 .with_description("API key name"),
808 FlagSchema::new("user")
809 .with_short('u')
810 .with_description("Target username"),
811 ]
812}
813
814pub fn completion_domains() -> Vec<(String, Vec<String>)> {
820 vec![
821 ("server".to_string(), vec![]),
822 ("service".to_string(), vec![]),
823 ("replica".to_string(), vec![]),
824 ("tick".to_string(), vec![]),
825 ("query".to_string(), vec!["q".to_string()]),
826 ("insert".to_string(), vec!["i".to_string()]),
827 ("get".to_string(), vec![]),
828 ("delete".to_string(), vec!["del".to_string()]),
829 ("health".to_string(), vec![]),
830 ("status".to_string(), vec![]),
831 ("inspect".to_string(), vec![]),
832 ("migrate-from-redis".to_string(), vec![]),
833 ("salvage".to_string(), vec![]),
834 ("mcp".to_string(), vec![]),
835 ("auth".to_string(), vec![]),
836 ("connect".to_string(), vec![]),
837 ("version".to_string(), vec![]),
838 ]
839}
840
841pub fn completion_global_flags() -> Vec<(&'static str, Option<char>)> {
843 vec![
844 ("help", Some('h')),
845 ("json", Some('j')),
846 ("output", Some('o')),
847 ("verbose", Some('v')),
848 ("no-color", None),
849 ("version", None),
850 ]
851}
852
853#[cfg(test)]
854mod tests {
855 use super::*;
856
857 #[test]
858 fn test_all_commands_defined() {
859 let cmds = all_commands();
860 let names: Vec<&str> = cmds.iter().map(|c| c.name).collect();
861 assert!(names.contains(&"server"));
862 assert!(names.contains(&"query"));
863 assert!(names.contains(&"insert"));
864 assert!(names.contains(&"get"));
865 assert!(names.contains(&"delete"));
866 assert!(names.contains(&"health"));
867 assert!(names.contains(&"tick"));
868 assert!(names.contains(&"migrate-from-redis"));
869 assert!(names.contains(&"salvage"));
870 assert!(names.contains(&"status"));
871 assert!(names.contains(&"inspect"));
872 assert!(names.contains(&"connect"));
873 assert!(names.contains(&"version"));
874 }
875
876 #[test]
877 fn test_inspect_has_flags() {
878 let cmds = all_commands();
879 let inspect = cmds.iter().find(|c| c.name == "inspect").unwrap();
880 let flag_names: Vec<&str> = inspect.flags.iter().map(|f| f.long.as_str()).collect();
881 assert!(flag_names.contains(&"path"));
882 assert!(flag_names.contains(&"at"));
883 }
884
885 #[test]
886 fn test_server_has_flags() {
887 let cmds = all_commands();
888 let server = cmds.iter().find(|c| c.name == "server").unwrap();
889 let flag_names: Vec<&str> = server.flags.iter().map(|f| f.long.as_str()).collect();
890 assert!(flag_names.contains(&"path"));
891 assert!(flag_names.contains(&"bind"));
892 assert!(flag_names.contains(&"http-max-handlers"));
894 assert!(flag_names.contains(&"http-handler-timeout-ms"));
895 assert!(flag_names.contains(&"http-retry-after-secs"));
896 }
897
898 #[test]
899 fn test_server_help_text_lists_http_limit_flags() {
900 let help = command_help_text("server").unwrap();
901 assert!(help.contains("--http-max-handlers"));
902 assert!(help.contains("--http-handler-timeout-ms"));
903 assert!(help.contains("--http-retry-after-secs"));
904 assert!(help.contains("REDDB_HTTP_MAX_HANDLERS"));
905 }
906
907 #[test]
908 fn test_replica_has_flags() {
909 let cmds = all_commands();
910 let replica = cmds.iter().find(|c| c.name == "replica").unwrap();
911 let flag_names: Vec<&str> = replica.flags.iter().map(|f| f.long.as_str()).collect();
912 assert!(flag_names.contains(&"primary-addr"));
913 assert!(flag_names.contains(&"path"));
914 assert!(flag_names.contains(&"bind"));
915 }
916
917 #[test]
918 fn test_main_help_text() {
919 let help = main_help_text();
920 assert!(help.contains("reddb"));
921 assert!(help.contains("Usage: red"));
922 assert!(help.contains("Commands:"));
923 assert!(help.contains("server"));
924 assert!(help.contains("query"));
925 assert!(help.contains("Global flags:"));
926 assert!(help.contains("--help"));
927 assert!(help.contains("Examples:"));
928 }
929
930 #[test]
931 fn test_command_help_text() {
932 let help = command_help_text("server").unwrap();
933 assert!(help.contains("red server"));
934 assert!(help.contains("--path"));
935 assert!(help.contains("--bind"));
936 }
937
938 #[test]
939 fn test_replica_command_help() {
940 let help = command_help_text("replica").unwrap();
941 assert!(help.contains("red replica"));
942 assert!(help.contains("--primary-addr"));
943 }
944
945 #[test]
946 fn test_migrate_from_redis_command_help() {
947 let help = command_help_text("migrate-from-redis").unwrap();
948 assert!(help.contains("red migrate-from-redis"));
949 assert!(help.contains("--dry-run"));
950 assert!(help.contains("--redis-url"));
951 assert!(help.contains("application-owned helper"));
952 }
953
954 #[test]
955 fn test_command_help_text_unknown() {
956 assert!(command_help_text("nonexistent").is_none());
957 }
958
959 #[test]
960 fn test_flag_builder() {
961 let flag = Flag::new("output", "Output format")
962 .with_short('o')
963 .with_default("text")
964 .with_arg("FORMAT");
965
966 assert_eq!(flag.long, "output");
967 assert_eq!(flag.short, Some('o'));
968 assert_eq!(flag.description, "Output format");
969 assert_eq!(flag.default, Some("text".to_string()));
970 assert_eq!(flag.arg, Some("FORMAT".to_string()));
971 }
972
973 #[test]
974 fn test_completion_domains() {
975 let domains = completion_domains();
976 let names: Vec<&str> = domains.iter().map(|(n, _)| n.as_str()).collect();
977 assert!(names.contains(&"server"));
978 assert!(names.contains(&"query"));
979 assert!(names.contains(&"health"));
980 }
981
982 #[test]
983 fn test_completion_global_flags() {
984 let flags = completion_global_flags();
985 assert!(flags.contains(&("help", Some('h'))));
986 assert!(flags.contains(&("json", Some('j'))));
987 assert!(flags.contains(&("verbose", Some('v'))));
988 assert!(flags.contains(&("no-color", None)));
989 }
990}