1use crossterm::tty::IsTty;
8use std::env;
9use std::io::stdout;
10
11const AGENT_ENV_VARS: &[&str] = &[
15 "CLAUDE_CODE", "CODEX_CLI", "CURSOR_SESSION", "AIDER_SESSION", "AGENT_MODE", "WINDSURF_SESSION", "CLINE_SESSION", "COPILOT_AGENT", ];
24
25const CI_ENV_VARS: &[&str] = &[
27 "CI", "GITHUB_ACTIONS", "GITLAB_CI", "JENKINS_URL", "CIRCLECI", "TRAVIS", "BUILDKITE", ];
35
36#[derive(Debug, Clone)]
38#[allow(clippy::struct_excessive_bools)]
39pub struct DetectionResult {
40 pub is_agent: bool,
42 pub detected_agent: Option<String>,
44 pub is_ci: bool,
46 pub detected_ci: Option<String>,
48 pub is_tty: bool,
50 pub no_color_set: bool,
52 pub override_mode: Option<OverrideMode>,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum OverrideMode {
59 ForceAgent,
61 ForceHuman,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum OutputPreference {
68 Rich,
70 Plain,
72}
73
74#[must_use]
94pub fn is_agent_environment() -> bool {
95 detect_environment().is_agent
96}
97
98#[must_use]
103pub fn detect_environment() -> DetectionResult {
104 let override_mode = check_overrides();
106 let force_color = force_color_enabled();
107
108 let (is_agent_var, detected_agent) = check_agent_vars();
110
111 let (is_ci_var, detected_ci) = check_ci_vars();
113
114 let no_color_set = env::var("NO_COLOR").is_ok();
116
117 let is_tty = stdout().is_tty();
119
120 let is_agent = match override_mode {
122 Some(OverrideMode::ForceAgent) => true,
123 Some(OverrideMode::ForceHuman) => false,
124 None => {
125 if force_color {
126 false
127 } else {
128 is_agent_var || is_ci_var || no_color_set || !is_tty
129 }
130 }
131 };
132
133 DetectionResult {
134 is_agent,
135 detected_agent,
136 is_ci: is_ci_var,
137 detected_ci,
138 is_tty,
139 no_color_set,
140 override_mode,
141 }
142}
143
144fn check_overrides() -> Option<OverrideMode> {
146 if env::var("FASTAPI_AGENT_MODE").is_ok_and(|v| v == "1") {
147 Some(OverrideMode::ForceAgent)
148 } else if env::var("FASTAPI_HUMAN_MODE").is_ok_and(|v| v == "1") {
149 Some(OverrideMode::ForceHuman)
150 } else {
151 None
152 }
153}
154
155fn force_color_enabled() -> bool {
157 env::var("FORCE_COLOR").is_ok_and(|v| v != "0")
158}
159
160fn check_agent_vars() -> (bool, Option<String>) {
162 for var in AGENT_ENV_VARS {
163 if env::var(var).is_ok() {
164 return (true, Some((*var).to_string()));
165 }
166 }
167 (false, None)
168}
169
170fn check_ci_vars() -> (bool, Option<String>) {
172 for var in CI_ENV_VARS {
173 if env::var(var).is_ok() {
174 return (true, Some((*var).to_string()));
175 }
176 }
177 (false, None)
178}
179
180#[must_use]
182pub fn detected_preference() -> OutputPreference {
183 let result = detect_environment();
184 if result.is_agent {
185 OutputPreference::Plain
186 } else {
187 OutputPreference::Rich
188 }
189}
190
191#[must_use]
193pub fn detection_diagnostics() -> String {
194 let result = detect_environment();
195 let force_color = force_color_enabled();
196 format!(
197 "DetectionResult {{ is_agent: {}, detected_agent: {:?}, is_ci: {}, \
198 detected_ci: {:?}, is_tty: {}, no_color_set: {}, force_color_set: {}, \
199 override_mode: {:?} }}",
200 result.is_agent,
201 result.detected_agent,
202 result.is_ci,
203 result.detected_ci,
204 result.is_tty,
205 result.no_color_set,
206 force_color,
207 result.override_mode
208 )
209}
210
211#[cfg(test)]
212#[allow(unsafe_code)]
213mod tests {
214 use super::*;
215 use serial_test::serial;
216 use std::env;
217
218 fn clean_env() {
226 unsafe {
228 for var in AGENT_ENV_VARS {
229 env::remove_var(var);
230 }
231 for var in CI_ENV_VARS {
232 env::remove_var(var);
233 }
234 env::remove_var("NO_COLOR");
235 env::remove_var("FORCE_COLOR");
236 env::remove_var("FASTAPI_AGENT_MODE");
237 env::remove_var("FASTAPI_HUMAN_MODE");
238 }
239 }
240
241 fn with_clean_env<F: FnOnce()>(f: F) {
243 clean_env();
244 f();
245 clean_env();
246 }
247
248 fn set_env(key: &str, value: &str) {
254 unsafe {
256 env::set_var(key, value);
257 }
258 }
259
260 #[test]
263 #[serial]
264 fn test_claude_code_detection() {
265 with_clean_env(|| {
266 set_env("CLAUDE_CODE", "1");
267 let result = detect_environment();
268 eprintln!("[TEST] Claude Code detection: {result:?}");
269 assert!(result.is_agent, "Should detect Claude Code as agent");
270 assert_eq!(result.detected_agent, Some("CLAUDE_CODE".to_string()));
271 });
272 }
273
274 #[test]
275 #[serial]
276 fn test_codex_cli_detection() {
277 with_clean_env(|| {
278 set_env("CODEX_CLI", "1");
279 let result = detect_environment();
280 eprintln!("[TEST] Codex CLI detection: {result:?}");
281 assert!(result.is_agent, "Should detect Codex CLI as agent");
282 assert_eq!(result.detected_agent, Some("CODEX_CLI".to_string()));
283 });
284 }
285
286 #[test]
287 #[serial]
288 fn test_cursor_session_detection() {
289 with_clean_env(|| {
290 set_env("CURSOR_SESSION", "abc123");
291 let result = detect_environment();
292 eprintln!("[TEST] Cursor detection: {result:?}");
293 assert!(result.is_agent, "Should detect Cursor as agent");
294 assert_eq!(result.detected_agent, Some("CURSOR_SESSION".to_string()));
295 });
296 }
297
298 #[test]
299 #[serial]
300 fn test_aider_session_detection() {
301 with_clean_env(|| {
302 set_env("AIDER_SESSION", "1");
303 let result = detect_environment();
304 eprintln!("[TEST] Aider detection: {result:?}");
305 assert!(result.is_agent, "Should detect Aider as agent");
306 });
307 }
308
309 #[test]
310 #[serial]
311 fn test_generic_agent_mode_detection() {
312 with_clean_env(|| {
313 set_env("AGENT_MODE", "1");
314 let result = detect_environment();
315 eprintln!("[TEST] Generic AGENT_MODE detection: {result:?}");
316 assert!(result.is_agent, "Should detect AGENT_MODE");
317 });
318 }
319
320 #[test]
321 #[serial]
322 fn test_windsurf_detection() {
323 with_clean_env(|| {
324 set_env("WINDSURF_SESSION", "1");
325 let result = detect_environment();
326 eprintln!("[TEST] Windsurf detection: {result:?}");
327 assert!(result.is_agent, "Should detect Windsurf");
328 });
329 }
330
331 #[test]
332 #[serial]
333 fn test_cline_detection() {
334 with_clean_env(|| {
335 set_env("CLINE_SESSION", "1");
336 let result = detect_environment();
337 eprintln!("[TEST] Cline detection: {result:?}");
338 assert!(result.is_agent, "Should detect Cline");
339 });
340 }
341
342 #[test]
343 #[serial]
344 fn test_copilot_agent_detection() {
345 with_clean_env(|| {
346 set_env("COPILOT_AGENT", "1");
347 let result = detect_environment();
348 eprintln!("[TEST] Copilot agent detection: {result:?}");
349 assert!(result.is_agent, "Should detect Copilot agent");
350 });
351 }
352
353 #[test]
356 #[serial]
357 fn test_generic_ci_detection() {
358 with_clean_env(|| {
359 set_env("CI", "true");
360 let result = detect_environment();
361 eprintln!("[TEST] Generic CI detection: {result:?}");
362 assert!(result.is_ci, "Should detect CI environment");
363 assert!(result.is_agent, "CI should trigger agent mode");
364 });
365 }
366
367 #[test]
368 #[serial]
369 fn test_github_actions_detection() {
370 with_clean_env(|| {
371 set_env("GITHUB_ACTIONS", "true");
372 let result = detect_environment();
373 eprintln!("[TEST] GitHub Actions detection: {result:?}");
374 assert!(result.is_ci);
375 assert_eq!(result.detected_ci, Some("GITHUB_ACTIONS".to_string()));
376 });
377 }
378
379 #[test]
380 #[serial]
381 fn test_gitlab_ci_detection() {
382 with_clean_env(|| {
383 set_env("GITLAB_CI", "true");
384 let result = detect_environment();
385 eprintln!("[TEST] GitLab CI detection: {result:?}");
386 assert!(result.is_ci);
387 });
388 }
389
390 #[test]
391 #[serial]
392 fn test_jenkins_detection() {
393 with_clean_env(|| {
394 set_env("JENKINS_URL", "http://jenkins.example.com");
395 let result = detect_environment();
396 eprintln!("[TEST] Jenkins detection: {result:?}");
397 assert!(result.is_ci);
398 });
399 }
400
401 #[test]
404 #[serial]
405 fn test_no_color_detection() {
406 with_clean_env(|| {
407 set_env("NO_COLOR", "1");
408 let result = detect_environment();
409 eprintln!("[TEST] NO_COLOR detection: {result:?}");
410 assert!(result.no_color_set, "Should detect NO_COLOR");
411 assert!(result.is_agent, "NO_COLOR should trigger plain mode");
412 });
413 }
414
415 #[test]
416 #[serial]
417 fn test_no_color_empty_value() {
418 with_clean_env(|| {
419 set_env("NO_COLOR", ""); let result = detect_environment();
421 eprintln!("[TEST] NO_COLOR empty value: {result:?}");
422 assert!(
423 result.no_color_set,
424 "Empty NO_COLOR should still be detected"
425 );
426 });
427 }
428
429 #[test]
432 #[serial]
433 fn test_force_color_overrides_ci() {
434 with_clean_env(|| {
435 set_env("CI", "true");
436 set_env("FORCE_COLOR", "1");
437 let result = detect_environment();
438 eprintln!("[TEST] FORCE_COLOR override: {result:?}");
439 assert!(!result.is_agent, "FORCE_COLOR should prefer rich output");
440 });
441 }
442
443 #[test]
446 #[serial]
447 fn test_force_agent_mode_override() {
448 with_clean_env(|| {
449 set_env("FASTAPI_AGENT_MODE", "1");
450 let result = detect_environment();
451 eprintln!("[TEST] FASTAPI_AGENT_MODE override: {result:?}");
452 assert!(result.is_agent, "Override should force agent mode");
453 assert_eq!(result.override_mode, Some(OverrideMode::ForceAgent));
454 });
455 }
456
457 #[test]
458 #[serial]
459 fn test_force_human_mode_override() {
460 with_clean_env(|| {
461 set_env("CLAUDE_CODE", "1");
463 set_env("FASTAPI_HUMAN_MODE", "1");
464 let result = detect_environment();
465 eprintln!("[TEST] FASTAPI_HUMAN_MODE override: {result:?}");
466 assert!(!result.is_agent, "Override should force human mode");
467 assert_eq!(result.override_mode, Some(OverrideMode::ForceHuman));
468 });
469 }
470
471 #[test]
472 #[serial]
473 fn test_agent_override_takes_precedence() {
474 with_clean_env(|| {
475 set_env("FASTAPI_AGENT_MODE", "1");
477 set_env("FASTAPI_HUMAN_MODE", "1");
478 let result = detect_environment();
479 eprintln!("[TEST] Both overrides set: {result:?}");
480 assert!(result.is_agent, "AGENT_MODE should take precedence");
481 });
482 }
483
484 #[test]
487 #[serial]
488 fn test_preference_plain_for_agent() {
489 with_clean_env(|| {
490 set_env("CLAUDE_CODE", "1");
491 let pref = detected_preference();
492 eprintln!("[TEST] Preference for agent: {pref:?}");
493 assert_eq!(pref, OutputPreference::Plain);
494 });
495 }
496
497 #[test]
498 #[serial]
499 fn test_preference_rich_for_human_tty() {
500 with_clean_env(|| {
501 let result = detect_environment();
504 eprintln!("[TEST] Clean env detection: {result:?}");
505 });
507 }
508
509 #[test]
512 #[serial]
513 fn test_diagnostics_format() {
514 with_clean_env(|| {
515 set_env("CLAUDE_CODE", "1");
516 let diag = detection_diagnostics();
517 eprintln!("[TEST] Diagnostics output: {diag}");
518 assert!(diag.contains("is_agent: true"));
519 assert!(diag.contains("CLAUDE_CODE"));
520 });
521 }
522
523 #[test]
526 #[serial]
527 fn test_multiple_agents_first_wins() {
528 with_clean_env(|| {
529 set_env("CLAUDE_CODE", "1");
530 set_env("CODEX_CLI", "1");
531 let result = detect_environment();
532 eprintln!("[TEST] Multiple agents: {result:?}");
533 assert!(result.is_agent);
534 assert_eq!(result.detected_agent, Some("CLAUDE_CODE".to_string()));
536 });
537 }
538
539 #[test]
540 #[serial]
541 fn test_ci_and_agent_both_detected() {
542 with_clean_env(|| {
543 set_env("CLAUDE_CODE", "1");
544 set_env("CI", "true");
545 let result = detect_environment();
546 eprintln!("[TEST] Agent + CI: {result:?}");
547 assert!(result.is_agent);
548 assert!(result.is_ci);
549 assert!(result.detected_agent.is_some());
550 assert!(result.detected_ci.is_some());
551 });
552 }
553
554 #[test]
555 #[serial]
556 fn test_clean_environment() {
557 with_clean_env(|| {
558 let result = detect_environment();
559 eprintln!("[TEST] Clean environment: {result:?}");
560 assert!(result.detected_agent.is_none());
561 assert!(result.detected_ci.is_none());
562 assert!(!result.no_color_set);
563 assert!(result.override_mode.is_none());
564 });
566 }
567}