1use std::fs;
2
3pub mod cli;
4pub mod processor;
5pub mod walker;
6
7use cli::{Commands, JoinArgs};
8
9pub fn run(command: Commands) -> anyhow::Result<()> {
11 match command {
12 Commands::Join(args) => run_join(args),
13 Commands::Update(_args) => {
14 println!("Update functionality is not yet implemented.");
15 println!("Please check for new releases at the GitHub repository:");
16 println!("https://github.com/luizvbo/join-ai/releases");
17 Ok(())
18 }
19 }
20}
21
22fn run_join(args: JoinArgs) -> anyhow::Result<()> {
24 println!(
26 "Processing files in folder: {}",
27 args.input_folder.display()
28 );
29 if let Some(patterns) = &args.patterns {
30 println!("Using patterns: {}", patterns.join(", "));
31 } else {
32 println!("Using patterns: all files");
33 }
34 if let Some(exclude_folders) = &args.exclude_folders {
35 println!("Excluding folders: {}", exclude_folders.join(", "));
36 }
37 if let Some(exclude_extensions) = &args.exclude_extensions {
38 println!("Excluding extensions: {}", exclude_extensions.join(", "));
39 }
40
41 if args.clear_file && args.output_file.exists() {
43 fs::remove_file(&args.output_file)?;
44 println!(
45 "Output file {} has been cleared.",
46 args.output_file.display()
47 );
48 }
49
50 let receiver = walker::find_files(&args)?;
52
53 processor::process_files(receiver, &args.output_file)?;
55
56 println!(
57 "Files have been processed and written to {}",
58 args.output_file.display()
59 );
60
61 Ok(())
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use crate::cli::{Commands, JoinArgs};
68 use assert_fs::TempDir;
69 use assert_fs::prelude::*;
70 use std::fs::{self};
71 use std::path::Path;
72
73 fn get_test_args(input_folder: &Path, output_file: &Path) -> JoinArgs {
75 JoinArgs {
76 input_folder: input_folder.to_path_buf(),
77 output_file: output_file.to_path_buf(),
78 patterns: None,
79 clear_file: false, exclude_folders: None,
81 exclude_extensions: None,
82 max_depth: None,
83 hidden: false,
84 no_follow: true,
85 }
86 }
87
88 fn run_join_and_read_output(args: JoinArgs) -> anyhow::Result<String> {
90 let output_path = args.output_file.clone();
91 run(Commands::Join(args))?;
92 Ok(fs::read_to_string(output_path).unwrap_or_default())
94 }
95
96 #[test]
97 fn test_filter_by_multiple_patterns() -> anyhow::Result<()> {
98 let dir = TempDir::new()?;
99 dir.child("Cargo.toml").write_str("[package]")?;
100 dir.child("README.md").write_str("# Project")?;
101 dir.child("src/main.rs").write_str("fn main(){}")?;
102
103 let output_file = dir.path().join("output.txt");
104 let mut args = get_test_args(dir.path(), &output_file);
105 args.patterns = Some(vec!["*.rs".to_string(), "*.toml".to_string()]);
106
107 let result = run_join_and_read_output(args)?;
108
109 assert!(result.contains("// FILE:"));
110 assert!(result.contains("main.rs"));
111 assert!(result.contains("Cargo.toml"));
112 assert!(!result.contains("README.md"));
113
114 Ok(())
115 }
116
117 #[test]
118 fn test_skip_binary_files() -> anyhow::Result<()> {
119 let dir = TempDir::new()?;
120 dir.child("text.txt").write_str("some text")?;
121 dir.child("binary.bin")
122 .write_binary(&[b'b', b'i', b'n', 0, b'a', b'r', b'y'])?;
123
124 let output_file = dir.path().join("output.txt");
125 let args = get_test_args(dir.path(), &output_file);
126
127 let result = run_join_and_read_output(args)?;
128
129 assert!(result.contains("text.txt"));
130 assert!(!result.contains("binary.bin"));
131
132 Ok(())
133 }
134
135 #[test]
136 fn test_exclude_folders() -> anyhow::Result<()> {
137 let dir = TempDir::new()?;
138 dir.child("src/main.rs").write_str("main")?;
139 dir.child("target/debug/app").write_str("binary")?;
140 dir.child("docs/guide.md").write_str("guide")?;
141
142 let output_file = dir.path().join("output.txt");
143 let mut args = get_test_args(dir.path(), &output_file);
144 args.exclude_folders = Some(vec!["target".to_string(), "docs".to_string()]);
145
146 let result = run_join_and_read_output(args)?;
147
148 assert!(result.contains("main.rs"));
149 assert!(!result.contains("app"));
150 assert!(!result.contains("guide.md"));
151
152 Ok(())
153 }
154
155 #[test]
156 fn test_exclude_extensions() -> anyhow::Result<()> {
157 let dir = TempDir::new()?;
158 dir.child("code.rs").write_str("main")?;
159 dir.child("notes.txt").write_str("notes")?;
160 dir.child("log.log").write_str("log")?;
161
162 let output_file = dir.path().join("output.txt");
163 let mut args = get_test_args(dir.path(), &output_file);
164 args.exclude_extensions = Some(vec!["log".to_string(), "txt".to_string()]);
165
166 let result = run_join_and_read_output(args)?;
167
168 assert!(result.contains("code.rs"));
169 assert!(!result.contains("notes.txt"));
170 assert!(!result.contains("log.log"));
171
172 Ok(())
173 }
174
175 #[test]
176 fn test_max_depth() -> anyhow::Result<()> {
177 let dir = TempDir::new()?;
178 dir.child("level1.txt").write_str("1")?;
179 dir.child("a/level2.txt").write_str("2")?;
180 dir.child("a/b/level3.txt").write_str("3")?;
181
182 let output_file = dir.path().join("output.txt");
183 let mut args = get_test_args(dir.path(), &output_file);
184 args.max_depth = Some(2); let result = run_join_and_read_output(args)?;
187
188 assert!(result.contains("level1.txt"));
189 assert!(result.contains("level2.txt"));
190 assert!(!result.contains("level3.txt"));
191
192 Ok(())
193 }
194
195 #[test]
196 fn test_hidden_files_are_skipped_by_default() -> anyhow::Result<()> {
197 let dir = TempDir::new()?;
198 dir.child(".env").write_str("secret")?;
199 dir.child("visible.txt").write_str("visible")?;
200
201 let output_file = dir.path().join("output.txt");
202 let args = get_test_args(dir.path(), &output_file);
203
204 let result = run_join_and_read_output(args)?;
205
206 assert!(!result.contains(".env"));
207 assert!(result.contains("visible.txt"));
208
209 Ok(())
210 }
211
212 #[test]
213 fn test_hidden_files_are_included_with_flag() -> anyhow::Result<()> {
214 let dir = TempDir::new()?;
215 dir.child(".env").write_str("secret")?;
216 dir.child("visible.txt").write_str("visible")?;
217
218 let output_file = dir.path().join("output.txt");
219 let mut args = get_test_args(dir.path(), &output_file);
220 args.hidden = true;
221
222 let result = run_join_and_read_output(args)?;
223
224 assert!(result.contains(".env"));
225 assert!(result.contains("visible.txt"));
226
227 Ok(())
228 }
229
230 #[test]
231 fn test_output_file_is_skipped() -> anyhow::Result<()> {
232 let dir = TempDir::new()?;
233 let output_file = dir.path().join("output.txt");
234 fs::write(&output_file, "initial content")?;
236 dir.child("input.txt").write_str("input")?;
237
238 let args = get_test_args(dir.path(), &output_file);
239 let result = run_join_and_read_output(args)?;
240
241 assert!(!result.contains("initial content"));
243 assert!(result.contains("input.txt"));
244
245 Ok(())
246 }
247
248 #[test]
249 fn test_clear_file_option() -> anyhow::Result<()> {
250 let dir = TempDir::new()?;
251 let output_file = dir.path().join("output.txt");
252 fs::write(&output_file, "this should be cleared")?;
253 dir.child("input.txt").write_str("new content")?;
254
255 let mut args = get_test_args(dir.path(), &output_file);
256 args.clear_file = true;
257
258 let result = run_join_and_read_output(args)?;
259
260 assert!(!result.contains("this should be cleared"));
261 assert!(result.contains("new content"));
262
263 Ok(())
264 }
265
266 #[test]
267 fn test_empty_directory_produces_empty_file() -> anyhow::Result<()> {
268 let dir = TempDir::new()?;
269 let output_file = dir.path().join("output.txt");
270 let args = get_test_args(dir.path(), &output_file);
271
272 let result = run_join_and_read_output(args)?;
273
274 assert!(result.is_empty());
275
276 Ok(())
277 }
278
279 #[test]
280 fn test_update_command_placeholder() -> anyhow::Result<()> {
281 let update_args = cli::UpdateArgs {};
284 run(Commands::Update(update_args))?;
285 Ok(())
286 }
287}