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 if !global {
748 crate::hooks::install_project_rules();
749 }
750 println!("\nRun 'lean-ctx gain' after using some commands to see your savings.");
751 return;
752 }
753
754 let shell_name = std::env::var("SHELL").unwrap_or_default();
755 let is_zsh = shell_name.contains("zsh");
756 let is_fish = shell_name.contains("fish");
757 let is_powershell = cfg!(windows) && shell_name.is_empty();
758
759 let binary = std::env::current_exe()
760 .map(|p| p.to_string_lossy().to_string())
761 .unwrap_or_else(|_| "lean-ctx".to_string());
762
763 if dry_run {
764 let rc = if is_powershell {
765 "Documents/PowerShell/Microsoft.PowerShell_profile.ps1".to_string()
766 } else if is_fish {
767 "~/.config/fish/config.fish".to_string()
768 } else if is_zsh {
769 "~/.zshrc".to_string()
770 } else {
771 "~/.bashrc".to_string()
772 };
773 println!("\nlean-ctx init --dry-run\n");
774 println!(" Would modify: {rc}");
775 println!(" Would backup: {rc}.lean-ctx.bak");
776 println!(" Would alias: git npm pnpm yarn cargo docker docker-compose kubectl");
777 println!(" gh pip pip3 ruff go golangci-lint eslint prettier tsc");
778 println!(" ls find grep curl wget php composer (24 commands + k)");
779 println!(" Would create: ~/.lean-ctx/");
780 println!(" Binary: {binary}");
781 println!("\n Safety: aliases auto-fallback to original command if lean-ctx is removed.");
782 println!("\n Run without --dry-run to apply.");
783 return;
784 }
785
786 if is_powershell {
787 init_powershell(&binary);
788 } else {
789 let bash_binary = to_bash_compatible_path(&binary);
790 if is_fish {
791 init_fish(&bash_binary);
792 } else {
793 init_posix(is_zsh, &bash_binary);
794 }
795 }
796
797 let lean_dir = dirs::home_dir().map(|h| h.join(".lean-ctx"));
798 if let Some(dir) = lean_dir {
799 if !dir.exists() {
800 let _ = std::fs::create_dir_all(&dir);
801 println!("Created {}", dir.display());
802 }
803 }
804
805 let rc = if is_powershell {
806 "$PROFILE"
807 } else if is_fish {
808 "config.fish"
809 } else if is_zsh {
810 ".zshrc"
811 } else {
812 ".bashrc"
813 };
814
815 println!("\nlean-ctx init complete (24 aliases installed)");
816 println!();
817 println!(" Disable temporarily: lean-ctx-off");
818 println!(" Re-enable: lean-ctx-on");
819 println!(" Check status: lean-ctx-status");
820 println!(" Full uninstall: lean-ctx uninstall");
821 println!(" Diagnose issues: lean-ctx doctor");
822 println!(" Preview changes: lean-ctx init --global --dry-run");
823 println!();
824 if is_powershell {
825 println!(" Restart PowerShell or run: . {rc}");
826 } else {
827 println!(" Restart your shell or run: source ~/{rc}");
828 }
829 println!();
830 println!("For AI tool integration: lean-ctx init --agent <tool>");
831 println!(" Supported: claude, cursor, gemini, codex, windsurf, cline, copilot, pi");
832}
833
834fn backup_shell_config(path: &std::path::Path) {
835 if !path.exists() {
836 return;
837 }
838 let bak = path.with_extension("lean-ctx.bak");
839 if std::fs::copy(path, &bak).is_ok() {
840 println!(
841 " Backup: {}",
842 bak.file_name()
843 .map(|n| format!("~/{}", n.to_string_lossy()))
844 .unwrap_or_else(|| bak.display().to_string())
845 );
846 }
847}
848
849fn init_powershell(binary: &str) {
850 let profile_dir = dirs::home_dir().map(|h| h.join("Documents").join("PowerShell"));
851 let profile_path = match profile_dir {
852 Some(dir) => {
853 let _ = std::fs::create_dir_all(&dir);
854 dir.join("Microsoft.PowerShell_profile.ps1")
855 }
856 None => {
857 eprintln!("Could not resolve PowerShell profile directory");
858 return;
859 }
860 };
861
862 let binary_escaped = binary.replace('\\', "\\\\");
863 let functions = format!(
864 r#"
865# lean-ctx shell hook — transparent CLI compression (90+ patterns)
866if (-not $env:LEAN_CTX_ACTIVE) {{
867 $LeanCtxBin = "{binary_escaped}"
868 function _lc {{
869 & $LeanCtxBin -c @args
870 if ($LASTEXITCODE -eq 127 -or $LASTEXITCODE -eq 126) {{
871 $cmd = $args[0]; $rest = $args[1..($args.Length)]
872 & $cmd @rest
873 }}
874 }}
875 if (Get-Command lean-ctx -ErrorAction SilentlyContinue) {{
876 function git {{ _lc git @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 curl {{ _lc curl @args }}
886 function wget {{ _lc wget @args }}
887 foreach ($c in @('npm','pnpm','yarn','eslint','prettier','tsc')) {{
888 $a = Get-Command $c -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
889 if ($a) {{
890 Set-Variable -Name "_lc_$c" -Value $a.Source -Scope Script
891 New-Item -Path "function:$c" -Value ([scriptblock]::Create("_lc `$script:_lc_$c @args")) -Force | Out-Null
892 }}
893 }}
894 }}
895}}
896"#
897 );
898
899 backup_shell_config(&profile_path);
900
901 if let Ok(existing) = std::fs::read_to_string(&profile_path) {
902 if existing.contains("lean-ctx shell hook") {
903 let cleaned = remove_lean_ctx_block_ps(&existing);
904 match std::fs::write(&profile_path, format!("{cleaned}{functions}")) {
905 Ok(()) => {
906 println!("Updated lean-ctx functions in {}", profile_path.display());
907 println!(" Binary: {binary}");
908 return;
909 }
910 Err(e) => {
911 eprintln!("Error updating {}: {e}", profile_path.display());
912 return;
913 }
914 }
915 }
916 }
917
918 match std::fs::OpenOptions::new()
919 .append(true)
920 .create(true)
921 .open(&profile_path)
922 {
923 Ok(mut f) => {
924 use std::io::Write;
925 let _ = f.write_all(functions.as_bytes());
926 println!("Added lean-ctx functions to {}", profile_path.display());
927 println!(" Binary: {binary}");
928 }
929 Err(e) => eprintln!("Error writing {}: {e}", profile_path.display()),
930 }
931}
932
933fn remove_lean_ctx_block_ps(content: &str) -> String {
934 let mut result = String::new();
935 let mut in_block = false;
936 let mut brace_depth = 0i32;
937
938 for line in content.lines() {
939 if line.contains("lean-ctx shell hook") {
940 in_block = true;
941 continue;
942 }
943 if in_block {
944 brace_depth += line.matches('{').count() as i32;
945 brace_depth -= line.matches('}').count() as i32;
946 if brace_depth <= 0 && (line.trim() == "}" || line.trim().is_empty()) {
947 if line.trim() == "}" {
948 in_block = false;
949 brace_depth = 0;
950 }
951 continue;
952 }
953 continue;
954 }
955 result.push_str(line);
956 result.push('\n');
957 }
958 result
959}
960
961fn init_fish(binary: &str) {
962 let config = dirs::home_dir()
963 .map(|h| h.join(".config/fish/config.fish"))
964 .unwrap_or_default();
965
966 let aliases = format!(
967 "\n# lean-ctx shell hook — transparent CLI compression (90+ patterns)\n\
968 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\
969 \n\
970 function _lc\n\
971 \t'{binary}' -c $argv\n\
972 \tset -l _lc_rc $status\n\
973 \tif test $_lc_rc -eq 127 -o $_lc_rc -eq 126\n\
974 \t\tcommand $argv\n\
975 \telse\n\
976 \t\treturn $_lc_rc\n\
977 \tend\n\
978 end\n\
979 \n\
980 function lean-ctx-on\n\
981 \tfor _lc_cmd in $_lean_ctx_cmds\n\
982 \t\talias $_lc_cmd '_lc '$_lc_cmd\n\
983 \tend\n\
984 \talias k '_lc kubectl'\n\
985 \tset -gx LEAN_CTX_ENABLED 1\n\
986 \techo 'lean-ctx: ON'\n\
987 end\n\
988 \n\
989 function lean-ctx-off\n\
990 \tfor _lc_cmd in $_lean_ctx_cmds\n\
991 \t\tfunctions --erase $_lc_cmd 2>/dev/null; true\n\
992 \tend\n\
993 \tfunctions --erase k 2>/dev/null; true\n\
994 \tset -e LEAN_CTX_ENABLED\n\
995 \techo 'lean-ctx: OFF'\n\
996 end\n\
997 \n\
998 function lean-ctx-status\n\
999 \tif set -q LEAN_CTX_ENABLED\n\
1000 \t\techo 'lean-ctx: ON'\n\
1001 \telse\n\
1002 \t\techo 'lean-ctx: OFF'\n\
1003 \tend\n\
1004 end\n\
1005 \n\
1006 if not set -q LEAN_CTX_ACTIVE; and test (set -q LEAN_CTX_ENABLED; and echo $LEAN_CTX_ENABLED; or echo 1) != '0'\n\
1007 \tif command -q lean-ctx\n\
1008 \t\tlean-ctx-on\n\
1009 \tend\n\
1010 end\n\
1011 # lean-ctx shell hook — end\n"
1012 );
1013
1014 backup_shell_config(&config);
1015
1016 if let Ok(existing) = std::fs::read_to_string(&config) {
1017 if existing.contains("lean-ctx shell hook") {
1018 let cleaned = remove_lean_ctx_block(&existing);
1019 match std::fs::write(&config, format!("{cleaned}{aliases}")) {
1020 Ok(()) => {
1021 println!("Updated lean-ctx aliases in {}", config.display());
1022 println!(" Binary: {binary}");
1023 return;
1024 }
1025 Err(e) => {
1026 eprintln!("Error updating {}: {e}", config.display());
1027 return;
1028 }
1029 }
1030 }
1031 }
1032
1033 match std::fs::OpenOptions::new()
1034 .append(true)
1035 .create(true)
1036 .open(&config)
1037 {
1038 Ok(mut f) => {
1039 use std::io::Write;
1040 let _ = f.write_all(aliases.as_bytes());
1041 println!("Added lean-ctx aliases to {}", config.display());
1042 println!(" Binary: {binary}");
1043 }
1044 Err(e) => eprintln!("Error writing {}: {e}", config.display()),
1045 }
1046}
1047
1048fn init_posix(is_zsh: bool, binary: &str) {
1049 let rc_file = if is_zsh {
1050 dirs::home_dir()
1051 .map(|h| h.join(".zshrc"))
1052 .unwrap_or_default()
1053 } else {
1054 dirs::home_dir()
1055 .map(|h| h.join(".bashrc"))
1056 .unwrap_or_default()
1057 };
1058
1059 let aliases = format!(
1060 r#"
1061# lean-ctx shell hook — transparent CLI compression (90+ patterns)
1062_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)
1063
1064_lc() {{
1065 '{binary}' -c "$@"
1066 local _lc_rc=$?
1067 if [ "$_lc_rc" -eq 127 ] || [ "$_lc_rc" -eq 126 ]; then
1068 command "$@"
1069 else
1070 return "$_lc_rc"
1071 fi
1072}}
1073
1074lean-ctx-on() {{
1075 for _lc_cmd in "${{_lean_ctx_cmds[@]}}"; do
1076 # shellcheck disable=SC2139
1077 alias "$_lc_cmd"='_lc '"$_lc_cmd"
1078 done
1079 alias k='_lc kubectl'
1080 export LEAN_CTX_ENABLED=1
1081 echo "lean-ctx: ON"
1082}}
1083
1084lean-ctx-off() {{
1085 for _lc_cmd in "${{_lean_ctx_cmds[@]}}"; do
1086 unalias "$_lc_cmd" 2>/dev/null || true
1087 done
1088 unalias k 2>/dev/null || true
1089 unset LEAN_CTX_ENABLED
1090 echo "lean-ctx: OFF"
1091}}
1092
1093lean-ctx-status() {{
1094 if [ -n "${{LEAN_CTX_ENABLED:-}}" ]; then
1095 echo "lean-ctx: ON"
1096 else
1097 echo "lean-ctx: OFF"
1098 fi
1099}}
1100
1101if [ -z "${{LEAN_CTX_ACTIVE:-}}" ] && [ "${{LEAN_CTX_ENABLED:-1}}" != "0" ]; then
1102 command -v lean-ctx >/dev/null 2>&1 && lean-ctx-on
1103fi
1104# lean-ctx shell hook — end
1105"#
1106 );
1107
1108 backup_shell_config(&rc_file);
1109
1110 if let Ok(existing) = std::fs::read_to_string(&rc_file) {
1111 if existing.contains("lean-ctx shell hook") {
1112 let cleaned = remove_lean_ctx_block(&existing);
1113 match std::fs::write(&rc_file, format!("{cleaned}{aliases}")) {
1114 Ok(()) => {
1115 println!("Updated lean-ctx aliases in {}", rc_file.display());
1116 println!(" Binary: {binary}");
1117 return;
1118 }
1119 Err(e) => {
1120 eprintln!("Error updating {}: {e}", rc_file.display());
1121 return;
1122 }
1123 }
1124 }
1125 }
1126
1127 match std::fs::OpenOptions::new()
1128 .append(true)
1129 .create(true)
1130 .open(&rc_file)
1131 {
1132 Ok(mut f) => {
1133 use std::io::Write;
1134 let _ = f.write_all(aliases.as_bytes());
1135 println!("Added lean-ctx aliases to {}", rc_file.display());
1136 println!(" Binary: {binary}");
1137 }
1138 Err(e) => eprintln!("Error writing {}: {e}", rc_file.display()),
1139 }
1140}
1141
1142fn remove_lean_ctx_block(content: &str) -> String {
1143 if content.contains("# lean-ctx shell hook — end") {
1145 return remove_lean_ctx_block_by_marker(content);
1146 }
1147 remove_lean_ctx_block_legacy(content)
1148}
1149
1150fn remove_lean_ctx_block_by_marker(content: &str) -> String {
1151 let mut result = String::new();
1152 let mut in_block = false;
1153
1154 for line in content.lines() {
1155 if !in_block && line.contains("lean-ctx shell hook") && !line.contains("end") {
1156 in_block = true;
1157 continue;
1158 }
1159 if in_block {
1160 if line.trim() == "# lean-ctx shell hook — end" {
1161 in_block = false;
1162 }
1163 continue;
1164 }
1165 result.push_str(line);
1166 result.push('\n');
1167 }
1168 result
1169}
1170
1171fn remove_lean_ctx_block_legacy(content: &str) -> String {
1172 let mut result = String::new();
1173 let mut in_block = false;
1174
1175 for line in content.lines() {
1176 if line.contains("lean-ctx shell hook") {
1177 in_block = true;
1178 continue;
1179 }
1180 if in_block {
1181 if line.trim() == "fi" || line.trim() == "end" || line.trim().is_empty() {
1182 if line.trim() == "fi" || line.trim() == "end" {
1183 in_block = false;
1184 }
1185 continue;
1186 }
1187 if !line.starts_with("alias ") && !line.starts_with('\t') && !line.starts_with("if ") {
1188 in_block = false;
1189 result.push_str(line);
1190 result.push('\n');
1191 }
1192 continue;
1193 }
1194 result.push_str(line);
1195 result.push('\n');
1196 }
1197 result
1198}
1199
1200pub fn load_shell_history_pub() -> Vec<String> {
1201 load_shell_history()
1202}
1203
1204fn load_shell_history() -> Vec<String> {
1205 let shell = std::env::var("SHELL").unwrap_or_default();
1206 let home = match dirs::home_dir() {
1207 Some(h) => h,
1208 None => return Vec::new(),
1209 };
1210
1211 let history_file = if shell.contains("zsh") {
1212 home.join(".zsh_history")
1213 } else if shell.contains("fish") {
1214 home.join(".local/share/fish/fish_history")
1215 } else if cfg!(windows) && shell.is_empty() {
1216 home.join("AppData")
1217 .join("Roaming")
1218 .join("Microsoft")
1219 .join("Windows")
1220 .join("PowerShell")
1221 .join("PSReadLine")
1222 .join("ConsoleHost_history.txt")
1223 } else {
1224 home.join(".bash_history")
1225 };
1226
1227 match std::fs::read_to_string(&history_file) {
1228 Ok(content) => content
1229 .lines()
1230 .filter_map(|l| {
1231 let trimmed = l.trim();
1232 if trimmed.starts_with(':') {
1233 trimmed.split(';').nth(1).map(|s| s.to_string())
1234 } else {
1235 Some(trimmed.to_string())
1236 }
1237 })
1238 .filter(|l| !l.is_empty())
1239 .collect(),
1240 Err(_) => Vec::new(),
1241 }
1242}
1243
1244fn print_savings(original: usize, sent: usize) {
1245 let saved = original.saturating_sub(sent);
1246 if original > 0 && saved > 0 {
1247 let pct = (saved as f64 / original as f64 * 100.0).round() as usize;
1248 println!("[{saved} tok saved ({pct}%)]");
1249 }
1250}
1251
1252pub fn cmd_theme(args: &[String]) {
1253 let sub = args.first().map(|s| s.as_str()).unwrap_or("list");
1254 let r = theme::rst();
1255 let b = theme::bold();
1256 let d = theme::dim();
1257
1258 match sub {
1259 "list" => {
1260 let cfg = config::Config::load();
1261 let active = cfg.theme.as_str();
1262 println!();
1263 println!(" {b}Available themes:{r}");
1264 println!(" {ln}", ln = "─".repeat(40));
1265 for name in theme::PRESET_NAMES {
1266 let marker = if *name == active { " ◀ active" } else { "" };
1267 let t = theme::from_preset(name).unwrap();
1268 let preview = format!(
1269 "{p}██{r}{s}██{r}{a}██{r}{sc}██{r}{w}██{r}",
1270 p = t.primary.fg(),
1271 s = t.secondary.fg(),
1272 a = t.accent.fg(),
1273 sc = t.success.fg(),
1274 w = t.warning.fg(),
1275 );
1276 println!(" {preview} {b}{name:<12}{r}{d}{marker}{r}");
1277 }
1278 if let Some(path) = theme::theme_file_path() {
1279 if path.exists() {
1280 let custom = theme::load_theme("_custom_");
1281 let preview = format!(
1282 "{p}██{r}{s}██{r}{a}██{r}{sc}██{r}{w}██{r}",
1283 p = custom.primary.fg(),
1284 s = custom.secondary.fg(),
1285 a = custom.accent.fg(),
1286 sc = custom.success.fg(),
1287 w = custom.warning.fg(),
1288 );
1289 let marker = if active == "custom" {
1290 " ◀ active"
1291 } else {
1292 ""
1293 };
1294 println!(" {preview} {b}{:<12}{r}{d}{marker}{r}", custom.name,);
1295 }
1296 }
1297 println!();
1298 println!(" {d}Set theme: lean-ctx theme set <name>{r}");
1299 println!();
1300 }
1301 "set" => {
1302 if args.len() < 2 {
1303 eprintln!("Usage: lean-ctx theme set <name>");
1304 std::process::exit(1);
1305 }
1306 let name = &args[1];
1307 if theme::from_preset(name).is_none() && name != "custom" {
1308 eprintln!(
1309 "Unknown theme '{name}'. Available: {}",
1310 theme::PRESET_NAMES.join(", ")
1311 );
1312 std::process::exit(1);
1313 }
1314 let mut cfg = config::Config::load();
1315 cfg.theme = name.to_string();
1316 match cfg.save() {
1317 Ok(()) => {
1318 let t = theme::load_theme(name);
1319 println!(" {sc}✓{r} Theme set to {b}{name}{r}", sc = t.success.fg(),);
1320 let preview = t.gradient_bar(0.75, 30);
1321 println!(" {preview}");
1322 }
1323 Err(e) => eprintln!("Error: {e}"),
1324 }
1325 }
1326 "export" => {
1327 let cfg = config::Config::load();
1328 let t = theme::load_theme(&cfg.theme);
1329 println!("{}", t.to_toml());
1330 }
1331 "import" => {
1332 if args.len() < 2 {
1333 eprintln!("Usage: lean-ctx theme import <path>");
1334 std::process::exit(1);
1335 }
1336 let path = std::path::Path::new(&args[1]);
1337 if !path.exists() {
1338 eprintln!("File not found: {}", args[1]);
1339 std::process::exit(1);
1340 }
1341 match std::fs::read_to_string(path) {
1342 Ok(content) => match toml::from_str::<theme::Theme>(&content) {
1343 Ok(imported) => match theme::save_theme(&imported) {
1344 Ok(()) => {
1345 let mut cfg = config::Config::load();
1346 cfg.theme = "custom".to_string();
1347 let _ = cfg.save();
1348 println!(
1349 " {sc}✓{r} Imported theme '{name}' → ~/.lean-ctx/theme.toml",
1350 sc = imported.success.fg(),
1351 name = imported.name,
1352 );
1353 println!(" Config updated: theme = custom");
1354 }
1355 Err(e) => eprintln!("Error saving theme: {e}"),
1356 },
1357 Err(e) => eprintln!("Invalid theme file: {e}"),
1358 },
1359 Err(e) => eprintln!("Error reading file: {e}"),
1360 }
1361 }
1362 "preview" => {
1363 let name = args.get(1).map(|s| s.as_str()).unwrap_or("default");
1364 let t = match theme::from_preset(name) {
1365 Some(t) => t,
1366 None => {
1367 eprintln!("Unknown theme: {name}");
1368 std::process::exit(1);
1369 }
1370 };
1371 println!();
1372 println!(
1373 " {icon} {title} {d}Theme Preview: {name}{r}",
1374 icon = t.header_icon(),
1375 title = t.brand_title(),
1376 );
1377 println!(" {ln}", ln = t.border_line(50));
1378 println!();
1379 println!(
1380 " {b}{sc} 1.2M {r} {b}{sec} 87.3% {r} {b}{wrn} 4,521 {r} {b}{acc} $12.50 {r}",
1381 sc = t.success.fg(),
1382 sec = t.secondary.fg(),
1383 wrn = t.warning.fg(),
1384 acc = t.accent.fg(),
1385 );
1386 println!(" {d} tokens saved compression commands USD saved{r}");
1387 println!();
1388 println!(
1389 " {b}{txt}Gradient Bar{r} {bar}",
1390 txt = t.text.fg(),
1391 bar = t.gradient_bar(0.85, 30),
1392 );
1393 println!(
1394 " {b}{txt}Sparkline{r} {spark}",
1395 txt = t.text.fg(),
1396 spark = t.gradient_sparkline(&[20, 40, 30, 80, 60, 90, 70]),
1397 );
1398 println!();
1399 println!(" {top}", top = t.box_top(50));
1400 println!(
1401 " {side} {b}{txt}Box content with themed borders{r} {side_r}",
1402 side = t.box_side(),
1403 side_r = t.box_side(),
1404 txt = t.text.fg(),
1405 );
1406 println!(" {bot}", bot = t.box_bottom(50));
1407 println!();
1408 }
1409 _ => {
1410 eprintln!("Usage: lean-ctx theme [list|set|export|import|preview]");
1411 std::process::exit(1);
1412 }
1413 }
1414}
1415
1416#[cfg(test)]
1417mod tests {
1418 use super::*;
1419
1420 #[test]
1421 fn test_remove_lean_ctx_block_posix() {
1422 let input = r#"# existing config
1423export PATH="$HOME/bin:$PATH"
1424
1425# lean-ctx shell hook — transparent CLI compression (90+ patterns)
1426if [ -z "$LEAN_CTX_ACTIVE" ]; then
1427alias git='lean-ctx -c git'
1428alias npm='lean-ctx -c npm'
1429fi
1430
1431# other stuff
1432export EDITOR=vim
1433"#;
1434 let result = remove_lean_ctx_block(input);
1435 assert!(!result.contains("lean-ctx"), "block should be removed");
1436 assert!(result.contains("export PATH"), "other content preserved");
1437 assert!(
1438 result.contains("export EDITOR"),
1439 "trailing content preserved"
1440 );
1441 }
1442
1443 #[test]
1444 fn test_remove_lean_ctx_block_fish() {
1445 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";
1446 let result = remove_lean_ctx_block(input);
1447 assert!(!result.contains("lean-ctx"), "block should be removed");
1448 assert!(result.contains("set -x FOO"), "other content preserved");
1449 assert!(result.contains("set -x BAZ"), "trailing content preserved");
1450 }
1451
1452 #[test]
1453 fn test_remove_lean_ctx_block_ps() {
1454 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";
1455 let result = remove_lean_ctx_block_ps(input);
1456 assert!(
1457 !result.contains("lean-ctx shell hook"),
1458 "block should be removed"
1459 );
1460 assert!(result.contains("$env:FOO"), "other content preserved");
1461 assert!(result.contains("$env:EDITOR"), "trailing content preserved");
1462 }
1463
1464 #[test]
1465 fn test_remove_lean_ctx_block_ps_nested() {
1466 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";
1467 let result = remove_lean_ctx_block_ps(input);
1468 assert!(
1469 !result.contains("lean-ctx shell hook"),
1470 "block should be removed"
1471 );
1472 assert!(!result.contains("_lc"), "function should be removed");
1473 assert!(result.contains("$env:FOO"), "other content preserved");
1474 assert!(result.contains("$env:EDITOR"), "trailing content preserved");
1475 }
1476
1477 #[test]
1478 fn test_remove_block_no_lean_ctx() {
1479 let input = "# normal bashrc\nexport PATH=\"$HOME/bin:$PATH\"\n";
1480 let result = remove_lean_ctx_block(input);
1481 assert!(result.contains("export PATH"), "content unchanged");
1482 }
1483
1484 #[test]
1485 fn test_remove_lean_ctx_block_new_format_with_end_marker() {
1486 let input = r#"# existing config
1487export PATH="$HOME/bin:$PATH"
1488
1489# lean-ctx shell hook — transparent CLI compression (90+ patterns)
1490_lean_ctx_cmds=(git npm pnpm)
1491
1492lean-ctx-on() {
1493 for _lc_cmd in "${_lean_ctx_cmds[@]}"; do
1494 alias "$_lc_cmd"='lean-ctx -c '"$_lc_cmd"
1495 done
1496 export LEAN_CTX_ENABLED=1
1497 echo "lean-ctx: ON"
1498}
1499
1500lean-ctx-off() {
1501 unset LEAN_CTX_ENABLED
1502 echo "lean-ctx: OFF"
1503}
1504
1505if [ -z "${LEAN_CTX_ACTIVE:-}" ] && [ "${LEAN_CTX_ENABLED:-1}" != "0" ]; then
1506 lean-ctx-on
1507fi
1508# lean-ctx shell hook — end
1509
1510# other stuff
1511export EDITOR=vim
1512"#;
1513 let result = remove_lean_ctx_block(input);
1514 assert!(!result.contains("lean-ctx-on"), "block should be removed");
1515 assert!(!result.contains("lean-ctx shell hook"), "marker removed");
1516 assert!(result.contains("export PATH"), "other content preserved");
1517 assert!(
1518 result.contains("export EDITOR"),
1519 "trailing content preserved"
1520 );
1521 }
1522}