winbrew_cli/
cli.rs

1use clap::{Parser, Subcommand, ValueEnum};
2
3#[derive(Parser)]
4#[command(
5    name = "winbrew",
6    version = concat!(env!("CARGO_PKG_VERSION"), " (", env!("WINBREW_GIT_HASH"), ")"),
7    about = "A modern package manager for Windows that tracks and cleanly removes software.",
8    arg_required_else_help = true
9)]
10pub struct Cli {
11    /// Increase error detail output.
12    #[arg(short, long, global = true, action = clap::ArgAction::Count, help_heading = "Output")]
13    pub verbose: u8,
14
15    #[command(subcommand)]
16    pub command: Command,
17}
18
19#[derive(Debug, PartialEq, Eq, Subcommand)]
20pub enum Command {
21    /// List packages installed by winbrew
22    List {
23        #[arg(value_name = "QUERY", num_args = 0..)]
24        query: Vec<String>,
25    },
26
27    /// Search the package catalog
28    Search {
29        #[arg(value_name = "QUERY", num_args = 1..)]
30        query: Vec<String>,
31    },
32
33    /// Install a package from the catalog
34    /// Use `@winget/<id>` or `@scoop/<bucket>/<id>` for exact package IDs.
35    Install {
36        #[arg(value_name = "QUERY", num_args = 1..)]
37        query: Vec<String>,
38
39        #[arg(long, help_heading = "Safety")]
40        ignore_checksum_security: bool,
41
42        /// Render the install plan without making changes
43        #[arg(long, help_heading = "Output")]
44        plan: bool,
45    },
46
47    /// Show effective runtime settings and paths
48    Info,
49
50    /// Print the winbrew version
51    Version,
52
53    /// Check local winbrew installation health
54    Doctor {
55        /// Emit machine-readable JSON instead of the standard UI output
56        #[arg(long, help_heading = "Output")]
57        json: bool,
58
59        /// Treat warnings as failures and exit non-zero when warnings are found
60        #[arg(long, help_heading = "Output")]
61        warn_as_error: bool,
62    },
63
64    /// Refresh the package catalog
65    Update,
66
67    /// Remove a package and its tracked files
68    Remove {
69        #[arg(value_name = "PACKAGE", num_args = 1.., required = true)]
70        name: Vec<String>,
71
72        #[arg(long, short = 'y', help_heading = "Safety")]
73        yes: bool,
74
75        #[arg(long, help_heading = "Safety")]
76        force: bool,
77    },
78
79    /// Replay committed journals into the local database
80    Repair {
81        /// Proceed without prompting for confirmation
82        #[arg(long, short = 'y', help_heading = "Safety")]
83        yes: bool,
84    },
85
86    /// Get or set winbrew configuration values.
87    ///
88    /// Configuration keys are hierarchical:
89    /// - `core.*` controls runtime behavior and logging
90    /// - `paths.*` controls storage locations
91    ///
92    /// Examples:
93    /// - `winbrew config get core.log_level`
94    /// - `winbrew config get paths.root`
95    /// - `winbrew config set core.color false`
96    /// - `winbrew config unset paths.cache`
97    Config {
98        #[command(subcommand)]
99        command: ConfigCommand,
100    },
101
102    /// Generate shell completions for winbrew.
103    Completions {
104        #[arg(value_enum)]
105        shell: CompletionShell,
106    },
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
110pub enum CompletionShell {
111    Bash,
112    Fish,
113    Zsh,
114    #[value(name = "powershell")]
115    PowerShell,
116}
117
118#[derive(Debug, PartialEq, Eq, Subcommand)]
119/// Get or set winbrew configuration values.
120///
121/// Configuration keys are hierarchical:
122/// - `core.*` controls runtime behavior and logging
123/// - `paths.*` controls storage locations
124///
125/// Examples:
126/// - `winbrew config get core.log_level`
127/// - `winbrew config get paths.root`
128/// - `winbrew config set core.color false`
129/// - `winbrew config unset paths.cache`
130pub enum ConfigCommand {
131    /// List all configuration values
132    List,
133
134    /// Read a configuration value
135    Get {
136        #[arg(value_name = "KEY")]
137        key: String,
138    },
139
140    /// Store a configuration value
141    Set {
142        #[arg(value_name = "KEY")]
143        key: String,
144
145        #[arg(value_name = "VALUE")]
146        value: Option<String>,
147    },
148
149    /// Remove a configuration value
150    Unset {
151        #[arg(value_name = "KEY")]
152        key: String,
153    },
154}
155
156#[cfg(test)]
157mod tests {
158    use super::{Cli, Command, ConfigCommand};
159    use clap::Parser;
160
161    #[test]
162    fn parse_verbose_doctor() {
163        let cli = Cli::parse_from(["brew", "doctor", "-vv"]);
164
165        assert_eq!(cli.verbose, 2);
166        assert_eq!(
167            cli.command,
168            Command::Doctor {
169                json: false,
170                warn_as_error: false,
171            }
172        );
173    }
174
175    #[test]
176    fn parse_config_unset_core_log_level() {
177        let cli = Cli::parse_from(["brew", "config", "unset", "core.log_level"]);
178
179        assert_eq!(
180            cli.command,
181            Command::Config {
182                command: ConfigCommand::Unset {
183                    key: "core.log_level".to_string(),
184                },
185            }
186        );
187    }
188
189    #[test]
190    fn parse_doctor() {
191        let cli = Cli::parse_from(["brew", "doctor"]);
192
193        assert_eq!(
194            cli.command,
195            Command::Doctor {
196                json: false,
197                warn_as_error: false,
198            }
199        );
200    }
201
202    #[test]
203    fn parse_doctor_json() {
204        let cli = Cli::parse_from(["brew", "doctor", "--json"]);
205
206        assert_eq!(
207            cli.command,
208            Command::Doctor {
209                json: true,
210                warn_as_error: false,
211            }
212        );
213    }
214
215    #[test]
216    fn parse_doctor_warn_as_error() {
217        let cli = Cli::parse_from(["brew", "doctor", "--warn-as-error"]);
218
219        assert_eq!(
220            cli.command,
221            Command::Doctor {
222                json: false,
223                warn_as_error: true,
224            }
225        );
226    }
227}