Building a CLI with Guide
TL;DR: Use GuideBuilder to assemble genesis modules into a coherent CLI with progressive-disclosure verbosity, TTY-aware output format, and auto-generated suggestions.
Context & Prerequisites
This guide explains how to build a CLI tool using the guide module. Before starting, ensure you have:
- Added
genesis-vibesto yourCargo.toml - Familiarity with
clapargument parsing
Adding verbosity and format to your CLI
Embed CliVerbosity and CliFormat as flattened clap args:
use clap::Parser;
use genesis::guide::{CliVerbosity, CliFormat, Verbosity, OutputFormat};
#[derive(Parser)]
#[command(name = "my-tool")]
struct Cli {
#[command(flatten)]
verbose: CliVerbosity,
#[command(flatten)]
format: CliFormat,
}
fn main() {
let cli = Cli::parse();
let verbosity = cli.verbose.verbosity();
let format = cli.format.format();
// ...
}
This gives you:
-v/-vv/-vvvfor progressive verbosity-q/--quietfor silencing output--json/--humanfor output format, with auto-detection
Using the Output helper
The Output type wraps an Envelope with a fluent builder API:
#![allow(unused)]
fn main() {
use genesis::guide::{Output, Verbosity};
use std::io::Write;
fn greet(name: &str, verbosity: Verbosity) -> Result<(), Box<dyn std::error::Error>> {
let output = Output::success(format!("Hello, {name}!"))
.with_warning("Name contains uppercase characters")
.with_next_step("Try `my-tool wave` for a wave");
let mut stdout = std::io::stdout();
let mut stderr = std::io::stderr();
output.print(verbosity, &mut stdout, &mut stderr)?;
Ok(())
}
}
Format-dispatching with emit()
Use Output::emit() to let the format decide — human-readable text or JSON envelope:
#![allow(unused)]
fn main() {
use genesis::guide::{Output, OutputFormat, Verbosity};
fn list_items(
cli_version: &str,
format: OutputFormat,
verbosity: Verbosity,
) -> Result<(), Box<dyn std::error::Error>> {
let output = Output::success(vec!["item1", "item2", "item3"]);
output.emit(
cli_version,
format,
verbosity,
&mut std::io::stdout(),
&mut std::io::stderr(),
)?;
Ok(())
}
}
emit()takes your tool’scli_versionand the currentVerbosity, and requires the data payload to implementSerialize(needed for the JSON branch).
Progressive-disclosure verbosity
The verbosity levels control what the user sees. CliVerbosity maps the -v count
via Verbosity::from_verbose_count():
| Count | -v flags | Level | Shows |
|---|---|---|---|
| 0 | -q / --quiet | Quiet | Errors only |
| 1 | (none) | Normal | Result + next step |
| 2 | -v | Verbose | + warnings + context |
| 3+ | -vv / -vvv | Debug | + internals + trace |
Use Verbosity::help_footer() to display a hint in your CLI help text:
fn main() {
println!("{}", Verbosity::help_footer()); // "Use -v for..."
}
Assembling a CLI with GuideBuilder
GuideBuilder (created via Guide::builder()) collects all genesis modules into
a coherent CLI runner. It registers valid commands for typo detection, sets up
verbosity, and prepares config integration:
#![allow(unused)]
fn main() {
use genesis::guide::Guide;
let guide = Guide::builder("my-tool", env!("CARGO_PKG_VERSION"))
.about("Does something useful")
.commands(&["init", "doctor", "status"])
.build();
// Run a command with format-aware output
// guide.run(|g| { ... });
// guide.run_formatted(format, |g| { ... });
}
See the source code of
waiordontfor completeGuideusage examples.
Exit codes
Guide::run and Guide::run_formatted follow a documented exit-code
contract, stable across versions:
| Exit | Meaning |
|---|---|
0 | Success |
1 | User-facing error — the handler returned Err, or the Output is an error envelope |
2 | Internal failure — I/O error while emitting output (not the user’s fault) |
Panics are never masked: there is no panic hook, and stack traces are
never swallowed. A panicking command unwinds with Rust’s default
runtime behavior (typically exit 101). Eval harnesses and shell
scripts can therefore distinguish “graceful failure with hint
envelope” (1) from “crashed” (any other nonzero).
Error handling with ErrorSink
ErrorSink handles errors with self-healing suggestions — it prints the error,
optionally persists it to the error scratch, and optionally suggests the tool’s
feedback subcommand:
#![allow(unused)]
fn main() {
use genesis::guide::ErrorSink;
use genesis::suggestions::Suggestion;
let sink = ErrorSink::new("my-tool");
let err = std::io::Error::new(std::io::ErrorKind::NotFound, "config.toml not found");
// Print the error with a suggestion footer to stderr
let mut stderr = std::io::stderr();
sink.handle(&err, &mut stderr);
// Or with an explicit suggestion footer override
let fix = Suggestion::Fix {
description: "create a default config".to_string(),
command: Some("my-tool init".to_string()),
};
sink.handle_with_footer(&err, &fix, &mut stderr);
}
ErrorSink is configurable: scratch persists the last error, suggest prints
a Suggestion::Fix footer, context includes the full ContextBundle, and
feedback_subcommand names the subcommand to suggest (set None to disable).
Verbosity is honored via the verbosity field.
Troubleshooting: Common Fail-States
| Symptom | Cause | Fix |
|---|---|---|
--json flag not recognized | CliFormat not embedded in args | Add #[command(flatten)] format: CliFormat |
| Human output still shows JSON | TTY detection fails in CI | Explicitly pass --human or --json |
| Warnings not shown | Verbosity too low | Use -v or check Verbosity >= Verbose |
Further Exploration
- Using the Envelope — detailed envelope patterns
- Adding a DoctorCheck — diagnostic framework for your CLI’s
doctorsubcommand