context_builder/
cli.rs

1use clap::Parser;
2
3/// CLI tool to aggregate directory contents into a single Markdown file optimized for LLM consumption
4#[derive(Parser, Debug)]
5#[clap(author, version, about, arg_required_else_help = true)]
6pub struct Args {
7    /// Directory path to process
8    #[clap(short = 'd', long, default_value = ".")]
9    pub input: String,
10
11    /// Output file path
12    #[clap(short, long, default_value = "output.md")]
13    pub output: String,
14
15    /// File extensions to include (e.g., --filter rs --filter toml)
16    #[clap(short = 'f', long)]
17    pub filter: Vec<String>,
18
19    /// Folder or file names to ignore (e.g., --ignore target --ignore lock)
20    #[clap(short = 'i', long)]
21    pub ignore: Vec<String>,
22
23    /// Preview mode: only print the file tree to the console, don't generate the documentation file
24    #[clap(long)]
25    pub preview: bool,
26
27    /// Add line numbers to code blocks in the output
28    #[clap(long)]
29    pub line_numbers: bool,
30}
31
32#[cfg(test)]
33mod tests {
34    use super::Args;
35    use clap::Parser;
36
37    #[test]
38    fn defaults_error_with_no_args_due_to_help_setting() {
39        // With arg_required_else_help = true, parsing with no args should error
40        let res = Args::try_parse_from(["context-builder"]);
41        assert!(res.is_err(), "Expected error due to arg_required_else_help");
42    }
43
44    #[test]
45    fn parses_all_flags_and_options() {
46        let args = Args::try_parse_from([
47            "context-builder",
48            "--input",
49            "some/dir",
50            "--output",
51            "ctx.md",
52            "--filter",
53            "rs",
54            "--filter",
55            "toml",
56            "--ignore",
57            "target",
58            "--ignore",
59            "node_modules",
60            "--preview",
61            "--line-numbers",
62        ])
63        .expect("should parse");
64
65        assert_eq!(args.input, "some/dir");
66        assert_eq!(args.output, "ctx.md");
67        assert_eq!(args.filter, vec!["rs".to_string(), "toml".to_string()]);
68        assert_eq!(
69            args.ignore,
70            vec!["target".to_string(), "node_modules".to_string()]
71        );
72        assert!(args.preview);
73        assert!(args.line_numbers);
74    }
75
76    #[test]
77    fn short_flags_parse_correctly() {
78        let args = Args::try_parse_from([
79            "context-builder",
80            "-d",
81            ".",
82            "-o",
83            "out.md",
84            "-f",
85            "md",
86            "-f",
87            "rs",
88            "-i",
89            "target",
90            "-i",
91            ".git",
92        ])
93        .expect("should parse");
94
95        assert_eq!(args.input, ".");
96        assert_eq!(args.output, "out.md");
97        assert_eq!(args.filter, vec!["md".to_string(), "rs".to_string()]);
98        assert_eq!(args.ignore, vec!["target".to_string(), ".git".to_string()]);
99        assert!(!args.preview);
100        assert!(!args.line_numbers);
101    }
102
103    #[test]
104    fn defaults_for_options_when_not_provided() {
105        let args = Args::try_parse_from([
106            "context-builder",
107            "-d",
108            "proj",
109            // no output, filter, ignore, or flags
110        ])
111        .expect("should parse");
112
113        assert_eq!(args.input, "proj");
114        assert_eq!(args.output, "output.md");
115        assert!(args.filter.is_empty());
116        assert!(args.ignore.is_empty());
117        assert!(!args.preview);
118        assert!(!args.line_numbers);
119    }
120}