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