Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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-vibes to your Cargo.toml
  • Familiarity with clap argument 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 / -vvv for progressive verbosity
  • -q / --quiet for silencing output
  • --json / --human for 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’s cli_version and the current Verbosity, and requires the data payload to implement Serialize (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 flagsLevelShows
0-q / --quietQuietErrors only
1(none)NormalResult + next step
2-vVerbose+ warnings + context
3+-vv / -vvvDebug+ 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 wai or dont for complete Guide usage examples.

Exit codes

Guide::run and Guide::run_formatted follow a documented exit-code contract, stable across versions:

ExitMeaning
0Success
1User-facing error — the handler returned Err, or the Output is an error envelope
2Internal 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

SymptomCauseFix
--json flag not recognizedCliFormat not embedded in argsAdd #[command(flatten)] format: CliFormat
Human output still shows JSONTTY detection fails in CIExplicitly pass --human or --json
Warnings not shownVerbosity too lowUse -v or check Verbosity >= Verbose

Further Exploration