context_creator/
lib.rs

1//! Context Creator - 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;
15use std::sync::Arc;
16
17pub use cli::Config;
18pub use core::{cache::FileCache, context_builder::ContextOptions, walker::WalkOptions};
19pub use utils::error::ContextCreatorError;
20
21/// Main entry point for the context creator library
22pub fn run(mut config: Config) -> Result<()> {
23    // Load configuration from file first
24    config.load_from_file()?;
25
26    // Validate configuration BEFORE processing
27    // This ensures mutual exclusivity checks work correctly
28    config.validate()?;
29
30    // Handle remote repository if specified
31    let _temp_dir = if let Some(repo_url) = &config.repo {
32        if config.verbose {
33            eprintln!("šŸ”§ Starting context-creator with remote repository: {repo_url}");
34        }
35
36        // Fetch the repository
37        let temp_dir = crate::remote::fetch_repository(repo_url, config.verbose)?;
38        let repo_path = crate::remote::get_repo_path(&temp_dir, repo_url)?;
39
40        // Update config to use the cloned repository
41        config.paths = Some(vec![repo_path]);
42
43        Some(temp_dir) // Keep temp_dir alive until end of function
44    } else {
45        None
46    };
47
48    // No need to update config since get_directories() handles resolution
49
50    // Setup logging based on verbosity
51    if config.verbose {
52        eprintln!("šŸ”§ Starting context-creator with configuration:");
53        eprintln!("  Directories: {:?}", config.get_directories());
54        eprintln!("  Max tokens: {:?}", config.max_tokens);
55        eprintln!("  LLM tool: {}", config.llm_tool.command());
56        eprintln!("  Progress: {}", config.progress);
57        eprintln!("  Quiet: {}", config.quiet);
58        if let Some(output) = &config.output_file {
59            eprintln!("  Output file: {}", output.display());
60        }
61        if let Some(prompt) = config.get_prompt() {
62            eprintln!("  Prompt: {prompt}");
63        }
64    }
65
66    // Configuration was already validated above
67
68    // Create walker with options
69    if config.verbose {
70        eprintln!("🚶 Creating directory walker with options...");
71    }
72    let walk_options = WalkOptions::from_config(&config)?;
73
74    // Create context options
75    if config.verbose {
76        eprintln!("šŸ“„ Creating context generation options...");
77    }
78    let context_options = ContextOptions::from_config(&config)?;
79
80    // Create shared file cache
81    if config.verbose {
82        eprintln!("šŸ’¾ Creating file cache for I/O optimization...");
83    }
84    let cache = Arc::new(FileCache::new());
85
86    // Process all directories
87    let mut all_outputs = Vec::new();
88
89    let directories = config.get_directories();
90    for (index, directory) in directories.iter().enumerate() {
91        if config.progress && !config.quiet && directories.len() > 1 {
92            eprintln!(
93                "šŸ“‚ Processing directory {} of {}: {}",
94                index + 1,
95                directories.len(),
96                directory.display()
97            );
98        }
99
100        let output = process_directory(
101            directory,
102            walk_options.clone(),
103            context_options.clone(),
104            cache.clone(),
105            &config,
106        )?;
107        all_outputs.push((directory.clone(), output));
108    }
109
110    // Combine outputs from all directories
111    let output = if all_outputs.len() == 1 {
112        // Single directory - return output as-is
113        all_outputs.into_iter().next().unwrap().1
114    } else {
115        // Multiple directories - combine with headers
116        let mut combined = String::new();
117        combined.push_str("# Code Context - Multiple Directories\n\n");
118
119        for (path, content) in all_outputs {
120            combined.push_str(&format!("## Directory: {}\n\n", path.display()));
121            combined.push_str(&content);
122            combined.push_str("\n\n");
123        }
124
125        combined
126    };
127
128    // Handle output based on configuration
129    let resolved_prompt = config.get_prompt();
130    match (
131        config.output_file.as_ref(),
132        resolved_prompt.as_ref(),
133        config.copy,
134    ) {
135        (Some(file), None, false) => {
136            // Write to file
137            std::fs::write(file, output)?;
138            if !config.quiet {
139                println!(" Written to {}", file.display());
140            }
141        }
142        (None, Some(prompt), false) => {
143            // Send to LLM CLI with prompt
144            if config.progress && !config.quiet {
145                eprintln!("šŸ¤– Sending context to {}...", config.llm_tool.command());
146            }
147            execute_with_llm(prompt, &output, &config)?;
148        }
149        (None, Some(prompt), true) => {
150            // Copy to clipboard then send to LLM
151            copy_to_clipboard(&output)?;
152            if !config.quiet {
153                println!("āœ“ Copied to clipboard");
154            }
155            if config.progress && !config.quiet {
156                eprintln!("šŸ¤– Sending context to {}...", config.llm_tool.command());
157            }
158            execute_with_llm(prompt, &output, &config)?;
159        }
160        (None, None, true) => {
161            // Copy to clipboard
162            copy_to_clipboard(&output)?;
163            if !config.quiet {
164                println!("āœ“ Copied to clipboard");
165            }
166        }
167        (None, None, false) => {
168            // Print to stdout
169            print!("{output}");
170        }
171        (Some(_), _, true) => {
172            // This should have been caught by validation
173            return Err(ContextCreatorError::InvalidConfiguration(
174                "Cannot specify both --copy and --output".to_string(),
175            )
176            .into());
177        }
178        (Some(_), Some(_), _) => {
179            return Err(ContextCreatorError::InvalidConfiguration(
180                "Cannot specify both output file and prompt".to_string(),
181            )
182            .into());
183        }
184    }
185
186    Ok(())
187}
188
189/// Process a directory and generate markdown output
190fn process_directory(
191    path: &Path,
192    walk_options: WalkOptions,
193    context_options: ContextOptions,
194    cache: Arc<FileCache>,
195    config: &Config,
196) -> Result<String> {
197    // Walk the directory
198    if config.progress && !config.quiet {
199        eprintln!("šŸ” Scanning directory: {}", path.display());
200    }
201    let mut files = core::walker::walk_directory(path, walk_options)?;
202
203    if config.progress && !config.quiet {
204        eprintln!("šŸ“ Found {} files", files.len());
205    }
206
207    // Perform semantic analysis if requested
208    if config.trace_imports || config.include_callers || config.include_types {
209        if config.progress && !config.quiet {
210            eprintln!("šŸ”— Analyzing semantic dependencies...");
211        }
212        core::walker::perform_semantic_analysis(&mut files, config, &cache)?;
213
214        if config.progress && !config.quiet {
215            let import_count: usize = files.iter().map(|f| f.imports.len()).sum();
216            eprintln!("āœ… Found {import_count} import relationships");
217        }
218
219        // Expand file list based on semantic relationships
220        if config.progress && !config.quiet {
221            eprintln!("šŸ“‚ Expanding file list based on semantic relationships...");
222        }
223
224        // Convert Vec<FileInfo> to HashMap for expansion
225        let mut files_map = std::collections::HashMap::new();
226        for file in files {
227            files_map.insert(file.path.clone(), file);
228        }
229
230        // Expand the file list
231        files_map = core::file_expander::expand_file_list(files_map, config, &cache)?;
232
233        // Convert back to Vec<FileInfo>
234        files = files_map.into_values().collect();
235
236        if config.progress && !config.quiet {
237            eprintln!("šŸ“Š Expanded to {} files", files.len());
238        }
239    }
240
241    if config.verbose {
242        eprintln!("šŸ“‹ File list:");
243        for file in &files {
244            eprintln!(
245                "  {} ({})",
246                file.relative_path.display(),
247                file.file_type_display()
248            );
249        }
250    }
251
252    // Prioritize files if needed
253    let prioritized_files = if context_options.max_tokens.is_some() {
254        if config.progress && !config.quiet {
255            eprintln!("šŸŽÆ Prioritizing files for token limit...");
256        }
257        core::prioritizer::prioritize_files(files, &context_options, cache.clone())?
258    } else {
259        files
260    };
261
262    if config.progress && !config.quiet {
263        eprintln!(
264            "šŸ“ Generating markdown from {} files...",
265            prioritized_files.len()
266        );
267    }
268
269    // Generate markdown
270    let markdown =
271        core::context_builder::generate_markdown(prioritized_files, context_options, cache)?;
272
273    if config.progress && !config.quiet {
274        eprintln!("āœ… Markdown generation complete");
275    }
276
277    Ok(markdown)
278}
279
280/// Execute LLM CLI with the generated context
281fn execute_with_llm(prompt: &str, context: &str, config: &Config) -> Result<()> {
282    use std::io::Write;
283    use std::process::{Command, Stdio};
284
285    let full_input = format!("{prompt}\n\n{context}");
286    let tool_command = config.llm_tool.command();
287
288    let mut child = Command::new(tool_command)
289        .stdin(Stdio::piped())
290        .stdout(Stdio::inherit())
291        .stderr(Stdio::inherit())
292        .spawn()
293        .map_err(|e| {
294            if e.kind() == std::io::ErrorKind::NotFound {
295                ContextCreatorError::LlmToolNotFound {
296                    tool: tool_command.to_string(),
297                    install_instructions: config.llm_tool.install_instructions().to_string(),
298                }
299            } else {
300                ContextCreatorError::SubprocessError(e.to_string())
301            }
302        })?;
303
304    if let Some(mut stdin) = child.stdin.take() {
305        stdin.write_all(full_input.as_bytes())?;
306        stdin.flush()?;
307    }
308
309    let status = child.wait()?;
310    if !status.success() {
311        return Err(ContextCreatorError::SubprocessError(format!(
312            "{tool_command} exited with status: {status}"
313        ))
314        .into());
315    }
316
317    if !config.quiet {
318        eprintln!("\nāœ“ {tool_command} completed successfully");
319    }
320
321    Ok(())
322}
323
324/// Copy content to system clipboard
325fn copy_to_clipboard(content: &str) -> Result<()> {
326    use arboard::Clipboard;
327
328    let mut clipboard = Clipboard::new().map_err(|e| {
329        ContextCreatorError::ClipboardError(format!("Failed to access clipboard: {e}"))
330    })?;
331
332    clipboard.set_text(content).map_err(|e| {
333        ContextCreatorError::ClipboardError(format!("Failed to copy to clipboard: {e}"))
334    })?;
335
336    Ok(())
337}