1use std::path::Path;
2
3use crate::core::compressor;
4use crate::core::config;
5use crate::core::deps as dep_extract;
6use crate::core::entropy;
7use crate::core::patterns::deps_cmd;
8use crate::core::protocol;
9use crate::core::signatures;
10use crate::core::stats;
11use crate::core::theme;
12use crate::core::tokens::count_tokens;
13use crate::hooks::to_bash_compatible_path;
14
15pub fn cmd_read(args: &[String]) {
16 if args.is_empty() {
17 eprintln!("Usage: lean-ctx read <file> [--mode full|map|signatures|aggressive|entropy]");
18 std::process::exit(1);
19 }
20
21 let path = &args[0];
22 let mode = args
23 .iter()
24 .position(|a| a == "--mode" || a == "-m")
25 .and_then(|i| args.get(i + 1))
26 .map(|s| s.as_str())
27 .unwrap_or("full");
28
29 let content = match crate::tools::ctx_read::read_file_lossy(path) {
30 Ok(c) => c,
31 Err(e) => {
32 eprintln!("Error: {e}");
33 std::process::exit(1);
34 }
35 };
36
37 let ext = Path::new(path)
38 .extension()
39 .and_then(|e| e.to_str())
40 .unwrap_or("");
41 let short = protocol::shorten_path(path);
42 let line_count = content.lines().count();
43 let original_tokens = count_tokens(&content);
44
45 let mode = if mode == "auto" {
46 let sig = crate::core::mode_predictor::FileSignature::from_path(path, original_tokens);
47 let predictor = crate::core::mode_predictor::ModePredictor::new();
48 predictor
49 .predict_best_mode(&sig)
50 .unwrap_or_else(|| "full".to_string())
51 } else {
52 mode.to_string()
53 };
54 let mode = mode.as_str();
55
56 match mode {
57 "map" => {
58 let sigs = signatures::extract_signatures(&content, ext);
59 let dep_info = dep_extract::extract_deps(&content, ext);
60
61 println!("{short} [{line_count}L]");
62 if !dep_info.imports.is_empty() {
63 println!(" deps: {}", dep_info.imports.join(", "));
64 }
65 if !dep_info.exports.is_empty() {
66 println!(" exports: {}", dep_info.exports.join(", "));
67 }
68 let key_sigs: Vec<_> = sigs
69 .iter()
70 .filter(|s| s.is_exported || s.indent == 0)
71 .collect();
72 if !key_sigs.is_empty() {
73 println!(" API:");
74 for sig in &key_sigs {
75 println!(" {}", sig.to_compact());
76 }
77 }
78 let sent = count_tokens(&short.to_string());
79 print_savings(original_tokens, sent);
80 }
81 "signatures" => {
82 let sigs = signatures::extract_signatures(&content, ext);
83 println!("{short} [{line_count}L]");
84 for sig in &sigs {
85 println!("{}", sig.to_compact());
86 }
87 let sent = count_tokens(&short.to_string());
88 print_savings(original_tokens, sent);
89 }
90 "aggressive" => {
91 let compressed = compressor::aggressive_compress(&content, Some(ext));
92 println!("{short} [{line_count}L]");
93 println!("{compressed}");
94 let sent = count_tokens(&compressed);
95 print_savings(original_tokens, sent);
96 }
97 "entropy" => {
98 let result = entropy::entropy_compress(&content);
99 let avg_h = entropy::analyze_entropy(&content).avg_entropy;
100 println!("{short} [{line_count}L] (H̄={avg_h:.1})");
101 for tech in &result.techniques {
102 println!("{tech}");
103 }
104 println!("{}", result.output);
105 let sent = count_tokens(&result.output);
106 print_savings(original_tokens, sent);
107 }
108 _ => {
109 println!("{short} [{line_count}L]");
110 println!("{content}");
111 }
112 }
113}
114
115pub fn cmd_diff(args: &[String]) {
116 if args.len() < 2 {
117 eprintln!("Usage: lean-ctx diff <file1> <file2>");
118 std::process::exit(1);
119 }
120
121 let content1 = match crate::tools::ctx_read::read_file_lossy(&args[0]) {
122 Ok(c) => c,
123 Err(e) => {
124 eprintln!("Error reading {}: {e}", args[0]);
125 std::process::exit(1);
126 }
127 };
128
129 let content2 = match crate::tools::ctx_read::read_file_lossy(&args[1]) {
130 Ok(c) => c,
131 Err(e) => {
132 eprintln!("Error reading {}: {e}", args[1]);
133 std::process::exit(1);
134 }
135 };
136
137 let diff = compressor::diff_content(&content1, &content2);
138 let original = count_tokens(&content1) + count_tokens(&content2);
139 let sent = count_tokens(&diff);
140
141 println!(
142 "diff {} {}",
143 protocol::shorten_path(&args[0]),
144 protocol::shorten_path(&args[1])
145 );
146 println!("{diff}");
147 print_savings(original, sent);
148}
149
150pub fn cmd_grep(args: &[String]) {
151 if args.is_empty() {
152 eprintln!("Usage: lean-ctx grep <pattern> [path]");
153 std::process::exit(1);
154 }
155
156 let pattern = &args[0];
157 let path = args.get(1).map(|s| s.as_str()).unwrap_or(".");
158
159 let command = if cfg!(windows) {
160 format!(
161 "findstr /S /N /R \"{}\" {}\\*",
162 pattern,
163 path.replace('/', "\\")
164 )
165 } else {
166 format!("grep -rn '{}' {}", pattern.replace('\'', "'\\''"), path)
167 };
168 let code = crate::shell::exec(&command);
169 std::process::exit(code);
170}
171
172pub fn cmd_find(args: &[String]) {
173 if args.is_empty() {
174 eprintln!("Usage: lean-ctx find <pattern> [path]");
175 std::process::exit(1);
176 }
177
178 let pattern = &args[0];
179 let path = args.get(1).map(|s| s.as_str()).unwrap_or(".");
180 let command = if cfg!(windows) {
181 format!("dir /S /B {}\\{}", path.replace('/', "\\"), pattern)
182 } else {
183 format!("find {path} -name \"{pattern}\" -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/target/*'")
184 };
185 let code = crate::shell::exec(&command);
186 std::process::exit(code);
187}
188
189pub fn cmd_ls(args: &[String]) {
190 let path = args.first().map(|s| s.as_str()).unwrap_or(".");
191 let command = if cfg!(windows) {
192 format!("dir {}", path.replace('/', "\\"))
193 } else {
194 format!("ls -la {path}")
195 };
196 let code = crate::shell::exec(&command);
197 std::process::exit(code);
198}
199
200pub fn cmd_deps(args: &[String]) {
201 let path = args.first().map(|s| s.as_str()).unwrap_or(".");
202
203 match deps_cmd::detect_and_compress(path) {
204 Some(result) => println!("{result}"),
205 None => {
206 eprintln!("No dependency file found in {path}");
207 std::process::exit(1);
208 }
209 }
210}
211
212pub fn cmd_discover(_args: &[String]) {
213 let history = load_shell_history();
214 if history.is_empty() {
215 println!("No shell history found.");
216 return;
217 }
218
219 let result = crate::tools::ctx_discover::analyze_history(&history, 20);
220 println!("{}", crate::tools::ctx_discover::format_cli_output(&result));
221}
222
223pub fn cmd_session() {
224 let history = load_shell_history();
225 let gain = stats::load_stats();
226
227 let compressible_commands = [
228 "git ",
229 "npm ",
230 "yarn ",
231 "pnpm ",
232 "cargo ",
233 "docker ",
234 "kubectl ",
235 "gh ",
236 "pip ",
237 "pip3 ",
238 "eslint",
239 "prettier",
240 "ruff ",
241 "go ",
242 "golangci-lint",
243 "curl ",
244 "wget ",
245 "grep ",
246 "rg ",
247 "find ",
248 "ls ",
249 ];
250
251 let mut total = 0u32;
252 let mut via_hook = 0u32;
253
254 for line in &history {
255 let cmd = line.trim().to_lowercase();
256 if cmd.starts_with("lean-ctx") {
257 via_hook += 1;
258 total += 1;
259 } else {
260 for p in &compressible_commands {
261 if cmd.starts_with(p) {
262 total += 1;
263 break;
264 }
265 }
266 }
267 }
268
269 let pct = if total > 0 {
270 (via_hook as f64 / total as f64 * 100.0).round() as u32
271 } else {
272 0
273 };
274
275 println!("lean-ctx session statistics\n");
276 println!(
277 "Adoption: {}% ({}/{} compressible commands)",
278 pct, via_hook, total
279 );
280 println!("Saved: {} tokens total", gain.total_saved);
281 println!("Calls: {} compressed", gain.total_calls);
282
283 if total > via_hook {
284 let missed = total - via_hook;
285 let est = missed * 150;
286 println!(
287 "Missed: {} commands (~{} tokens saveable)",
288 missed, est
289 );
290 }
291
292 println!("\nRun 'lean-ctx discover' for details on missed commands.");
293}
294
295pub fn cmd_wrapped(args: &[String]) {
296 let period = if args.iter().any(|a| a == "--month") {
297 "month"
298 } else if args.iter().any(|a| a == "--all") {
299 "all"
300 } else {
301 "week"
302 };
303
304 let report = crate::core::wrapped::WrappedReport::generate(period);
305 println!("{}", report.format_ascii());
306}
307
308pub fn cmd_sessions(args: &[String]) {
309 use crate::core::session::SessionState;
310
311 let action = args.first().map(|s| s.as_str()).unwrap_or("list");
312
313 match action {
314 "list" | "ls" => {
315 let sessions = SessionState::list_sessions();
316 if sessions.is_empty() {
317 println!("No sessions found.");
318 return;
319 }
320 println!("Sessions ({}):\n", sessions.len());
321 for s in sessions.iter().take(20) {
322 let task = s.task.as_deref().unwrap_or("(no task)");
323 let task_short: String = task.chars().take(50).collect();
324 let date = s.updated_at.format("%Y-%m-%d %H:%M");
325 println!(
326 " {} | v{:3} | {:5} calls | {:>8} tok | {} | {}",
327 s.id,
328 s.version,
329 s.tool_calls,
330 format_tokens_cli(s.tokens_saved),
331 date,
332 task_short
333 );
334 }
335 if sessions.len() > 20 {
336 println!(" ... +{} more", sessions.len() - 20);
337 }
338 }
339 "show" => {
340 let id = args.get(1);
341 let session = if let Some(id) = id {
342 SessionState::load_by_id(id)
343 } else {
344 SessionState::load_latest()
345 };
346 match session {
347 Some(s) => println!("{}", s.format_compact()),
348 None => println!("Session not found."),
349 }
350 }
351 "cleanup" => {
352 let days = args.get(1).and_then(|s| s.parse::<i64>().ok()).unwrap_or(7);
353 let removed = SessionState::cleanup_old_sessions(days);
354 println!("Cleaned up {removed} session(s) older than {days} days.");
355 }
356 _ => {
357 eprintln!("Usage: lean-ctx sessions [list|show [id]|cleanup [days]]");
358 std::process::exit(1);
359 }
360 }
361}
362
363pub fn cmd_benchmark(args: &[String]) {
364 use crate::core::benchmark;
365
366 let action = args.first().map(|s| s.as_str()).unwrap_or("run");
367
368 match action {
369 "run" => {
370 let path = args.get(1).map(|s| s.as_str()).unwrap_or(".");
371 let is_json = args.iter().any(|a| a == "--json");
372
373 let result = benchmark::run_project_benchmark(path);
374 if is_json {
375 println!("{}", benchmark::format_json(&result));
376 } else {
377 println!("{}", benchmark::format_terminal(&result));
378 }
379 }
380 "report" => {
381 let path = args.get(1).map(|s| s.as_str()).unwrap_or(".");
382 let result = benchmark::run_project_benchmark(path);
383 println!("{}", benchmark::format_markdown(&result));
384 }
385 _ => {
386 if std::path::Path::new(action).exists() {
387 let result = benchmark::run_project_benchmark(action);
388 println!("{}", benchmark::format_terminal(&result));
389 } else {
390 eprintln!("Usage: lean-ctx benchmark run [path] [--json]");
391 eprintln!(" lean-ctx benchmark report [path]");
392 std::process::exit(1);
393 }
394 }
395 }
396}
397
398fn format_tokens_cli(tokens: u64) -> String {
399 if tokens >= 1_000_000 {
400 format!("{:.1}M", tokens as f64 / 1_000_000.0)
401 } else if tokens >= 1_000 {
402 format!("{:.1}K", tokens as f64 / 1_000.0)
403 } else {
404 format!("{tokens}")
405 }
406}
407
408pub fn cmd_stats(args: &[String]) {
409 match args.first().map(|s| s.as_str()) {
410 Some("reset-cep") => {
411 crate::core::stats::reset_cep();
412 println!("CEP stats reset. Shell hook data preserved.");
413 }
414 Some("json") => {
415 let store = crate::core::stats::load();
416 println!(
417 "{}",
418 serde_json::to_string_pretty(&store).unwrap_or_else(|_| "{}".to_string())
419 );
420 }
421 _ => {
422 let store = crate::core::stats::load();
423 let input_saved = store
424 .total_input_tokens
425 .saturating_sub(store.total_output_tokens);
426 let pct = if store.total_input_tokens > 0 {
427 input_saved as f64 / store.total_input_tokens as f64 * 100.0
428 } else {
429 0.0
430 };
431 println!("Commands: {}", store.total_commands);
432 println!("Input: {} tokens", store.total_input_tokens);
433 println!("Output: {} tokens", store.total_output_tokens);
434 println!("Saved: {} tokens ({:.1}%)", input_saved, pct);
435 println!();
436 println!("CEP sessions: {}", store.cep.sessions);
437 println!(
438 "CEP tokens: {} → {}",
439 store.cep.total_tokens_original, store.cep.total_tokens_compressed
440 );
441 println!();
442 println!("Subcommands: stats reset-cep | stats json");
443 }
444 }
445}
446
447pub fn cmd_config(args: &[String]) {
448 let cfg = config::Config::load();
449
450 if args.is_empty() {
451 println!("{}", cfg.show());
452 return;
453 }
454
455 match args[0].as_str() {
456 "init" | "create" => {
457 let default = config::Config::default();
458 match default.save() {
459 Ok(()) => {
460 let path = config::Config::path()
461 .map(|p| p.to_string_lossy().to_string())
462 .unwrap_or_else(|| "~/.lean-ctx/config.toml".to_string());
463 println!("Created default config at {path}");
464 }
465 Err(e) => eprintln!("Error: {e}"),
466 }
467 }
468 "set" => {
469 if args.len() < 3 {
470 eprintln!("Usage: lean-ctx config set <key> <value>");
471 std::process::exit(1);
472 }
473 let mut cfg = cfg;
474 let key = &args[1];
475 let val = &args[2];
476 match key.as_str() {
477 "ultra_compact" => cfg.ultra_compact = val == "true",
478 "tee_on_error" | "tee_mode" => {
479 cfg.tee_mode = match val.as_str() {
480 "true" | "failures" => config::TeeMode::Failures,
481 "always" => config::TeeMode::Always,
482 "false" | "never" => config::TeeMode::Never,
483 _ => {
484 eprintln!("Valid tee_mode values: always, failures, never");
485 std::process::exit(1);
486 }
487 };
488 }
489 "checkpoint_interval" => {
490 cfg.checkpoint_interval = val.parse().unwrap_or(15);
491 }
492 "theme" => {
493 if theme::from_preset(val).is_some() || val == "custom" {
494 cfg.theme = val.to_string();
495 } else {
496 eprintln!(
497 "Unknown theme '{val}'. Available: {}",
498 theme::PRESET_NAMES.join(", ")
499 );
500 std::process::exit(1);
501 }
502 }
503 "slow_command_threshold_ms" => {
504 cfg.slow_command_threshold_ms = val.parse().unwrap_or(5000);
505 }
506 "passthrough_urls" => {
507 cfg.passthrough_urls = val.split(',').map(|s| s.trim().to_string()).collect();
508 }
509 _ => {
510 eprintln!("Unknown config key: {key}");
511 std::process::exit(1);
512 }
513 }
514 match cfg.save() {
515 Ok(()) => println!("Updated {key} = {val}"),
516 Err(e) => eprintln!("Error saving config: {e}"),
517 }
518 }
519 _ => {
520 eprintln!("Usage: lean-ctx config [init|set <key> <value>]");
521 std::process::exit(1);
522 }
523 }
524}
525
526pub fn cmd_cheatsheet() {
527 println!(
528 "\x1b[1;36m╔══════════════════════════════════════════════════════════════╗\x1b[0m
529\x1b[1;36m║\x1b[0m \x1b[1;37mlean-ctx Workflow Cheat Sheet\x1b[0m \x1b[2mv2.9.7\x1b[0m \x1b[1;36m║\x1b[0m
530\x1b[1;36m╚══════════════════════════════════════════════════════════════╝\x1b[0m
531
532\x1b[1;33m━━━ BEFORE YOU START ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m
533 ctx_session load \x1b[2m# restore previous session\x1b[0m
534 ctx_overview task=\"...\" \x1b[2m# task-aware file map\x1b[0m
535 ctx_graph action=build \x1b[2m# index project (first time)\x1b[0m
536 ctx_knowledge action=recall \x1b[2m# check stored project facts\x1b[0m
537
538\x1b[1;32m━━━ WHILE CODING ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m
539 ctx_read mode=full \x1b[2m# first read (cached, re-reads: 99% saved)\x1b[0m
540 ctx_read mode=map \x1b[2m# context-only files (~93% saved)\x1b[0m
541 ctx_read mode=diff \x1b[2m# after editing (~98% saved)\x1b[0m
542 ctx_read mode=sigs \x1b[2m# API surface of large files (~95%)\x1b[0m
543 ctx_multi_read \x1b[2m# read multiple files at once\x1b[0m
544 ctx_search \x1b[2m# search with compressed results (~70%)\x1b[0m
545 ctx_shell \x1b[2m# run CLI with compressed output (~60-90%)\x1b[0m
546
547\x1b[1;35m━━━ AFTER CODING ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m
548 ctx_session finding \"...\" \x1b[2m# record what you discovered\x1b[0m
549 ctx_session decision \"...\" \x1b[2m# record architectural choices\x1b[0m
550 ctx_knowledge action=remember \x1b[2m# store permanent project facts\x1b[0m
551 ctx_knowledge action=consolidate \x1b[2m# auto-extract session insights\x1b[0m
552 ctx_metrics \x1b[2m# see session statistics\x1b[0m
553
554\x1b[1;34m━━━ MULTI-AGENT ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m
555 ctx_agent action=register \x1b[2m# announce yourself\x1b[0m
556 ctx_agent action=list \x1b[2m# see other active agents\x1b[0m
557 ctx_agent action=post \x1b[2m# share findings\x1b[0m
558 ctx_agent action=read \x1b[2m# check messages\x1b[0m
559
560\x1b[1;31m━━━ READ MODE DECISION TREE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m
561 Will edit? → \x1b[1mfull\x1b[0m (re-reads: 13 tokens) → after edit: \x1b[1mdiff\x1b[0m
562 API only? → \x1b[1msignatures\x1b[0m
563 Deps/exports? → \x1b[1mmap\x1b[0m
564 Very large? → \x1b[1mentropy\x1b[0m (information-dense lines)
565 Browsing? → \x1b[1maggressive\x1b[0m (syntax stripped)
566
567\x1b[1;36m━━━ MONITORING ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m
568 lean-ctx gain \x1b[2m# visual savings dashboard\x1b[0m
569 lean-ctx gain --live \x1b[2m# live auto-updating (Ctrl+C)\x1b[0m
570 lean-ctx dashboard \x1b[2m# web dashboard with charts\x1b[0m
571 lean-ctx wrapped \x1b[2m# weekly savings report\x1b[0m
572 lean-ctx discover \x1b[2m# find uncompressed commands\x1b[0m
573 lean-ctx doctor \x1b[2m# diagnose installation\x1b[0m
574 lean-ctx update \x1b[2m# self-update to latest\x1b[0m
575
576\x1b[2m Full guide: https://leanctx.com/docs/workflow\x1b[0m"
577 );
578}
579
580pub fn cmd_slow_log(args: &[String]) {
581 use crate::core::slow_log;
582
583 let action = args.first().map(|s| s.as_str()).unwrap_or("list");
584 match action {
585 "list" | "ls" | "" => println!("{}", slow_log::list()),
586 "clear" | "purge" => println!("{}", slow_log::clear()),
587 _ => {
588 eprintln!("Usage: lean-ctx slow-log [list|clear]");
589 std::process::exit(1);
590 }
591 }
592}
593
594pub fn cmd_tee(args: &[String]) {
595 let tee_dir = match dirs::home_dir() {
596 Some(h) => h.join(".lean-ctx").join("tee"),
597 None => {
598 eprintln!("Cannot determine home directory");
599 std::process::exit(1);
600 }
601 };
602
603 let action = args.first().map(|s| s.as_str()).unwrap_or("list");
604 match action {
605 "list" | "ls" => {
606 if !tee_dir.exists() {
607 println!("No tee logs found (~/.lean-ctx/tee/ does not exist)");
608 return;
609 }
610 let mut entries: Vec<_> = std::fs::read_dir(&tee_dir)
611 .unwrap_or_else(|e| {
612 eprintln!("Error: {e}");
613 std::process::exit(1);
614 })
615 .filter_map(|e| e.ok())
616 .filter(|e| e.path().extension().and_then(|x| x.to_str()) == Some("log"))
617 .collect();
618 entries.sort_by_key(|e| e.file_name());
619
620 if entries.is_empty() {
621 println!("No tee logs found.");
622 return;
623 }
624
625 println!("Tee logs ({}):\n", entries.len());
626 for entry in &entries {
627 let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
628 let name = entry.file_name();
629 let size_str = if size > 1024 {
630 format!("{}K", size / 1024)
631 } else {
632 format!("{}B", size)
633 };
634 println!(" {:<60} {}", name.to_string_lossy(), size_str);
635 }
636 println!("\nUse 'lean-ctx tee clear' to delete all logs.");
637 }
638 "clear" | "purge" => {
639 if !tee_dir.exists() {
640 println!("No tee logs to clear.");
641 return;
642 }
643 let mut count = 0u32;
644 if let Ok(entries) = std::fs::read_dir(&tee_dir) {
645 for entry in entries.flatten() {
646 if entry.path().extension().and_then(|x| x.to_str()) == Some("log")
647 && std::fs::remove_file(entry.path()).is_ok()
648 {
649 count += 1;
650 }
651 }
652 }
653 println!("Cleared {count} tee log(s) from {}", tee_dir.display());
654 }
655 "show" => {
656 let filename = args.get(1);
657 if filename.is_none() {
658 eprintln!("Usage: lean-ctx tee show <filename>");
659 std::process::exit(1);
660 }
661 let path = tee_dir.join(filename.unwrap());
662 match crate::tools::ctx_read::read_file_lossy(&path.to_string_lossy()) {
663 Ok(content) => print!("{content}"),
664 Err(e) => {
665 eprintln!("Error reading {}: {e}", path.display());
666 std::process::exit(1);
667 }
668 }
669 }
670 "last" => {
671 if !tee_dir.exists() {
672 println!("No tee logs found.");
673 return;
674 }
675 let mut entries: Vec<_> = std::fs::read_dir(&tee_dir)
676 .ok()
677 .into_iter()
678 .flat_map(|d| d.filter_map(|e| e.ok()))
679 .filter(|e| e.path().extension().and_then(|x| x.to_str()) == Some("log"))
680 .collect();
681 entries.sort_by_key(|e| {
682 e.metadata()
683 .and_then(|m| m.modified())
684 .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
685 });
686 match entries.last() {
687 Some(entry) => {
688 let path = entry.path();
689 println!(
690 "--- {} ---\n",
691 path.file_name().unwrap_or_default().to_string_lossy()
692 );
693 match crate::tools::ctx_read::read_file_lossy(&path.to_string_lossy()) {
694 Ok(content) => print!("{content}"),
695 Err(e) => eprintln!("Error: {e}"),
696 }
697 }
698 None => println!("No tee logs found."),
699 }
700 }
701 _ => {
702 eprintln!("Usage: lean-ctx tee [list|clear|show <file>|last]");
703 std::process::exit(1);
704 }
705 }
706}
707
708pub fn cmd_filter(args: &[String]) {
709 let action = args.first().map(|s| s.as_str()).unwrap_or("list");
710 match action {
711 "list" | "ls" => match crate::core::filters::FilterEngine::load() {
712 Some(engine) => {
713 let rules = engine.list_rules();
714 println!("Loaded {} filter rule(s):\n", rules.len());
715 for rule in &rules {
716 println!("{rule}");
717 }
718 }
719 None => {
720 println!("No custom filters found.");
721 println!("Create one: lean-ctx filter init");
722 }
723 },
724 "validate" => {
725 let path = args.get(1);
726 if path.is_none() {
727 eprintln!("Usage: lean-ctx filter validate <file.toml>");
728 std::process::exit(1);
729 }
730 match crate::core::filters::validate_filter_file(path.unwrap()) {
731 Ok(count) => println!("Valid: {count} rule(s) parsed successfully."),
732 Err(e) => {
733 eprintln!("Validation failed: {e}");
734 std::process::exit(1);
735 }
736 }
737 }
738 "init" => match crate::core::filters::create_example_filter() {
739 Ok(path) => {
740 println!("Created example filter: {path}");
741 println!("Edit it to add your custom compression rules.");
742 }
743 Err(e) => {
744 eprintln!("{e}");
745 std::process::exit(1);
746 }
747 },
748 _ => {
749 eprintln!("Usage: lean-ctx filter [list|validate <file>|init]");
750 std::process::exit(1);
751 }
752 }
753}
754
755pub fn cmd_init(args: &[String]) {
756 let global = args.iter().any(|a| a == "--global" || a == "-g");
757 let dry_run = args.iter().any(|a| a == "--dry-run");
758
759 let agents: Vec<&str> = args
760 .windows(2)
761 .filter(|w| w[0] == "--agent")
762 .map(|w| w[1].as_str())
763 .collect();
764
765 if !agents.is_empty() {
766 for agent_name in &agents {
767 crate::hooks::install_agent_hook(agent_name, global);
768 }
769 if !global {
770 crate::hooks::install_project_rules();
771 }
772 println!("\nRun 'lean-ctx gain' after using some commands to see your savings.");
773 return;
774 }
775
776 let shell_name = std::env::var("SHELL").unwrap_or_default();
777 let is_zsh = shell_name.contains("zsh");
778 let is_fish = shell_name.contains("fish");
779 let is_powershell = cfg!(windows) && shell_name.is_empty();
780
781 let binary = std::env::current_exe()
782 .map(|p| p.to_string_lossy().to_string())
783 .unwrap_or_else(|_| "lean-ctx".to_string());
784
785 if dry_run {
786 let rc = if is_powershell {
787 "Documents/PowerShell/Microsoft.PowerShell_profile.ps1".to_string()
788 } else if is_fish {
789 "~/.config/fish/config.fish".to_string()
790 } else if is_zsh {
791 "~/.zshrc".to_string()
792 } else {
793 "~/.bashrc".to_string()
794 };
795 println!("\nlean-ctx init --dry-run\n");
796 println!(" Would modify: {rc}");
797 println!(" Would backup: {rc}.lean-ctx.bak");
798 println!(" Would alias: git npm pnpm yarn cargo docker docker-compose kubectl");
799 println!(" gh pip pip3 ruff go golangci-lint eslint prettier tsc");
800 println!(" ls find grep curl wget php composer (24 commands + k)");
801 println!(" Would create: ~/.lean-ctx/");
802 println!(" Binary: {binary}");
803 println!("\n Safety: aliases auto-fallback to original command if lean-ctx is removed.");
804 println!("\n Run without --dry-run to apply.");
805 return;
806 }
807
808 if is_powershell {
809 init_powershell(&binary);
810 } else {
811 let bash_binary = to_bash_compatible_path(&binary);
812 if is_fish {
813 init_fish(&bash_binary);
814 } else {
815 init_posix(is_zsh, &bash_binary);
816 }
817 }
818
819 let lean_dir = dirs::home_dir().map(|h| h.join(".lean-ctx"));
820 if let Some(dir) = lean_dir {
821 if !dir.exists() {
822 let _ = std::fs::create_dir_all(&dir);
823 println!("Created {}", dir.display());
824 }
825 }
826
827 let rc = if is_powershell {
828 "$PROFILE"
829 } else if is_fish {
830 "config.fish"
831 } else if is_zsh {
832 ".zshrc"
833 } else {
834 ".bashrc"
835 };
836
837 println!("\nlean-ctx init complete (24 aliases installed)");
838 println!();
839 println!(" Disable temporarily: lean-ctx-off");
840 println!(" Re-enable: lean-ctx-on");
841 println!(" Check status: lean-ctx-status");
842 println!(" Full uninstall: lean-ctx uninstall");
843 println!(" Diagnose issues: lean-ctx doctor");
844 println!(" Preview changes: lean-ctx init --global --dry-run");
845 println!();
846 if is_powershell {
847 println!(" Restart PowerShell or run: . {rc}");
848 } else {
849 println!(" Restart your shell or run: source ~/{rc}");
850 }
851 println!();
852 println!("For AI tool integration: lean-ctx init --agent <tool>");
853 println!(" Supported: claude, cursor, gemini, codex, windsurf, cline, copilot, pi");
854}
855
856fn backup_shell_config(path: &std::path::Path) {
857 if !path.exists() {
858 return;
859 }
860 let bak = path.with_extension("lean-ctx.bak");
861 if std::fs::copy(path, &bak).is_ok() {
862 println!(
863 " Backup: {}",
864 bak.file_name()
865 .map(|n| format!("~/{}", n.to_string_lossy()))
866 .unwrap_or_else(|| bak.display().to_string())
867 );
868 }
869}
870
871fn init_powershell(binary: &str) {
872 let profile_dir = dirs::home_dir().map(|h| h.join("Documents").join("PowerShell"));
873 let profile_path = match profile_dir {
874 Some(dir) => {
875 let _ = std::fs::create_dir_all(&dir);
876 dir.join("Microsoft.PowerShell_profile.ps1")
877 }
878 None => {
879 eprintln!("Could not resolve PowerShell profile directory");
880 return;
881 }
882 };
883
884 let binary_escaped = binary.replace('\\', "\\\\");
885 let functions = format!(
886 r#"
887# lean-ctx shell hook — transparent CLI compression (90+ patterns)
888if (-not $env:LEAN_CTX_ACTIVE) {{
889 $LeanCtxBin = "{binary_escaped}"
890 function _lc {{
891 & $LeanCtxBin -c @args
892 if ($LASTEXITCODE -eq 127 -or $LASTEXITCODE -eq 126) {{
893 $cmd = $args[0]; $rest = $args[1..($args.Length)]
894 & $cmd @rest
895 }}
896 }}
897 function lean-ctx-raw {{ $env:LEAN_CTX_RAW = '1'; & @args; Remove-Item Env:LEAN_CTX_RAW -ErrorAction SilentlyContinue }}
898 if (Get-Command lean-ctx -ErrorAction SilentlyContinue) {{
899 function git {{ _lc git @args }}
900 function cargo {{ _lc cargo @args }}
901 function docker {{ _lc docker @args }}
902 function kubectl {{ _lc kubectl @args }}
903 function gh {{ _lc gh @args }}
904 function pip {{ _lc pip @args }}
905 function pip3 {{ _lc pip3 @args }}
906 function ruff {{ _lc ruff @args }}
907 function go {{ _lc go @args }}
908 function curl {{ _lc curl @args }}
909 function wget {{ _lc wget @args }}
910 foreach ($c in @('npm','pnpm','yarn','eslint','prettier','tsc')) {{
911 $a = Get-Command $c -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
912 if ($a) {{
913 Set-Variable -Name "_lc_$c" -Value $a.Source -Scope Script
914 New-Item -Path "function:$c" -Value ([scriptblock]::Create("_lc `$script:_lc_$c @args")) -Force | Out-Null
915 }}
916 }}
917 }}
918}}
919"#
920 );
921
922 backup_shell_config(&profile_path);
923
924 if let Ok(existing) = std::fs::read_to_string(&profile_path) {
925 if existing.contains("lean-ctx shell hook") {
926 let cleaned = remove_lean_ctx_block_ps(&existing);
927 match std::fs::write(&profile_path, format!("{cleaned}{functions}")) {
928 Ok(()) => {
929 println!("Updated lean-ctx functions in {}", profile_path.display());
930 println!(" Binary: {binary}");
931 return;
932 }
933 Err(e) => {
934 eprintln!("Error updating {}: {e}", profile_path.display());
935 return;
936 }
937 }
938 }
939 }
940
941 match std::fs::OpenOptions::new()
942 .append(true)
943 .create(true)
944 .open(&profile_path)
945 {
946 Ok(mut f) => {
947 use std::io::Write;
948 let _ = f.write_all(functions.as_bytes());
949 println!("Added lean-ctx functions to {}", profile_path.display());
950 println!(" Binary: {binary}");
951 }
952 Err(e) => eprintln!("Error writing {}: {e}", profile_path.display()),
953 }
954}
955
956fn remove_lean_ctx_block_ps(content: &str) -> String {
957 let mut result = String::new();
958 let mut in_block = false;
959 let mut brace_depth = 0i32;
960
961 for line in content.lines() {
962 if line.contains("lean-ctx shell hook") {
963 in_block = true;
964 continue;
965 }
966 if in_block {
967 brace_depth += line.matches('{').count() as i32;
968 brace_depth -= line.matches('}').count() as i32;
969 if brace_depth <= 0 && (line.trim() == "}" || line.trim().is_empty()) {
970 if line.trim() == "}" {
971 in_block = false;
972 brace_depth = 0;
973 }
974 continue;
975 }
976 continue;
977 }
978 result.push_str(line);
979 result.push('\n');
980 }
981 result
982}
983
984fn init_fish(binary: &str) {
985 let config = dirs::home_dir()
986 .map(|h| h.join(".config/fish/config.fish"))
987 .unwrap_or_default();
988
989 let aliases = format!(
990 "\n# lean-ctx shell hook — transparent CLI compression (90+ patterns)\n\
991 set -g _lean_ctx_cmds git npm pnpm yarn cargo docker docker-compose kubectl gh pip pip3 ruff go golangci-lint eslint prettier tsc ls find grep curl wget\n\
992 \n\
993 function _lc\n\
994 \t'{binary}' -c $argv\n\
995 \tset -l _lc_rc $status\n\
996 \tif test $_lc_rc -eq 127 -o $_lc_rc -eq 126\n\
997 \t\tcommand $argv\n\
998 \telse\n\
999 \t\treturn $_lc_rc\n\
1000 \tend\n\
1001 end\n\
1002 \n\
1003 function lean-ctx-on\n\
1004 \tfor _lc_cmd in $_lean_ctx_cmds\n\
1005 \t\talias $_lc_cmd '_lc '$_lc_cmd\n\
1006 \tend\n\
1007 \talias k '_lc kubectl'\n\
1008 \tset -gx LEAN_CTX_ENABLED 1\n\
1009 \techo 'lean-ctx: ON'\n\
1010 end\n\
1011 \n\
1012 function lean-ctx-off\n\
1013 \tfor _lc_cmd in $_lean_ctx_cmds\n\
1014 \t\tfunctions --erase $_lc_cmd 2>/dev/null; true\n\
1015 \tend\n\
1016 \tfunctions --erase k 2>/dev/null; true\n\
1017 \tset -e LEAN_CTX_ENABLED\n\
1018 \techo 'lean-ctx: OFF'\n\
1019 end\n\
1020 \n\
1021 function lean-ctx-raw\n\
1022 \tset -lx LEAN_CTX_RAW 1\n\
1023 \tcommand $argv\n\
1024 end\n\
1025 \n\
1026 function lean-ctx-status\n\
1027 \tif set -q LEAN_CTX_ENABLED\n\
1028 \t\techo 'lean-ctx: ON'\n\
1029 \telse\n\
1030 \t\techo 'lean-ctx: OFF'\n\
1031 \tend\n\
1032 end\n\
1033 \n\
1034 if not set -q LEAN_CTX_ACTIVE; and test (set -q LEAN_CTX_ENABLED; and echo $LEAN_CTX_ENABLED; or echo 1) != '0'\n\
1035 \tif command -q lean-ctx\n\
1036 \t\tlean-ctx-on\n\
1037 \tend\n\
1038 end\n\
1039 # lean-ctx shell hook — end\n"
1040 );
1041
1042 backup_shell_config(&config);
1043
1044 if let Ok(existing) = std::fs::read_to_string(&config) {
1045 if existing.contains("lean-ctx shell hook") {
1046 let cleaned = remove_lean_ctx_block(&existing);
1047 match std::fs::write(&config, format!("{cleaned}{aliases}")) {
1048 Ok(()) => {
1049 println!("Updated lean-ctx aliases in {}", config.display());
1050 println!(" Binary: {binary}");
1051 return;
1052 }
1053 Err(e) => {
1054 eprintln!("Error updating {}: {e}", config.display());
1055 return;
1056 }
1057 }
1058 }
1059 }
1060
1061 match std::fs::OpenOptions::new()
1062 .append(true)
1063 .create(true)
1064 .open(&config)
1065 {
1066 Ok(mut f) => {
1067 use std::io::Write;
1068 let _ = f.write_all(aliases.as_bytes());
1069 println!("Added lean-ctx aliases to {}", config.display());
1070 println!(" Binary: {binary}");
1071 }
1072 Err(e) => eprintln!("Error writing {}: {e}", config.display()),
1073 }
1074}
1075
1076fn init_posix(is_zsh: bool, binary: &str) {
1077 let rc_file = if is_zsh {
1078 dirs::home_dir()
1079 .map(|h| h.join(".zshrc"))
1080 .unwrap_or_default()
1081 } else {
1082 dirs::home_dir()
1083 .map(|h| h.join(".bashrc"))
1084 .unwrap_or_default()
1085 };
1086
1087 let aliases = format!(
1088 r#"
1089# lean-ctx shell hook — transparent CLI compression (90+ patterns)
1090_lean_ctx_cmds=(git npm pnpm yarn cargo docker docker-compose kubectl gh pip pip3 ruff go golangci-lint eslint prettier tsc ls find grep curl wget php composer)
1091
1092_lc() {{
1093 '{binary}' -c "$@"
1094 local _lc_rc=$?
1095 if [ "$_lc_rc" -eq 127 ] || [ "$_lc_rc" -eq 126 ]; then
1096 command "$@"
1097 else
1098 return "$_lc_rc"
1099 fi
1100}}
1101
1102lean-ctx-on() {{
1103 for _lc_cmd in "${{_lean_ctx_cmds[@]}}"; do
1104 # shellcheck disable=SC2139
1105 alias "$_lc_cmd"='_lc '"$_lc_cmd"
1106 done
1107 alias k='_lc kubectl'
1108 export LEAN_CTX_ENABLED=1
1109 echo "lean-ctx: ON"
1110}}
1111
1112lean-ctx-off() {{
1113 for _lc_cmd in "${{_lean_ctx_cmds[@]}}"; do
1114 unalias "$_lc_cmd" 2>/dev/null || true
1115 done
1116 unalias k 2>/dev/null || true
1117 unset LEAN_CTX_ENABLED
1118 echo "lean-ctx: OFF"
1119}}
1120
1121lean-ctx-raw() {{
1122 LEAN_CTX_RAW=1 command "$@"
1123}}
1124
1125lean-ctx-status() {{
1126 if [ -n "${{LEAN_CTX_ENABLED:-}}" ]; then
1127 echo "lean-ctx: ON"
1128 else
1129 echo "lean-ctx: OFF"
1130 fi
1131}}
1132
1133if [ -z "${{LEAN_CTX_ACTIVE:-}}" ] && [ "${{LEAN_CTX_ENABLED:-1}}" != "0" ]; then
1134 command -v lean-ctx >/dev/null 2>&1 && lean-ctx-on
1135fi
1136# lean-ctx shell hook — end
1137"#
1138 );
1139
1140 backup_shell_config(&rc_file);
1141
1142 if let Ok(existing) = std::fs::read_to_string(&rc_file) {
1143 if existing.contains("lean-ctx shell hook") {
1144 let cleaned = remove_lean_ctx_block(&existing);
1145 match std::fs::write(&rc_file, format!("{cleaned}{aliases}")) {
1146 Ok(()) => {
1147 println!("Updated lean-ctx aliases in {}", rc_file.display());
1148 println!(" Binary: {binary}");
1149 return;
1150 }
1151 Err(e) => {
1152 eprintln!("Error updating {}: {e}", rc_file.display());
1153 return;
1154 }
1155 }
1156 }
1157 }
1158
1159 match std::fs::OpenOptions::new()
1160 .append(true)
1161 .create(true)
1162 .open(&rc_file)
1163 {
1164 Ok(mut f) => {
1165 use std::io::Write;
1166 let _ = f.write_all(aliases.as_bytes());
1167 println!("Added lean-ctx aliases to {}", rc_file.display());
1168 println!(" Binary: {binary}");
1169 }
1170 Err(e) => eprintln!("Error writing {}: {e}", rc_file.display()),
1171 }
1172}
1173
1174fn remove_lean_ctx_block(content: &str) -> String {
1175 if content.contains("# lean-ctx shell hook — end") {
1177 return remove_lean_ctx_block_by_marker(content);
1178 }
1179 remove_lean_ctx_block_legacy(content)
1180}
1181
1182fn remove_lean_ctx_block_by_marker(content: &str) -> String {
1183 let mut result = String::new();
1184 let mut in_block = false;
1185
1186 for line in content.lines() {
1187 if !in_block && line.contains("lean-ctx shell hook") && !line.contains("end") {
1188 in_block = true;
1189 continue;
1190 }
1191 if in_block {
1192 if line.trim() == "# lean-ctx shell hook — end" {
1193 in_block = false;
1194 }
1195 continue;
1196 }
1197 result.push_str(line);
1198 result.push('\n');
1199 }
1200 result
1201}
1202
1203fn remove_lean_ctx_block_legacy(content: &str) -> String {
1204 let mut result = String::new();
1205 let mut in_block = false;
1206
1207 for line in content.lines() {
1208 if line.contains("lean-ctx shell hook") {
1209 in_block = true;
1210 continue;
1211 }
1212 if in_block {
1213 if line.trim() == "fi" || line.trim() == "end" || line.trim().is_empty() {
1214 if line.trim() == "fi" || line.trim() == "end" {
1215 in_block = false;
1216 }
1217 continue;
1218 }
1219 if !line.starts_with("alias ") && !line.starts_with('\t') && !line.starts_with("if ") {
1220 in_block = false;
1221 result.push_str(line);
1222 result.push('\n');
1223 }
1224 continue;
1225 }
1226 result.push_str(line);
1227 result.push('\n');
1228 }
1229 result
1230}
1231
1232pub fn load_shell_history_pub() -> Vec<String> {
1233 load_shell_history()
1234}
1235
1236fn load_shell_history() -> Vec<String> {
1237 let shell = std::env::var("SHELL").unwrap_or_default();
1238 let home = match dirs::home_dir() {
1239 Some(h) => h,
1240 None => return Vec::new(),
1241 };
1242
1243 let history_file = if shell.contains("zsh") {
1244 home.join(".zsh_history")
1245 } else if shell.contains("fish") {
1246 home.join(".local/share/fish/fish_history")
1247 } else if cfg!(windows) && shell.is_empty() {
1248 home.join("AppData")
1249 .join("Roaming")
1250 .join("Microsoft")
1251 .join("Windows")
1252 .join("PowerShell")
1253 .join("PSReadLine")
1254 .join("ConsoleHost_history.txt")
1255 } else {
1256 home.join(".bash_history")
1257 };
1258
1259 match std::fs::read_to_string(&history_file) {
1260 Ok(content) => content
1261 .lines()
1262 .filter_map(|l| {
1263 let trimmed = l.trim();
1264 if trimmed.starts_with(':') {
1265 trimmed.split(';').nth(1).map(|s| s.to_string())
1266 } else {
1267 Some(trimmed.to_string())
1268 }
1269 })
1270 .filter(|l| !l.is_empty())
1271 .collect(),
1272 Err(_) => Vec::new(),
1273 }
1274}
1275
1276fn print_savings(original: usize, sent: usize) {
1277 let saved = original.saturating_sub(sent);
1278 if original > 0 && saved > 0 {
1279 let pct = (saved as f64 / original as f64 * 100.0).round() as usize;
1280 println!("[{saved} tok saved ({pct}%)]");
1281 }
1282}
1283
1284pub fn cmd_theme(args: &[String]) {
1285 let sub = args.first().map(|s| s.as_str()).unwrap_or("list");
1286 let r = theme::rst();
1287 let b = theme::bold();
1288 let d = theme::dim();
1289
1290 match sub {
1291 "list" => {
1292 let cfg = config::Config::load();
1293 let active = cfg.theme.as_str();
1294 println!();
1295 println!(" {b}Available themes:{r}");
1296 println!(" {ln}", ln = "─".repeat(40));
1297 for name in theme::PRESET_NAMES {
1298 let marker = if *name == active { " ◀ active" } else { "" };
1299 let t = theme::from_preset(name).unwrap();
1300 let preview = format!(
1301 "{p}██{r}{s}██{r}{a}██{r}{sc}██{r}{w}██{r}",
1302 p = t.primary.fg(),
1303 s = t.secondary.fg(),
1304 a = t.accent.fg(),
1305 sc = t.success.fg(),
1306 w = t.warning.fg(),
1307 );
1308 println!(" {preview} {b}{name:<12}{r}{d}{marker}{r}");
1309 }
1310 if let Some(path) = theme::theme_file_path() {
1311 if path.exists() {
1312 let custom = theme::load_theme("_custom_");
1313 let preview = format!(
1314 "{p}██{r}{s}██{r}{a}██{r}{sc}██{r}{w}██{r}",
1315 p = custom.primary.fg(),
1316 s = custom.secondary.fg(),
1317 a = custom.accent.fg(),
1318 sc = custom.success.fg(),
1319 w = custom.warning.fg(),
1320 );
1321 let marker = if active == "custom" {
1322 " ◀ active"
1323 } else {
1324 ""
1325 };
1326 println!(" {preview} {b}{:<12}{r}{d}{marker}{r}", custom.name,);
1327 }
1328 }
1329 println!();
1330 println!(" {d}Set theme: lean-ctx theme set <name>{r}");
1331 println!();
1332 }
1333 "set" => {
1334 if args.len() < 2 {
1335 eprintln!("Usage: lean-ctx theme set <name>");
1336 std::process::exit(1);
1337 }
1338 let name = &args[1];
1339 if theme::from_preset(name).is_none() && name != "custom" {
1340 eprintln!(
1341 "Unknown theme '{name}'. Available: {}",
1342 theme::PRESET_NAMES.join(", ")
1343 );
1344 std::process::exit(1);
1345 }
1346 let mut cfg = config::Config::load();
1347 cfg.theme = name.to_string();
1348 match cfg.save() {
1349 Ok(()) => {
1350 let t = theme::load_theme(name);
1351 println!(" {sc}✓{r} Theme set to {b}{name}{r}", sc = t.success.fg(),);
1352 let preview = t.gradient_bar(0.75, 30);
1353 println!(" {preview}");
1354 }
1355 Err(e) => eprintln!("Error: {e}"),
1356 }
1357 }
1358 "export" => {
1359 let cfg = config::Config::load();
1360 let t = theme::load_theme(&cfg.theme);
1361 println!("{}", t.to_toml());
1362 }
1363 "import" => {
1364 if args.len() < 2 {
1365 eprintln!("Usage: lean-ctx theme import <path>");
1366 std::process::exit(1);
1367 }
1368 let path = std::path::Path::new(&args[1]);
1369 if !path.exists() {
1370 eprintln!("File not found: {}", args[1]);
1371 std::process::exit(1);
1372 }
1373 match std::fs::read_to_string(path) {
1374 Ok(content) => match toml::from_str::<theme::Theme>(&content) {
1375 Ok(imported) => match theme::save_theme(&imported) {
1376 Ok(()) => {
1377 let mut cfg = config::Config::load();
1378 cfg.theme = "custom".to_string();
1379 let _ = cfg.save();
1380 println!(
1381 " {sc}✓{r} Imported theme '{name}' → ~/.lean-ctx/theme.toml",
1382 sc = imported.success.fg(),
1383 name = imported.name,
1384 );
1385 println!(" Config updated: theme = custom");
1386 }
1387 Err(e) => eprintln!("Error saving theme: {e}"),
1388 },
1389 Err(e) => eprintln!("Invalid theme file: {e}"),
1390 },
1391 Err(e) => eprintln!("Error reading file: {e}"),
1392 }
1393 }
1394 "preview" => {
1395 let name = args.get(1).map(|s| s.as_str()).unwrap_or("default");
1396 let t = match theme::from_preset(name) {
1397 Some(t) => t,
1398 None => {
1399 eprintln!("Unknown theme: {name}");
1400 std::process::exit(1);
1401 }
1402 };
1403 println!();
1404 println!(
1405 " {icon} {title} {d}Theme Preview: {name}{r}",
1406 icon = t.header_icon(),
1407 title = t.brand_title(),
1408 );
1409 println!(" {ln}", ln = t.border_line(50));
1410 println!();
1411 println!(
1412 " {b}{sc} 1.2M {r} {b}{sec} 87.3% {r} {b}{wrn} 4,521 {r} {b}{acc} $12.50 {r}",
1413 sc = t.success.fg(),
1414 sec = t.secondary.fg(),
1415 wrn = t.warning.fg(),
1416 acc = t.accent.fg(),
1417 );
1418 println!(" {d} tokens saved compression commands USD saved{r}");
1419 println!();
1420 println!(
1421 " {b}{txt}Gradient Bar{r} {bar}",
1422 txt = t.text.fg(),
1423 bar = t.gradient_bar(0.85, 30),
1424 );
1425 println!(
1426 " {b}{txt}Sparkline{r} {spark}",
1427 txt = t.text.fg(),
1428 spark = t.gradient_sparkline(&[20, 40, 30, 80, 60, 90, 70]),
1429 );
1430 println!();
1431 println!(" {top}", top = t.box_top(50));
1432 println!(
1433 " {side} {b}{txt}Box content with themed borders{r} {side_r}",
1434 side = t.box_side(),
1435 side_r = t.box_side(),
1436 txt = t.text.fg(),
1437 );
1438 println!(" {bot}", bot = t.box_bottom(50));
1439 println!();
1440 }
1441 _ => {
1442 eprintln!("Usage: lean-ctx theme [list|set|export|import|preview]");
1443 std::process::exit(1);
1444 }
1445 }
1446}
1447
1448#[cfg(test)]
1449mod tests {
1450 use super::*;
1451
1452 #[test]
1453 fn test_remove_lean_ctx_block_posix() {
1454 let input = r#"# existing config
1455export PATH="$HOME/bin:$PATH"
1456
1457# lean-ctx shell hook — transparent CLI compression (90+ patterns)
1458if [ -z "$LEAN_CTX_ACTIVE" ]; then
1459alias git='lean-ctx -c git'
1460alias npm='lean-ctx -c npm'
1461fi
1462
1463# other stuff
1464export EDITOR=vim
1465"#;
1466 let result = remove_lean_ctx_block(input);
1467 assert!(!result.contains("lean-ctx"), "block should be removed");
1468 assert!(result.contains("export PATH"), "other content preserved");
1469 assert!(
1470 result.contains("export EDITOR"),
1471 "trailing content preserved"
1472 );
1473 }
1474
1475 #[test]
1476 fn test_remove_lean_ctx_block_fish() {
1477 let input = "# other fish config\nset -x FOO bar\n\n# lean-ctx shell hook — transparent CLI compression (90+ patterns)\nif not set -q LEAN_CTX_ACTIVE\n\talias git 'lean-ctx -c git'\n\talias npm 'lean-ctx -c npm'\nend\n\n# more config\nset -x BAZ qux\n";
1478 let result = remove_lean_ctx_block(input);
1479 assert!(!result.contains("lean-ctx"), "block should be removed");
1480 assert!(result.contains("set -x FOO"), "other content preserved");
1481 assert!(result.contains("set -x BAZ"), "trailing content preserved");
1482 }
1483
1484 #[test]
1485 fn test_remove_lean_ctx_block_ps() {
1486 let input = "# PowerShell profile\n$env:FOO = 'bar'\n\n# lean-ctx shell hook — transparent CLI compression (90+ patterns)\nif (-not $env:LEAN_CTX_ACTIVE) {\n $LeanCtxBin = \"C:\\\\bin\\\\lean-ctx.exe\"\n function git { & $LeanCtxBin -c \"git $($args -join ' ')\" }\n}\n\n# other stuff\n$env:EDITOR = 'vim'\n";
1487 let result = remove_lean_ctx_block_ps(input);
1488 assert!(
1489 !result.contains("lean-ctx shell hook"),
1490 "block should be removed"
1491 );
1492 assert!(result.contains("$env:FOO"), "other content preserved");
1493 assert!(result.contains("$env:EDITOR"), "trailing content preserved");
1494 }
1495
1496 #[test]
1497 fn test_remove_lean_ctx_block_ps_nested() {
1498 let input = "# PowerShell profile\n$env:FOO = 'bar'\n\n# lean-ctx shell hook — transparent CLI compression (90+ patterns)\nif (-not $env:LEAN_CTX_ACTIVE) {\n $LeanCtxBin = \"lean-ctx\"\n function _lc {\n & $LeanCtxBin -c \"$($args -join ' ')\"\n }\n if (Get-Command lean-ctx -ErrorAction SilentlyContinue) {\n function git { _lc git @args }\n foreach ($c in @('npm','pnpm')) {\n if ($a) {\n Set-Variable -Name \"_lc_$c\" -Value $a.Source -Scope Script\n }\n }\n }\n}\n\n# other stuff\n$env:EDITOR = 'vim'\n";
1499 let result = remove_lean_ctx_block_ps(input);
1500 assert!(
1501 !result.contains("lean-ctx shell hook"),
1502 "block should be removed"
1503 );
1504 assert!(!result.contains("_lc"), "function should be removed");
1505 assert!(result.contains("$env:FOO"), "other content preserved");
1506 assert!(result.contains("$env:EDITOR"), "trailing content preserved");
1507 }
1508
1509 #[test]
1510 fn test_remove_block_no_lean_ctx() {
1511 let input = "# normal bashrc\nexport PATH=\"$HOME/bin:$PATH\"\n";
1512 let result = remove_lean_ctx_block(input);
1513 assert!(result.contains("export PATH"), "content unchanged");
1514 }
1515
1516 #[test]
1517 fn test_remove_lean_ctx_block_new_format_with_end_marker() {
1518 let input = r#"# existing config
1519export PATH="$HOME/bin:$PATH"
1520
1521# lean-ctx shell hook — transparent CLI compression (90+ patterns)
1522_lean_ctx_cmds=(git npm pnpm)
1523
1524lean-ctx-on() {
1525 for _lc_cmd in "${_lean_ctx_cmds[@]}"; do
1526 alias "$_lc_cmd"='lean-ctx -c '"$_lc_cmd"
1527 done
1528 export LEAN_CTX_ENABLED=1
1529 echo "lean-ctx: ON"
1530}
1531
1532lean-ctx-off() {
1533 unset LEAN_CTX_ENABLED
1534 echo "lean-ctx: OFF"
1535}
1536
1537if [ -z "${LEAN_CTX_ACTIVE:-}" ] && [ "${LEAN_CTX_ENABLED:-1}" != "0" ]; then
1538 lean-ctx-on
1539fi
1540# lean-ctx shell hook — end
1541
1542# other stuff
1543export EDITOR=vim
1544"#;
1545 let result = remove_lean_ctx_block(input);
1546 assert!(!result.contains("lean-ctx-on"), "block should be removed");
1547 assert!(!result.contains("lean-ctx shell hook"), "marker removed");
1548 assert!(result.contains("export PATH"), "other content preserved");
1549 assert!(
1550 result.contains("export EDITOR"),
1551 "trailing content preserved"
1552 );
1553 }
1554}