use std::io::{IsTerminal, Write};
use super::meta::{CommandMeta, Confirmation};
pub const REFUSE_NON_INTERACTIVE: &str =
"destructive command requires --yes in non-interactive mode";
fn is_interactive() -> bool {
std::io::stdin().is_terminal() && std::io::stdout().is_terminal()
}
pub fn gate(token_word: &str, json: bool, yes: bool) -> anyhow::Result<()> {
gate_with_interactive(token_word, json, yes, is_interactive())
}
pub fn gate_meta(meta: &CommandMeta, json: bool, yes: bool) -> anyhow::Result<()> {
match meta.confirmation {
Some(Confirmation::TypeToken { message, token }) => {
if !yes && !json && is_interactive() {
eprintln!();
eprintln!("{message}");
eprintln!();
}
gate(token, json, yes)
}
None => Ok(()),
}
}
fn gate_with_interactive(
token_word: &str,
json: bool,
yes: bool,
interactive: bool,
) -> anyhow::Result<()> {
if yes {
return Ok(());
}
if json || !interactive {
anyhow::bail!(REFUSE_NON_INTERACTIVE);
}
eprint!("Type {token_word} to confirm: ");
std::io::stderr().flush().ok();
let mut answer = String::new();
std::io::stdin().read_line(&mut answer)?;
if answer.trim() != token_word {
anyhow::bail!("confirmation declined");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn yes_passes_silently_even_in_json() {
assert!(gate("DESTROY", true, true).is_ok());
assert!(gate("RESET", false, true).is_ok());
}
#[test]
fn json_without_yes_refuses() {
let err = gate("DESTROY", true, false).unwrap_err();
assert_eq!(err.to_string(), REFUSE_NON_INTERACTIVE);
}
#[test]
fn non_tty_without_yes_refuses() {
let err = gate_with_interactive("DESTROY", false, false, false).unwrap_err();
assert_eq!(err.to_string(), REFUSE_NON_INTERACTIVE);
}
#[test]
fn yes_short_circuits_before_tty_check() {
assert!(gate_with_interactive("RESET", false, true, false).is_ok());
}
}