Skip to main content

lean_ctx/
cli.rs

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 -and -not $env:LEAN_CTX_DISABLED) {{
889  $LeanCtxBin = "{binary_escaped}"
890  function _lc {{
891    if ($env:LEAN_CTX_DISABLED) {{ & $args[0] $args[1..($args.Length)]; return }}
892    & $LeanCtxBin -c @args
893    if ($LASTEXITCODE -eq 127 -or $LASTEXITCODE -eq 126) {{
894      $cmd = $args[0]; $rest = $args[1..($args.Length)]
895      & $cmd @rest
896    }}
897  }}
898  function lean-ctx-raw {{ $env:LEAN_CTX_RAW = '1'; & @args; Remove-Item Env:LEAN_CTX_RAW -ErrorAction SilentlyContinue }}
899  if (Get-Command lean-ctx -ErrorAction SilentlyContinue) {{
900    function git {{ _lc git @args }}
901    function cargo {{ _lc cargo @args }}
902    function docker {{ _lc docker @args }}
903    function kubectl {{ _lc kubectl @args }}
904    function gh {{ _lc gh @args }}
905    function pip {{ _lc pip @args }}
906    function pip3 {{ _lc pip3 @args }}
907    function ruff {{ _lc ruff @args }}
908    function go {{ _lc go @args }}
909    function curl {{ _lc curl @args }}
910    function wget {{ _lc wget @args }}
911    foreach ($c in @('npm','pnpm','yarn','eslint','prettier','tsc')) {{
912      $a = Get-Command $c -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
913      if ($a) {{
914        Set-Variable -Name "_lc_$c" -Value $a.Source -Scope Script
915        New-Item -Path "function:$c" -Value ([scriptblock]::Create("_lc `$script:_lc_$c @args")) -Force | Out-Null
916      }}
917    }}
918  }}
919}}
920"#
921    );
922
923    backup_shell_config(&profile_path);
924
925    if let Ok(existing) = std::fs::read_to_string(&profile_path) {
926        if existing.contains("lean-ctx shell hook") {
927            let cleaned = remove_lean_ctx_block_ps(&existing);
928            match std::fs::write(&profile_path, format!("{cleaned}{functions}")) {
929                Ok(()) => {
930                    println!("Updated lean-ctx functions in {}", profile_path.display());
931                    println!("  Binary: {binary}");
932                    return;
933                }
934                Err(e) => {
935                    eprintln!("Error updating {}: {e}", profile_path.display());
936                    return;
937                }
938            }
939        }
940    }
941
942    match std::fs::OpenOptions::new()
943        .append(true)
944        .create(true)
945        .open(&profile_path)
946    {
947        Ok(mut f) => {
948            use std::io::Write;
949            let _ = f.write_all(functions.as_bytes());
950            println!("Added lean-ctx functions to {}", profile_path.display());
951            println!("  Binary: {binary}");
952        }
953        Err(e) => eprintln!("Error writing {}: {e}", profile_path.display()),
954    }
955}
956
957fn remove_lean_ctx_block_ps(content: &str) -> String {
958    let mut result = String::new();
959    let mut in_block = false;
960    let mut brace_depth = 0i32;
961
962    for line in content.lines() {
963        if line.contains("lean-ctx shell hook") {
964            in_block = true;
965            continue;
966        }
967        if in_block {
968            brace_depth += line.matches('{').count() as i32;
969            brace_depth -= line.matches('}').count() as i32;
970            if brace_depth <= 0 && (line.trim() == "}" || line.trim().is_empty()) {
971                if line.trim() == "}" {
972                    in_block = false;
973                    brace_depth = 0;
974                }
975                continue;
976            }
977            continue;
978        }
979        result.push_str(line);
980        result.push('\n');
981    }
982    result
983}
984
985fn init_fish(binary: &str) {
986    let config = dirs::home_dir()
987        .map(|h| h.join(".config/fish/config.fish"))
988        .unwrap_or_default();
989
990    let aliases = format!(
991        "\n# lean-ctx shell hook — transparent CLI compression (90+ patterns)\n\
992        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\
993        \n\
994        function _lc\n\
995        \tif set -q LEAN_CTX_DISABLED\n\
996        \t\tcommand $argv\n\
997        \t\treturn\n\
998        \tend\n\
999        \t'{binary}' -c $argv\n\
1000        \tset -l _lc_rc $status\n\
1001        \tif test $_lc_rc -eq 127 -o $_lc_rc -eq 126\n\
1002        \t\tcommand $argv\n\
1003        \telse\n\
1004        \t\treturn $_lc_rc\n\
1005        \tend\n\
1006        end\n\
1007        \n\
1008        function lean-ctx-on\n\
1009        \tfor _lc_cmd in $_lean_ctx_cmds\n\
1010        \t\talias $_lc_cmd '_lc '$_lc_cmd\n\
1011        \tend\n\
1012        \talias k '_lc kubectl'\n\
1013        \tset -gx LEAN_CTX_ENABLED 1\n\
1014        \techo 'lean-ctx: ON'\n\
1015        end\n\
1016        \n\
1017        function lean-ctx-off\n\
1018        \tfor _lc_cmd in $_lean_ctx_cmds\n\
1019        \t\tfunctions --erase $_lc_cmd 2>/dev/null; true\n\
1020        \tend\n\
1021        \tfunctions --erase k 2>/dev/null; true\n\
1022        \tset -e LEAN_CTX_ENABLED\n\
1023        \techo 'lean-ctx: OFF'\n\
1024        end\n\
1025        \n\
1026        function lean-ctx-raw\n\
1027        \tset -lx LEAN_CTX_RAW 1\n\
1028        \tcommand $argv\n\
1029        end\n\
1030        \n\
1031        function lean-ctx-status\n\
1032        \tif set -q LEAN_CTX_DISABLED\n\
1033        \t\techo 'lean-ctx: DISABLED (LEAN_CTX_DISABLED is set)'\n\
1034        \telse if set -q LEAN_CTX_ENABLED\n\
1035        \t\techo 'lean-ctx: ON'\n\
1036        \telse\n\
1037        \t\techo 'lean-ctx: OFF'\n\
1038        \tend\n\
1039        end\n\
1040        \n\
1041        if not set -q LEAN_CTX_ACTIVE; and not set -q LEAN_CTX_DISABLED; and test (set -q LEAN_CTX_ENABLED; and echo $LEAN_CTX_ENABLED; or echo 1) != '0'\n\
1042        \tif command -q lean-ctx\n\
1043        \t\tlean-ctx-on\n\
1044        \tend\n\
1045        end\n\
1046        # lean-ctx shell hook — end\n"
1047    );
1048
1049    backup_shell_config(&config);
1050
1051    if let Ok(existing) = std::fs::read_to_string(&config) {
1052        if existing.contains("lean-ctx shell hook") {
1053            let cleaned = remove_lean_ctx_block(&existing);
1054            match std::fs::write(&config, format!("{cleaned}{aliases}")) {
1055                Ok(()) => {
1056                    println!("Updated lean-ctx aliases in {}", config.display());
1057                    println!("  Binary: {binary}");
1058                    return;
1059                }
1060                Err(e) => {
1061                    eprintln!("Error updating {}: {e}", config.display());
1062                    return;
1063                }
1064            }
1065        }
1066    }
1067
1068    match std::fs::OpenOptions::new()
1069        .append(true)
1070        .create(true)
1071        .open(&config)
1072    {
1073        Ok(mut f) => {
1074            use std::io::Write;
1075            let _ = f.write_all(aliases.as_bytes());
1076            println!("Added lean-ctx aliases to {}", config.display());
1077            println!("  Binary: {binary}");
1078        }
1079        Err(e) => eprintln!("Error writing {}: {e}", config.display()),
1080    }
1081}
1082
1083fn init_posix(is_zsh: bool, binary: &str) {
1084    let rc_file = if is_zsh {
1085        dirs::home_dir()
1086            .map(|h| h.join(".zshrc"))
1087            .unwrap_or_default()
1088    } else {
1089        dirs::home_dir()
1090            .map(|h| h.join(".bashrc"))
1091            .unwrap_or_default()
1092    };
1093
1094    let aliases = format!(
1095        r#"
1096# lean-ctx shell hook — transparent CLI compression (90+ patterns)
1097_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)
1098
1099_lc() {{
1100    if [ -n "${{LEAN_CTX_DISABLED:-}}" ]; then
1101        command "$@"
1102        return
1103    fi
1104    '{binary}' -c "$@"
1105    local _lc_rc=$?
1106    if [ "$_lc_rc" -eq 127 ] || [ "$_lc_rc" -eq 126 ]; then
1107        command "$@"
1108    else
1109        return "$_lc_rc"
1110    fi
1111}}
1112
1113lean-ctx-on() {{
1114    for _lc_cmd in "${{_lean_ctx_cmds[@]}}"; do
1115        # shellcheck disable=SC2139
1116        alias "$_lc_cmd"='_lc '"$_lc_cmd"
1117    done
1118    alias k='_lc kubectl'
1119    export LEAN_CTX_ENABLED=1
1120    echo "lean-ctx: ON"
1121}}
1122
1123lean-ctx-off() {{
1124    for _lc_cmd in "${{_lean_ctx_cmds[@]}}"; do
1125        unalias "$_lc_cmd" 2>/dev/null || true
1126    done
1127    unalias k 2>/dev/null || true
1128    unset LEAN_CTX_ENABLED
1129    echo "lean-ctx: OFF"
1130}}
1131
1132lean-ctx-raw() {{
1133    LEAN_CTX_RAW=1 command "$@"
1134}}
1135
1136lean-ctx-status() {{
1137    if [ -n "${{LEAN_CTX_DISABLED:-}}" ]; then
1138        echo "lean-ctx: DISABLED (LEAN_CTX_DISABLED is set)"
1139    elif [ -n "${{LEAN_CTX_ENABLED:-}}" ]; then
1140        echo "lean-ctx: ON"
1141    else
1142        echo "lean-ctx: OFF"
1143    fi
1144}}
1145
1146if [ -z "${{LEAN_CTX_ACTIVE:-}}" ] && [ -z "${{LEAN_CTX_DISABLED:-}}" ] && [ "${{LEAN_CTX_ENABLED:-1}}" != "0" ]; then
1147    command -v lean-ctx >/dev/null 2>&1 && lean-ctx-on
1148fi
1149# lean-ctx shell hook — end
1150"#
1151    );
1152
1153    backup_shell_config(&rc_file);
1154
1155    if let Ok(existing) = std::fs::read_to_string(&rc_file) {
1156        if existing.contains("lean-ctx shell hook") {
1157            let cleaned = remove_lean_ctx_block(&existing);
1158            match std::fs::write(&rc_file, format!("{cleaned}{aliases}")) {
1159                Ok(()) => {
1160                    println!("Updated lean-ctx aliases in {}", rc_file.display());
1161                    println!("  Binary: {binary}");
1162                    return;
1163                }
1164                Err(e) => {
1165                    eprintln!("Error updating {}: {e}", rc_file.display());
1166                    return;
1167                }
1168            }
1169        }
1170    }
1171
1172    match std::fs::OpenOptions::new()
1173        .append(true)
1174        .create(true)
1175        .open(&rc_file)
1176    {
1177        Ok(mut f) => {
1178            use std::io::Write;
1179            let _ = f.write_all(aliases.as_bytes());
1180            println!("Added lean-ctx aliases to {}", rc_file.display());
1181            println!("  Binary: {binary}");
1182        }
1183        Err(e) => eprintln!("Error writing {}: {e}", rc_file.display()),
1184    }
1185}
1186
1187fn remove_lean_ctx_block(content: &str) -> String {
1188    // New format uses explicit end marker; old format ends at first top-level `fi`/`end`.
1189    if content.contains("# lean-ctx shell hook — end") {
1190        return remove_lean_ctx_block_by_marker(content);
1191    }
1192    remove_lean_ctx_block_legacy(content)
1193}
1194
1195fn remove_lean_ctx_block_by_marker(content: &str) -> String {
1196    let mut result = String::new();
1197    let mut in_block = false;
1198
1199    for line in content.lines() {
1200        if !in_block && line.contains("lean-ctx shell hook") && !line.contains("end") {
1201            in_block = true;
1202            continue;
1203        }
1204        if in_block {
1205            if line.trim() == "# lean-ctx shell hook — end" {
1206                in_block = false;
1207            }
1208            continue;
1209        }
1210        result.push_str(line);
1211        result.push('\n');
1212    }
1213    result
1214}
1215
1216fn remove_lean_ctx_block_legacy(content: &str) -> String {
1217    let mut result = String::new();
1218    let mut in_block = false;
1219
1220    for line in content.lines() {
1221        if line.contains("lean-ctx shell hook") {
1222            in_block = true;
1223            continue;
1224        }
1225        if in_block {
1226            if line.trim() == "fi" || line.trim() == "end" || line.trim().is_empty() {
1227                if line.trim() == "fi" || line.trim() == "end" {
1228                    in_block = false;
1229                }
1230                continue;
1231            }
1232            if !line.starts_with("alias ") && !line.starts_with('\t') && !line.starts_with("if ") {
1233                in_block = false;
1234                result.push_str(line);
1235                result.push('\n');
1236            }
1237            continue;
1238        }
1239        result.push_str(line);
1240        result.push('\n');
1241    }
1242    result
1243}
1244
1245pub fn load_shell_history_pub() -> Vec<String> {
1246    load_shell_history()
1247}
1248
1249fn load_shell_history() -> Vec<String> {
1250    let shell = std::env::var("SHELL").unwrap_or_default();
1251    let home = match dirs::home_dir() {
1252        Some(h) => h,
1253        None => return Vec::new(),
1254    };
1255
1256    let history_file = if shell.contains("zsh") {
1257        home.join(".zsh_history")
1258    } else if shell.contains("fish") {
1259        home.join(".local/share/fish/fish_history")
1260    } else if cfg!(windows) && shell.is_empty() {
1261        home.join("AppData")
1262            .join("Roaming")
1263            .join("Microsoft")
1264            .join("Windows")
1265            .join("PowerShell")
1266            .join("PSReadLine")
1267            .join("ConsoleHost_history.txt")
1268    } else {
1269        home.join(".bash_history")
1270    };
1271
1272    match std::fs::read_to_string(&history_file) {
1273        Ok(content) => content
1274            .lines()
1275            .filter_map(|l| {
1276                let trimmed = l.trim();
1277                if trimmed.starts_with(':') {
1278                    trimmed.split(';').nth(1).map(|s| s.to_string())
1279                } else {
1280                    Some(trimmed.to_string())
1281                }
1282            })
1283            .filter(|l| !l.is_empty())
1284            .collect(),
1285        Err(_) => Vec::new(),
1286    }
1287}
1288
1289fn print_savings(original: usize, sent: usize) {
1290    let saved = original.saturating_sub(sent);
1291    if original > 0 && saved > 0 {
1292        let pct = (saved as f64 / original as f64 * 100.0).round() as usize;
1293        println!("[{saved} tok saved ({pct}%)]");
1294    }
1295}
1296
1297pub fn cmd_theme(args: &[String]) {
1298    let sub = args.first().map(|s| s.as_str()).unwrap_or("list");
1299    let r = theme::rst();
1300    let b = theme::bold();
1301    let d = theme::dim();
1302
1303    match sub {
1304        "list" => {
1305            let cfg = config::Config::load();
1306            let active = cfg.theme.as_str();
1307            println!();
1308            println!("  {b}Available themes:{r}");
1309            println!("  {ln}", ln = "─".repeat(40));
1310            for name in theme::PRESET_NAMES {
1311                let marker = if *name == active { " ◀ active" } else { "" };
1312                let t = theme::from_preset(name).unwrap();
1313                let preview = format!(
1314                    "{p}██{r}{s}██{r}{a}██{r}{sc}██{r}{w}██{r}",
1315                    p = t.primary.fg(),
1316                    s = t.secondary.fg(),
1317                    a = t.accent.fg(),
1318                    sc = t.success.fg(),
1319                    w = t.warning.fg(),
1320                );
1321                println!("  {preview}  {b}{name:<12}{r}{d}{marker}{r}");
1322            }
1323            if let Some(path) = theme::theme_file_path() {
1324                if path.exists() {
1325                    let custom = theme::load_theme("_custom_");
1326                    let preview = format!(
1327                        "{p}██{r}{s}██{r}{a}██{r}{sc}██{r}{w}██{r}",
1328                        p = custom.primary.fg(),
1329                        s = custom.secondary.fg(),
1330                        a = custom.accent.fg(),
1331                        sc = custom.success.fg(),
1332                        w = custom.warning.fg(),
1333                    );
1334                    let marker = if active == "custom" {
1335                        " ◀ active"
1336                    } else {
1337                        ""
1338                    };
1339                    println!("  {preview}  {b}{:<12}{r}{d}{marker}{r}", custom.name,);
1340                }
1341            }
1342            println!();
1343            println!("  {d}Set theme: lean-ctx theme set <name>{r}");
1344            println!();
1345        }
1346        "set" => {
1347            if args.len() < 2 {
1348                eprintln!("Usage: lean-ctx theme set <name>");
1349                std::process::exit(1);
1350            }
1351            let name = &args[1];
1352            if theme::from_preset(name).is_none() && name != "custom" {
1353                eprintln!(
1354                    "Unknown theme '{name}'. Available: {}",
1355                    theme::PRESET_NAMES.join(", ")
1356                );
1357                std::process::exit(1);
1358            }
1359            let mut cfg = config::Config::load();
1360            cfg.theme = name.to_string();
1361            match cfg.save() {
1362                Ok(()) => {
1363                    let t = theme::load_theme(name);
1364                    println!("  {sc}✓{r} Theme set to {b}{name}{r}", sc = t.success.fg(),);
1365                    let preview = t.gradient_bar(0.75, 30);
1366                    println!("  {preview}");
1367                }
1368                Err(e) => eprintln!("Error: {e}"),
1369            }
1370        }
1371        "export" => {
1372            let cfg = config::Config::load();
1373            let t = theme::load_theme(&cfg.theme);
1374            println!("{}", t.to_toml());
1375        }
1376        "import" => {
1377            if args.len() < 2 {
1378                eprintln!("Usage: lean-ctx theme import <path>");
1379                std::process::exit(1);
1380            }
1381            let path = std::path::Path::new(&args[1]);
1382            if !path.exists() {
1383                eprintln!("File not found: {}", args[1]);
1384                std::process::exit(1);
1385            }
1386            match std::fs::read_to_string(path) {
1387                Ok(content) => match toml::from_str::<theme::Theme>(&content) {
1388                    Ok(imported) => match theme::save_theme(&imported) {
1389                        Ok(()) => {
1390                            let mut cfg = config::Config::load();
1391                            cfg.theme = "custom".to_string();
1392                            let _ = cfg.save();
1393                            println!(
1394                                "  {sc}✓{r} Imported theme '{name}' → ~/.lean-ctx/theme.toml",
1395                                sc = imported.success.fg(),
1396                                name = imported.name,
1397                            );
1398                            println!("  Config updated: theme = custom");
1399                        }
1400                        Err(e) => eprintln!("Error saving theme: {e}"),
1401                    },
1402                    Err(e) => eprintln!("Invalid theme file: {e}"),
1403                },
1404                Err(e) => eprintln!("Error reading file: {e}"),
1405            }
1406        }
1407        "preview" => {
1408            let name = args.get(1).map(|s| s.as_str()).unwrap_or("default");
1409            let t = match theme::from_preset(name) {
1410                Some(t) => t,
1411                None => {
1412                    eprintln!("Unknown theme: {name}");
1413                    std::process::exit(1);
1414                }
1415            };
1416            println!();
1417            println!(
1418                "  {icon} {title}  {d}Theme Preview: {name}{r}",
1419                icon = t.header_icon(),
1420                title = t.brand_title(),
1421            );
1422            println!("  {ln}", ln = t.border_line(50));
1423            println!();
1424            println!(
1425                "  {b}{sc} 1.2M      {r}  {b}{sec} 87.3%     {r}  {b}{wrn} 4,521    {r}  {b}{acc} $12.50   {r}",
1426                sc = t.success.fg(),
1427                sec = t.secondary.fg(),
1428                wrn = t.warning.fg(),
1429                acc = t.accent.fg(),
1430            );
1431            println!("  {d} tokens saved   compression    commands       USD saved{r}");
1432            println!();
1433            println!(
1434                "  {b}{txt}Gradient Bar{r}      {bar}",
1435                txt = t.text.fg(),
1436                bar = t.gradient_bar(0.85, 30),
1437            );
1438            println!(
1439                "  {b}{txt}Sparkline{r}         {spark}",
1440                txt = t.text.fg(),
1441                spark = t.gradient_sparkline(&[20, 40, 30, 80, 60, 90, 70]),
1442            );
1443            println!();
1444            println!("  {top}", top = t.box_top(50));
1445            println!(
1446                "  {side}  {b}{txt}Box content with themed borders{r}                  {side_r}",
1447                side = t.box_side(),
1448                side_r = t.box_side(),
1449                txt = t.text.fg(),
1450            );
1451            println!("  {bot}", bot = t.box_bottom(50));
1452            println!();
1453        }
1454        _ => {
1455            eprintln!("Usage: lean-ctx theme [list|set|export|import|preview]");
1456            std::process::exit(1);
1457        }
1458    }
1459}
1460
1461#[cfg(test)]
1462mod tests {
1463    use super::*;
1464
1465    #[test]
1466    fn test_remove_lean_ctx_block_posix() {
1467        let input = r#"# existing config
1468export PATH="$HOME/bin:$PATH"
1469
1470# lean-ctx shell hook — transparent CLI compression (90+ patterns)
1471if [ -z "$LEAN_CTX_ACTIVE" ]; then
1472alias git='lean-ctx -c git'
1473alias npm='lean-ctx -c npm'
1474fi
1475
1476# other stuff
1477export EDITOR=vim
1478"#;
1479        let result = remove_lean_ctx_block(input);
1480        assert!(!result.contains("lean-ctx"), "block should be removed");
1481        assert!(result.contains("export PATH"), "other content preserved");
1482        assert!(
1483            result.contains("export EDITOR"),
1484            "trailing content preserved"
1485        );
1486    }
1487
1488    #[test]
1489    fn test_remove_lean_ctx_block_fish() {
1490        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";
1491        let result = remove_lean_ctx_block(input);
1492        assert!(!result.contains("lean-ctx"), "block should be removed");
1493        assert!(result.contains("set -x FOO"), "other content preserved");
1494        assert!(result.contains("set -x BAZ"), "trailing content preserved");
1495    }
1496
1497    #[test]
1498    fn test_remove_lean_ctx_block_ps() {
1499        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";
1500        let result = remove_lean_ctx_block_ps(input);
1501        assert!(
1502            !result.contains("lean-ctx shell hook"),
1503            "block should be removed"
1504        );
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_lean_ctx_block_ps_nested() {
1511        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";
1512        let result = remove_lean_ctx_block_ps(input);
1513        assert!(
1514            !result.contains("lean-ctx shell hook"),
1515            "block should be removed"
1516        );
1517        assert!(!result.contains("_lc"), "function should be removed");
1518        assert!(result.contains("$env:FOO"), "other content preserved");
1519        assert!(result.contains("$env:EDITOR"), "trailing content preserved");
1520    }
1521
1522    #[test]
1523    fn test_remove_block_no_lean_ctx() {
1524        let input = "# normal bashrc\nexport PATH=\"$HOME/bin:$PATH\"\n";
1525        let result = remove_lean_ctx_block(input);
1526        assert!(result.contains("export PATH"), "content unchanged");
1527    }
1528
1529    #[test]
1530    fn test_remove_lean_ctx_block_new_format_with_end_marker() {
1531        let input = r#"# existing config
1532export PATH="$HOME/bin:$PATH"
1533
1534# lean-ctx shell hook — transparent CLI compression (90+ patterns)
1535_lean_ctx_cmds=(git npm pnpm)
1536
1537lean-ctx-on() {
1538    for _lc_cmd in "${_lean_ctx_cmds[@]}"; do
1539        alias "$_lc_cmd"='lean-ctx -c '"$_lc_cmd"
1540    done
1541    export LEAN_CTX_ENABLED=1
1542    echo "lean-ctx: ON"
1543}
1544
1545lean-ctx-off() {
1546    unset LEAN_CTX_ENABLED
1547    echo "lean-ctx: OFF"
1548}
1549
1550if [ -z "${LEAN_CTX_ACTIVE:-}" ] && [ "${LEAN_CTX_ENABLED:-1}" != "0" ]; then
1551    lean-ctx-on
1552fi
1553# lean-ctx shell hook — end
1554
1555# other stuff
1556export EDITOR=vim
1557"#;
1558        let result = remove_lean_ctx_block(input);
1559        assert!(!result.contains("lean-ctx-on"), "block should be removed");
1560        assert!(!result.contains("lean-ctx shell hook"), "marker removed");
1561        assert!(result.contains("export PATH"), "other content preserved");
1562        assert!(
1563            result.contains("export EDITOR"),
1564            "trailing content preserved"
1565        );
1566    }
1567}