1pub mod cli;
8pub mod config;
9pub mod core;
10pub mod remote;
11pub mod utils;
12
13use anyhow::Result;
14use std::path::Path;
15
16pub use cli::Config;
17pub use core::{digest::DigestOptions, walker::WalkOptions};
18pub use utils::error::CodeDigestError;
19
20pub fn run(mut config: Config) -> Result<()> {
22 let _temp_dir = if let Some(repo_url) = &config.repo {
24 if config.verbose {
25 eprintln!("š§ Starting code-digest with remote repository: {repo_url}");
26 }
27
28 let temp_dir = crate::remote::fetch_repository(repo_url, config.verbose)?;
30 let repo_path = crate::remote::get_repo_path(&temp_dir, repo_url)?;
31
32 config.directories = vec![repo_path];
34
35 Some(temp_dir) } else {
37 None
38 };
39
40 if config.verbose {
42 eprintln!("š§ Starting code-digest with configuration:");
43 eprintln!(" Directories: {:?}", config.directories);
44 eprintln!(" Max tokens: {:?}", config.max_tokens);
45 eprintln!(" LLM tool: {}", config.llm_tool.command());
46 eprintln!(" Progress: {}", config.progress);
47 eprintln!(" Quiet: {}", config.quiet);
48 if let Some(output) = &config.output_file {
49 eprintln!(" Output file: {}", output.display());
50 }
51 if let Some(prompt) = &config.prompt {
52 eprintln!(" Prompt: {prompt}");
53 }
54 }
55
56 config.validate()?;
58
59 if config.verbose {
61 eprintln!("š¶ Creating directory walker with options...");
62 }
63 let walk_options = WalkOptions::from_config(&config)?;
64
65 if config.verbose {
67 eprintln!("š Creating markdown digest options...");
68 }
69 let digest_options = DigestOptions::from_config(&config)?;
70
71 let mut all_outputs = Vec::new();
73
74 for (index, directory) in config.directories.iter().enumerate() {
75 if config.progress && !config.quiet && config.directories.len() > 1 {
76 eprintln!(
77 "š Processing directory {} of {}: {}",
78 index + 1,
79 config.directories.len(),
80 directory.display()
81 );
82 }
83
84 let output =
85 process_directory(directory, walk_options.clone(), digest_options.clone(), &config)?;
86 all_outputs.push((directory.clone(), output));
87 }
88
89 let output = if all_outputs.len() == 1 {
91 all_outputs.into_iter().next().unwrap().1
93 } else {
94 let mut combined = String::new();
96 combined.push_str("# Code Digest - Multiple Directories\n\n");
97
98 for (path, content) in all_outputs {
99 combined.push_str(&format!("## Directory: {}\n\n", path.display()));
100 combined.push_str(&content);
101 combined.push_str("\n\n");
102 }
103
104 combined
105 };
106
107 match (config.output_file.as_ref(), config.prompt.as_ref()) {
109 (Some(file), None) => {
110 std::fs::write(file, output)?;
112 if !config.quiet {
113 println!(" Written to {}", file.display());
114 }
115 }
116 (None, Some(prompt)) => {
117 if config.progress && !config.quiet {
119 eprintln!("š¤ Sending context to {}...", config.llm_tool.command());
120 }
121 execute_with_llm(prompt, &output, &config)?;
122 }
123 (None, None) => {
124 print!("{output}");
126 }
127 (Some(_), Some(_)) => {
128 return Err(CodeDigestError::InvalidConfiguration(
129 "Cannot specify both output file and prompt".to_string(),
130 )
131 .into());
132 }
133 }
134
135 Ok(())
136}
137
138fn process_directory(
140 path: &Path,
141 walk_options: WalkOptions,
142 digest_options: DigestOptions,
143 config: &Config,
144) -> Result<String> {
145 if config.progress && !config.quiet {
147 eprintln!("š Scanning directory: {}", path.display());
148 }
149 let files = core::walker::walk_directory(path, walk_options)?;
150
151 if config.progress && !config.quiet {
152 eprintln!("š Found {} files", files.len());
153 }
154
155 if config.verbose {
156 eprintln!("š File list:");
157 for file in &files {
158 eprintln!(" {} ({})", file.relative_path.display(), file.file_type_display());
159 }
160 }
161
162 let prioritized_files = if digest_options.max_tokens.is_some() {
164 if config.progress && !config.quiet {
165 eprintln!("šÆ Prioritizing files for token limit...");
166 }
167 core::prioritizer::prioritize_files(files, &digest_options)?
168 } else {
169 files
170 };
171
172 if config.progress && !config.quiet {
173 eprintln!("š Generating markdown from {} files...", prioritized_files.len());
174 }
175
176 let markdown = core::digest::generate_markdown(prioritized_files, digest_options)?;
178
179 if config.progress && !config.quiet {
180 eprintln!("ā
Markdown generation complete");
181 }
182
183 Ok(markdown)
184}
185
186fn execute_with_llm(prompt: &str, context: &str, config: &Config) -> Result<()> {
188 use std::io::Write;
189 use std::process::{Command, Stdio};
190
191 let full_input = format!("{prompt}\n\n{context}");
192 let tool_command = config.llm_tool.command();
193
194 let mut child = Command::new(tool_command)
195 .stdin(Stdio::piped())
196 .stdout(Stdio::inherit())
197 .stderr(Stdio::inherit())
198 .spawn()
199 .map_err(|e| {
200 if e.kind() == std::io::ErrorKind::NotFound {
201 CodeDigestError::LlmToolNotFound {
202 tool: tool_command.to_string(),
203 install_instructions: config.llm_tool.install_instructions().to_string(),
204 }
205 } else {
206 CodeDigestError::SubprocessError(e.to_string())
207 }
208 })?;
209
210 if let Some(mut stdin) = child.stdin.take() {
211 stdin.write_all(full_input.as_bytes())?;
212 stdin.flush()?;
213 }
214
215 let status = child.wait()?;
216 if !status.success() {
217 return Err(CodeDigestError::SubprocessError(format!(
218 "{tool_command} exited with status: {status}"
219 ))
220 .into());
221 }
222
223 if !config.quiet {
224 eprintln!("\nā {tool_command} completed successfully");
225 }
226
227 Ok(())
228}