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