code_digest/
lib.rs

1//! Code Digest - High-performance CLI tool to convert codebases to Markdown for LLM context
2//!
3//! This library provides the core functionality for traversing directories,
4//! processing files, and generating formatted Markdown output suitable for
5//! large language model consumption.
6
7pub 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
20/// Main entry point for the code digest library
21pub fn run(mut config: Config) -> Result<()> {
22    // Handle remote repository if specified
23    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        // Fetch the repository
29        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        // Update config to use the cloned repository
33        config.directories = vec![repo_path];
34
35        Some(temp_dir) // Keep temp_dir alive until end of function
36    } else {
37        None
38    };
39
40    // Setup logging based on verbosity
41    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    // Validate configuration
57    config.validate()?;
58
59    // Create walker with options
60    if config.verbose {
61        eprintln!("🚶 Creating directory walker with options...");
62    }
63    let walk_options = WalkOptions::from_config(&config)?;
64
65    // Create digest options
66    if config.verbose {
67        eprintln!("šŸ“„ Creating markdown digest options...");
68    }
69    let digest_options = DigestOptions::from_config(&config)?;
70
71    // Process all directories
72    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    // Combine outputs from all directories
90    let output = if all_outputs.len() == 1 {
91        // Single directory - return output as-is
92        all_outputs.into_iter().next().unwrap().1
93    } else {
94        // Multiple directories - combine with headers
95        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    // Handle output based on configuration
108    match (config.output_file.as_ref(), config.prompt.as_ref()) {
109        (Some(file), None) => {
110            // Write to file
111            std::fs::write(file, output)?;
112            if !config.quiet {
113                println!(" Written to {}", file.display());
114            }
115        }
116        (None, Some(prompt)) => {
117            // Send to LLM CLI with prompt
118            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 to stdout
125            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
138/// Process a directory and generate markdown output
139fn process_directory(
140    path: &Path,
141    walk_options: WalkOptions,
142    digest_options: DigestOptions,
143    config: &Config,
144) -> Result<String> {
145    // Walk the directory
146    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    // Prioritize files if needed
163    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    // Generate markdown
177    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
186/// Execute LLM CLI with the generated context
187fn 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}