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)]
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,toml)
16    #[clap(short = 'f', long, value_delimiter = ',')]
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    /// Token count mode: estimate the total token count of the final document
28    #[clap(long)]
29    pub token_count: bool,
30
31    /// Add line numbers to code blocks in the output
32    #[clap(long)]
33    pub line_numbers: bool,
34
35    /// Automatically answer yes to all prompts
36
37    #[clap(short = 'y', long)]
38    pub yes: bool,
39
40    /// Output only diffs (omit full file contents; requires auto-diff & timestamped output)
41
42    /// Defaults to false.
43    #[clap(long, default_value_t = false)]
44    pub diff_only: bool,
45}
46
47#[cfg(test)]
48mod tests {
49    use super::Args;
50    use clap::Parser;
51
52    #[test]
53    fn parses_with_no_args() {
54        let res = Args::try_parse_from(["context-builder"]);
55        assert!(res.is_ok(), "Expected success when no args are provided");
56    }
57
58    #[test]
59
60    fn parses_all_flags_and_options() {
61        let args = Args::try_parse_from([
62            "context-builder",
63            "--input",
64            "some/dir",
65            "--output",
66            "ctx.md",
67            "--filter",
68            "rs",
69            "--filter",
70            "toml",
71            "--ignore",
72            "target",
73            "--ignore",
74            "node_modules",
75            "--preview",
76            "--token-count",
77            "--line-numbers",
78            "--diff-only",
79        ])
80        .expect("should parse");
81
82        assert_eq!(args.input, "some/dir");
83
84        assert_eq!(args.output, "ctx.md");
85
86        assert_eq!(args.filter, vec!["rs".to_string(), "toml".to_string()]);
87
88        assert_eq!(
89            args.ignore,
90            vec!["target".to_string(), "node_modules".to_string()]
91        );
92
93        assert!(args.preview);
94
95        assert!(args.token_count);
96
97        assert!(args.line_numbers);
98
99        assert!(args.diff_only);
100    }
101
102    #[test]
103    fn short_flags_parse_correctly() {
104        let args = Args::try_parse_from([
105            "context-builder",
106            "-d",
107            ".",
108            "-o",
109            "out.md",
110            "-f",
111            "md",
112            "-f",
113            "rs",
114            "-i",
115            "target",
116            "-i",
117            ".git",
118        ])
119        .expect("should parse");
120
121        assert_eq!(args.input, ".");
122        assert_eq!(args.output, "out.md");
123        assert_eq!(args.filter, vec!["md".to_string(), "rs".to_string()]);
124        assert_eq!(args.ignore, vec!["target".to_string(), ".git".to_string()]);
125        assert!(!args.preview);
126        assert!(!args.line_numbers);
127    }
128
129    #[test]
130
131    fn defaults_for_options_when_not_provided() {
132        let args = Args::try_parse_from(["context-builder", "-d", "proj"]).expect("should parse");
133
134        assert_eq!(args.input, "proj");
135
136        assert_eq!(args.output, "output.md");
137
138        assert!(args.filter.is_empty());
139
140        assert!(args.ignore.is_empty());
141
142        assert!(!args.preview);
143
144        assert!(!args.line_numbers);
145
146        assert!(!args.diff_only);
147    }
148
149    #[test]
150    fn parses_diff_only_flag() {
151        let args = Args::try_parse_from(["context-builder", "--diff-only"])
152            .expect("should parse diff-only flag");
153        assert!(args.diff_only);
154    }
155}