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    /// Extract function/class signatures only (requires tree-sitter feature)
56    #[clap(long)]
57    pub signatures: bool,
58
59    /// Extract code structure (imports, exports, symbol counts) - requires tree-sitter feature
60    #[clap(long)]
61    pub structure: bool,
62
63    /// Truncation mode for max-tokens: "smart" (AST boundaries) or "byte"
64    #[clap(long, value_name = "MODE", default_value = "smart")]
65    pub truncate: String,
66
67    /// Filter signatures by visibility: "all", "public", or "private"
68    #[clap(long, default_value = "all")]
69    pub visibility: String,
70}
71
72#[cfg(test)]
73mod tests {
74    use super::Args;
75    use clap::Parser;
76
77    #[test]
78    fn parses_with_no_args() {
79        let res = Args::try_parse_from(["context-builder"]);
80        assert!(res.is_ok(), "Expected success when no args are provided");
81    }
82
83    #[test]
84    fn parses_all_flags_and_options() {
85        let args = Args::try_parse_from([
86            "context-builder",
87            "--input",
88            "some/dir",
89            "--output",
90            "ctx.md",
91            "--filter",
92            "rs",
93            "--filter",
94            "toml",
95            "--ignore",
96            "target",
97            "--ignore",
98            "node_modules",
99            "--preview",
100            "--token-count",
101            "--line-numbers",
102            "--diff-only",
103            "--clear-cache",
104        ])
105        .expect("should parse");
106
107        assert_eq!(args.input, "some/dir");
108        assert_eq!(args.output, "ctx.md");
109        assert_eq!(args.filter, vec!["rs".to_string(), "toml".to_string()]);
110        assert_eq!(
111            args.ignore,
112            vec!["target".to_string(), "node_modules".to_string()]
113        );
114        assert!(args.preview);
115        assert!(args.token_count);
116        assert!(args.line_numbers);
117        assert!(args.diff_only);
118        assert!(args.clear_cache);
119    }
120
121    #[test]
122    fn short_flags_parse_correctly() {
123        let args = Args::try_parse_from([
124            "context-builder",
125            "-d",
126            ".",
127            "-o",
128            "out.md",
129            "-f",
130            "md",
131            "-f",
132            "rs",
133            "-i",
134            "target",
135            "-i",
136            ".git",
137        ])
138        .expect("should parse");
139
140        assert_eq!(args.input, ".");
141        assert_eq!(args.output, "out.md");
142        assert_eq!(args.filter, vec!["md".to_string(), "rs".to_string()]);
143        assert_eq!(args.ignore, vec!["target".to_string(), ".git".to_string()]);
144        assert!(!args.preview);
145        assert!(!args.line_numbers);
146        assert!(!args.clear_cache);
147    }
148
149    #[test]
150    fn defaults_for_options_when_not_provided() {
151        let args = Args::try_parse_from(["context-builder", "-d", "proj"]).expect("should parse");
152
153        assert_eq!(args.input, "proj");
154        assert_eq!(args.output, "output.md");
155        assert!(args.filter.is_empty());
156        assert!(args.ignore.is_empty());
157        assert!(!args.preview);
158        assert!(!args.line_numbers);
159        assert!(!args.diff_only);
160        assert!(!args.clear_cache);
161    }
162
163    #[test]
164    fn parses_diff_only_flag() {
165        let args = Args::try_parse_from(["context-builder", "--diff-only"])
166            .expect("should parse diff-only flag");
167        assert!(args.diff_only);
168        assert!(!args.clear_cache);
169    }
170
171    #[test]
172    fn parses_clear_cache_flag() {
173        let args = Args::try_parse_from(["context-builder", "--clear-cache"])
174            .expect("should parse clear-cache flag");
175        assert!(args.clear_cache);
176        assert!(!args.diff_only);
177    }
178
179    #[test]
180    fn parses_signatures_flag() {
181        let args = Args::try_parse_from(["context-builder", "--signatures"])
182            .expect("should parse signatures flag");
183        assert!(args.signatures);
184    }
185
186    #[test]
187    fn parses_structure_flag() {
188        let args = Args::try_parse_from(["context-builder", "--structure"])
189            .expect("should parse structure flag");
190        assert!(args.structure);
191    }
192
193    #[test]
194    fn parses_truncate_mode() {
195        let args = Args::try_parse_from(["context-builder", "--truncate", "byte"])
196            .expect("should parse truncate flag");
197        assert_eq!(args.truncate, "byte");
198
199        let args_default =
200            Args::try_parse_from(["context-builder"]).expect("should parse with default truncate");
201        assert_eq!(args_default.truncate, "smart");
202    }
203
204    #[test]
205    fn parses_visibility_filter() {
206        let args = Args::try_parse_from(["context-builder", "--visibility", "public"])
207            .expect("should parse visibility flag");
208        assert_eq!(args.visibility, "public");
209
210        let args_default = Args::try_parse_from(["context-builder"])
211            .expect("should parse with default visibility");
212        assert_eq!(args_default.visibility, "all");
213    }
214}