1use clap::{Args as ClapArgs, ColorChoice, Parser, Subcommand};
2use std::path::PathBuf;
3
4#[derive(Parser, Debug, Clone)]
7#[command(author, version, about, long_about = None, color = ColorChoice::Always)]
8pub struct Cli {
9 #[command(subcommand)]
11 pub command: Commands,
12}
13
14#[derive(Subcommand, Debug, Clone)]
16pub enum Commands {
17 Join(JoinArgs),
19 Update(UpdateArgs),
21}
22
23#[derive(ClapArgs, Debug, Clone)]
25pub struct JoinArgs {
26 #[arg(required = true)]
28 pub input_folder: PathBuf,
29
30 #[arg(short, long, default_value = "concatenated.txt")]
32 pub output_file: PathBuf,
33
34 #[arg(short = 'p', long, action = clap::ArgAction::Append, value_name = "PATTERN")]
38 pub patterns: Option<Vec<String>>,
39
40 #[arg(short = 'x', long, action = clap::ArgAction::Append, value_name = "PATTERN")]
44 pub exclude: Option<Vec<String>>,
45
46 #[arg(short, long)]
48 pub clear_file: bool,
49
50 #[arg(long)]
53 pub max_depth: Option<usize>,
54
55 #[arg(long)]
57 pub hidden: bool,
58
59 #[arg(long, default_value_t = true)]
61 pub no_follow: bool,
62}
63
64#[derive(ClapArgs, Debug, Clone)]
66pub struct UpdateArgs {}
67
68#[cfg(test)]
70mod tests {
71 use super::*;
72 use clap::error::ErrorKind;
73
74 #[test]
77 fn test_basic_join_command_and_defaults() {
78 let args = vec!["join-ai", "join", "./my-project"];
79 let cli = Cli::try_parse_from(args).unwrap();
80
81 match cli.command {
82 Commands::Join(join_args) => {
83 assert_eq!(join_args.input_folder, PathBuf::from("./my-project"));
84 assert_eq!(join_args.output_file, PathBuf::from("concatenated.txt"));
86 assert!(!join_args.clear_file);
87 assert!(!join_args.hidden);
88 assert!(join_args.patterns.is_none());
89 assert!(join_args.exclude.is_none());
90 assert!(join_args.max_depth.is_none());
91 assert!(join_args.no_follow); }
93 _ => panic!("Expected Join command to be parsed"),
94 }
95 }
96
97 #[test]
100 fn test_all_join_options_are_parsed() {
101 let args = vec![
102 "join-ai",
103 "join",
104 "src",
105 "-o",
106 "output.txt",
107 "-p",
108 "*.rs",
109 "-p",
110 "*.toml",
111 "--clear-file",
112 "-x",
113 "target/",
114 "--exclude",
115 "*.log",
116 "--max-depth",
117 "10",
118 "--hidden",
119 ];
120 let cli = Cli::try_parse_from(args).unwrap();
121
122 match cli.command {
123 Commands::Join(join_args) => {
124 assert_eq!(join_args.input_folder, PathBuf::from("src"));
125 assert_eq!(join_args.output_file, PathBuf::from("output.txt"));
126 assert_eq!(
127 join_args.patterns,
128 Some(vec!["*.rs".to_string(), "*.toml".to_string()])
129 );
130 assert!(join_args.clear_file);
131 assert_eq!(
132 join_args.exclude,
133 Some(vec!["target/".to_string(), "*.log".to_string()])
134 );
135 assert_eq!(join_args.max_depth, Some(10));
136 assert!(join_args.hidden);
137 assert!(join_args.no_follow);
138 }
139 _ => panic!("Expected Join command to be parsed"),
140 }
141 }
142
143 #[test]
145 fn test_update_subcommand_is_parsed() {
146 let args = vec!["join-ai", "update"];
147 let cli = Cli::try_parse_from(args).unwrap();
148
149 assert!(matches!(cli.command, Commands::Update(_)));
150 }
151
152 #[test]
154 fn test_missing_required_argument_fails() {
155 let args = vec!["join-ai", "join", "-o", "output.txt"];
156 let result = Cli::try_parse_from(args);
157
158 assert!(
159 result.is_err(),
160 "Parsing should fail without the required input_folder"
161 );
162 assert_eq!(
163 result.unwrap_err().kind(),
164 ErrorKind::MissingRequiredArgument
165 );
166 }
167
168 #[test]
170 fn test_no_subcommand_fails() {
171 let args = vec!["join-ai"];
172 let result = Cli::try_parse_from(args);
173
174 assert!(result.is_err(), "Parsing should fail without a subcommand");
175 assert_eq!(
176 result.unwrap_err().kind(),
177 ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
178 );
179 }
180}