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    /// Initialize a new context-builder.toml config file in the current directory
48    #[clap(long)]
49    pub init: bool,
50}
51
52#[cfg(test)]
53mod tests {
54    use super::Args;
55    use clap::Parser;
56
57    #[test]
58    fn parses_with_no_args() {
59        let res = Args::try_parse_from(["context-builder"]);
60        assert!(res.is_ok(), "Expected success when no args are provided");
61    }
62
63    #[test]
64    fn parses_all_flags_and_options() {
65        let args = Args::try_parse_from([
66            "context-builder",
67            "--input",
68            "some/dir",
69            "--output",
70            "ctx.md",
71            "--filter",
72            "rs",
73            "--filter",
74            "toml",
75            "--ignore",
76            "target",
77            "--ignore",
78            "node_modules",
79            "--preview",
80            "--token-count",
81            "--line-numbers",
82            "--diff-only",
83            "--clear-cache",
84        ])
85        .expect("should parse");
86
87        assert_eq!(args.input, "some/dir");
88        assert_eq!(args.output, "ctx.md");
89        assert_eq!(args.filter, vec!["rs".to_string(), "toml".to_string()]);
90        assert_eq!(
91            args.ignore,
92            vec!["target".to_string(), "node_modules".to_string()]
93        );
94        assert!(args.preview);
95        assert!(args.token_count);
96        assert!(args.line_numbers);
97        assert!(args.diff_only);
98        assert!(args.clear_cache);
99    }
100
101    #[test]
102    fn short_flags_parse_correctly() {
103        let args = Args::try_parse_from([
104            "context-builder",
105            "-d",
106            ".",
107            "-o",
108            "out.md",
109            "-f",
110            "md",
111            "-f",
112            "rs",
113            "-i",
114            "target",
115            "-i",
116            ".git",
117        ])
118        .expect("should parse");
119
120        assert_eq!(args.input, ".");
121        assert_eq!(args.output, "out.md");
122        assert_eq!(args.filter, vec!["md".to_string(), "rs".to_string()]);
123        assert_eq!(args.ignore, vec!["target".to_string(), ".git".to_string()]);
124        assert!(!args.preview);
125        assert!(!args.line_numbers);
126        assert!(!args.clear_cache);
127    }
128
129    #[test]
130    fn defaults_for_options_when_not_provided() {
131        let args = Args::try_parse_from(["context-builder", "-d", "proj"]).expect("should parse");
132
133        assert_eq!(args.input, "proj");
134        assert_eq!(args.output, "output.md");
135        assert!(args.filter.is_empty());
136        assert!(args.ignore.is_empty());
137        assert!(!args.preview);
138        assert!(!args.line_numbers);
139        assert!(!args.diff_only);
140        assert!(!args.clear_cache);
141    }
142
143    #[test]
144    fn parses_diff_only_flag() {
145        let args = Args::try_parse_from(["context-builder", "--diff-only"])
146            .expect("should parse diff-only flag");
147        assert!(args.diff_only);
148        assert!(!args.clear_cache);
149    }
150
151    #[test]
152    fn parses_clear_cache_flag() {
153        let args = Args::try_parse_from(["context-builder", "--clear-cache"])
154            .expect("should parse clear-cache flag");
155        assert!(args.clear_cache);
156        assert!(!args.diff_only);
157    }
158}