Skip to main content

mermaid_cli/utils/
confirm.rs

1//! Shared interactive-confirmation gate for destructive / untrusted actions.
2//!
3//! One fail-closed y/N primitive, reused by `mermaid update` (#110), `mermaid
4//! restore` (#113), and the MCP untrusted-package gate (#10) so the
5//! TTY-detection + `--yes`/`--force` policy lives in exactly one place.
6
7use std::io::{self, IsTerminal, Write};
8
9use anyhow::{Result, bail};
10
11/// Whether `s` is `y`/`yes` (case-insensitive). Default is NO — anything else,
12/// including empty input / EOF, is declined.
13pub fn is_affirmative(s: &str) -> bool {
14    s.eq_ignore_ascii_case("y") || s.eq_ignore_ascii_case("yes")
15}
16
17/// Fail-closed policy: refuse a destructive/untrusted action when there is no
18/// interactive terminal to confirm at and the explicit opt-in was not passed.
19/// Also defeats `yes | mermaid …`, since a pipe is not a TTY.
20pub fn should_refuse_noninteractive(is_tty: bool, assume_yes: bool) -> bool {
21    !is_tty && !assume_yes
22}
23
24/// Confirm a destructive action (default NO). `assume_yes` (a `--yes`/`--force`
25/// flag) is the explicit opt-in for scripted use; without it a non-interactive
26/// session refuses rather than proceeding silently. `prompt` is printed
27/// verbatim, followed by ` [y/N]: `.
28pub fn confirm_or_refuse(prompt: &str, assume_yes: bool) -> Result<bool> {
29    if assume_yes {
30        return Ok(true);
31    }
32    if should_refuse_noninteractive(io::stdin().is_terminal(), assume_yes) {
33        bail!(
34            "Refusing to proceed without confirmation in a non-interactive session. \
35             Re-run in an interactive terminal, or pass --force/--yes to allow it."
36        );
37    }
38    print!("{prompt} [y/N]: ");
39    io::stdout().flush()?;
40    let mut input = String::new();
41    io::stdin().read_line(&mut input)?;
42    Ok(is_affirmative(input.trim()))
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    #[test]
50    fn affirmative_accepts_only_y_yes() {
51        assert!(is_affirmative("y"));
52        assert!(is_affirmative("Y"));
53        assert!(is_affirmative("yes"));
54        assert!(is_affirmative("YES"));
55        assert!(!is_affirmative("n"));
56        assert!(!is_affirmative(""));
57        assert!(!is_affirmative("sure"));
58    }
59
60    #[test]
61    fn refuses_noninteractive_without_optin() {
62        // No TTY + no --yes → refuse (fail closed).
63        assert!(should_refuse_noninteractive(false, false));
64        // --yes overrides the missing TTY.
65        assert!(!should_refuse_noninteractive(false, true));
66        // A real TTY is fine either way.
67        assert!(!should_refuse_noninteractive(true, false));
68    }
69
70    #[test]
71    fn confirm_returns_true_immediately_when_assume_yes() {
72        // assume_yes short-circuits before any stdin/TTY interaction.
73        assert!(confirm_or_refuse("do the thing?", true).expect("assume_yes is infallible"));
74    }
75}