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, Clone)]
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    #[clap(short = 'y', long)]
37    pub yes: bool,
38
39    /// Output only diffs (omit full file contents; requires auto-diff & timestamped output)
40    #[clap(long, default_value_t = false)]
41    pub diff_only: bool,
42
43    /// Clear the cached project state and exit
44    #[clap(long)]
45    pub clear_cache: bool,
46}
47
48#[cfg(test)]
49mod tests {
50    use super::Args;
51    use clap::Parser;
52
53    #[test]
54    fn parses_with_no_args() {
55        let res = Args::try_parse_from(["context-builder"]);
56        assert!(res.is_ok(), "Expected success when no args are provided");
57    }
58
59    #[test]
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            "--clear-cache",
80        ])
81        .expect("should parse");
82
83        assert_eq!(args.input, "some/dir");
84        assert_eq!(args.output, "ctx.md");
85        assert_eq!(args.filter, vec!["rs".to_string(), "toml".to_string()]);
86        assert_eq!(
87            args.ignore,
88            vec!["target".to_string(), "node_modules".to_string()]
89        );
90        assert!(args.preview);
91        assert!(args.token_count);
92        assert!(args.line_numbers);
93        assert!(args.diff_only);
94        assert!(args.clear_cache);
95    }
96
97    #[test]
98    fn short_flags_parse_correctly() {
99        let args = Args::try_parse_from([
100            "context-builder",
101            "-d",
102            ".",
103            "-o",
104            "out.md",
105            "-f",
106            "md",
107            "-f",
108            "rs",
109            "-i",
110            "target",
111            "-i",
112            ".git",
113        ])
114        .expect("should parse");
115
116        assert_eq!(args.input, ".");
117        assert_eq!(args.output, "out.md");
118        assert_eq!(args.filter, vec!["md".to_string(), "rs".to_string()]);
119        assert_eq!(args.ignore, vec!["target".to_string(), ".git".to_string()]);
120        assert!(!args.preview);
121        assert!(!args.line_numbers);
122        assert!(!args.clear_cache);
123    }
124
125    #[test]
126    fn defaults_for_options_when_not_provided() {
127        let args = Args::try_parse_from(["context-builder", "-d", "proj"]).expect("should parse");
128
129        assert_eq!(args.input, "proj");
130        assert_eq!(args.output, "output.md");
131        assert!(args.filter.is_empty());
132        assert!(args.ignore.is_empty());
133        assert!(!args.preview);
134        assert!(!args.line_numbers);
135        assert!(!args.diff_only);
136        assert!(!args.clear_cache);
137    }
138
139    #[test]
140    fn parses_diff_only_flag() {
141        let args = Args::try_parse_from(["context-builder", "--diff-only"])
142            .expect("should parse diff-only flag");
143        assert!(args.diff_only);
144        assert!(!args.clear_cache);
145    }
146
147    #[test]
148    fn parses_clear_cache_flag() {
149        let args = Args::try_parse_from(["context-builder", "--clear-cache"])
150            .expect("should parse clear-cache flag");
151        assert!(args.clear_cache);
152        assert!(!args.diff_only);
153    }
154}