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}