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:5555] [--http-bind 127.0.0.1:5055] [--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:5555] [--http-bind 0.0.0.0:5055] [--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:5055] [--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: "replica",
127 summary: "Start as a read replica connected to a primary",
128 usage: "red replica --primary-addr http://primary:5555 [--grpc] [--http] [--grpc-bind 127.0.0.1:5555] [--http-bind 127.0.0.1:5055] [--path ./data/reddb.rdb]",
129 flags: replica_flags(),
130 },
131 CommandDef {
132 name: "status",
133 summary: "Show replication status",
134 usage: "red status [--bind 0.0.0.0:6380]",
135 flags: status_flags(),
136 },
137 CommandDef {
138 name: "mcp",
139 summary: "Start MCP server for AI agent integration",
140 usage: "red mcp [--path /data]",
141 flags: mcp_flags(),
142 },
143 CommandDef {
144 name: "auth",
145 summary: "Manage authentication (users, tokens, roles)",
146 usage: "red auth <subcommand>",
147 flags: auth_flags(),
148 },
149 CommandDef {
150 name: "connect",
151 summary: "Connect to a remote RedDB server (interactive REPL)",
152 usage: "red connect [--token <token>] [--query <sql>] <addr>",
153 flags: connect_flags(),
154 },
155 CommandDef {
156 name: "dump",
157 summary: "Export one or all collections as JSONL for backup/migration",
158 usage: "red dump [--path file] [--collection NAME] [-o FILE]",
159 flags: dump_flags(),
160 },
161 CommandDef {
162 name: "restore",
163 summary: "Import a previously dumped JSONL file into the database",
164 usage: "red restore [--path file] -i FILE [--collection NAME]",
165 flags: restore_flags(),
166 },
167 CommandDef {
168 name: "pitr-list",
169 summary: "List available point-in-time restore points from a snapshot archive",
170 usage: "red pitr-list --snapshot-prefix DIR --wal-prefix DIR",
171 flags: pitr_list_flags(),
172 },
173 CommandDef {
174 name: "pitr-restore",
175 summary: "Restore a database to a specific point in time from snapshots + WAL archive",
176 usage: "red pitr-restore --target-time UNIX_MS --dest PATH --snapshot-prefix DIR --wal-prefix DIR",
177 flags: pitr_restore_flags(),
178 },
179 CommandDef {
180 name: "doctor",
181 summary: "Health-check a running server against operator thresholds (PLAN.md Phase 5.5)",
182 usage: "red doctor [--bind 127.0.0.1:5055] [--token <admin>] [--json] [--backup-age-warn-secs 600] [--backup-age-crit-secs 3600] [--wal-lag-warn 1000] [--wal-lag-crit 10000]",
183 flags: doctor_flags(),
184 },
185 CommandDef {
186 name: "bootstrap",
187 summary: "One-shot first-admin bootstrap for headless containers / K8s Jobs",
188 usage: "red bootstrap --path PATH --vault [--username USER] [--password-stdin] [--print-certificate] [--json]",
189 flags: bootstrap_flags(),
190 },
191 CommandDef {
192 name: "version",
193 summary: "Show RedDB version information",
194 usage: "red version",
195 flags: vec![],
196 },
197 CommandDef {
198 name: "vcs",
199 summary: "Version-control operations (Git for Data)",
200 usage: "red vcs <commit|branch|branches|tag|tags|checkout|merge|log|status|lca|resolve> [args] [flags]",
201 flags: vcs_flags(),
202 },
203 ]
204}
205
206pub fn main_help_text() -> String {
208 let mut out = String::with_capacity(1024);
209
210 out.push_str("reddb -- unified multi-model database engine\n");
211 out.push('\n');
212 out.push_str("Usage: red <command> [args] [flags]\n");
213 out.push('\n');
214
215 out.push_str("Commands:\n");
216 for cmd in all_commands() {
217 out.push_str(&format!(" {:<14} {}\n", cmd.name, cmd.summary));
218 }
219 out.push_str(&format!(" {:<14} {}\n", "help", "Show help for a command"));
220 out.push('\n');
221
222 out.push_str("Global flags:\n");
223 out.push_str(&format!(" {:<24} {}\n", "-h, --help", "Show help"));
224 out.push_str(&format!(" {:<24} {}\n", "-j, --json", "Force JSON output"));
225 out.push_str(&format!(
226 " {:<24} {}\n",
227 "-o, --output FORMAT", "Output format [text|json|yaml]"
228 ));
229 out.push_str(&format!(" {:<24} {}\n", "-v, --verbose", "Verbose output"));
230 out.push_str(&format!(
231 " {:<24} {}\n",
232 " --no-color", "Disable colors"
233 ));
234 out.push_str(&format!(" {:<24} {}\n", " --version", "Show version"));
235 out.push('\n');
236
237 out.push_str("Examples:\n");
238 out.push_str(" red server --path ./data/reddb.rdb\n");
239 out.push_str(" red server --grpc-bind 127.0.0.1:5555 --http-bind 127.0.0.1:5055 --path ./data/reddb.rdb\n");
240 out.push_str(" red server --wire-bind 127.0.0.1:5050 --path ./data/reddb.rdb\n");
241 out.push_str(" sudo red service install --binary /usr/local/bin/red --grpc-bind 0.0.0.0:5555 --http-bind 0.0.0.0:5055 --path /var/lib/reddb/data.rdb\n");
242 out.push_str(" red replica --primary-addr http://primary:5555 --path ./data/replica.rdb\n");
243 out.push_str(" red query \"SELECT * FROM users\"\n");
244 out.push_str(" red insert users '{\"name\": \"Alice\"}'\n");
245 out.push_str(" red get users abc123\n");
246 out.push_str(" red health\n");
247 out.push_str(
248 " red tick --bind 127.0.0.1:5055 --operations maintenance,retention,checkpoint\n",
249 );
250 out.push_str(" red auth create-user alice --password secret --role admin\n");
251 out.push_str(" red auth create-api-key alice --name \"ci-token\" --role write\n");
252 out.push_str(" red auth list-users\n");
253 out.push_str(" red auth login alice --password secret\n");
254 out.push_str(" red connect 127.0.0.1:5050\n");
255 out.push_str(" red connect --query \"SELECT * FROM users\" 127.0.0.1:5050\n");
256 out.push('\n');
257
258 out.push_str("Run 'red <command> --help' for more information on a command.\n");
259 out
260}
261
262pub fn command_help_text(name: &str) -> Option<String> {
264 let cmds = all_commands();
265 let cmd = cmds.iter().find(|c| c.name == name)?;
266
267 let mut out = String::with_capacity(512);
268
269 out.push_str(&format!("red {} -- {}\n", cmd.name, cmd.summary));
270 out.push('\n');
271 out.push_str(&format!("Usage: {}\n", cmd.usage));
272 out.push('\n');
273
274 if !cmd.flags.is_empty() {
275 out.push_str("Flags:\n");
276 for flag in &cmd.flags {
277 let short_part = match flag.short {
278 Some(ch) => format!("-{}, ", ch),
279 None => " ".to_string(),
280 };
281 let value_part = if flag.expects_value {
282 format!(" <{}>", flag.long.to_uppercase())
283 } else {
284 String::new()
285 };
286 let label = format!("{}--{}{}", short_part, flag.long, value_part);
287 let padding = if label.len() < 24 {
288 24 - label.len()
289 } else {
290 2
291 };
292 let default_text = match &flag.default {
293 Some(d) => format!(" (default: {})", d),
294 None => String::new(),
295 };
296 out.push_str(&format!(
297 " {}{}{}{}\n",
298 label,
299 " ".repeat(padding),
300 flag.description,
301 default_text,
302 ));
303 }
304 out.push('\n');
305 }
306
307 Some(out)
308}
309
310fn server_flags() -> Vec<FlagSchema> {
315 vec![
316 FlagSchema::new("path")
317 .with_short('d')
318 .with_description("Persistent database file path (omit for in-memory)")
319 .with_default("./data/reddb.rdb"),
320 FlagSchema::new("bind").with_short('b').with_description(
321 "Bind address (host:port) for the routed front-door or legacy single-transport mode",
322 ),
323 FlagSchema::boolean("grpc").with_description("Enable the gRPC API"),
324 FlagSchema::boolean("http").with_description("Serve the HTTP API"),
325 FlagSchema::new("grpc-bind").with_description("Explicit gRPC bind address (host:port)"),
326 FlagSchema::new("http-bind").with_description("Explicit HTTP bind address (host:port)"),
327 FlagSchema::new("wire-bind")
328 .with_description("Explicit wire bind address (host:port or unix:///path/to/socket)"),
329 FlagSchema::new("wire-tls-bind")
330 .with_description("Explicit wire TLS bind address (host:port)"),
331 FlagSchema::new("wire-tls-cert")
332 .with_description("Path to TLS certificate PEM for wire TLS"),
333 FlagSchema::new("wire-tls-key")
334 .with_description("Path to TLS private key PEM for wire TLS"),
335 FlagSchema::new("pg-bind").with_description(
336 "PostgreSQL wire protocol bind address (enables psql / JDBC / DBeaver clients)",
337 ),
338 FlagSchema::new("role")
339 .with_short('r')
340 .with_description("Replication role")
341 .with_choices(&["standalone", "primary", "replica"])
342 .with_default("standalone"),
343 FlagSchema::new("primary-addr").with_description("Primary gRPC address for replica mode"),
344 FlagSchema::boolean("read-only").with_description("Open the database in read-only mode"),
345 FlagSchema::boolean("no-create-if-missing")
346 .with_description("Fail instead of creating the database file"),
347 FlagSchema::new("vault")
348 .with_description("Enable encrypted auth vault (reserved pages in main .rdb file)")
349 .with_default("false"),
350 FlagSchema::new("log-dir").with_description(
351 "Directory for rotating log files (defaults to the parent of --path / ./logs)",
352 ),
353 FlagSchema::new("log-level")
354 .with_description(
355 "Log level filter — trace / debug / info / warn / error, or a RUST_LOG expression",
356 )
357 .with_default("info"),
358 FlagSchema::new("log-format")
359 .with_description("Log output format")
360 .with_choices(&["pretty", "json"])
361 .with_default("pretty"),
362 FlagSchema::new("log-keep-days")
363 .with_description("Number of rotated log files to keep")
364 .with_default("14"),
365 FlagSchema::boolean("no-log-file")
366 .with_description("Disable rotating file logs (stderr only)"),
367 ]
368}
369
370fn replica_flags() -> Vec<FlagSchema> {
371 vec![
372 FlagSchema::new("primary-addr")
373 .with_short('p')
374 .with_description("Primary gRPC address (e.g. http://primary:50051)"),
375 FlagSchema::new("path")
376 .with_short('d')
377 .with_description("Local replica database file path")
378 .with_default("./data/reddb.rdb"),
379 FlagSchema::new("bind").with_short('b').with_description(
380 "Bind address (host:port) for the routed front-door or legacy single-transport mode",
381 ),
382 FlagSchema::boolean("grpc").with_description("Enable the gRPC API"),
383 FlagSchema::boolean("http").with_description("Serve the HTTP API"),
384 FlagSchema::new("grpc-bind").with_description("Explicit gRPC bind address (host:port)"),
385 FlagSchema::new("http-bind").with_description("Explicit HTTP bind address (host:port)"),
386 FlagSchema::new("wire-bind")
387 .with_description("Explicit wire bind address (host:port or unix:///path/to/socket)"),
388 FlagSchema::new("vault")
389 .with_description("Enable encrypted auth vault (reserved pages in main .rdb file)")
390 .with_default("false"),
391 ]
392}
393
394fn vcs_flags() -> Vec<FlagSchema> {
395 vec![
396 FlagSchema::new("path")
397 .with_short('d')
398 .with_description("Persistent database file path (omit for in-memory)"),
399 FlagSchema::new("connection")
400 .with_short('c')
401 .with_description("Connection id for workset scoping")
402 .with_default("1"),
403 FlagSchema::new("branch").with_description("Branch name (for log/checkout/merge)"),
404 FlagSchema::new("from").with_description("Source ref or commit (branch create / merge)"),
405 FlagSchema::new("to").with_description("Upper bound for log range"),
406 FlagSchema::new("author")
407 .with_description("Commit author name")
408 .with_default("reddb"),
409 FlagSchema::new("email")
410 .with_description("Commit author email")
411 .with_default("reddb@localhost"),
412 FlagSchema::new("message")
413 .with_short('m')
414 .with_description("Commit message"),
415 FlagSchema::new("limit")
416 .with_description("Max log entries")
417 .with_default("20"),
418 FlagSchema::boolean("ff-only").with_description("Merge only if fast-forward"),
419 FlagSchema::boolean("no-ff").with_description("Always create a merge commit"),
420 ]
421}
422
423fn service_flags() -> Vec<FlagSchema> {
424 vec![
425 FlagSchema::new("binary")
426 .with_description("Path to the red binary")
427 .with_default("/usr/local/bin/red"),
428 FlagSchema::new("service-name")
429 .with_description("systemd unit name")
430 .with_default("reddb"),
431 FlagSchema::new("user")
432 .with_description("Service user")
433 .with_default("reddb"),
434 FlagSchema::new("group")
435 .with_description("Service group")
436 .with_default("reddb"),
437 FlagSchema::new("path")
438 .with_short('d')
439 .with_description("Persistent database file path")
440 .with_default("/var/lib/reddb/data.rdb"),
441 FlagSchema::new("bind").with_short('b').with_description(
442 "Bind address (host:port) for the routed front-door or legacy single-transport mode",
443 ),
444 FlagSchema::boolean("grpc").with_description("Enable the gRPC API in the service"),
445 FlagSchema::boolean("http").with_description("Install an HTTP service"),
446 FlagSchema::new("grpc-bind").with_description("Explicit gRPC bind address (host:port)"),
447 FlagSchema::new("http-bind").with_description("Explicit HTTP bind address (host:port)"),
448 ]
449}
450
451fn query_flags() -> Vec<FlagSchema> {
452 vec![
453 FlagSchema::new("bind")
454 .with_short('b')
455 .with_description("Server address")
456 .with_default("0.0.0.0:6380"),
457 FlagSchema::new("path").with_description("Open a local .rdb file in embedded mode"),
458 FlagSchema::new("param")
459 .with_short('p')
460 .with_description("Positional parameter for $1, $2, ... (repeatable)"),
461 FlagSchema::new("param-type").with_description("Type override for the preceding --param"),
462 ]
463}
464
465fn insert_flags() -> Vec<FlagSchema> {
466 vec![FlagSchema::new("bind")
467 .with_short('b')
468 .with_description("Server address")
469 .with_default("0.0.0.0:6380")]
470}
471
472fn get_flags() -> Vec<FlagSchema> {
473 vec![FlagSchema::new("bind")
474 .with_short('b')
475 .with_description("Server address")
476 .with_default("0.0.0.0:6380")]
477}
478
479fn delete_flags() -> Vec<FlagSchema> {
480 vec![FlagSchema::new("bind")
481 .with_short('b')
482 .with_description("Server address")
483 .with_default("0.0.0.0:6380")]
484}
485
486fn health_flags() -> Vec<FlagSchema> {
487 vec![
488 FlagSchema::new("bind")
489 .with_short('b')
490 .with_description("Server address; defaults by transport"),
491 FlagSchema::boolean("grpc").with_description("Probe a gRPC listener (default transport)"),
492 FlagSchema::boolean("http").with_description("Probe an HTTP listener"),
493 ]
494}
495
496fn bootstrap_flags() -> Vec<FlagSchema> {
497 vec![
498 FlagSchema::new("path")
499 .with_short('d')
500 .with_description("Persistent database file path"),
501 FlagSchema::boolean("vault")
502 .with_description("Required: seal credentials in the encrypted vault"),
503 FlagSchema::new("username")
504 .with_short('u')
505 .with_description("Admin username (defaults to REDDB_USERNAME)"),
506 FlagSchema::new("password")
507 .with_description("Admin password (DEV ONLY; prefer --password-stdin)"),
508 FlagSchema::boolean("password-stdin")
509 .with_description("Read the admin password from stdin (one line)"),
510 FlagSchema::boolean("print-certificate")
511 .with_description("Print only the certificate to stdout"),
512 ]
513}
514
515fn doctor_flags() -> Vec<FlagSchema> {
516 vec![
517 FlagSchema::new("bind")
518 .with_description("HTTP address of the server to probe")
519 .with_default("127.0.0.1:5055"),
520 FlagSchema::new("token")
521 .with_description("Admin bearer token; defaults to RED_ADMIN_TOKEN env"),
522 FlagSchema::boolean("json")
523 .with_description("Emit a single JSON object instead of human text"),
524 FlagSchema::new("backup-age-warn-secs")
525 .with_description("Warn when last successful backup is older than N seconds")
526 .with_default("600"),
527 FlagSchema::new("backup-age-crit-secs")
528 .with_description("Critical when last successful backup is older than N seconds")
529 .with_default("3600"),
530 FlagSchema::new("wal-lag-warn")
531 .with_description("Warn when WAL archive lag exceeds N records")
532 .with_default("1000"),
533 FlagSchema::new("wal-lag-crit")
534 .with_description("Critical when WAL archive lag exceeds N records")
535 .with_default("10000"),
536 ]
537}
538
539fn dump_flags() -> Vec<FlagSchema> {
540 vec![
541 FlagSchema::new("path")
542 .with_description("Local database file to dump from")
543 .with_default("./data/reddb.rdb"),
544 FlagSchema::new("collection")
545 .with_short('c')
546 .with_description("Single collection to dump (omit for all)"),
547 FlagSchema::new("output")
548 .with_short('o')
549 .with_description("Destination file (defaults to stdout)"),
550 ]
551}
552
553fn restore_flags() -> Vec<FlagSchema> {
554 vec![
555 FlagSchema::new("path")
556 .with_description("Local database file to restore into")
557 .with_default("./data/reddb.rdb"),
558 FlagSchema::new("input")
559 .with_short('i')
560 .with_description("Dump file to read (required)"),
561 FlagSchema::new("collection")
562 .with_short('c')
563 .with_description("Override target collection name"),
564 ]
565}
566
567fn pitr_list_flags() -> Vec<FlagSchema> {
568 vec![
569 FlagSchema::new("snapshot-prefix")
570 .with_description("Directory (or remote prefix) holding .snapshot files"),
571 FlagSchema::new("wal-prefix")
572 .with_description("Directory (or remote prefix) holding archived WAL segments"),
573 ]
574}
575
576fn pitr_restore_flags() -> Vec<FlagSchema> {
577 vec![
578 FlagSchema::new("target-time")
579 .with_description("Recovery target — UNIX ms (0 = latest available)"),
580 FlagSchema::new("dest")
581 .with_description("Destination database file path for the restored DB"),
582 FlagSchema::new("snapshot-prefix")
583 .with_description("Directory (or remote prefix) holding .snapshot files"),
584 FlagSchema::new("wal-prefix")
585 .with_description("Directory (or remote prefix) holding archived WAL segments"),
586 ]
587}
588
589fn tick_flags() -> Vec<FlagSchema> {
590 vec![
591 FlagSchema::new("bind")
592 .with_short('b')
593 .with_description("Server HTTP bind address")
594 .with_default("127.0.0.1:5055"),
595 FlagSchema::new("operations")
596 .with_description("Comma-separated operations: maintenance,retention,checkpoint"),
597 FlagSchema::boolean("dry-run")
598 .with_description("Validate operations without applying changes"),
599 ]
600}
601
602fn migrate_from_redis_flags() -> Vec<FlagSchema> {
603 vec![
604 FlagSchema::boolean("dry-run")
605 .with_description("Validate Redis and RedDB connectivity without cache writes"),
606 FlagSchema::new("redis-url")
607 .with_description("Redis URL to validate, for example redis://127.0.0.1:6379/0"),
608 FlagSchema::new("path")
609 .with_short('d')
610 .with_description("Local RedDB .rdb file to open for connectivity validation"),
611 FlagSchema::new("phase")
612 .with_description("Migration phase: dry-run | dual-write")
613 .with_default("dry-run"),
614 FlagSchema::new("namespace")
615 .with_description("Blob Cache namespace recorded in dry-run output")
616 .with_default("redis-migration"),
617 ]
618}
619
620fn status_flags() -> Vec<FlagSchema> {
621 vec![FlagSchema::new("bind")
622 .with_short('b')
623 .with_description("Server address")
624 .with_default("0.0.0.0:6380")]
625}
626
627fn mcp_flags() -> Vec<FlagSchema> {
628 vec![FlagSchema::new("path")
629 .with_short('d')
630 .with_description("Data directory path (omit for in-memory)")
631 .with_default("")]
632}
633
634fn connect_flags() -> Vec<FlagSchema> {
635 vec![
636 FlagSchema::new("token")
637 .with_short('t')
638 .with_description("Auth token (session or API key)"),
639 FlagSchema::new("query")
640 .with_short('q')
641 .with_description("Execute a single query and exit"),
642 FlagSchema::new("user")
643 .with_short('u')
644 .with_description("Username for login"),
645 FlagSchema::new("password")
646 .with_short('p')
647 .with_description("Password for login"),
648 ]
649}
650
651fn auth_flags() -> Vec<FlagSchema> {
652 vec![
653 FlagSchema::new("bind")
654 .with_short('b')
655 .with_description("Server address")
656 .with_default("0.0.0.0:6380"),
657 FlagSchema::new("password")
658 .with_short('p')
659 .with_description("User password"),
660 FlagSchema::new("role")
661 .with_short('r')
662 .with_description("User role")
663 .with_choices(&["read", "write", "admin"]),
664 FlagSchema::new("name")
665 .with_short('n')
666 .with_description("API key name"),
667 FlagSchema::new("user")
668 .with_short('u')
669 .with_description("Target username"),
670 ]
671}
672
673pub fn completion_domains() -> Vec<(String, Vec<String>)> {
679 vec![
680 ("server".to_string(), vec![]),
681 ("service".to_string(), vec![]),
682 ("replica".to_string(), vec![]),
683 ("tick".to_string(), vec![]),
684 ("query".to_string(), vec!["q".to_string()]),
685 ("insert".to_string(), vec!["i".to_string()]),
686 ("get".to_string(), vec![]),
687 ("delete".to_string(), vec!["del".to_string()]),
688 ("health".to_string(), vec![]),
689 ("status".to_string(), vec![]),
690 ("migrate-from-redis".to_string(), vec![]),
691 ("mcp".to_string(), vec![]),
692 ("auth".to_string(), vec![]),
693 ("connect".to_string(), vec![]),
694 ("version".to_string(), vec![]),
695 ]
696}
697
698pub fn completion_global_flags() -> Vec<(&'static str, Option<char>)> {
700 vec![
701 ("help", Some('h')),
702 ("json", Some('j')),
703 ("output", Some('o')),
704 ("verbose", Some('v')),
705 ("no-color", None),
706 ("version", None),
707 ]
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713
714 #[test]
715 fn test_all_commands_defined() {
716 let cmds = all_commands();
717 let names: Vec<&str> = cmds.iter().map(|c| c.name).collect();
718 assert!(names.contains(&"server"));
719 assert!(names.contains(&"query"));
720 assert!(names.contains(&"insert"));
721 assert!(names.contains(&"get"));
722 assert!(names.contains(&"delete"));
723 assert!(names.contains(&"health"));
724 assert!(names.contains(&"tick"));
725 assert!(names.contains(&"migrate-from-redis"));
726 assert!(names.contains(&"status"));
727 assert!(names.contains(&"connect"));
728 assert!(names.contains(&"version"));
729 }
730
731 #[test]
732 fn test_server_has_flags() {
733 let cmds = all_commands();
734 let server = cmds.iter().find(|c| c.name == "server").unwrap();
735 let flag_names: Vec<&str> = server.flags.iter().map(|f| f.long.as_str()).collect();
736 assert!(flag_names.contains(&"path"));
737 assert!(flag_names.contains(&"bind"));
738 }
739
740 #[test]
741 fn test_replica_has_flags() {
742 let cmds = all_commands();
743 let replica = cmds.iter().find(|c| c.name == "replica").unwrap();
744 let flag_names: Vec<&str> = replica.flags.iter().map(|f| f.long.as_str()).collect();
745 assert!(flag_names.contains(&"primary-addr"));
746 assert!(flag_names.contains(&"path"));
747 assert!(flag_names.contains(&"bind"));
748 }
749
750 #[test]
751 fn test_main_help_text() {
752 let help = main_help_text();
753 assert!(help.contains("reddb"));
754 assert!(help.contains("Usage: red"));
755 assert!(help.contains("Commands:"));
756 assert!(help.contains("server"));
757 assert!(help.contains("query"));
758 assert!(help.contains("Global flags:"));
759 assert!(help.contains("--help"));
760 assert!(help.contains("Examples:"));
761 }
762
763 #[test]
764 fn test_command_help_text() {
765 let help = command_help_text("server").unwrap();
766 assert!(help.contains("red server"));
767 assert!(help.contains("--path"));
768 assert!(help.contains("--bind"));
769 }
770
771 #[test]
772 fn test_replica_command_help() {
773 let help = command_help_text("replica").unwrap();
774 assert!(help.contains("red replica"));
775 assert!(help.contains("--primary-addr"));
776 }
777
778 #[test]
779 fn test_migrate_from_redis_command_help() {
780 let help = command_help_text("migrate-from-redis").unwrap();
781 assert!(help.contains("red migrate-from-redis"));
782 assert!(help.contains("--dry-run"));
783 assert!(help.contains("--redis-url"));
784 assert!(help.contains("application-owned helper"));
785 }
786
787 #[test]
788 fn test_command_help_text_unknown() {
789 assert!(command_help_text("nonexistent").is_none());
790 }
791
792 #[test]
793 fn test_flag_builder() {
794 let flag = Flag::new("output", "Output format")
795 .with_short('o')
796 .with_default("text")
797 .with_arg("FORMAT");
798
799 assert_eq!(flag.long, "output");
800 assert_eq!(flag.short, Some('o'));
801 assert_eq!(flag.description, "Output format");
802 assert_eq!(flag.default, Some("text".to_string()));
803 assert_eq!(flag.arg, Some("FORMAT".to_string()));
804 }
805
806 #[test]
807 fn test_completion_domains() {
808 let domains = completion_domains();
809 let names: Vec<&str> = domains.iter().map(|(n, _)| n.as_str()).collect();
810 assert!(names.contains(&"server"));
811 assert!(names.contains(&"query"));
812 assert!(names.contains(&"health"));
813 }
814
815 #[test]
816 fn test_completion_global_flags() {
817 let flags = completion_global_flags();
818 assert!(flags.contains(&("help", Some('h'))));
819 assert!(flags.contains(&("json", Some('j'))));
820 assert!(flags.contains(&("verbose", Some('v'))));
821 assert!(flags.contains(&("no-color", None)));
822 }
823}