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