Skip to main content

truf_cli/
lib.rs

1use clap::{Parser, Subcommand, ValueEnum};
2
3pub mod config;
4
5/// Multi-registry name discovery and reservation CLI.
6#[derive(Debug, Parser)]
7#[command(name = "truf", version, about)]
8pub struct Cli {
9    /// Output format.
10    #[arg(short = 'o', long, global = true, default_value = "table")]
11    pub output: OutputFormatArg,
12
13    /// Shorthand for --output json.
14    #[arg(long, global = true)]
15    pub json: bool,
16
17    /// Increase verbosity (-v, -vv, -vvv).
18    #[arg(short, long, global = true, action = clap::ArgAction::Count)]
19    pub verbose: u8,
20
21    /// Suppress non-essential output.
22    #[arg(short, long, global = true)]
23    pub quiet: bool,
24
25    /// Color output control.
26    #[arg(long, global = true, default_value = "auto")]
27    pub color: ColorChoice,
28
29    /// HTTP request timeout in seconds.
30    #[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 for available names across registries.
64    Search {
65        /// Seed word for candidate generation.
66        seed: Option<String>,
67
68        /// Registries to search (comma-separated or repeated).
69        #[arg(short = 'r', long, value_delimiter = ',', default_values_t = ["crates".to_string(), "npm".to_string(), "pypi".to_string()])]
70        registry: Vec<String>,
71
72        /// Minimum candidate name length.
73        #[arg(long)]
74        min_len: Option<usize>,
75
76        /// Maximum candidate name length.
77        #[arg(long)]
78        max_len: Option<usize>,
79
80        /// Required name prefix.
81        #[arg(long)]
82        prefix: Option<String>,
83
84        /// Required name suffix.
85        #[arg(long)]
86        suffix: Option<String>,
87
88        /// Required substring.
89        #[arg(long)]
90        contains: Option<String>,
91
92        /// Maximum number of results.
93        #[arg(short = 'n', long = "limit", default_value = "25")]
94        limit: usize,
95
96        /// Maximum concurrent registry checks.
97        #[arg(long, default_value = "8")]
98        concurrency: usize,
99
100        /// Show all results including taken names.
101        #[arg(long)]
102        all: bool,
103    },
104
105    /// Check specific names across registries.
106    Check {
107        /// Names to check.
108        names: Vec<String>,
109
110        /// Registries to check (comma-separated or repeated).
111        #[arg(short = 'r', long, value_delimiter = ',', default_values_t = ["crates".to_string(), "npm".to_string(), "pypi".to_string()])]
112        registry: Vec<String>,
113
114        /// Maximum concurrent registry checks.
115        #[arg(long, default_value = "8")]
116        concurrency: usize,
117
118        /// Only show available names.
119        #[arg(short = 'a', long)]
120        available_only: bool,
121    },
122
123    /// Show registry status and configuration.
124    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}