1mod defaults;
5
6use crate::error::ConfigError;
7use crate::paths::{config_file_path, CacheLayout};
8use serde::{Deserialize, Serialize};
9use std::path::Path;
10
11pub const SHIM_NAMES: &[&str] = &[
14 "npm", "pnpm", "yarn", "bun", "git", "rg", "grep", "find", "ls", "tree", "tsc", "eslint",
15 "pytest", "cargo", "go", "docker",
16];
17
18#[derive(Debug, Clone, Deserialize, Serialize)]
19#[serde(default)]
20pub struct Config {
21 pub enabled: bool,
22 pub store_raw_outputs: bool,
23 pub redact_secrets: bool,
24 pub max_raw_output_bytes: u64,
25 pub min_raw_tokens_to_reduce: u64,
26 pub max_emitted_lines_first_seen: usize,
27 pub max_emitted_lines_large_delta: usize,
28 pub max_emitted_lines_small_delta: usize,
29 pub small_delta_max_changed_lines: usize,
30 pub small_delta_max_changed_ratio: f64,
31 pub estimate_tokens_method: String,
32 pub retention_days: u32,
33 pub intercept: InterceptConfig,
34}
35
36#[derive(Debug, Clone, Deserialize, Serialize)]
37#[serde(default)]
38pub struct InterceptConfig {
39 pub npm: bool,
40 pub pnpm: bool,
41 pub yarn: bool,
42 pub bun: bool,
43 pub tsc: bool,
44 pub eslint: bool,
45 pub pytest: bool,
46 pub cargo: bool,
47 pub go: bool,
48 pub git: bool,
49 pub rg: bool,
50 pub grep: bool,
51 pub find: bool,
52 pub ls: bool,
53 pub tree: bool,
54 pub docker: bool,
55}
56
57impl InterceptConfig {
58 pub fn is_enabled(&self, shim: &str) -> bool {
60 match shim {
61 "npm" => self.npm,
62 "pnpm" => self.pnpm,
63 "yarn" => self.yarn,
64 "bun" => self.bun,
65 "tsc" => self.tsc,
66 "eslint" => self.eslint,
67 "pytest" => self.pytest,
68 "cargo" => self.cargo,
69 "go" => self.go,
70 "git" => self.git,
71 "rg" => self.rg,
72 "grep" => self.grep,
73 "find" => self.find,
74 "ls" => self.ls,
75 "tree" => self.tree,
76 "docker" => self.docker,
77 _ => false,
78 }
79 }
80
81 pub fn enabled_shims(&self) -> Vec<&'static str> {
83 SHIM_NAMES
84 .iter()
85 .copied()
86 .filter(|name| self.is_enabled(name))
87 .collect()
88 }
89}
90
91fn merge_toml(base: &mut toml::Value, over: toml::Value) {
93 match over {
94 toml::Value::Table(over_table) => {
95 if let toml::Value::Table(base_table) = base {
96 for (key, value) in over_table {
97 match base_table.get_mut(&key) {
98 Some(existing) => merge_toml(existing, value),
99 None => {
100 base_table.insert(key, value);
101 }
102 }
103 }
104 } else {
105 *base = toml::Value::Table(over_table);
106 }
107 }
108 other => *base = other,
109 }
110}
111
112impl Config {
113 pub fn load(repo_root: &Path) -> Result<Config, ConfigError> {
115 let mut merged =
116 toml::Value::try_from(Config::default()).expect("default config always serializes");
117
118 if let Ok(path) = config_file_path() {
119 if path.exists() {
120 let text = std::fs::read_to_string(&path)?;
121 let value: toml::Value =
122 toml::from_str(&text).map_err(|source| ConfigError::Toml {
123 path: path.clone(),
124 source,
125 })?;
126 merge_toml(&mut merged, value);
127 }
128 }
129
130 let project = repo_root.join(".dejavu.toml");
131 if project.exists() {
132 let text = std::fs::read_to_string(&project)?;
133 let value: toml::Value = toml::from_str(&text).map_err(|source| ConfigError::Toml {
134 path: project.clone(),
135 source,
136 })?;
137 merge_toml(&mut merged, value);
138 }
139
140 merged.try_into().map_err(|source| ConfigError::Toml {
141 path: repo_root.to_path_buf(),
142 source,
143 })
144 }
145
146 pub fn write_effective(&self, layout: &CacheLayout) -> Result<(), ConfigError> {
148 let json = serde_json::to_string_pretty(self)?;
149 std::fs::write(layout.effective_config(), json)?;
150 Ok(())
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn defaults_match_spec_section_18() {
160 let c = Config::default();
161 assert!(c.enabled);
162 assert!(c.store_raw_outputs);
163 assert!(c.redact_secrets);
164 assert_eq!(c.max_raw_output_bytes, 5_242_880);
165 assert_eq!(c.min_raw_tokens_to_reduce, 800);
166 assert_eq!(c.small_delta_max_changed_lines, 80);
167 assert!((c.small_delta_max_changed_ratio - 0.20).abs() < 1e-9);
168 assert_eq!(c.retention_days, 14);
169 assert!(c.intercept.is_enabled("git"));
170 assert!(c.intercept.is_enabled("docker"));
171 assert!(!c.intercept.is_enabled("unknown-tool"));
172 }
173
174 #[test]
175 fn merge_overrides_only_given_keys() {
176 let mut base = toml::Value::try_from(Config::default()).unwrap();
177 let over: toml::Value =
178 toml::from_str("min_raw_tokens_to_reduce = 100\n[intercept]\ndocker = false\n")
179 .unwrap();
180 merge_toml(&mut base, over);
181 let c: Config = base.try_into().unwrap();
182
183 assert_eq!(c.min_raw_tokens_to_reduce, 100); assert!(!c.intercept.docker); assert!(c.intercept.git); assert!(c.enabled); assert_eq!(c.retention_days, 14); }
189
190 #[test]
191 fn enabled_shims_reflects_intercept() {
192 let mut c = Config::default();
193 c.intercept.docker = false;
194 c.intercept.go = false;
195 let shims = c.intercept.enabled_shims();
196 assert!(shims.contains(&"git"));
197 assert!(!shims.contains(&"docker"));
198 assert!(!shims.contains(&"go"));
199 assert_eq!(shims.len(), SHIM_NAMES.len() - 2);
200 }
201}