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    // Handle remote repository if specified
24    let _temp_dir = if let Some(repo_url) = &config.repo {
25        if config.verbose {
26            eprintln!("šŸ”§ Starting context-creator with remote repository: {repo_url}");
27        }
28
29        // Fetch the repository
30        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        // Update config to use the cloned repository
34        config.paths = Some(vec![repo_path]);
35
36        Some(temp_dir) // Keep temp_dir alive until end of function
37    } else {
38        None
39    };
40
41    // No need to update config since get_directories() handles resolution
42
43    // Setup logging based on verbosity
44    if config.verbose {
45        eprintln!("šŸ”§ Starting context-creator 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    // Validate configuration
60    config.validate()?;
61
62    // Create walker with options
63    if config.verbose {
64        eprintln!("🚶 Creating directory walker with options...");
65    }
66    let walk_options = WalkOptions::from_config(&config)?;
67
68    // Create context options
69    if config.verbose {
70        eprintln!("šŸ“„ Creating context generation options...");
71    }
72    let context_options = ContextOptions::from_config(&config)?;
73
74    // Create shared file cache
75    if config.verbose {
76        eprintln!("šŸ’¾ Creating file cache for I/O optimization...");
77    }
78    let cache = Arc::new(FileCache::new());
79
80    // Process all directories
81    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            context_options.clone(),
98            cache.clone(),
99            &config,
100        )?;
101        all_outputs.push((directory.clone(), output));
102    }
103
104    // Combine outputs from all directories
105    let output = if all_outputs.len() == 1 {
106        // Single directory - return output as-is
107        all_outputs.into_iter().next().unwrap().1
108    } else {
109        // Multiple directories - combine with headers
110        let mut combined = String::new();
111        combined.push_str("# Code Context - 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    // Handle output based on configuration
123    let resolved_prompt = config.get_prompt();
124    match (
125        config.output_file.as_ref(),
126        resolved_prompt.as_ref(),
127        config.copy,
128    ) {
129        (Some(file), None, false) => {
130            // Write to file
131            std::fs::write(file, output)?;
132            if !config.quiet {
133                println!(" Written to {}", file.display());
134            }
135        }
136        (None, Some(prompt), false) => {
137            // Send to LLM CLI with prompt
138            if config.progress && !config.quiet {
139                eprintln!("šŸ¤– Sending context to {}...", config.llm_tool.command());
140            }
141            execute_with_llm(prompt, &output, &config)?;
142        }
143        (None, Some(prompt), true) => {
144            // Copy to clipboard then send to LLM
145            copy_to_clipboard(&output)?;
146            if !config.quiet {
147                println!("āœ“ Copied to clipboard");
148            }
149            if config.progress && !config.quiet {
150                eprintln!("šŸ¤– Sending context to {}...", config.llm_tool.command());
151            }
152            execute_with_llm(prompt, &output, &config)?;
153        }
154        (None, None, true) => {
155            // Copy to clipboard
156            copy_to_clipboard(&output)?;
157            if !config.quiet {
158                println!("āœ“ Copied to clipboard");
159            }
160        }
161        (None, None, false) => {
162            // Print to stdout
163            print!("{output}");
164        }
165        (Some(_), _, true) => {
166            // This should have been caught by validation
167            return Err(ContextCreatorError::InvalidConfiguration(
168                "Cannot specify both --copy and --output".to_string(),
169            )
170            .into());
171        }
172        (Some(_), Some(_), _) => {
173            return Err(ContextCreatorError::InvalidConfiguration(
174                "Cannot specify both output file and prompt".to_string(),
175            )
176            .into());
177        }
178    }
179
180    Ok(())
181}
182
183/// Process a directory and generate markdown output
184fn process_directory(
185    path: &Path,
186    walk_options: WalkOptions,
187    context_options: ContextOptions,
188    cache: Arc<FileCache>,
189    config: &Config,
190) -> Result<String> {
191    // Walk the directory
192    if config.progress && !config.quiet {
193        eprintln!("šŸ” Scanning directory: {}", path.display());
194    }
195    let mut files = core::walker::walk_directory(path, walk_options)?;
196
197    if config.progress && !config.quiet {
198        eprintln!("šŸ“ Found {} files", files.len());
199    }
200
201    // Perform semantic analysis if requested
202    if config.trace_imports || config.include_callers || config.include_types {
203        if config.progress && !config.quiet {
204            eprintln!("šŸ”— Analyzing semantic dependencies...");
205        }
206        core::walker::perform_semantic_analysis(&mut files, config, &cache)?;
207
208        if config.progress && !config.quiet {
209            let import_count: usize = files.iter().map(|f| f.imports.len()).sum();
210            eprintln!("āœ… Found {import_count} import relationships");
211        }
212    }
213
214    if config.verbose {
215        eprintln!("šŸ“‹ File list:");
216        for file in &files {
217            eprintln!(
218                "  {} ({})",
219                file.relative_path.display(),
220                file.file_type_display()
221            );
222        }
223    }
224
225    // Prioritize files if needed
226    let prioritized_files = if context_options.max_tokens.is_some() {
227        if config.progress && !config.quiet {
228            eprintln!("šŸŽÆ Prioritizing files for token limit...");
229        }
230        core::prioritizer::prioritize_files(files, &context_options, cache.clone())?
231    } else {
232        files
233    };
234
235    if config.progress && !config.quiet {
236        eprintln!(
237            "šŸ“ Generating markdown from {} files...",
238            prioritized_files.len()
239        );
240    }
241
242    // Generate markdown
243    let markdown =
244        core::context_builder::generate_markdown(prioritized_files, context_options, cache)?;
245
246    if config.progress && !config.quiet {
247        eprintln!("āœ… Markdown generation complete");
248    }
249
250    Ok(markdown)
251}
252
253/// Execute LLM CLI with the generated context
254fn execute_with_llm(prompt: &str, context: &str, config: &Config) -> Result<()> {
255    use std::io::Write;
256    use std::process::{Command, Stdio};
257
258    let full_input = format!("{prompt}\n\n{context}");
259    let tool_command = config.llm_tool.command();
260
261    let mut child = Command::new(tool_command)
262        .stdin(Stdio::piped())
263        .stdout(Stdio::inherit())
264        .stderr(Stdio::inherit())
265        .spawn()
266        .map_err(|e| {
267            if e.kind() == std::io::ErrorKind::NotFound {
268                ContextCreatorError::LlmToolNotFound {
269                    tool: tool_command.to_string(),
270                    install_instructions: config.llm_tool.install_instructions().to_string(),
271                }
272            } else {
273                ContextCreatorError::SubprocessError(e.to_string())
274            }
275        })?;
276
277    if let Some(mut stdin) = child.stdin.take() {
278        stdin.write_all(full_input.as_bytes())?;
279        stdin.flush()?;
280    }
281
282    let status = child.wait()?;
283    if !status.success() {
284        return Err(ContextCreatorError::SubprocessError(format!(
285            "{tool_command} exited with status: {status}"
286        ))
287        .into());
288    }
289
290    if !config.quiet {
291        eprintln!("\nāœ“ {tool_command} completed successfully");
292    }
293
294    Ok(())
295}
296
297/// Copy content to system clipboard
298fn copy_to_clipboard(content: &str) -> Result<()> {
299    use arboard::Clipboard;
300
301    let mut clipboard = Clipboard::new().map_err(|e| {
302        ContextCreatorError::ClipboardError(format!("Failed to access clipboard: {e}"))
303    })?;
304
305    clipboard.set_text(content).map_err(|e| {
306        ContextCreatorError::ClipboardError(format!("Failed to copy to clipboard: {e}"))
307    })?;
308
309    Ok(())
310}