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: "migrate-pager-zone",
127 summary: "Convert a closed legacy sidecar-backed store to the zoned .rdb format",
128 usage: "red migrate-pager-zone --path ./data/reddb.rdb [--json]",
129 flags: migrate_pager_zone_flags(),
130 },
131 CommandDef {
132 name: "compact",
133 summary: "Rewrite a closed checkpointed native paged database into a fresh directory",
134 usage: "red compact --source data.rdb --destination-dir compacted [--json]",
135 flags: vec![
136 FlagSchema::new("source").with_description("Closed, checkpointed source .rdb; preserved unchanged"),
137 FlagSchema::new("destination-dir").with_description("New output directory; must not exist"),
138 ],
139 },
140 CommandDef {
141 name: "scrub-paged-trees",
142 summary: "Check tree structure in a closed checkpointed paged database",
143 usage: "red scrub-paged-trees --source data.rdb [--json]",
144 flags: vec![FlagSchema::new("source").with_description("Closed, checkpointed source .rdb; preserved unchanged")],
145 },
146 CommandDef {
147 name: "salvage",
148 summary: "Extract verified data from a damaged embedded store into a fresh .rdb",
149 usage: "red salvage --source damaged.rdb --destination recovered.rdb [--json]",
150 flags: salvage_flags(),
151 },
152 CommandDef {
153 name: "replica",
154 summary: "Start as a read replica connected to a primary",
155 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]",
156 flags: replica_flags(),
157 },
158 CommandDef {
159 name: "status",
160 summary: "Show replication status",
161 usage: "red status [--bind 0.0.0.0:6380]",
162 flags: status_flags(),
163 },
164 CommandDef {
165 name: "inspect",
166 summary: "Inspect on-disk database state (catalog snapshot)",
167 usage: "red inspect catalog --path <FILE> [--at <SEQ>] [--json]",
168 flags: inspect_flags(),
169 },
170 CommandDef {
171 name: "mcp",
172 summary: "Start MCP server for AI agent integration",
173 usage: "red mcp [--path /data | --url <URI>] [--token <token>]",
174 flags: mcp_flags(),
175 },
176 CommandDef {
177 name: "auth",
178 summary: "Manage authentication (users, tokens, roles)",
179 usage: "red auth <subcommand>",
180 flags: auth_flags(),
181 },
182 CommandDef {
183 name: "connect",
184 summary: "Connect to a remote RedDB server (interactive REPL)",
185 usage: "red connect [--token <token>] [--query <sql>] <addr>",
186 flags: connect_flags(),
187 },
188 CommandDef {
189 name: "dump",
190 summary: "Export one or all collections as JSONL for backup/migration",
191 usage: "red dump [--path file] [--collection NAME] [-o FILE]",
192 flags: dump_flags(),
193 },
194 CommandDef {
195 name: "restore",
196 summary: "Import a previously dumped JSONL file into the database",
197 usage: "red restore [--path file] -i FILE [--collection NAME]",
198 flags: restore_flags(),
199 },
200 CommandDef {
201 name: "pitr-list",
202 summary: "List available point-in-time restore points from a snapshot archive",
203 usage: "red pitr-list --snapshot-prefix DIR --wal-prefix DIR",
204 flags: pitr_list_flags(),
205 },
206 CommandDef {
207 name: "pitr-restore",
208 summary: "Restore a database to a specific point in time from snapshots + WAL archive",
209 usage: "red pitr-restore --target-time UNIX_MS --dest PATH --snapshot-prefix DIR --wal-prefix DIR",
210 flags: pitr_restore_flags(),
211 },
212 CommandDef {
213 name: "doctor",
214 summary: "Health-check a running server against operator thresholds (PLAN.md Phase 5.5)",
215 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]",
216 flags: doctor_flags(),
217 },
218 CommandDef {
219 name: "bootstrap",
220 summary: "One-shot first-admin bootstrap for headless containers / K8s Jobs",
221 usage: "red bootstrap --path PATH --vault [--username USER] [--password-stdin] [--print-certificate] [--json]",
222 flags: bootstrap_flags(),
223 },
224 CommandDef {
225 name: "version",
226 summary: "Show RedDB version information",
227 usage: "red version",
228 flags: vec![],
229 },
230 CommandDef {
231 name: "vcs",
232 summary: "Version-control operations (Git for Data)",
233 usage: "red vcs <commit|branch|branches|tag|tags|checkout|merge|log|status|lca|resolve> [args] [flags]",
234 flags: vcs_flags(),
235 },
236 CommandDef {
237 name: "ui",
238 summary: "Open a graphical UI against a local .rdb or a remote red:///reds:// instance over a RedWire-over-WS bridge",
239 usage: "red ui file://./data.rdb | red ui red://host:port [--token TOKEN] [--ui-dir DIR] [--port N] [--tls-ca PEM] [--no-browser]",
240 flags: ui_flags(),
241 },
242 ]
243}
244
245pub fn flags_for_command(command: Option<&str>) -> Vec<FlagSchema> {
250 let mut flags = super::types::global_flags();
251 if matches!(command, Some("dump")) {
252 flags.retain(|flag| flag.long != "output" && flag.short != Some('o'));
253 }
254
255 if command == Some("admin") {
256 flags.extend(admin_flags());
257 return flags;
258 }
259 if command == Some("rpc") {
260 flags.extend(rpc_flags());
261 return flags;
262 }
263 if let Some(definition) = all_commands()
264 .into_iter()
265 .find(|definition| Some(definition.name) == command)
266 {
267 flags.extend(definition.flags);
268 }
269 flags
270}
271
272pub fn main_help_text() -> String {
274 let mut out = String::with_capacity(1024);
275
276 out.push_str("reddb -- unified multi-model database engine\n");
277 out.push('\n');
278 out.push_str("Usage: red <command> [args] [flags]\n");
279 out.push('\n');
280
281 out.push_str("Commands:\n");
282 for cmd in all_commands() {
283 out.push_str(&format!(" {:<14} {}\n", cmd.name, cmd.summary));
284 }
285 out.push_str(&format!(" {:<14} {}\n", "help", "Show help for a command"));
286 out.push('\n');
287
288 out.push_str("Global flags:\n");
289 out.push_str(&format!(" {:<24} {}\n", "-h, --help", "Show help"));
290 out.push_str(&format!(" {:<24} {}\n", "-j, --json", "Force JSON output"));
291 out.push_str(&format!(
292 " {:<24} {}\n",
293 "-o, --output FORMAT", "Output format [text|json|yaml]"
294 ));
295 out.push_str(&format!(" {:<24} {}\n", "-v, --verbose", "Verbose output"));
296 out.push_str(&format!(
297 " {:<24} {}\n",
298 " --no-color", "Disable colors"
299 ));
300 out.push_str(&format!(" {:<24} {}\n", " --version", "Show version"));
301 out.push('\n');
302
303 out.push_str("Examples:\n");
304 out.push_str(" red server --path ./data/reddb.rdb\n");
305 out.push_str(" red server --grpc-bind 127.0.0.1:55055 --http-bind 127.0.0.1:5000 --path ./data/reddb.rdb\n");
306 out.push_str(" red server --wire-bind 127.0.0.1:5050 --path ./data/reddb.rdb\n");
307 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");
308 out.push_str(" red replica --primary-addr http://primary:55055 --path ./data/replica.rdb\n");
309 out.push_str(" red query \"SELECT * FROM users\"\n");
310 out.push_str(" red insert users '{\"name\": \"Alice\"}'\n");
311 out.push_str(" red get users abc123\n");
312 out.push_str(" red health\n");
313 out.push_str(
314 " red tick --bind 127.0.0.1:5000 --operations maintenance,retention,checkpoint\n",
315 );
316 out.push_str(" red auth create-user alice --password secret --role admin\n");
317 out.push_str(" red auth create-api-key alice --name \"ci-token\" --role write\n");
318 out.push_str(" red auth list-users\n");
319 out.push_str(" red auth login alice --password secret\n");
320 out.push_str(" red connect 127.0.0.1:5050\n");
321 out.push_str(" red connect --query \"SELECT * FROM users\" 127.0.0.1:5050\n");
322 out.push('\n');
323
324 out.push_str("Run 'red <command> --help' for more information on a command.\n");
325 out
326}
327
328pub fn command_help_text(name: &str) -> Option<String> {
330 let cmds = all_commands();
331 let cmd = cmds.iter().find(|c| c.name == name)?;
332
333 let mut out = String::with_capacity(512);
334
335 out.push_str(&format!("red {} -- {}\n", cmd.name, cmd.summary));
336 out.push('\n');
337 out.push_str(&format!("Usage: {}\n", cmd.usage));
338 out.push('\n');
339
340 if !cmd.flags.is_empty() {
341 out.push_str("Flags:\n");
342 for flag in &cmd.flags {
343 let short_part = match flag.short {
344 Some(ch) => format!("-{}, ", ch),
345 None => " ".to_string(),
346 };
347 let value_part = if flag.expects_value {
348 format!(" <{}>", flag.long.to_uppercase())
349 } else {
350 String::new()
351 };
352 let label = format!("{}--{}{}", short_part, flag.long, value_part);
353 let padding = if label.len() < 24 {
354 24 - label.len()
355 } else {
356 2
357 };
358 let default_text = match &flag.default {
359 Some(d) => format!(" (default: {})", d),
360 None => String::new(),
361 };
362 out.push_str(&format!(
363 " {}{}{}{}\n",
364 label,
365 " ".repeat(padding),
366 flag.description,
367 default_text,
368 ));
369 }
370 out.push('\n');
371 }
372
373 Some(out)
374}
375
376fn server_flags() -> Vec<FlagSchema> {
381 vec![
382 FlagSchema::new("path")
383 .with_short('d')
384 .with_description("Persistent database file path (omit for in-memory)")
385 .with_default("./data/reddb.rdb"),
386 FlagSchema::new("bind").with_short('b').with_description(
387 "Bind address (host:port) for the routed front-door or legacy single-transport mode",
388 ),
389 FlagSchema::boolean("grpc").with_description("Enable the gRPC API"),
390 FlagSchema::boolean("http").with_description("Serve the HTTP API"),
391 FlagSchema::new("grpc-bind").with_description("Explicit gRPC bind address (host:port)"),
392 FlagSchema::new("http-bind").with_description("Explicit HTTP bind address (host:port)"),
393 FlagSchema::new("http-tls-bind")
394 .with_description("HTTPS bind address (host:port). Runs alongside --http-bind"),
395 FlagSchema::new("http-tls-cert").with_description("Path to HTTPS server certificate PEM"),
396 FlagSchema::new("http-tls-key").with_description("Path to HTTPS server private key PEM"),
397 FlagSchema::new("http-tls-client-ca")
398 .with_description("Path to PEM CA bundle for client cert verification (mTLS)"),
399 FlagSchema::new("wire-bind")
400 .with_description("Explicit wire bind address (host:port or unix:///path/to/socket)"),
401 FlagSchema::new("wire-tls-bind")
402 .with_description("Explicit wire TLS bind address (host:port)"),
403 FlagSchema::new("wire-tls-cert")
404 .with_description("Path to TLS certificate PEM for wire TLS"),
405 FlagSchema::new("wire-tls-key")
406 .with_description("Path to TLS private key PEM for wire TLS"),
407 FlagSchema::new("pg-bind").with_description(
408 "PostgreSQL wire protocol bind address (enables psql / JDBC / DBeaver clients)",
409 ),
410 FlagSchema::new("role")
411 .with_short('r')
412 .with_description("Replication role")
413 .with_choices(&["standalone", "primary", "replica"])
414 .with_default("standalone"),
415 storage_profile_flag(),
416 storage_packaging_flag(),
417 storage_preset_flag(),
418 FlagSchema::new("replica-count")
419 .with_description("Expected replica count for storage profile validation"),
420 FlagSchema::boolean("managed-backup")
421 .with_description("Declare managed backup for storage profile validation"),
422 FlagSchema::boolean("wal-retention")
423 .with_description("Declare WAL retention for storage profile validation"),
424 FlagSchema::new("primary-addr").with_description("Primary gRPC address for replica mode"),
425 FlagSchema::boolean("read-only").with_description("Open the database in read-only mode"),
426 FlagSchema::boolean("bounded-disk-residency").with_description(
427 "Opt into bounded disk residency for standalone single-file databases; upgrades the writer format",
428 ),
429 FlagSchema::boolean("no-create-if-missing")
430 .with_description("Fail instead of creating the database file"),
431 FlagSchema::boolean("auth").with_description("Enable authentication for this boot"),
432 FlagSchema::boolean("require-auth")
433 .with_description("Reject anonymous requests; implies --auth"),
434 FlagSchema::boolean("vault")
435 .with_description("Enable encrypted auth vault (reserved pages in main .rdb file)"),
436 FlagSchema::boolean("no-auth").with_description(
437 "Hard-disable auth: anonymous access, ignores REDDB_USERNAME/PASSWORD/vault, \
438 prints a startup warning. Local-dev shortcut — NEVER use in production.",
439 ),
440 FlagSchema::boolean("dev")
441 .with_description("Alias for --no-auth (local development convenience)."),
442 FlagSchema::new("bootstrap-preset")
443 .with_description(
444 "First-boot preset. With --vault on a fresh --path, red server \
445 self-bootstraps the paged vault in place (no separate `red bootstrap`) \
446 then applies the preset and serves; a re-boot against the existing \
447 vault just serves (idempotent — no re-bootstrap, no new certificate).",
448 )
449 .with_choices(&["simple", "production", "regulated", "cloud"]),
450 FlagSchema::new("bootstrap-manifest").with_description(
451 "Path to manifest-driven first boot JSON. Applied once on a fresh database \
452 before serving; later boots observe bootstrap state and skip re-applying \
453 the manifest (idempotent).",
454 ),
455 FlagSchema::new("bootstrap-admin")
456 .with_description("First admin username for production/cloud bootstrap"),
457 FlagSchema::new("bootstrap-admin-password").with_description(
458 "First admin password (DEV ONLY; prefer --bootstrap-admin-password-file)",
459 ),
460 FlagSchema::new("bootstrap-admin-password-file")
461 .with_description("File containing first admin password"),
462 FlagSchema::new("cloud-head-admin")
463 .with_description("Cloud preset head/platform admin username"),
464 FlagSchema::new("cloud-head-admin-password").with_description(
465 "Cloud preset head/platform admin password (DEV ONLY; prefer file flag)",
466 ),
467 FlagSchema::new("cloud-head-admin-password-file")
468 .with_description("File containing cloud head admin password"),
469 FlagSchema::new("customer-admin").with_description("Cloud preset customer admin username"),
470 FlagSchema::new("customer-admin-password")
471 .with_description("Cloud preset customer admin password (DEV ONLY; prefer file flag)"),
472 FlagSchema::new("customer-admin-password-file")
473 .with_description("File containing cloud customer admin password"),
474 FlagSchema::new("bootstrap-cert-out").with_description(
475 "Write the first-boot unseal certificate to this file (e.g. /run/reddb/cert.pem) \
476 without emitting it to logs, so a distroless init can read it back for the next \
477 boot's REDDB_CERTIFICATE_FILE unseal. Written only on the bootstrap-creating boot; \
478 a re-boot against the existing vault does not rewrite it.",
479 ),
480 FlagSchema::boolean("ui")
481 .with_description("Serve the pinned red-ui bundle as static assets"),
482 FlagSchema::new("ui-dir").with_description("Directory containing the red-ui bundle"),
483 FlagSchema::new("workers")
484 .with_short('w')
485 .with_description("Worker thread count (default: auto-detect from CPUs)"),
486 FlagSchema::new("log-dir").with_description(
487 "Directory for rotating log files (defaults to the parent of --path / ./logs)",
488 ),
489 FlagSchema::new("log-level")
490 .with_description(
491 "Log level filter — trace / debug / info / warn / error, or a RUST_LOG expression",
492 )
493 .with_default("info"),
494 FlagSchema::new("log-format")
495 .with_description("Log output format")
496 .with_choices(&["pretty", "json"])
497 .with_default("pretty"),
498 FlagSchema::new("log-keep-days")
499 .with_description("Number of rotated log files to keep")
500 .with_default("14"),
501 FlagSchema::boolean("no-log-file")
502 .with_description("Disable rotating file logs (stderr only)"),
503 FlagSchema::new("http-max-handlers").with_description(
504 "Max admitted in-flight HTTP requests, including queued execution \
505 (env: REDDB_HTTP_MAX_HANDLERS; \
506 red_config: red.http.max_handlers; default: (2 x num_cpus).clamp(8, 256))",
507 ),
508 FlagSchema::new("http-execution-workers").with_description(
509 "Max buffered non-health HTTP engine calls executed concurrently; excess admitted \
510 requests wait FIFO (env: REDDB_HTTP_EXECUTION_WORKERS; \
511 red_config: red.http.execution_workers; default: num_cpus)",
512 ),
513 FlagSchema::new("http-handler-timeout-ms")
514 .with_description(
515 "Per-handler total-time budget in ms (env: REDDB_HTTP_HANDLER_TIMEOUT_MS; \
516 red_config: red.http.handler_timeout_ms)",
517 )
518 .with_default("30000"),
519 FlagSchema::new("http-retry-after-secs")
520 .with_description(
521 "Retry-After seconds on limiter 503 (env: REDDB_HTTP_RETRY_AFTER_SECS; \
522 red_config: red.http.retry_after_secs; clamped to [1, 30])",
523 )
524 .with_default("5"),
525 FlagSchema::new("http-max-inflight-per-principal").with_description(
526 "Max concurrent in-flight HTTP requests per principal; over-cap requests \
527 get a structured 429 (env: REDDB_HTTP_MAX_INFLIGHT_PER_PRINCIPAL; \
528 red_config: red.http.max_inflight_per_principal; 0 disables; default: \
529 min(64, max_handlers - 1))",
530 ),
531 ]
532}
533
534fn replica_flags() -> Vec<FlagSchema> {
535 vec![
536 FlagSchema::new("primary-addr")
537 .with_short('p')
538 .with_description("Primary gRPC address (e.g. http://primary:55055)"),
539 FlagSchema::new("path")
540 .with_short('d')
541 .with_description("Local replica database file path")
542 .with_default("./data/reddb.rdb"),
543 FlagSchema::new("bind").with_short('b').with_description(
544 "Bind address (host:port) for the routed front-door or legacy single-transport mode",
545 ),
546 FlagSchema::boolean("grpc").with_description("Enable the gRPC API"),
547 FlagSchema::boolean("http").with_description("Serve the HTTP API"),
548 FlagSchema::new("grpc-bind").with_description("Explicit gRPC bind address (host:port)"),
549 FlagSchema::new("http-bind").with_description("Explicit HTTP bind address (host:port)"),
550 FlagSchema::new("wire-bind")
551 .with_description("Explicit wire bind address (host:port or unix:///path/to/socket)"),
552 storage_profile_flag(),
553 storage_packaging_flag(),
554 storage_preset_flag(),
555 FlagSchema::new("replica-count")
556 .with_description("Expected replica count for storage profile validation"),
557 FlagSchema::boolean("managed-backup")
558 .with_description("Declare managed backup for storage profile validation"),
559 FlagSchema::boolean("wal-retention")
560 .with_description("Declare WAL retention for storage profile validation"),
561 FlagSchema::boolean("auth").with_description("Enable authentication for this boot"),
562 FlagSchema::boolean("require-auth")
563 .with_description("Reject anonymous requests; implies --auth"),
564 FlagSchema::boolean("vault")
565 .with_description("Enable encrypted auth vault (reserved pages in main .rdb file)"),
566 FlagSchema::boolean("no-auth")
567 .with_description("Hard-disable auth: anonymous access, ignores vault"),
568 ]
569}
570
571fn ui_flags() -> Vec<FlagSchema> {
572 vec![
573 FlagSchema::boolean("server")
574 .with_description("Force the browser-served bridge path (skip the desktop deep link)"),
575 FlagSchema::boolean("desktop").with_description(
576 "Force the desktop app via the redui:// deep link (no browser fallback)",
577 ),
578 FlagSchema::new("ui-dir").with_description(
579 "Directory to serve the UI bundle from (defaults to the built-in fixture)",
580 ),
581 FlagSchema::new("port")
582 .with_description("Loopback port for the bridge (0 / omit picks an ephemeral port)"),
583 FlagSchema::new("tls-ca").with_description(
584 "PEM CA bundle to trust for a reds:// target (on top of system roots)",
585 ),
586 FlagSchema::new("token").with_short('t').with_description(
587 "Bearer token (session/API key). Held by red and injected into the \
588 RedWire handshake — the UI never sees it (env: RED_UI_TOKEN)",
589 ),
590 FlagSchema::boolean("no-browser").with_description(
591 "Do not open the default browser (also honoured via RED_UI_NO_BROWSER)",
592 ),
593 ]
594}
595
596fn vcs_flags() -> Vec<FlagSchema> {
597 vec![
598 FlagSchema::new("path")
599 .with_short('d')
600 .with_description("Persistent database file path (omit for in-memory)"),
601 FlagSchema::new("connection")
602 .with_short('c')
603 .with_description("Connection id for workset scoping")
604 .with_default("1"),
605 FlagSchema::new("branch").with_description("Branch name (for log/checkout/merge)"),
606 FlagSchema::new("from").with_description("Source ref or commit (branch create / merge)"),
607 FlagSchema::new("to").with_description("Upper bound for log range"),
608 FlagSchema::new("author")
609 .with_description("Commit author name")
610 .with_default("reddb"),
611 FlagSchema::new("email")
612 .with_description("Commit author email")
613 .with_default("reddb@localhost"),
614 FlagSchema::new("message")
615 .with_short('m')
616 .with_description("Commit message"),
617 FlagSchema::new("limit")
618 .with_description("Max log entries")
619 .with_default("20"),
620 FlagSchema::new("mode")
621 .with_description("Reset mode: soft | mixed | hard")
622 .with_default("mixed"),
623 FlagSchema::boolean("ff-only").with_description("Merge only if fast-forward"),
624 FlagSchema::boolean("no-ff").with_description("Always create a merge commit"),
625 ]
626}
627
628fn service_flags() -> Vec<FlagSchema> {
629 vec![
630 FlagSchema::new("binary")
631 .with_description("Path to the red binary")
632 .with_default("/usr/local/bin/red"),
633 FlagSchema::new("service-name")
634 .with_description("systemd unit name")
635 .with_default("reddb"),
636 FlagSchema::new("user")
637 .with_description("Service user")
638 .with_default("reddb"),
639 FlagSchema::new("group")
640 .with_description("Service group")
641 .with_default("reddb"),
642 FlagSchema::new("path")
643 .with_short('d')
644 .with_description("Persistent database file path")
645 .with_default(reddb_file::DEFAULT_SERVICE_DATABASE_PATH),
646 FlagSchema::new("bind").with_short('b').with_description(
647 "Bind address (host:port) for the routed front-door or legacy single-transport mode",
648 ),
649 FlagSchema::boolean("grpc").with_description("Enable the gRPC API in the service"),
650 FlagSchema::boolean("http").with_description("Install an HTTP service"),
651 FlagSchema::new("grpc-bind").with_description("Explicit gRPC bind address (host:port)"),
652 FlagSchema::new("http-bind").with_description("Explicit HTTP bind address (host:port)"),
653 ]
654}
655
656fn query_flags() -> Vec<FlagSchema> {
657 vec![
658 FlagSchema::new("bind")
659 .with_short('b')
660 .with_description("Server address")
661 .with_default("0.0.0.0:6380"),
662 FlagSchema::new("path").with_description("Open a local .rdb file in embedded mode"),
663 FlagSchema::new("param")
664 .with_short('p')
665 .with_description("Positional parameter for $1, $2, ... (repeatable)"),
666 FlagSchema::new("param-type").with_description("Type override for the preceding --param"),
667 FlagSchema::new("format").with_description(
668 "Row output format for query results (table|json|ndjson|csv|tsv|toon)",
669 ),
670 FlagSchema::new("save")
671 .with_description("Persist an ephemeral data-file query session as a new .rdb file"),
672 FlagSchema::boolean("dry-run")
673 .with_description("Preview the statement via EXPLAIN without executing it"),
674 ]
675}
676
677fn insert_flags() -> Vec<FlagSchema> {
678 data_target_flags(Some('p'))
679}
680
681fn get_flags() -> Vec<FlagSchema> {
682 data_target_flags(Some('p'))
683}
684
685fn delete_flags() -> Vec<FlagSchema> {
686 data_target_flags(Some('p'))
687}
688
689fn health_flags() -> Vec<FlagSchema> {
690 vec![
691 FlagSchema::new("bind")
692 .with_short('b')
693 .with_description("Server address; defaults by transport"),
694 FlagSchema::boolean("grpc").with_description("Probe a gRPC listener (default transport)"),
695 FlagSchema::boolean("http").with_description("Probe an HTTP listener"),
696 ]
697}
698
699fn bootstrap_flags() -> Vec<FlagSchema> {
700 vec![
701 FlagSchema::new("path")
702 .with_short('d')
703 .with_description("Persistent database file path"),
704 FlagSchema::boolean("vault")
705 .with_description("Required: seal credentials in the encrypted vault"),
706 FlagSchema::new("username")
707 .with_short('u')
708 .with_description("Admin username (defaults to REDDB_USERNAME)"),
709 FlagSchema::new("password")
710 .with_description("Admin password (DEV ONLY; prefer --password-stdin)"),
711 FlagSchema::boolean("password-stdin")
712 .with_description("Read the admin password from stdin (one line)"),
713 FlagSchema::boolean("print-certificate")
714 .with_description("Print only the certificate to stdout"),
715 ]
716}
717
718fn doctor_flags() -> Vec<FlagSchema> {
719 vec![
720 FlagSchema::new("bind")
721 .with_description("HTTP address of the server to probe")
722 .with_default("127.0.0.1:5000"),
723 FlagSchema::new("token")
724 .with_description("Admin bearer token; defaults to RED_ADMIN_TOKEN env"),
725 FlagSchema::new("backup-age-warn-secs")
726 .with_description("Warn when last successful backup is older than N seconds")
727 .with_default("600"),
728 FlagSchema::new("backup-age-crit-secs")
729 .with_description("Critical when last successful backup is older than N seconds")
730 .with_default("3600"),
731 FlagSchema::new("wal-lag-warn")
732 .with_description("Warn when WAL archive lag exceeds N records")
733 .with_default("1000"),
734 FlagSchema::new("wal-lag-crit")
735 .with_description("Critical when WAL archive lag exceeds N records")
736 .with_default("10000"),
737 ]
738}
739
740fn dump_flags() -> Vec<FlagSchema> {
741 vec![
742 FlagSchema::new("path")
743 .with_description("Local database file to dump from")
744 .with_default("./data/reddb.rdb"),
745 storage_profile_flag(),
746 storage_packaging_flag(),
747 storage_preset_flag(),
748 FlagSchema::new("collection")
749 .with_short('c')
750 .with_description("Single collection to dump (omit for all)"),
751 FlagSchema::new("output")
752 .with_short('o')
753 .with_description("Destination file (defaults to stdout)"),
754 ]
755}
756
757fn restore_flags() -> Vec<FlagSchema> {
758 vec![
759 FlagSchema::new("path")
760 .with_description("Local database file to restore into")
761 .with_default("./data/reddb.rdb"),
762 storage_profile_flag(),
763 storage_packaging_flag(),
764 storage_preset_flag(),
765 FlagSchema::boolean("bounded-disk-residency")
766 .with_description("Keep restored rows within the resolved memory budget"),
767 FlagSchema::new("input")
768 .with_short('i')
769 .with_description("Dump file to read (required)"),
770 FlagSchema::new("collection")
771 .with_short('c')
772 .with_description("Override target collection name"),
773 ]
774}
775
776fn pitr_list_flags() -> Vec<FlagSchema> {
777 vec![
778 FlagSchema::new("snapshot-prefix")
779 .with_description("Directory (or remote prefix) holding .snapshot files"),
780 FlagSchema::new("wal-prefix")
781 .with_description("Directory (or remote prefix) holding archived WAL segments"),
782 ]
783}
784
785fn pitr_restore_flags() -> Vec<FlagSchema> {
786 vec![
787 FlagSchema::new("target-time")
788 .with_description("Recovery target — UNIX ms (0 = latest available)"),
789 FlagSchema::new("dest")
790 .with_description("Destination database file path for the restored DB"),
791 FlagSchema::new("snapshot-prefix")
792 .with_description("Directory (or remote prefix) holding .snapshot files"),
793 FlagSchema::new("wal-prefix")
794 .with_description("Directory (or remote prefix) holding archived WAL segments"),
795 ]
796}
797
798fn tick_flags() -> Vec<FlagSchema> {
799 vec![
800 FlagSchema::new("bind")
801 .with_short('b')
802 .with_description("Server HTTP bind address")
803 .with_default("127.0.0.1:5000"),
804 FlagSchema::new("operations")
805 .with_description("Comma-separated operations: maintenance,retention,checkpoint"),
806 FlagSchema::boolean("dry-run")
807 .with_description("Validate operations without applying changes"),
808 ]
809}
810
811fn migrate_from_redis_flags() -> Vec<FlagSchema> {
812 vec![
813 FlagSchema::boolean("dry-run")
814 .with_description("Validate Redis and RedDB connectivity without cache writes"),
815 FlagSchema::new("redis-url")
816 .with_description("Redis URL to validate, for example redis://127.0.0.1:6379/0"),
817 FlagSchema::new("path")
818 .with_short('d')
819 .with_description("Local RedDB .rdb file to open for connectivity validation"),
820 FlagSchema::new("phase")
821 .with_description("Migration phase: dry-run | dual-write")
822 .with_default("dry-run"),
823 FlagSchema::new("namespace")
824 .with_description("Blob Cache namespace recorded in dry-run output")
825 .with_default("redis-migration"),
826 ]
827}
828
829fn migrate_pager_zone_flags() -> Vec<FlagSchema> {
830 vec![FlagSchema::new("path")
831 .with_short('d')
832 .with_description("Closed legacy sidecar-backed .rdb file to convert")]
833}
834
835fn salvage_flags() -> Vec<FlagSchema> {
836 vec![
837 FlagSchema::new("source").with_description("Damaged source .rdb file to read"),
838 FlagSchema::new("destination").with_description("Fresh destination .rdb file to create"),
839 ]
840}
841
842fn status_flags() -> Vec<FlagSchema> {
843 data_target_flags(Some('p'))
844}
845
846fn inspect_flags() -> Vec<FlagSchema> {
847 vec![
848 FlagSchema::new("path")
849 .with_short('d')
850 .with_description("Path to the on-disk database file"),
851 FlagSchema::new("at")
852 .with_description("Catalog at snapshot sequence (requires metadata journal)"),
853 ]
854}
855
856fn mcp_flags() -> Vec<FlagSchema> {
857 vec![
858 FlagSchema::new("uri")
859 .with_description("Connection URI (overrides --url and REDDB_MCP_URI)"),
860 FlagSchema::new("path")
861 .with_short('d')
862 .with_description("Data directory path (omit for in-memory)")
863 .with_default(""),
864 FlagSchema::new("url")
865 .with_description("Remote or embedded MCP connection URI; overrides REDDB_MCP_URI"),
866 FlagSchema::new("token")
867 .with_description("Bearer token fallback when --url has no userinfo"),
868 FlagSchema::boolean("allow-write")
873 .with_description("Permit tools and statements that modify data or schema"),
874 FlagSchema::boolean("allow-admin")
875 .with_description("Permit vault and auth tools (implies --allow-write)"),
876 ]
877}
878
879fn connect_flags() -> Vec<FlagSchema> {
880 vec![
881 FlagSchema::new("token")
882 .with_short('t')
883 .with_description("Auth token (session or API key)"),
884 FlagSchema::new("query")
885 .with_short('q')
886 .with_description("Execute a single query and exit"),
887 FlagSchema::new("user")
888 .with_short('u')
889 .with_description("Username for login"),
890 FlagSchema::new("password")
891 .with_short('p')
892 .with_description("Password for login"),
893 ]
894}
895
896fn auth_flags() -> Vec<FlagSchema> {
897 vec![
898 FlagSchema::new("bind")
899 .with_short('b')
900 .with_description("Server address")
901 .with_default("0.0.0.0:6380"),
902 FlagSchema::new("password")
903 .with_short('p')
904 .with_description("User password"),
905 FlagSchema::new("role")
906 .with_short('r')
907 .with_description("User role")
908 .with_choices(&["read", "write", "admin"]),
909 FlagSchema::new("name")
910 .with_short('n')
911 .with_description("API key name"),
912 FlagSchema::new("user")
913 .with_short('u')
914 .with_description("Target username"),
915 ]
916}
917
918fn admin_flags() -> Vec<FlagSchema> {
919 vec![
920 FlagSchema::new("bind")
921 .with_short('b')
922 .with_description("Server HTTP address")
923 .with_default("127.0.0.1:5000"),
924 FlagSchema::new("token")
925 .with_short('t')
926 .with_description("Admin bearer token (env: RED_ADMIN_TOKEN)"),
927 FlagSchema::boolean("csv").with_description("Emit CSV for tabular admin catalog commands"),
928 FlagSchema::new("limit").with_description("Max rows for list/stats/query commands"),
929 FlagSchema::new("type").with_description("Filter collections by model"),
930 FlagSchema::boolean("include-internal").with_description("Include internal collections"),
931 FlagSchema::new("collection").with_description("Filter by collection"),
932 FlagSchema::boolean("yes")
933 .with_short('y')
934 .with_description("Skip destructive-command confirmation"),
935 FlagSchema::boolean("if-exists")
936 .with_description("Suppress errors for a missing collection"),
937 ]
938}
939
940fn rpc_flags() -> Vec<FlagSchema> {
941 vec![
942 FlagSchema::boolean("stdio")
943 .with_description("Speak JSON-RPC 2.0 line-delimited over stdin/stdout"),
944 FlagSchema::new("path")
945 .with_short('d')
946 .with_description("Persistent database file path (omit for in-memory)"),
947 FlagSchema::new("connect")
948 .with_short('c')
949 .with_description("Proxy to a remote gRPC server"),
950 FlagSchema::new("token")
951 .with_short('t')
952 .with_description("Auth token forwarded to the remote server"),
953 ]
954}
955
956fn data_target_flags(path_short: Option<char>) -> Vec<FlagSchema> {
957 let path = FlagSchema::new("path").with_description("Open a local .rdb file in embedded mode");
958 let path = match path_short {
959 Some(short) => path.with_short(short),
960 None => path,
961 };
962 vec![
963 FlagSchema::new("bind")
964 .with_short('b')
965 .with_description("Server address")
966 .with_default("0.0.0.0:6380"),
967 path,
968 ]
969}
970
971fn storage_profile_flag() -> FlagSchema {
972 FlagSchema::new("storage-profile")
973 .with_description("Storage deploy profile")
974 .with_choices(&["embedded", "serverless", "primary-replica", "cluster"])
975}
976
977fn storage_packaging_flag() -> FlagSchema {
978 FlagSchema::new("storage-packaging")
979 .with_description("Storage packaging contract")
980 .with_choices(&["single-file", "operational-directory"])
981}
982
983fn storage_preset_flag() -> FlagSchema {
984 FlagSchema::new("storage-preset")
985 .with_description("Storage/deploy preset")
986 .with_choices(&[
987 "embedded",
988 "serverless",
989 "primary-replica-dev",
990 "primary-replica-small",
991 "primary-replica-production-ha",
992 "primary-replica-backup",
993 "primary-replica-wal-retention",
994 "cluster",
995 ])
996}
997
998pub fn completion_domains() -> Vec<(String, Vec<String>)> {
1004 vec![
1005 ("server".to_string(), vec![]),
1006 ("service".to_string(), vec![]),
1007 ("replica".to_string(), vec![]),
1008 ("tick".to_string(), vec![]),
1009 ("query".to_string(), vec!["q".to_string()]),
1010 ("insert".to_string(), vec!["i".to_string()]),
1011 ("get".to_string(), vec![]),
1012 ("delete".to_string(), vec!["del".to_string()]),
1013 ("health".to_string(), vec![]),
1014 ("status".to_string(), vec![]),
1015 ("inspect".to_string(), vec![]),
1016 ("migrate-from-redis".to_string(), vec![]),
1017 ("salvage".to_string(), vec![]),
1018 ("compact".to_string(), vec![]),
1019 ("scrub-paged-trees".to_string(), vec![]),
1020 ("mcp".to_string(), vec![]),
1021 ("auth".to_string(), vec![]),
1022 ("connect".to_string(), vec![]),
1023 ("version".to_string(), vec![]),
1024 ]
1025}
1026
1027pub fn completion_global_flags() -> Vec<(&'static str, Option<char>)> {
1029 vec![
1030 ("help", Some('h')),
1031 ("json", Some('j')),
1032 ("output", Some('o')),
1033 ("verbose", Some('v')),
1034 ("no-color", None),
1035 ("version", None),
1036 ]
1037}
1038
1039#[cfg(test)]
1040mod tests {
1041 use super::*;
1042
1043 #[test]
1044 fn test_all_commands_defined() {
1045 let cmds = all_commands();
1046 let names: Vec<&str> = cmds.iter().map(|c| c.name).collect();
1047 assert!(names.contains(&"server"));
1048 assert!(names.contains(&"query"));
1049 assert!(names.contains(&"insert"));
1050 assert!(names.contains(&"get"));
1051 assert!(names.contains(&"delete"));
1052 assert!(names.contains(&"health"));
1053 assert!(names.contains(&"tick"));
1054 assert!(names.contains(&"migrate-from-redis"));
1055 assert!(names.contains(&"salvage"));
1056 assert!(names.contains(&"status"));
1057 assert!(names.contains(&"inspect"));
1058 assert!(names.contains(&"connect"));
1059 assert!(names.contains(&"version"));
1060 }
1061
1062 #[test]
1063 fn test_inspect_has_flags() {
1064 let cmds = all_commands();
1065 let inspect = cmds.iter().find(|c| c.name == "inspect").unwrap();
1066 let flag_names: Vec<&str> = inspect.flags.iter().map(|f| f.long.as_str()).collect();
1067 assert!(flag_names.contains(&"path"));
1068 assert!(flag_names.contains(&"at"));
1069 }
1070
1071 #[test]
1072 fn test_server_has_flags() {
1073 let cmds = all_commands();
1074 let server = cmds.iter().find(|c| c.name == "server").unwrap();
1075 let flag_names: Vec<&str> = server.flags.iter().map(|f| f.long.as_str()).collect();
1076 assert!(flag_names.contains(&"path"));
1077 assert!(flag_names.contains(&"bind"));
1078 assert!(flag_names.contains(&"http-max-handlers"));
1080 assert!(flag_names.contains(&"http-execution-workers"));
1081 assert!(flag_names.contains(&"http-handler-timeout-ms"));
1082 assert!(flag_names.contains(&"http-retry-after-secs"));
1083 }
1084
1085 #[test]
1086 fn test_server_help_text_lists_http_limit_flags() {
1087 let help = command_help_text("server").unwrap();
1088 assert!(help.contains("--http-max-handlers"));
1089 assert!(help.contains("--http-execution-workers"));
1090 assert!(help.contains("--http-handler-timeout-ms"));
1091 assert!(help.contains("--http-retry-after-secs"));
1092 assert!(help.contains("REDDB_HTTP_MAX_HANDLERS"));
1093 assert!(help.contains("REDDB_HTTP_EXECUTION_WORKERS"));
1094 }
1095
1096 #[test]
1097 fn test_replica_has_flags() {
1098 let cmds = all_commands();
1099 let replica = cmds.iter().find(|c| c.name == "replica").unwrap();
1100 let flag_names: Vec<&str> = replica.flags.iter().map(|f| f.long.as_str()).collect();
1101 assert!(flag_names.contains(&"primary-addr"));
1102 assert!(flag_names.contains(&"path"));
1103 assert!(flag_names.contains(&"bind"));
1104 }
1105
1106 #[test]
1107 fn test_main_help_text() {
1108 let help = main_help_text();
1109 assert!(help.contains("reddb"));
1110 assert!(help.contains("Usage: red"));
1111 assert!(help.contains("Commands:"));
1112 assert!(help.contains("server"));
1113 assert!(help.contains("query"));
1114 assert!(help.contains("Global flags:"));
1115 assert!(help.contains("--help"));
1116 assert!(help.contains("Examples:"));
1117 }
1118
1119 #[test]
1120 fn test_command_help_text() {
1121 let help = command_help_text("server").unwrap();
1122 assert!(help.contains("red server"));
1123 assert!(help.contains("--path"));
1124 assert!(help.contains("--bind"));
1125 }
1126
1127 #[test]
1128 fn test_replica_command_help() {
1129 let help = command_help_text("replica").unwrap();
1130 assert!(help.contains("red replica"));
1131 assert!(help.contains("--primary-addr"));
1132 }
1133
1134 #[test]
1135 fn test_migrate_from_redis_command_help() {
1136 let help = command_help_text("migrate-from-redis").unwrap();
1137 assert!(help.contains("red migrate-from-redis"));
1138 assert!(help.contains("--dry-run"));
1139 assert!(help.contains("--redis-url"));
1140 assert!(help.contains("application-owned helper"));
1141 }
1142
1143 #[test]
1144 fn test_migrate_pager_zone_command_help() {
1145 let help = command_help_text("migrate-pager-zone").unwrap();
1146 assert!(help.contains("red migrate-pager-zone"));
1147 assert!(help.contains("--path"));
1148 assert!(help.contains("legacy sidecar-backed"));
1149 }
1150
1151 #[test]
1152 fn test_command_help_text_unknown() {
1153 assert!(command_help_text("nonexistent").is_none());
1154 }
1155
1156 #[test]
1157 fn test_flag_builder() {
1158 let flag = Flag::new("output", "Output format")
1159 .with_short('o')
1160 .with_default("text")
1161 .with_arg("FORMAT");
1162
1163 assert_eq!(flag.long, "output");
1164 assert_eq!(flag.short, Some('o'));
1165 assert_eq!(flag.description, "Output format");
1166 assert_eq!(flag.default, Some("text".to_string()));
1167 assert_eq!(flag.arg, Some("FORMAT".to_string()));
1168 }
1169
1170 #[test]
1171 fn test_completion_domains() {
1172 let domains = completion_domains();
1173 let names: Vec<&str> = domains.iter().map(|(n, _)| n.as_str()).collect();
1174 assert!(names.contains(&"server"));
1175 assert!(names.contains(&"query"));
1176 assert!(names.contains(&"health"));
1177 }
1178
1179 #[test]
1180 fn test_completion_global_flags() {
1181 let flags = completion_global_flags();
1182 assert!(flags.contains(&("help", Some('h'))));
1183 assert!(flags.contains(&("json", Some('j'))));
1184 assert!(flags.contains(&("verbose", Some('v'))));
1185 assert!(flags.contains(&("no-color", None)));
1186 }
1187}