Skip to main content

lean_ctx/cli/
read_cmd.rs

1use std::path::Path;
2
3use crate::core::compressor;
4use crate::core::deps as dep_extract;
5use crate::core::entropy;
6use crate::core::patterns::deps_cmd;
7use crate::core::protocol;
8use crate::core::signatures;
9use crate::core::stats;
10use crate::core::tokens::count_tokens;
11
12use super::common::print_savings;
13
14pub fn cmd_read(args: &[String]) {
15    if args.is_empty() {
16        eprintln!(
17            "Usage: lean-ctx read <file> [--mode full|map|signatures|aggressive|entropy] [--fresh]"
18        );
19        std::process::exit(1);
20    }
21
22    let path = &args[0];
23    let mode = args
24        .iter()
25        .position(|a| a == "--mode" || a == "-m")
26        .and_then(|i| args.get(i + 1))
27        .map_or("full", std::string::String::as_str);
28    let force_fresh = args.iter().any(|a| a == "--fresh" || a == "--no-cache");
29
30    let short = protocol::shorten_path(path);
31
32    if !force_fresh && mode == "full" {
33        use crate::core::cli_cache::{self, CacheResult};
34        match cli_cache::check_and_read(path) {
35            CacheResult::Hit { entry, file_ref } => {
36                let msg = cli_cache::format_hit(&entry, &file_ref, &short);
37                println!("{msg}");
38                stats::record("cli_read", entry.original_tokens, count_tokens(&msg));
39                return;
40            }
41            CacheResult::Miss { content } if content.is_empty() => {
42                eprintln!("Error: could not read {path}");
43                std::process::exit(1);
44            }
45            CacheResult::Miss { content } => {
46                let line_count = content.lines().count();
47                println!("{short} [{line_count}L]");
48                println!("{content}");
49                stats::record("cli_read", count_tokens(&content), count_tokens(&content));
50                return;
51            }
52        }
53    }
54
55    let content = match crate::tools::ctx_read::read_file_lossy(path) {
56        Ok(c) => c,
57        Err(e) => {
58            eprintln!("Error: {e}");
59            std::process::exit(1);
60        }
61    };
62
63    let ext = Path::new(path)
64        .extension()
65        .and_then(|e| e.to_str())
66        .unwrap_or("");
67    let line_count = content.lines().count();
68    let original_tokens = count_tokens(&content);
69
70    let mode = if mode == "auto" {
71        let sig = crate::core::mode_predictor::FileSignature::from_path(path, original_tokens);
72        let predictor = crate::core::mode_predictor::ModePredictor::new();
73        predictor
74            .predict_best_mode(&sig)
75            .unwrap_or_else(|| "full".to_string())
76    } else {
77        mode.to_string()
78    };
79    let mode = mode.as_str();
80
81    match mode {
82        "map" => {
83            let sigs = signatures::extract_signatures(&content, ext);
84            let dep_info = dep_extract::extract_deps(&content, ext);
85
86            println!("{short} [{line_count}L]");
87            if !dep_info.imports.is_empty() {
88                println!("  deps: {}", dep_info.imports.join(", "));
89            }
90            if !dep_info.exports.is_empty() {
91                println!("  exports: {}", dep_info.exports.join(", "));
92            }
93            let key_sigs: Vec<_> = sigs
94                .iter()
95                .filter(|s| s.is_exported || s.indent == 0)
96                .collect();
97            if !key_sigs.is_empty() {
98                println!("  API:");
99                for sig in &key_sigs {
100                    println!("    {}", sig.to_compact());
101                }
102            }
103            let sent = count_tokens(&short.clone());
104            print_savings(original_tokens, sent);
105        }
106        "signatures" => {
107            let sigs = signatures::extract_signatures(&content, ext);
108            println!("{short} [{line_count}L]");
109            for sig in &sigs {
110                println!("{}", sig.to_compact());
111            }
112            let sent = count_tokens(&short.clone());
113            print_savings(original_tokens, sent);
114        }
115        "aggressive" => {
116            let compressed = compressor::aggressive_compress(&content, Some(ext));
117            println!("{short} [{line_count}L]");
118            println!("{compressed}");
119            let sent = count_tokens(&compressed);
120            print_savings(original_tokens, sent);
121        }
122        "entropy" => {
123            let result = entropy::entropy_compress(&content);
124            let avg_h = entropy::analyze_entropy(&content).avg_entropy;
125            println!("{short} [{line_count}L] (H̄={avg_h:.1})");
126            for tech in &result.techniques {
127                println!("{tech}");
128            }
129            println!("{}", result.output);
130            let sent = count_tokens(&result.output);
131            print_savings(original_tokens, sent);
132        }
133        _ => {
134            println!("{short} [{line_count}L]");
135            println!("{content}");
136        }
137    }
138}
139
140pub fn cmd_diff(args: &[String]) {
141    if args.len() < 2 {
142        eprintln!("Usage: lean-ctx diff <file1> <file2>");
143        std::process::exit(1);
144    }
145
146    let content1 = match crate::tools::ctx_read::read_file_lossy(&args[0]) {
147        Ok(c) => c,
148        Err(e) => {
149            eprintln!("Error reading {}: {e}", args[0]);
150            std::process::exit(1);
151        }
152    };
153
154    let content2 = match crate::tools::ctx_read::read_file_lossy(&args[1]) {
155        Ok(c) => c,
156        Err(e) => {
157            eprintln!("Error reading {}: {e}", args[1]);
158            std::process::exit(1);
159        }
160    };
161
162    let diff = compressor::diff_content(&content1, &content2);
163    let original = count_tokens(&content1) + count_tokens(&content2);
164    let sent = count_tokens(&diff);
165
166    println!(
167        "diff {} {}",
168        protocol::shorten_path(&args[0]),
169        protocol::shorten_path(&args[1])
170    );
171    println!("{diff}");
172    print_savings(original, sent);
173}
174
175pub fn cmd_grep(args: &[String]) {
176    if args.is_empty() {
177        eprintln!("Usage: lean-ctx grep <pattern> [path]");
178        std::process::exit(1);
179    }
180
181    let pattern = &args[0];
182    let path = args.get(1).map_or(".", std::string::String::as_str);
183
184    let re = match regex::Regex::new(pattern) {
185        Ok(r) => r,
186        Err(e) => {
187            eprintln!("Invalid regex pattern: {e}");
188            std::process::exit(1);
189        }
190    };
191
192    let mut found = false;
193    for entry in ignore::WalkBuilder::new(path)
194        .hidden(true)
195        .git_ignore(true)
196        .git_global(true)
197        .git_exclude(true)
198        .max_depth(Some(10))
199        .build()
200        .flatten()
201    {
202        if !entry.file_type().is_some_and(|ft| ft.is_file()) {
203            continue;
204        }
205        let file_path = entry.path();
206        if let Ok(content) = std::fs::read_to_string(file_path) {
207            for (i, line) in content.lines().enumerate() {
208                if re.is_match(line) {
209                    println!("{}:{}:{}", file_path.display(), i + 1, line);
210                    found = true;
211                }
212            }
213        }
214    }
215
216    if !found {
217        std::process::exit(1);
218    }
219}
220
221pub fn cmd_find(args: &[String]) {
222    if args.is_empty() {
223        eprintln!("Usage: lean-ctx find <pattern> [path]");
224        std::process::exit(1);
225    }
226
227    let raw_pattern = &args[0];
228    let path = args.get(1).map_or(".", std::string::String::as_str);
229
230    let is_glob = raw_pattern.contains('*') || raw_pattern.contains('?');
231    let glob_matcher = if is_glob {
232        glob::Pattern::new(&raw_pattern.to_lowercase()).ok()
233    } else {
234        None
235    };
236    let substring = raw_pattern.to_lowercase();
237
238    let mut found = false;
239    for entry in ignore::WalkBuilder::new(path)
240        .hidden(true)
241        .git_ignore(true)
242        .git_global(true)
243        .git_exclude(true)
244        .max_depth(Some(10))
245        .build()
246        .flatten()
247    {
248        let name = entry.file_name().to_string_lossy().to_lowercase();
249        let matches = if let Some(ref g) = glob_matcher {
250            g.matches(&name)
251        } else {
252            name.contains(&substring)
253        };
254        if matches {
255            println!("{}", entry.path().display());
256            found = true;
257        }
258    }
259
260    if !found {
261        std::process::exit(1);
262    }
263}
264
265pub fn cmd_ls(args: &[String]) {
266    let path = args.first().map_or(".", std::string::String::as_str);
267    let command = if cfg!(windows) {
268        format!("dir {}", path.replace('/', "\\"))
269    } else {
270        format!("ls {path}")
271    };
272    let code = crate::shell::exec(&command);
273    std::process::exit(code);
274}
275
276pub fn cmd_deps(args: &[String]) {
277    let path = args.first().map_or(".", std::string::String::as_str);
278
279    if let Some(result) = deps_cmd::detect_and_compress(path) {
280        println!("{result}");
281    } else {
282        eprintln!("No dependency file found in {path}");
283        std::process::exit(1);
284    }
285}