1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum OutputPolicy {
16 Passthrough,
20
21 Verbatim,
25
26 Compressible,
30}
31
32impl OutputPolicy {
33 pub fn is_protected(&self) -> bool {
36 matches!(self, Self::Passthrough | Self::Verbatim)
37 }
38}
39
40pub fn classify(command: &str, user_excluded: &[String]) -> OutputPolicy {
47 if is_passthrough(command, user_excluded) {
48 return OutputPolicy::Passthrough;
49 }
50 if super::compress::is_verbatim_output(command) {
51 return OutputPolicy::Verbatim;
52 }
53 OutputPolicy::Compressible
54}
55
56fn is_passthrough(command: &str, user_excluded: &[String]) -> bool {
57 super::compress::is_excluded_command(command, user_excluded)
58}
59
60pub fn is_user_excluded(command: &str, user_excluded: &[String]) -> bool {
65 if user_excluded.is_empty() {
66 return false;
67 }
68 let cmd = command.trim().to_lowercase();
69 user_excluded.iter().any(|excl| {
70 let excl_lower = excl.trim().to_lowercase();
71 !excl_lower.is_empty() && (cmd == excl_lower || cmd.starts_with(&format!("{excl_lower} ")))
72 })
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn gh_auth_is_passthrough() {
81 assert_eq!(classify("gh auth login", &[]), OutputPolicy::Passthrough);
82 }
83
84 #[test]
85 fn gh_api_is_verbatim() {
86 assert_eq!(
88 classify("gh api repos/owner/repo/issues", &[]),
89 OutputPolicy::Verbatim
90 );
91 }
92
93 #[test]
94 fn user_excluded_is_passthrough() {
95 let excl = vec!["mycommand".to_string()];
96 assert_eq!(
97 classify("mycommand --flag", &excl),
98 OutputPolicy::Passthrough
99 );
100 }
101
102 #[test]
103 fn curl_is_verbatim() {
104 assert_eq!(
105 classify("curl https://api.example.com", &[]),
106 OutputPolicy::Verbatim
107 );
108 }
109
110 #[test]
111 fn cat_is_verbatim() {
112 assert_eq!(classify("cat package.json", &[]), OutputPolicy::Verbatim);
113 }
114
115 #[test]
116 fn cargo_build_is_compressible() {
117 assert_eq!(classify("cargo build", &[]), OutputPolicy::Compressible);
118 }
119
120 #[test]
121 fn npm_test_is_compressible() {
122 assert_eq!(classify("npm test", &[]), OutputPolicy::Compressible);
123 }
124
125 #[test]
126 fn dev_server_is_passthrough() {
127 assert_eq!(classify("npm run dev", &[]), OutputPolicy::Passthrough);
128 assert_eq!(classify("cargo watch", &[]), OutputPolicy::Passthrough);
129 assert_eq!(classify("cargo run", &[]), OutputPolicy::Passthrough);
130 }
131
132 #[test]
133 fn git_diff_is_verbatim() {
134 let policy = classify("git diff", &[]);
139 assert_eq!(policy, OutputPolicy::Compressible);
144 }
145
146 #[test]
147 fn auth_commands_are_passthrough() {
148 assert_eq!(classify("az login", &[]), OutputPolicy::Passthrough);
149 assert_eq!(
150 classify("gcloud auth login", &[]),
151 OutputPolicy::Passthrough
152 );
153 assert_eq!(classify("firebase login", &[]), OutputPolicy::Passthrough);
154 }
155
156 #[test]
157 fn jq_is_verbatim() {
158 assert_eq!(
159 classify("jq '.items' data.json", &[]),
160 OutputPolicy::Verbatim
161 );
162 }
163
164 #[test]
165 fn unknown_command_is_compressible() {
166 assert_eq!(
167 classify("some-random-tool --flag", &[]),
168 OutputPolicy::Compressible
169 );
170 }
171
172 #[test]
173 fn piped_jq_is_verbatim() {
174 assert_eq!(
175 classify("kubectl get pods -o json | jq '.items[]'", &[]),
176 OutputPolicy::Verbatim
177 );
178 }
179
180 #[test]
181 fn policy_is_protected() {
182 assert!(OutputPolicy::Passthrough.is_protected());
183 assert!(OutputPolicy::Verbatim.is_protected());
184 assert!(!OutputPolicy::Compressible.is_protected());
185 }
186
187 #[test]
190 fn issue_198_gh_api_jq() {
191 assert_eq!(
193 classify("gh api repos/yvgude/lean-ctx/issues/198 --jq '.body'", &[]),
194 OutputPolicy::Verbatim
195 );
196 }
197
198 #[test]
199 fn issue_159_cat_pubspec() {
200 assert_eq!(classify("cat pubspec.yaml", &[]), OutputPolicy::Verbatim);
201 }
202
203 #[test]
204 fn issue_114_git_stash() {
205 let p = classify("git stash show", &[]);
210 assert_eq!(p, OutputPolicy::Compressible);
211 }
212
213 #[test]
214 fn issue_194_git_diff_raw() {
215 let p = classify("git diff --cached", &[]);
217 assert_eq!(p, OutputPolicy::Compressible);
218 }
219
220 #[test]
221 fn npm_install_is_compressible() {
222 assert_eq!(
224 classify("npm install -g deepseek-tui", &[]),
225 OutputPolicy::Compressible
226 );
227 }
228
229 #[test]
230 fn pip_install_is_compressible() {
231 assert_eq!(
232 classify("pip install flask", &[]),
233 OutputPolicy::Compressible
234 );
235 }
236
237 #[test]
238 fn kubectl_get_yaml_is_verbatim() {
239 assert_eq!(
240 classify("kubectl get pods -o yaml", &[]),
241 OutputPolicy::Verbatim
242 );
243 }
244
245 #[test]
246 fn docker_inspect_is_verbatim() {
247 assert_eq!(
248 classify("docker inspect my-container", &[]),
249 OutputPolicy::Verbatim
250 );
251 }
252
253 #[test]
254 fn terraform_output_is_verbatim() {
255 assert_eq!(classify("terraform output", &[]), OutputPolicy::Verbatim);
256 }
257
258 #[test]
259 fn heroku_logs_is_verbatim() {
260 assert_eq!(classify("heroku logs --tail", &[]), OutputPolicy::Verbatim);
261 }
262
263 #[test]
264 fn gh_pr_list_is_compressible() {
265 assert_eq!(classify("gh pr list", &[]), OutputPolicy::Compressible);
266 }
267
268 #[test]
269 fn emspace_data_is_verbatim_and_listings_compressible() {
270 for data in [
271 "emspace page show core --json",
272 "emspace page show --body core",
273 "emspace schema pages",
274 "emspace run --query prog.js",
275 ] {
276 assert_eq!(classify(data, &[]), OutputPolicy::Verbatim, "{data}");
277 }
278 assert_eq!(
279 classify("emspace search budget", &[]),
280 OutputPolicy::Compressible
281 );
282 assert_eq!(
283 classify("emspace page show core", &[]),
284 OutputPolicy::Compressible
285 );
286 }
287
288 #[test]
289 fn lean_ctx_is_passthrough() {
290 assert_eq!(
291 classify("lean-ctx init powershell", &[]),
292 OutputPolicy::Passthrough
293 );
294 assert_eq!(
295 classify("lean-ctx overview", &[]),
296 OutputPolicy::Passthrough
297 );
298 }
299
300 #[test]
301 fn stripe_list_is_verbatim() {
302 assert_eq!(classify("stripe charges list", &[]), OutputPolicy::Verbatim);
303 }
304
305 #[test]
308 fn git_commit_is_passthrough() {
309 assert_eq!(
310 classify("git commit -m \"feat: add feature\"", &[]),
311 OutputPolicy::Passthrough
312 );
313 }
314
315 #[test]
316 fn git_push_is_passthrough() {
317 assert_eq!(
318 classify("git push origin main", &[]),
319 OutputPolicy::Passthrough
320 );
321 }
322
323 #[test]
324 fn git_pull_is_passthrough() {
325 assert_eq!(
326 classify("git pull --rebase", &[]),
327 OutputPolicy::Passthrough
328 );
329 }
330
331 #[test]
332 fn git_merge_is_passthrough() {
333 assert_eq!(
334 classify("git merge feature-branch", &[]),
335 OutputPolicy::Passthrough
336 );
337 }
338
339 #[test]
340 fn git_rebase_is_passthrough() {
341 assert_eq!(classify("git rebase main", &[]), OutputPolicy::Passthrough);
342 }
343
344 #[test]
345 fn git_cherry_pick_is_passthrough() {
346 assert_eq!(
347 classify("git cherry-pick abc1234", &[]),
348 OutputPolicy::Passthrough
349 );
350 }
351
352 #[test]
353 fn git_tag_is_passthrough() {
354 assert_eq!(classify("git tag v1.0.0", &[]), OutputPolicy::Passthrough);
355 }
356
357 #[test]
358 fn git_status_still_compressible() {
359 assert_eq!(classify("git status", &[]), OutputPolicy::Compressible);
360 }
361
362 #[test]
363 fn git_log_still_compressible() {
364 assert_eq!(
365 classify("git log --oneline", &[]),
366 OutputPolicy::Compressible
367 );
368 }
369}