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