Skip to main content

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