1pub mod cli;
8pub mod config;
9pub mod core;
10pub mod remote;
11pub mod utils;
12
13use anyhow::Result;
14use std::path::Path;
15use std::sync::Arc;
16
17pub use cli::Config;
18pub use core::{cache::FileCache, digest::DigestOptions, walker::WalkOptions};
19pub use utils::error::CodeDigestError;
20
21pub fn run(mut config: Config) -> Result<()> {
23 let _temp_dir = if let Some(repo_url) = &config.repo {
25 if config.verbose {
26 eprintln!("š§ Starting code-digest with remote repository: {repo_url}");
27 }
28
29 let temp_dir = crate::remote::fetch_repository(repo_url, config.verbose)?;
31 let repo_path = crate::remote::get_repo_path(&temp_dir, repo_url)?;
32
33 config.paths = Some(vec![repo_path]);
35
36 Some(temp_dir) } else {
38 None
39 };
40
41 if config.verbose {
45 eprintln!("š§ Starting code-digest with configuration:");
46 eprintln!(" Directories: {:?}", config.get_directories());
47 eprintln!(" Max tokens: {:?}", config.max_tokens);
48 eprintln!(" LLM tool: {}", config.llm_tool.command());
49 eprintln!(" Progress: {}", config.progress);
50 eprintln!(" Quiet: {}", config.quiet);
51 if let Some(output) = &config.output_file {
52 eprintln!(" Output file: {}", output.display());
53 }
54 if let Some(prompt) = config.get_prompt() {
55 eprintln!(" Prompt: {prompt}");
56 }
57 }
58
59 config.validate()?;
61
62 if config.verbose {
64 eprintln!("š¶ Creating directory walker with options...");
65 }
66 let walk_options = WalkOptions::from_config(&config)?;
67
68 if config.verbose {
70 eprintln!("š Creating markdown digest options...");
71 }
72 let digest_options = DigestOptions::from_config(&config)?;
73
74 if config.verbose {
76 eprintln!("š¾ Creating file cache for I/O optimization...");
77 }
78 let cache = Arc::new(FileCache::new());
79
80 let mut all_outputs = Vec::new();
82
83 let directories = config.get_directories();
84 for (index, directory) in directories.iter().enumerate() {
85 if config.progress && !config.quiet && directories.len() > 1 {
86 eprintln!(
87 "š Processing directory {} of {}: {}",
88 index + 1,
89 directories.len(),
90 directory.display()
91 );
92 }
93
94 let output = process_directory(
95 directory,
96 walk_options.clone(),
97 digest_options.clone(),
98 cache.clone(),
99 &config,
100 )?;
101 all_outputs.push((directory.clone(), output));
102 }
103
104 let output = if all_outputs.len() == 1 {
106 all_outputs.into_iter().next().unwrap().1
108 } else {
109 let mut combined = String::new();
111 combined.push_str("# Code Digest - Multiple Directories\n\n");
112
113 for (path, content) in all_outputs {
114 combined.push_str(&format!("## Directory: {}\n\n", path.display()));
115 combined.push_str(&content);
116 combined.push_str("\n\n");
117 }
118
119 combined
120 };
121
122 let resolved_prompt = config.get_prompt();
124 match (config.output_file.as_ref(), resolved_prompt.as_ref(), config.copy) {
125 (Some(file), None, false) => {
126 std::fs::write(file, output)?;
128 if !config.quiet {
129 println!(" Written to {}", file.display());
130 }
131 }
132 (None, Some(prompt), false) => {
133 if config.progress && !config.quiet {
135 eprintln!("š¤ Sending context to {}...", config.llm_tool.command());
136 }
137 execute_with_llm(prompt, &output, &config)?;
138 }
139 (None, Some(prompt), true) => {
140 copy_to_clipboard(&output)?;
142 if !config.quiet {
143 println!("ā Copied to clipboard");
144 }
145 if config.progress && !config.quiet {
146 eprintln!("š¤ Sending context to {}...", config.llm_tool.command());
147 }
148 execute_with_llm(prompt, &output, &config)?;
149 }
150 (None, None, true) => {
151 copy_to_clipboard(&output)?;
153 if !config.quiet {
154 println!("ā Copied to clipboard");
155 }
156 }
157 (None, None, false) => {
158 print!("{output}");
160 }
161 (Some(_), _, true) => {
162 return Err(CodeDigestError::InvalidConfiguration(
164 "Cannot specify both --copy and --output".to_string(),
165 )
166 .into());
167 }
168 (Some(_), Some(_), _) => {
169 return Err(CodeDigestError::InvalidConfiguration(
170 "Cannot specify both output file and prompt".to_string(),
171 )
172 .into());
173 }
174 }
175
176 Ok(())
177}
178
179fn process_directory(
181 path: &Path,
182 walk_options: WalkOptions,
183 digest_options: DigestOptions,
184 cache: Arc<FileCache>,
185 config: &Config,
186) -> Result<String> {
187 if config.progress && !config.quiet {
189 eprintln!("š Scanning directory: {}", path.display());
190 }
191 let files = core::walker::walk_directory(path, walk_options)?;
192
193 if config.progress && !config.quiet {
194 eprintln!("š Found {} files", files.len());
195 }
196
197 if config.verbose {
198 eprintln!("š File list:");
199 for file in &files {
200 eprintln!(" {} ({})", file.relative_path.display(), file.file_type_display());
201 }
202 }
203
204 let prioritized_files = if digest_options.max_tokens.is_some() {
206 if config.progress && !config.quiet {
207 eprintln!("šÆ Prioritizing files for token limit...");
208 }
209 core::prioritizer::prioritize_files(files, &digest_options, cache.clone())?
210 } else {
211 files
212 };
213
214 if config.progress && !config.quiet {
215 eprintln!("š Generating markdown from {} files...", prioritized_files.len());
216 }
217
218 let markdown = core::digest::generate_markdown(prioritized_files, digest_options, cache)?;
220
221 if config.progress && !config.quiet {
222 eprintln!("ā
Markdown generation complete");
223 }
224
225 Ok(markdown)
226}
227
228fn execute_with_llm(prompt: &str, context: &str, config: &Config) -> Result<()> {
230 use std::io::Write;
231 use std::process::{Command, Stdio};
232
233 let full_input = format!("{prompt}\n\n{context}");
234 let tool_command = config.llm_tool.command();
235
236 let mut child = Command::new(tool_command)
237 .stdin(Stdio::piped())
238 .stdout(Stdio::inherit())
239 .stderr(Stdio::inherit())
240 .spawn()
241 .map_err(|e| {
242 if e.kind() == std::io::ErrorKind::NotFound {
243 CodeDigestError::LlmToolNotFound {
244 tool: tool_command.to_string(),
245 install_instructions: config.llm_tool.install_instructions().to_string(),
246 }
247 } else {
248 CodeDigestError::SubprocessError(e.to_string())
249 }
250 })?;
251
252 if let Some(mut stdin) = child.stdin.take() {
253 stdin.write_all(full_input.as_bytes())?;
254 stdin.flush()?;
255 }
256
257 let status = child.wait()?;
258 if !status.success() {
259 return Err(CodeDigestError::SubprocessError(format!(
260 "{tool_command} exited with status: {status}"
261 ))
262 .into());
263 }
264
265 if !config.quiet {
266 eprintln!("\nā {tool_command} completed successfully");
267 }
268
269 Ok(())
270}
271
272fn copy_to_clipboard(content: &str) -> Result<()> {
274 use arboard::Clipboard;
275
276 let mut clipboard = Clipboard::new()
277 .map_err(|e| CodeDigestError::ClipboardError(format!("Failed to access clipboard: {e}")))?;
278
279 clipboard.set_text(content).map_err(|e| {
280 CodeDigestError::ClipboardError(format!("Failed to copy to clipboard: {e}"))
281 })?;
282
283 Ok(())
284}