Skip to main content

reddb_server/cli/
commands.rs

1/// RedDB command definitions.
2///
3/// Defines the command tree, Flag and Route types used by help and completion
4/// generators, and the schema for each built-in command.
5use super::types::FlagSchema;
6
7// ============================================================================
8// Help-layer types (used by help.rs and complete.rs)
9// ============================================================================
10
11/// Lightweight flag descriptor used by the help generator.
12#[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/// A single routable verb within a resource.
49#[derive(Debug, Clone)]
50pub struct Route {
51    pub verb: &'static str,
52    pub summary: &'static str,
53    pub usage: &'static str,
54}
55
56// ============================================================================
57// RedDB command definitions
58// ============================================================================
59
60/// Command descriptor for a top-level RedDB command.
61pub struct CommandDef {
62    pub name: &'static str,
63    pub summary: &'static str,
64    pub usage: &'static str,
65    pub flags: Vec<FlagSchema>,
66}
67
68/// Return all RedDB commands.
69pub 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
206/// Return the help text for the main `red` command.
207pub 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
262/// Return help text for a specific command.
263pub 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
310// ============================================================================
311// Per-command flag schemas
312// ============================================================================
313
314fn 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
673// ============================================================================
674// Completion data helpers
675// ============================================================================
676
677/// Return domain data for completion scripts.
678pub 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
698/// Return global flag data for completion scripts.
699pub 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}