1use clap::{Parser, Subcommand, ValueEnum};
2
3pub mod config;
4
5#[derive(Debug, Parser)]
7#[command(name = "truf", version, about)]
8pub struct Cli {
9 #[arg(short = 'o', long, global = true, default_value = "table")]
11 pub output: OutputFormatArg,
12
13 #[arg(long, global = true)]
15 pub json: bool,
16
17 #[arg(short, long, global = true, action = clap::ArgAction::Count)]
19 pub verbose: u8,
20
21 #[arg(short, long, global = true)]
23 pub quiet: bool,
24
25 #[arg(long, global = true, default_value = "auto")]
27 pub color: ColorChoice,
28
29 #[arg(long, global = true, default_value = "30")]
31 pub timeout: u64,
32
33 #[command(subcommand)]
34 pub command: Commands,
35}
36
37#[derive(Debug, Clone, Copy, ValueEnum)]
38pub enum OutputFormatArg {
39 Table,
40 Json,
41 Ndjson,
42}
43
44impl From<OutputFormatArg> for truf_registry::output::OutputFormat {
45 fn from(val: OutputFormatArg) -> Self {
46 match val {
47 OutputFormatArg::Table => Self::Table,
48 OutputFormatArg::Json => Self::Json,
49 OutputFormatArg::Ndjson => Self::Ndjson,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, ValueEnum)]
55pub enum ColorChoice {
56 Auto,
57 Always,
58 Never,
59}
60
61#[derive(Debug, Subcommand)]
62pub enum Commands {
63 Search {
65 seed: Option<String>,
67
68 #[arg(short = 'r', long, value_delimiter = ',', default_values_t = ["crates".to_string(), "npm".to_string(), "pypi".to_string()])]
70 registry: Vec<String>,
71
72 #[arg(long)]
74 min_len: Option<usize>,
75
76 #[arg(long)]
78 max_len: Option<usize>,
79
80 #[arg(long)]
82 prefix: Option<String>,
83
84 #[arg(long)]
86 suffix: Option<String>,
87
88 #[arg(long)]
90 contains: Option<String>,
91
92 #[arg(short = 'n', long = "limit", default_value = "25")]
94 limit: usize,
95
96 #[arg(long, default_value = "8")]
98 concurrency: usize,
99
100 #[arg(long)]
102 all: bool,
103 },
104
105 Check {
107 names: Vec<String>,
109
110 #[arg(short = 'r', long, value_delimiter = ',', default_values_t = ["crates".to_string(), "npm".to_string(), "pypi".to_string()])]
112 registry: Vec<String>,
113
114 #[arg(long, default_value = "8")]
116 concurrency: usize,
117
118 #[arg(short = 'a', long)]
120 available_only: bool,
121 },
122
123 Status,
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use clap::Parser;
131
132 #[test]
133 fn parse_check_subcommand() {
134 let cli = Cli::try_parse_from(["truf", "check", "foo", "bar"]).unwrap();
135 match cli.command {
136 Commands::Check {
137 names,
138 registry,
139 concurrency,
140 available_only,
141 } => {
142 assert_eq!(names, vec!["foo", "bar"]);
143 assert_eq!(registry, vec!["crates", "npm", "pypi"]);
144 assert_eq!(concurrency, 8);
145 assert!(!available_only);
146 }
147 _ => panic!("expected Commands::Check"),
148 }
149 }
150
151 #[test]
152 fn parse_check_multi_registry() {
153 let cli =
154 Cli::try_parse_from(["truf", "check", "foo", "--registry", "crates,npm,pypi"]).unwrap();
155 match cli.command {
156 Commands::Check {
157 names, registry, ..
158 } => {
159 assert_eq!(names, vec!["foo"]);
160 assert_eq!(registry, vec!["crates", "npm", "pypi"]);
161 }
162 _ => panic!("expected Commands::Check"),
163 }
164 }
165
166 #[test]
167 fn parse_search_subcommand() {
168 let cli = Cli::try_parse_from(["truf", "search", "test"]).unwrap();
169 match cli.command {
170 Commands::Search {
171 seed,
172 registry,
173 limit,
174 all,
175 ..
176 } => {
177 assert_eq!(seed, Some("test".to_string()));
178 assert_eq!(registry, vec!["crates", "npm", "pypi"]);
179 assert_eq!(limit, 25);
180 assert!(!all);
181 }
182 _ => panic!("expected Commands::Search"),
183 }
184 }
185
186 #[test]
187 fn parse_status_subcommand() {
188 let cli = Cli::try_parse_from(["truf", "status"]).unwrap();
189 assert!(matches!(cli.command, Commands::Status));
190 }
191
192 #[test]
193 fn parse_global_flags() {
194 let cli = Cli::try_parse_from(["truf", "--output", "json", "-v", "status"]).unwrap();
195 assert!(matches!(cli.output, OutputFormatArg::Json));
196 assert_eq!(cli.verbose, 1);
197 }
198
199 #[test]
200 fn parse_quiet_flag() {
201 let cli = Cli::try_parse_from(["truf", "--quiet", "status"]).unwrap();
202 assert!(cli.quiet);
203 }
204
205 #[test]
206 fn parse_timeout_flag() {
207 let cli = Cli::try_parse_from(["truf", "--timeout", "10", "status"]).unwrap();
208 assert_eq!(cli.timeout, 10);
209 }
210
211 #[test]
212 fn parse_timeout_default() {
213 let cli = Cli::try_parse_from(["truf", "status"]).unwrap();
214 assert_eq!(cli.timeout, 30);
215 }
216
217 #[test]
218 fn parse_json_flag() {
219 let cli = Cli::try_parse_from(["truf", "--json", "status"]).unwrap();
220 assert!(cli.json);
221 }
222
223 #[test]
224 fn parse_no_subcommand_fails() {
225 assert!(Cli::try_parse_from(["truf"]).is_err());
226 }
227}