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