use clap::ValueEnum;
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
#[clap(rename_all = "lower")]
pub enum OutputMode {
Human,
Json,
Quiet,
Mcp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlagInput {
Explicit(OutputMode),
JsonShort,
QuietShort,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResolvedOutput {
pub mode: OutputMode,
pub quiet_was_explicit: bool,
}
pub fn is_artifact_command(name: &str) -> bool {
matches!(name, "bib" | "csl" | "capabilities")
}
pub fn resolve(
forced_implicit: Option<OutputMode>,
flag: FlagInput,
env: Option<&str>,
is_tty: bool,
) -> ResolvedOutput {
if let Some(m) = forced_implicit {
return ResolvedOutput {
mode: m,
quiet_was_explicit: false,
};
}
let (mode, quiet_was_explicit) = match flag {
FlagInput::Explicit(OutputMode::Quiet) => (OutputMode::Quiet, true),
FlagInput::Explicit(m) => (m, false),
FlagInput::JsonShort => (OutputMode::Json, false),
FlagInput::QuietShort => (OutputMode::Quiet, true),
FlagInput::None => match env.and_then(parse_env_mode) {
Some(OutputMode::Quiet) => (OutputMode::Quiet, true),
Some(m) => (m, false),
None => {
if is_tty {
(OutputMode::Human, false)
} else {
(OutputMode::Quiet, false)
}
}
},
};
ResolvedOutput {
mode,
quiet_was_explicit,
}
}
pub fn parse_env_mode(s: &str) -> Option<OutputMode> {
match s.trim().to_ascii_lowercase().as_str() {
"human" => Some(OutputMode::Human),
"json" => Some(OutputMode::Json),
"quiet" => Some(OutputMode::Quiet),
"mcp" => Some(OutputMode::Mcp),
_ => None,
}
}
pub fn stdout_is_tty() -> bool {
use std::io::IsTerminal;
std::io::stdout().is_terminal()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn forced_mcp_wins_over_flag_env_and_tty() {
let out = resolve(
Some(OutputMode::Mcp),
FlagInput::Explicit(OutputMode::Quiet),
Some("human"),
true,
);
assert_eq!(out.mode, OutputMode::Mcp);
assert!(
!out.quiet_was_explicit,
"forced_implicit is system policy, never explicit user Quiet"
);
}
#[test]
fn explicit_flag_wins_over_env_and_tty() {
let out = resolve(
None,
FlagInput::Explicit(OutputMode::Json),
Some("human"),
true,
);
assert_eq!(out.mode, OutputMode::Json);
assert!(!out.quiet_was_explicit);
}
#[test]
fn json_short_flag_implies_json() {
let out = resolve(None, FlagInput::JsonShort, Some("human"), true);
assert_eq!(out.mode, OutputMode::Json);
assert!(!out.quiet_was_explicit);
}
#[test]
fn quiet_short_flag_implies_quiet() {
let out = resolve(None, FlagInput::QuietShort, Some("human"), true);
assert_eq!(out.mode, OutputMode::Quiet);
assert!(
out.quiet_was_explicit,
"`--quiet`/`-q` is an explicit Quiet signal"
);
}
#[test]
fn env_wins_when_no_flag() {
let out = resolve(None, FlagInput::None, Some("json"), true);
assert_eq!(out.mode, OutputMode::Json);
assert!(!out.quiet_was_explicit);
}
#[test]
fn env_is_case_insensitive_and_trims_whitespace() {
assert_eq!(parse_env_mode("HUMAN"), Some(OutputMode::Human));
assert_eq!(parse_env_mode(" Json "), Some(OutputMode::Json));
assert_eq!(parse_env_mode("MCP"), Some(OutputMode::Mcp));
}
#[test]
fn unrecognised_env_falls_through_to_tty() {
let tty = resolve(None, FlagInput::None, Some("garbage"), true);
let pipe = resolve(None, FlagInput::None, Some("garbage"), false);
assert_eq!(tty.mode, OutputMode::Human);
assert_eq!(pipe.mode, OutputMode::Quiet);
assert!(
!pipe.quiet_was_explicit,
"pipe-default Quiet is implicit, not explicit"
);
}
#[test]
fn empty_env_falls_through_to_tty() {
assert_eq!(parse_env_mode(""), None);
assert_eq!(parse_env_mode(" "), None);
let tty = resolve(None, FlagInput::None, Some(""), true);
assert_eq!(tty.mode, OutputMode::Human);
}
#[test]
fn tty_with_no_flag_no_env_yields_human() {
let out = resolve(None, FlagInput::None, None, true);
assert_eq!(out.mode, OutputMode::Human);
assert!(!out.quiet_was_explicit);
}
#[test]
fn no_tty_with_no_flag_no_env_yields_quiet() {
let out = resolve(None, FlagInput::None, None, false);
assert_eq!(out.mode, OutputMode::Quiet);
assert!(
!out.quiet_was_explicit,
"non-TTY default to Quiet is implicit (#219 / #220 / ADR-0017 Am1)"
);
}
#[test]
fn env_does_not_override_explicit_flag() {
let out = resolve(
None,
FlagInput::Explicit(OutputMode::Quiet),
Some("human"),
true,
);
assert_eq!(out.mode, OutputMode::Quiet);
assert!(
out.quiet_was_explicit,
"`--mode quiet` is an explicit Quiet signal"
);
}
#[test]
fn forced_implicit_overrides_env() {
let out = resolve(Some(OutputMode::Mcp), FlagInput::None, Some("human"), true);
assert_eq!(out.mode, OutputMode::Mcp);
}
#[test]
fn doiget_mode_quiet_env_is_explicit_quiet() {
let out = resolve(None, FlagInput::None, Some("quiet"), true);
assert_eq!(out.mode, OutputMode::Quiet);
assert!(out.quiet_was_explicit);
}
#[test]
fn non_tty_quiet_default_is_implicit_quiet() {
let out = resolve(None, FlagInput::None, None, false);
assert_eq!(out.mode, OutputMode::Quiet);
assert!(!out.quiet_was_explicit);
}
#[test]
fn artifact_command_classifier_covers_bib_csl_capabilities() {
assert!(is_artifact_command("bib"));
assert!(is_artifact_command("csl"));
assert!(is_artifact_command("capabilities"));
assert!(!is_artifact_command("audit-log"));
assert!(!is_artifact_command("fetch"));
assert!(!is_artifact_command("info"));
assert!(!is_artifact_command(""));
}
}