1pub mod cli;
8pub mod config;
9pub mod core;
10pub mod logging;
11pub mod remote;
12pub mod utils;
13
14use anyhow::Result;
15use std::path::Path;
16use std::sync::Arc;
17use tracing::{debug, info};
18
19pub use cli::Config;
20pub use core::{cache::FileCache, context_builder::ContextOptions, walker::WalkOptions};
21pub use utils::error::ContextCreatorError;
22
23pub fn run(mut config: Config) -> Result<()> {
25 config.load_from_file()?;
27
28 config.validate()?;
31
32 let _temp_dir = if let Some(repo_url) = &config.remote {
34 if config.verbose > 0 {
35 debug!(
36 "Starting context-creator with remote repository: {}",
37 repo_url
38 );
39 }
40
41 let temp_dir = crate::remote::fetch_repository(repo_url, config.verbose > 0)?;
43 let repo_path = crate::remote::get_repo_path(&temp_dir, repo_url)?;
44
45 config.paths = Some(vec![repo_path]);
47
48 Some(temp_dir) } else {
50 None
51 };
52
53 if config.verbose > 0 {
57 debug!("Starting context-creator with configuration:");
58 debug!(" Directories: {:?}", config.get_directories());
59 debug!(" Max tokens: {:?}", config.max_tokens);
60 debug!(" LLM tool: {}", config.llm_tool.command());
61 debug!(" Progress: {}", config.progress);
62 debug!(" Quiet: {}", config.quiet);
63 if let Some(output) = &config.output_file {
64 debug!(" Output file: {}", output.display());
65 }
66 if let Some(prompt) = config.get_prompt() {
67 debug!(" Prompt: {}", prompt);
68 }
69 }
70
71 if config.verbose > 0 {
75 debug!("Creating directory walker with options...");
76 }
77 let walk_options = WalkOptions::from_config(&config)?;
78
79 if config.verbose > 0 {
81 debug!("Creating context generation options...");
82 }
83 let context_options = ContextOptions::from_config(&config)?;
84
85 if config.verbose > 0 {
87 debug!("Creating file cache for I/O optimization...");
88 }
89 let cache = Arc::new(FileCache::new());
90
91 let mut all_outputs = Vec::new();
93
94 let directories = config.get_directories();
95 for (index, directory) in directories.iter().enumerate() {
96 if config.progress && !config.quiet && directories.len() > 1 {
97 info!(
98 "Processing directory {} of {}: {}",
99 index + 1,
100 directories.len(),
101 directory.display()
102 );
103 }
104
105 let output = process_directory(
106 directory,
107 walk_options.clone(),
108 context_options.clone(),
109 cache.clone(),
110 &config,
111 )?;
112 all_outputs.push((directory.clone(), output));
113 }
114
115 let output = if all_outputs.len() == 1 {
117 all_outputs.into_iter().next().unwrap().1
119 } else {
120 let mut combined = String::new();
122 combined.push_str("# Code Context - Multiple Directories\n\n");
123
124 for (path, content) in all_outputs {
125 combined.push_str(&format!("## Directory: {}\n\n", path.display()));
126 combined.push_str(&content);
127 combined.push_str("\n\n");
128 }
129
130 combined
131 };
132
133 let resolved_prompt = config.get_prompt();
135 match (
136 config.output_file.as_ref(),
137 resolved_prompt.as_ref(),
138 config.copy,
139 ) {
140 (Some(file), None, false) => {
141 std::fs::write(file, output)?;
143 if !config.quiet {
144 println!(" Written to {}", file.display());
145 }
146 }
147 (None, Some(prompt), false) => {
148 if config.progress && !config.quiet {
150 info!("Sending context to {}...", config.llm_tool.command());
151 }
152 execute_with_llm(prompt, &output, &config)?;
153 }
154 (None, Some(prompt), true) => {
155 copy_to_clipboard(&output)?;
157 if !config.quiet {
158 println!("✓ Copied to clipboard");
159 }
160 if config.progress && !config.quiet {
161 info!("Sending context to {}...", config.llm_tool.command());
162 }
163 execute_with_llm(prompt, &output, &config)?;
164 }
165 (None, None, true) => {
166 copy_to_clipboard(&output)?;
168 if !config.quiet {
169 println!("✓ Copied to clipboard");
170 }
171 }
172 (None, None, false) => {
173 print!("{output}");
175 }
176 (Some(_), _, true) => {
177 return Err(ContextCreatorError::InvalidConfiguration(
179 "Cannot specify both --copy and --output".to_string(),
180 )
181 .into());
182 }
183 (Some(_), Some(_), _) => {
184 return Err(ContextCreatorError::InvalidConfiguration(
185 "Cannot specify both output file and prompt".to_string(),
186 )
187 .into());
188 }
189 }
190
191 Ok(())
192}
193
194fn process_directory(
196 path: &Path,
197 walk_options: WalkOptions,
198 context_options: ContextOptions,
199 cache: Arc<FileCache>,
200 config: &Config,
201) -> Result<String> {
202 if config.progress && !config.quiet {
204 info!("Scanning directory: {}", path.display());
205 }
206 let mut files = core::walker::walk_directory(path, walk_options.clone())?;
207
208 if config.progress && !config.quiet {
209 info!("Found {} files", files.len());
210 }
211
212 if config.trace_imports || config.include_callers || config.include_types {
214 if config.progress && !config.quiet {
215 info!("Analyzing semantic dependencies...");
216 }
217
218 let project_analysis = core::project_analyzer::ProjectAnalysis::analyze_project(
220 path,
221 &walk_options,
222 config,
223 &cache,
224 )?;
225
226 let mut initial_files_map = std::collections::HashMap::new();
228 for file in files {
229 if let Some(analyzed_file) = project_analysis.get_file(&file.path) {
230 initial_files_map.insert(file.path.clone(), analyzed_file.clone());
231 } else {
232 initial_files_map.insert(file.path.clone(), file);
233 }
234 }
235
236 if config.progress && !config.quiet {
238 info!("Expanding file list based on semantic relationships...");
239 }
240
241 let files_map = core::file_expander::expand_file_list_with_context(
243 initial_files_map,
244 config,
245 &cache,
246 &walk_options,
247 &project_analysis.file_map,
248 )?;
249
250 files = files_map.into_values().collect();
252
253 let final_paths: std::collections::HashSet<_> =
255 files.iter().map(|f| f.path.clone()).collect();
256 for file in &mut files {
257 file.imported_by.retain(|path| final_paths.contains(path));
258 }
259
260 if config.progress && !config.quiet {
261 info!("Expanded to {} files", files.len());
262 }
263 }
264
265 if config.verbose > 0 {
266 debug!("File list:");
267 for file in &files {
268 debug!(
269 " {} ({})",
270 file.relative_path.display(),
271 file.file_type_display()
272 );
273 }
274 }
275
276 let prioritized_files = if context_options.max_tokens.is_some() {
278 if config.progress && !config.quiet {
279 info!("Prioritizing files for token limit...");
280 }
281 core::prioritizer::prioritize_files(files, &context_options, cache.clone())?
282 } else {
283 files
284 };
285
286 if config.progress && !config.quiet {
287 info!(
288 "Generating markdown from {} files...",
289 prioritized_files.len()
290 );
291 }
292
293 let markdown =
295 core::context_builder::generate_markdown(prioritized_files, context_options, cache)?;
296
297 if config.progress && !config.quiet {
298 info!("Markdown generation complete");
299 }
300
301 Ok(markdown)
302}
303
304fn execute_with_llm(prompt: &str, context: &str, config: &Config) -> Result<()> {
306 use std::io::Write;
307 use std::process::{Command, Stdio};
308
309 let full_input = format!("{prompt}\n\n{context}");
310 let tool_command = config.llm_tool.command();
311
312 let mut child = Command::new(tool_command)
313 .stdin(Stdio::piped())
314 .stdout(Stdio::inherit())
315 .stderr(Stdio::inherit())
316 .spawn()
317 .map_err(|e| {
318 if e.kind() == std::io::ErrorKind::NotFound {
319 ContextCreatorError::LlmToolNotFound {
320 tool: tool_command.to_string(),
321 install_instructions: config.llm_tool.install_instructions().to_string(),
322 }
323 } else {
324 ContextCreatorError::SubprocessError(e.to_string())
325 }
326 })?;
327
328 if let Some(mut stdin) = child.stdin.take() {
329 stdin.write_all(full_input.as_bytes())?;
330 stdin.flush()?;
331 }
332
333 let status = child.wait()?;
334 if !status.success() {
335 return Err(ContextCreatorError::SubprocessError(format!(
336 "{tool_command} exited with status: {status}"
337 ))
338 .into());
339 }
340
341 if !config.quiet {
342 info!("{} completed successfully", tool_command);
343 }
344
345 Ok(())
346}
347
348fn copy_to_clipboard(content: &str) -> Result<()> {
350 use arboard::Clipboard;
351
352 let mut clipboard = Clipboard::new().map_err(|e| {
353 ContextCreatorError::ClipboardError(format!("Failed to access clipboard: {e}"))
354 })?;
355
356 clipboard.set_text(content).map_err(|e| {
357 ContextCreatorError::ClipboardError(format!("Failed to copy to clipboard: {e}"))
358 })?;
359
360 Ok(())
361}