join_ai/
lib.rs

1use std::fs;
2
3pub mod cli;
4pub mod processor;
5pub mod walker;
6
7use cli::Args;
8
9/// The core logic of the application.
10pub fn run(args: Args) -> anyhow::Result<()> {
11    // Log the arguments being used
12    println!(
13        "Processing files in folder: {}",
14        args.input_folder.display()
15    );
16    if let Some(patterns) = &args.patterns {
17        println!("Using patterns: {}", patterns.join(", "));
18    } else {
19        println!("Using patterns: all files");
20    }
21    if let Some(exclude_folders) = &args.exclude_folders {
22        println!("Excluding folders: {}", exclude_folders.join(", "));
23    }
24    if let Some(exclude_extensions) = &args.exclude_extensions {
25        println!("Excluding extensions: {}", exclude_extensions.join(", "));
26    }
27
28    // Clear the output file if specified
29    if args.clear_file && args.output_file.exists() {
30        fs::remove_file(&args.output_file)?;
31        println!(
32            "Output file {} has been cleared.",
33            args.output_file.display()
34        );
35    }
36
37    // 1. Find all relevant files using the walker module
38    let receiver = walker::find_files(&args)?;
39
40    // 2. Process the files found by the walker
41    processor::process_files(receiver, &args.output_file)?;
42
43    println!(
44        "Files have been processed and written to {}",
45        args.output_file.display()
46    );
47
48    Ok(())
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use crate::cli::Args;
55    use assert_fs::TempDir;
56    use assert_fs::prelude::*;
57    use std::fs::File;
58    use std::io::Read;
59    use std::path::Path;
60
61    fn get_test_args(input_folder: &Path, output_file: &Path) -> Args {
62        Args {
63            input_folder: input_folder.to_path_buf(),
64            output_file: output_file.to_path_buf(),
65            patterns: None,
66            clear_file: true,
67            exclude_folders: None,
68            exclude_extensions: None,
69            max_depth: None,
70            hidden: false,
71            no_follow: true,
72        }
73    }
74
75    #[test]
76    fn test_filter_by_multiple_patterns() -> anyhow::Result<()> {
77        let dir = TempDir::new()?;
78        let input_dir_path = dir.path();
79
80        dir.child("Cargo.toml").write_str("[package]")?;
81        dir.child("README.md").write_str("# Project")?;
82        let src_dir = dir.child("src");
83        src_dir.create_dir_all()?;
84        src_dir.child("main.rs").write_str("fn main(){}")?;
85
86        let output_file = input_dir_path.join("output.txt");
87        let mut args = get_test_args(input_dir_path, &output_file);
88        // This now reflects the new CLI behavior
89        args.patterns = Some(vec!["*.rs".to_string(), "*.toml".to_string()]);
90
91        run(args)?;
92
93        let mut result = String::new();
94        File::open(&output_file)?.read_to_string(&mut result)?;
95
96        assert!(result.contains("main.rs"));
97        assert!(result.contains("Cargo.toml"));
98        assert!(!result.contains("README.md"));
99
100        Ok(())
101    }
102
103    #[test]
104    fn test_skip_binary_files() -> anyhow::Result<()> {
105        let dir = TempDir::new()?;
106        let input_dir_path = dir.path();
107
108        dir.child("text.txt").write_str("some text")?;
109        // A file containing a NUL byte is considered binary for our purposes.
110        dir.child("binary.bin")
111            .write_binary(&[b'b', b'i', b'n', 0, b'a', b'r', b'y'])?;
112
113        let output_file = input_dir_path.join("output.txt");
114        let args = get_test_args(input_dir_path, &output_file);
115
116        run(args)?;
117
118        let mut result = String::new();
119        File::open(&output_file)?.read_to_string(&mut result)?;
120
121        assert!(result.contains("text.txt"));
122        assert!(!result.contains("binary.bin"));
123
124        Ok(())
125    }
126}