1pub fn save_tee(command: &str, output: &str) -> Option<String> {
10 let tee_dir = crate::core::paths::state_dir().ok()?.join("tee");
11 std::fs::create_dir_all(&tee_dir).ok()?;
12
13 cleanup_old_tee_logs(&tee_dir);
14
15 let cmd_slug: String = command
16 .chars()
17 .take(40)
18 .map(|c| {
19 if c.is_alphanumeric() || c == '-' {
20 c
21 } else {
22 '_'
23 }
24 })
25 .collect();
26 let cmd_hash = blake3::hash(command.as_bytes()).to_hex();
31 let filename = format!("{cmd_slug}_{}.log", &cmd_hash.as_str()[..8]);
32 let path = tee_dir.join(&filename);
33
34 let masked = crate::core::redaction::redact_text(output);
35 let (redacted, _) = crate::core::secret_detection::scan_and_redact_from_config(&masked);
36 std::fs::write(&path, redacted).ok()?;
37 #[cfg(unix)]
38 {
39 use std::os::unix::fs::PermissionsExt;
40 let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
41 }
42 Some(path.to_string_lossy().to_string())
43}
44
45fn cleanup_old_tee_logs(tee_dir: &std::path::Path) {
46 let cutoff = std::time::SystemTime::now().checked_sub(std::time::Duration::from_hours(24));
47 let Some(cutoff) = cutoff else { return };
48
49 if let Ok(entries) = std::fs::read_dir(tee_dir) {
50 for entry in entries.flatten() {
51 if let Ok(meta) = entry.metadata()
52 && let Ok(modified) = meta.modified()
53 && modified < cutoff
54 {
55 let _ = std::fs::remove_file(entry.path());
56 }
57 }
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 #[test]
69 fn tee_path_is_content_addressed() {
70 let _lock = crate::core::data_dir::test_env_lock();
73 let first = save_tee("cargo test --lib", "output run 1").expect("tee saved");
74 let second = save_tee("cargo test --lib", "output run 2").expect("tee saved");
75 assert_eq!(first, second, "same command must map to the same tee path");
76
77 let other = save_tee("cargo build", "output").expect("tee saved");
78 assert_ne!(first, other, "different commands get different tee paths");
79
80 let content = std::fs::read_to_string(&second).unwrap();
82 assert!(content.contains("run 2"));
83
84 for p in [first, other] {
85 let _ = std::fs::remove_file(p);
86 }
87 }
88}