1pub 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
21pub fn run(mut config: Config) -> Result<()> {
23 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 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 config.paths = Some(vec![repo_path]);
35
36 Some(temp_dir) } else {
38 None
39 };
40
41 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 config.validate()?;
61
62 if config.verbose {
64 eprintln!("š¶ Creating directory walker with options...");
65 }
66 let walk_options = WalkOptions::from_config(&config)?;
67
68 if config.verbose {
70 eprintln!("š Creating context generation options...");
71 }
72 let context_options = ContextOptions::from_config(&config)?;
73
74 if config.verbose {
76 eprintln!("š¾ Creating file cache for I/O optimization...");
77 }
78 let cache = Arc::new(FileCache::new());
79
80 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 let output = if all_outputs.len() == 1 {
106 all_outputs.into_iter().next().unwrap().1
108 } else {
109 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 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 std::fs::write(file, output)?;
132 if !config.quiet {
133 println!(" Written to {}", file.display());
134 }
135 }
136 (None, Some(prompt), false) => {
137 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(&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(&output)?;
157 if !config.quiet {
158 println!("ā Copied to clipboard");
159 }
160 }
161 (None, None, false) => {
162 print!("{output}");
164 }
165 (Some(_), _, true) => {
166 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
183fn process_directory(
185 path: &Path,
186 walk_options: WalkOptions,
187 context_options: ContextOptions,
188 cache: Arc<FileCache>,
189 config: &Config,
190) -> Result<String> {
191 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 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 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 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
253fn 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
297fn 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}