alopex_cli/batch/
detector.rs

1use std::io::{self, IsTerminal};
2
3use crate::cli::{
4    Cli, ColumnarCommand, Command, HnswCommand, IndexCommand, KvCommand, KvTxnCommand,
5    ProfileCommand,
6};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct BatchMode {
10    pub is_batch: bool,
11    pub is_tty: bool,
12    pub source: BatchModeSource,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum BatchModeSource {
17    Explicit,
18    Environment,
19    PipeDetected,
20    Default,
21}
22
23impl BatchMode {
24    pub fn detect(cli: &Cli) -> Self {
25        let is_tty = io::stdin().is_terminal();
26        let env_mode = std::env::var("ALOPEX_MODE").ok();
27
28        Self::detect_with(cli, is_tty, env_mode.as_deref())
29    }
30
31    pub(crate) fn detect_with(cli: &Cli, is_tty: bool, env_mode: Option<&str>) -> Self {
32        if cli.batch {
33            return Self {
34                is_batch: true,
35                is_tty,
36                source: BatchModeSource::Explicit,
37            };
38        }
39
40        if matches!(env_mode, Some("batch")) {
41            return Self {
42                is_batch: true,
43                is_tty,
44                source: BatchModeSource::Environment,
45            };
46        }
47
48        if !is_tty {
49            return Self {
50                is_batch: true,
51                is_tty,
52                source: BatchModeSource::PipeDetected,
53            };
54        }
55
56        Self {
57            is_batch: false,
58            is_tty,
59            source: BatchModeSource::Default,
60        }
61    }
62
63    #[allow(dead_code)]
64    pub fn should_prompt(&self, cli: &Cli, is_destructive: bool) -> bool {
65        if cli.yes || self.is_batch {
66            return false;
67        }
68
69        is_destructive
70    }
71
72    #[allow(dead_code)]
73    pub fn should_show_progress(&self, explicit_progress: bool) -> bool {
74        let _ = self;
75        explicit_progress
76    }
77}
78
79#[allow(dead_code)]
80pub fn is_destructive_command(command: &Command) -> bool {
81    match command {
82        Command::Kv { command: kv_cmd } => matches!(
83            kv_cmd,
84            KvCommand::Delete { .. } | KvCommand::Txn(KvTxnCommand::Delete { .. })
85        ),
86        Command::Hnsw {
87            command: HnswCommand::Drop { .. },
88        } => true,
89        Command::Profile {
90            command: ProfileCommand::Delete { .. },
91        } => true,
92        Command::Columnar {
93            command: ColumnarCommand::Index(IndexCommand::Drop { .. }),
94        } => true,
95        _ => false,
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use clap::Parser;
103
104    fn parse_cli(args: &[&str]) -> Cli {
105        Cli::try_parse_from(args).expect("cli parse")
106    }
107
108    #[test]
109    fn detect_explicit_overrides_env_and_pipe() {
110        let cli = parse_cli(&["alopex", "--batch", "kv", "list"]);
111        let mode = BatchMode::detect_with(&cli, false, Some("batch"));
112
113        assert!(mode.is_batch);
114        assert_eq!(mode.source, BatchModeSource::Explicit);
115        assert!(!mode.is_tty);
116    }
117
118    #[test]
119    fn detect_env_overrides_pipe() {
120        let cli = parse_cli(&["alopex", "kv", "list"]);
121        let mode = BatchMode::detect_with(&cli, false, Some("batch"));
122
123        assert!(mode.is_batch);
124        assert_eq!(mode.source, BatchModeSource::Environment);
125        assert!(!mode.is_tty);
126    }
127
128    #[test]
129    fn detect_pipe_when_not_tty() {
130        let cli = parse_cli(&["alopex", "kv", "list"]);
131        let mode = BatchMode::detect_with(&cli, false, None);
132
133        assert!(mode.is_batch);
134        assert_eq!(mode.source, BatchModeSource::PipeDetected);
135        assert!(!mode.is_tty);
136    }
137
138    #[test]
139    fn detect_default_when_tty() {
140        let cli = parse_cli(&["alopex", "kv", "list"]);
141        let mode = BatchMode::detect_with(&cli, true, None);
142
143        assert!(!mode.is_batch);
144        assert_eq!(mode.source, BatchModeSource::Default);
145        assert!(mode.is_tty);
146    }
147
148    #[test]
149    fn should_prompt_respects_yes_and_batch() {
150        let cli = parse_cli(&["alopex", "--yes", "kv", "list"]);
151        let batch_mode = BatchMode {
152            is_batch: false,
153            is_tty: true,
154            source: BatchModeSource::Default,
155        };
156        let batch = BatchMode {
157            is_batch: true,
158            is_tty: true,
159            source: BatchModeSource::Explicit,
160        };
161
162        assert!(!batch_mode.should_prompt(&cli, true));
163        assert!(!batch.should_prompt(&cli, true));
164    }
165
166    #[test]
167    fn should_prompt_only_for_destructive_commands() {
168        let cli = parse_cli(&["alopex", "kv", "list"]);
169        let mode = BatchMode {
170            is_batch: false,
171            is_tty: true,
172            source: BatchModeSource::Default,
173        };
174
175        assert!(mode.should_prompt(&cli, true));
176        assert!(!mode.should_prompt(&cli, false));
177    }
178
179    #[test]
180    fn should_show_progress_respects_explicit_flag() {
181        let batch = BatchMode {
182            is_batch: true,
183            is_tty: false,
184            source: BatchModeSource::PipeDetected,
185        };
186        let interactive = BatchMode {
187            is_batch: false,
188            is_tty: true,
189            source: BatchModeSource::Default,
190        };
191
192        assert!(batch.should_show_progress(true));
193        assert!(!batch.should_show_progress(false));
194        assert!(!interactive.should_show_progress(false));
195        assert!(interactive.should_show_progress(true));
196    }
197
198    #[test]
199    fn destructive_command_detection() {
200        let kv_delete = Command::Kv {
201            command: KvCommand::Delete { key: "key".into() },
202        };
203        let kv_list = Command::Kv {
204            command: KvCommand::List { prefix: None },
205        };
206        let profile_delete = Command::Profile {
207            command: ProfileCommand::Delete { name: "dev".into() },
208        };
209        let hnsw_drop = Command::Hnsw {
210            command: HnswCommand::Drop { name: "idx".into() },
211        };
212
213        assert!(is_destructive_command(&kv_delete));
214        assert!(!is_destructive_command(&kv_list));
215        assert!(is_destructive_command(&profile_delete));
216        assert!(is_destructive_command(&hnsw_drop));
217
218        let kv_txn_delete = Command::Kv {
219            command: KvCommand::Txn(KvTxnCommand::Delete {
220                key: "key".into(),
221                txn_id: "txn".into(),
222            }),
223        };
224        let columnar_index_drop = Command::Columnar {
225            command: ColumnarCommand::Index(IndexCommand::Drop {
226                segment: "seg".into(),
227                column: "col".into(),
228            }),
229        };
230
231        assert!(is_destructive_command(&kv_txn_delete));
232        assert!(is_destructive_command(&columnar_index_drop));
233    }
234}