Skip to content

Add --porcelain flag for clean, pipeable commit-message output #6

@yegor256

Description

@yegor256

Problem or motivation

commitbee is designed for interactive terminal use. While the LLM generates a response, its raw JSON is streamed to the terminal token-by-token, spinners and progress lines flow alongside it, and COMMITBEE_LOG=debug adds tracing on top. The final sanitized commit message only appears at the end, on top of all that noise. There is no clean way today to get just the commit message out of commitbee — every piping attempt either leaks noise through or has to silence legitimate error messages with 2>/dev/null.

Concrete use cases that are blocked today:

  • Piping the generated message into another tool — a validator like commitlint, a preview viewer, a formatter, a custom shell script.
  • Editor / IDE integrations (VS Code, JetBrains, nvim, etc.) that capture stdout and insert into their commit-message buffer.
  • Populating the commit-message file from a prepare-commit-msg hook without the current 2>/dev/null workaround that also hides real errors.
  • Higher-level automation (release-notes pipelines, AI-review chains, etc.) that expects exactly one clean string on stdout.

Example usage once shipped:

# Pipe the generated message through any tool — validator, formatter, preview
commitbee --porcelain | commitlint
commitbee --porcelain | bat -l gitcommit

# Use from any shell script
commitbee --porcelain | your-script.sh

# Populate git's commit-msg file from a prepare-commit-msg hook
# ($1 is the path git passes to the hook)
commitbee --porcelain > "$1"

The existing workaround — commitbee --dry-run --yes 2>/dev/null — is undiscoverable, isn't a documented contract, and silences legitimate errors the user would otherwise want to see.

Note on the original phrasing: the live token stream users see today is JSON (the LLM returns a JSON object and commitbee streams it token-by-token before the sanitizer turns it into a Conventional Commit message). The final stdout contract under --porcelain is the sanitized plain-text commit message — the user-facing artifact — not the raw LLM JSON.

Proposed solution

Add a new top-level flag --porcelain that puts commitbee into a machine-readable output mode.

stdout contract:

  • stdout contains exactly the final sanitized commit message followed by a single \n.
  • UTF-8, no BOM, no ANSI escape sequences, no terminal hyperlinks.
  • On any error, stdout is empty; process exits non-zero.

What --porcelain silences:

  • The live-streamed LLM JSON response (the main source of noise today).
  • All progress indicators and spinners.
  • All info / warning / status lines.
  • All tracing output, regardless of RUST_LOG / COMMITBEE_LOG.
  • ANSI color in all styled output, including error reports.
  • Terminal hyperlinks in error reports.

What --porcelain forces non-interactive:

  • Implies --dry-run — no commit is created; the message is printed and the process exits.
  • Implies --no-split — split-commit UI is interactive-only.
  • The Edit / Refine / Cancel review menu is unreachable under --dry-run.
  • Disables the interactive --allow-secrets confirmation — with --allow-secrets passed, porcelain falls through to the non-interactive "fail closed" branch if secrets are detected.

Incompatibilities (rejected at argument-parse time with exit code 2):

Combination Reason
--porcelain --yes --yes commits for real; --porcelain only generates and prints. Mutually exclusive output destinations.
--porcelain --clipboard stdout contract vs. clipboard destination
--porcelain --show-prompt swallowing a debug flag silently would be deceptive
--porcelain --verbose same reasoning
--porcelain -n <N> multi-candidate UI is pointless without a picker
--porcelain <subcommand> --porcelain is only meaningful for the default generate flow; it does not apply to config, doctor, completions, hook, init, set-key, get-key, or eval

Errors still flow to stderr (color and hyperlinks disabled under porcelain). The process exits non-zero, so downstream tools detect failure without parsing stdout.

Stability boundary:

  • The structural contract (one sanitized commit message on stdout, trailing \n, UTF-8, no decoration) is stable from v0.7.0 onward.
  • The content of the message (wording, capitalization, type/scope choices) depends on the configured LLM and is not stable across prompts, models, or commitbee versions.

Acceptance criteria

  • commitbee --porcelain writes exactly <message>\n to stdout on a successful generation.
  • commitbee --porcelain writes zero bytes to stderr on success, regardless of RUST_LOG, COMMITBEE_LOG, or TTY state.
  • commitbee --porcelain exits 0 on success and non-zero on any failure (no staged changes, LLM unreachable, secrets detected, cancelled, etc.).
  • commitbee --porcelain combined with any of --yes, --clipboard, --show-prompt, --verbose, -n <N>, or a subcommand exits with an argument-parse error (exit code 2) and empty stdout.
  • commitbee --porcelain --allow-secrets with staged secrets does not hang on interactive stdin; it fails closed with a non-zero exit and empty stdout.
  • commitbee --porcelain piped to a non-TTY consumer produces identical output to TTY (no spinner bleed-through, no ANSI).
  • Integration tests in tests/porcelain.rs cover each of the above using assert_cmd + wiremock + tempfile with env_clear().
  • A structural lint test walks src/ and fails if println! / print! appears outside a pinned allowlist of known emission sites (prevents future stdout leaks).

Alternatives considered

  • --dry-run alone: prints the final message to stdout but still streams the live LLM response and progress lines, and can drop into the interactive review menu in a TTY. Not a stable machine-readable contract.
  • Shell redirection (commitbee --dry-run --yes 2>/dev/null): the current workaround, but silences legitimate errors, is undiscoverable, and isn't a documented contract.
  • Parsing or grepping commitbee's current stdout: fragile; breaks whenever output formatting changes.
  • --just-body (original suggestion): ambiguous — a Conventional Commit message has both a subject and a body, so "just body" reads as "only the body paragraph, omitting the subject." Rejected.
  • --format=raw|json as a single knob: semantically awkward because --format=raw implies a format is being chosen when the intent is the absence of formatting. Structured JSON output is a separate future concern — see Related.

Area

CLI / UX

Related

  • Future structured output: a separate --format=<json|…> modifier is expected to land when structured JSON output becomes a feature. It will be orthogonal to --porcelain — one controls stdout cleanliness, the other controls output shape. --porcelain alone will always mean "plain-text commit message."
  • Milestone: v0.7.0

Issue originally reported by @yegor256; description refined by the maintainer.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions