1use std::fs;
2
3pub mod cli;
5pub mod processor;
6pub mod walker;
7
8use cli::{Commands, JoinArgs};
9
10pub fn run(command: Commands) -> anyhow::Result<()> {
13 match command {
14 Commands::Join(args) => run_join(args),
15 Commands::Update(_args) => {
16 println!("Update functionality is not yet implemented.");
18 println!("Please check for new releases at the GitHub repository:");
19 println!("https://github.com/luizvbo/join-ai/releases");
20 Ok(())
21 }
22 }
23}
24
25fn run_join(args: JoinArgs) -> anyhow::Result<()> {
28 println!(
30 "Processing files in folder: {}",
31 args.input_folder.display()
32 );
33 if let Some(patterns) = &args.patterns {
34 println!("Using patterns: {}", patterns.join(", "));
35 } else {
36 println!("Using patterns: all files");
37 }
38 if let Some(exclude_patterns) = &args.exclude {
39 println!("Excluding patterns: {}", exclude_patterns.join(", "));
40 }
41
42 if args.clear_file && args.output_file.exists() {
44 fs::remove_file(&args.output_file)?;
45 println!(
46 "Output file {} has been cleared.",
47 args.output_file.display()
48 );
49 }
50
51 let receiver = walker::find_files(&args)?;
54
55 processor::process_files(receiver, &args.output_file)?;
58
59 println!(
60 "Files have been processed and written to {}",
61 args.output_file.display()
62 );
63
64 Ok(())
65}
66
67#[cfg(test)]
69mod tests {
70 use super::*;
71 use crate::cli::{Commands, JoinArgs};
72 use assert_fs::TempDir;
73 use assert_fs::prelude::*;
74 use std::fs::{self};
75 use std::path::Path;
76
77 fn get_test_args(input_folder: &Path, output_file: &Path) -> JoinArgs {
79 JoinArgs {
80 input_folder: input_folder.to_path_buf(),
81 output_file: output_file.to_path_buf(),
82 patterns: None,
83 exclude: None,
84 clear_file: false,
85 max_depth: None,
86 hidden: false,
87 no_follow: true,
88 }
89 }
90
91 fn run_join_and_read_output(args: JoinArgs) -> anyhow::Result<String> {
93 let output_path = args.output_file.clone();
94 run(Commands::Join(args))?;
95 Ok(fs::read_to_string(output_path).unwrap_or_default())
96 }
97
98 #[test]
100 fn test_filter_by_multiple_patterns() -> anyhow::Result<()> {
101 let dir = TempDir::new()?;
102 dir.child("Cargo.toml").write_str("[package]")?;
103 dir.child("README.md").write_str("# Project")?;
104 dir.child("src/main.rs").write_str("fn main(){}")?;
105
106 let output_file = dir.path().join("output.txt");
107 let mut args = get_test_args(dir.path(), &output_file);
108 args.patterns = Some(vec!["*.rs".to_string(), "*.toml".to_string()]);
109
110 let result = run_join_and_read_output(args)?;
111
112 assert!(result.contains("// FILE:"));
113 assert!(result.contains("main.rs"));
114 assert!(result.contains("Cargo.toml"));
115 assert!(!result.contains("README.md"));
116
117 Ok(())
118 }
119
120 #[test]
122 fn test_skip_binary_files() -> anyhow::Result<()> {
123 let dir = TempDir::new()?;
124 dir.child("text.txt").write_str("some text")?;
125 dir.child("binary.bin")
126 .write_binary(&[b'b', b'i', b'n', 0, b'a', b'r', b'y'])?;
127
128 let output_file = dir.path().join("output.txt");
129 let args = get_test_args(dir.path(), &output_file);
130
131 let result = run_join_and_read_output(args)?;
132
133 assert!(result.contains("text.txt"));
134 assert!(!result.contains("binary.bin"));
135
136 Ok(())
137 }
138
139 #[test]
141 fn test_max_depth() -> anyhow::Result<()> {
142 let dir = TempDir::new()?;
143 dir.child("level1.txt").write_str("1")?;
144 dir.child("a/level2.txt").write_str("2")?;
145 dir.child("a/b/level3.txt").write_str("3")?;
146
147 let output_file = dir.path().join("output.txt");
148 let mut args = get_test_args(dir.path(), &output_file);
149 args.max_depth = Some(2);
150
151 let result = run_join_and_read_output(args)?;
152
153 assert!(result.contains("level1.txt"));
154 assert!(result.contains("level2.txt"));
155 assert!(!result.contains("level3.txt"));
156
157 Ok(())
158 }
159
160 #[test]
162 fn test_hidden_files_are_skipped_by_default() -> anyhow::Result<()> {
163 let dir = TempDir::new()?;
164 dir.child(".env").write_str("secret")?;
165 dir.child("visible.txt").write_str("visible")?;
166
167 let output_file = dir.path().join("output.txt");
168 let args = get_test_args(dir.path(), &output_file);
169
170 let result = run_join_and_read_output(args)?;
171
172 assert!(!result.contains(".env"));
173 assert!(result.contains("visible.txt"));
174
175 Ok(())
176 }
177
178 #[test]
180 fn test_hidden_files_are_included_with_flag() -> anyhow::Result<()> {
181 let dir = TempDir::new()?;
182 dir.child(".env").write_str("secret")?;
183 dir.child("visible.txt").write_str("visible")?;
184
185 let output_file = dir.path().join("output.txt");
186 let mut args = get_test_args(dir.path(), &output_file);
187 args.hidden = true;
188
189 let result = run_join_and_read_output(args)?;
190
191 assert!(result.contains(".env"));
192 assert!(result.contains("visible.txt"));
193
194 Ok(())
195 }
196
197 #[test]
199 fn test_output_file_is_skipped() -> anyhow::Result<()> {
200 let dir = TempDir::new()?;
201 let output_file = dir.path().join("output.txt");
202 fs::write(&output_file, "initial content")?;
203 dir.child("input.txt").write_str("input")?;
204
205 let args = get_test_args(dir.path(), &output_file);
206 let result = run_join_and_read_output(args)?;
207
208 assert!(!result.contains("initial content"));
209 assert!(result.contains("input.txt"));
210
211 Ok(())
212 }
213
214 #[test]
216 fn test_clear_file_option() -> anyhow::Result<()> {
217 let dir = TempDir::new()?;
218 let output_file = dir.path().join("output.txt");
219 fs::write(&output_file, "this should be cleared")?;
220 dir.child("input.txt").write_str("new content")?;
221
222 let mut args = get_test_args(dir.path(), &output_file);
223 args.clear_file = true;
224
225 let result = run_join_and_read_output(args)?;
226
227 assert!(!result.contains("this should be cleared"));
228 assert!(result.contains("new content"));
229
230 Ok(())
231 }
232
233 #[test]
235 fn test_empty_directory_produces_empty_file() -> anyhow::Result<()> {
236 let dir = TempDir::new()?;
237 let output_file = dir.path().join("output.txt");
238 let args = get_test_args(dir.path(), &output_file);
239
240 let result = run_join_and_read_output(args)?;
241
242 assert!(result.is_empty());
243
244 Ok(())
245 }
246
247 #[test]
249 fn test_update_command_placeholder() -> anyhow::Result<()> {
250 let update_args = cli::UpdateArgs {};
251 run(Commands::Update(update_args))?;
252 Ok(())
253 }
254
255 #[test]
259 fn test_exclude_by_folder_pattern() -> anyhow::Result<()> {
260 let dir = TempDir::new()?;
261 dir.child("src/main.rs").write_str("main")?;
262 dir.child("target/debug/app").write_str("binary")?;
263 dir.child("docs/guide.md").write_str("guide")?;
264
265 let output_file = dir.path().join("output.txt");
266 let mut args = get_test_args(dir.path(), &output_file);
267 args.exclude = Some(vec!["target/".to_string(), "docs/".to_string()]);
268
269 let result = run_join_and_read_output(args)?;
270
271 assert!(result.contains("main.rs"));
272 assert!(!result.contains("app"));
273 assert!(!result.contains("guide.md"));
274
275 Ok(())
276 }
277
278 #[test]
280 fn test_exclude_by_extension_pattern() -> anyhow::Result<()> {
281 let dir = TempDir::new()?;
282 dir.child("code.rs").write_str("main")?;
283 dir.child("notes.md").write_str("notes")?;
284 dir.child("log.log").write_str("log")?;
285
286 let output_file = dir.path().join("output.txt");
287 let mut args = get_test_args(dir.path(), &output_file);
288 args.exclude = Some(vec!["*.log".to_string(), "*.md".to_string()]);
289
290 let result = run_join_and_read_output(args)?;
291
292 assert!(result.contains("code.rs"));
293 assert!(!result.contains("notes.md"));
294 assert!(!result.contains("log.log"));
295
296 Ok(())
297 }
298
299 #[test]
301 fn test_exclude_by_multiple_patterns() -> anyhow::Result<()> {
302 let dir = TempDir::new()?;
303 dir.child("src/main.rs").write_str("main")?;
304 dir.child("src/error.log").write_str("log")?;
305 dir.child("target/app").write_str("binary")?;
306
307 let output_file = dir.path().join("output.txt");
308 let mut args = get_test_args(dir.path(), &output_file);
309 args.exclude = Some(vec!["target/".to_string(), "*.log".to_string()]);
310
311 let result = run_join_and_read_output(args)?;
312
313 assert!(result.contains("main.rs"));
314 assert!(!result.contains("error.log"));
315 assert!(!result.contains("app"));
316
317 Ok(())
318 }
319
320 #[test]
322 fn test_exclude_takes_precedence_over_include() -> anyhow::Result<()> {
323 let dir = TempDir::new()?;
324 dir.child("src/main.rs").write_str("main")?;
325 dir.child("src/lib.rs").write_str("lib")?;
326 dir.child("tests/integration_test.rs").write_str("test")?;
327
328 let output_file = dir.path().join("output.txt");
329 let mut args = get_test_args(dir.path(), &output_file);
330 args.patterns = Some(vec!["*.rs".to_string()]); args.exclude = Some(vec!["tests/".to_string()]); let result = run_join_and_read_output(args)?;
334
335 assert!(result.contains("main.rs"));
336 assert!(result.contains("lib.rs"));
337 assert!(!result.contains("integration_test.rs"));
338
339 Ok(())
340 }
341
342 #[test]
344 fn test_exclude_specific_file() -> anyhow::Result<()> {
345 let dir = TempDir::new()?;
346 dir.child("src/main.rs").write_str("main")?;
347 dir.child("src/config.rs").write_str("config")?;
348 dir.child("README.md").write_str("readme")?;
349
350 let output_file = dir.path().join("output.txt");
351 let mut args = get_test_args(dir.path(), &output_file);
352 args.exclude = Some(vec!["src/config.rs".to_string()]);
353
354 let result = run_join_and_read_output(args)?;
355
356 assert!(result.contains("main.rs"));
357 assert!(result.contains("README.md"));
358 assert!(!result.contains("config.rs"));
359
360 Ok(())
361 }
362}