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