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}