Skip to main content

changepacks_cli/
lib.rs

1//! # changepacks-cli
2//!
3//! Command-line interface for the changepacks version management tool.
4//!
5//! Provides clap-based argument parsing, interactive prompts via inquire, and async
6//! command handlers for check, update, publish, config, and init operations. All commands
7//! use the `Prompter` trait for testability and support colored terminal output.
8
9use anyhow::Result;
10
11use changepacks_core::UpdateType;
12use clap::{Parser, Subcommand, ValueEnum};
13
14use crate::{
15    commands::{
16        ChangepackArgs, CheckArgs, ConfigArgs, InitArgs, PublishArgs, UpdateArgs,
17        handle_changepack, handle_check, handle_config, handle_init, handle_publish, handle_update,
18    },
19    options::{CliLanguage, FilterOptions},
20};
21pub mod commands;
22mod context;
23pub use context::*;
24mod finders;
25pub mod options;
26pub mod prompter;
27
28pub use prompter::UserCancelled;
29
30#[derive(ValueEnum, Debug, Clone)]
31enum CliUpdateType {
32    Major,
33    Minor,
34    Patch,
35}
36
37impl From<CliUpdateType> for UpdateType {
38    fn from(value: CliUpdateType) -> Self {
39        match value {
40            CliUpdateType::Major => Self::Major,
41            CliUpdateType::Minor => Self::Minor,
42            CliUpdateType::Patch => Self::Patch,
43        }
44    }
45}
46
47#[derive(Parser, Debug)]
48#[command(
49    name = "changepacks",
50    author,
51    version,
52    about = "A unified version management and changelog tool for multi-language projects",
53    help_template = "{name} {version}\n{about}\n\n{usage-heading} {usage}\n\n{all-args}"
54)]
55struct Cli {
56    #[command(subcommand)]
57    command: Option<Commands>,
58
59    #[arg(short, long)]
60    filter: Option<FilterOptions>,
61
62    #[arg(short, long, default_value = "false")]
63    remote: bool,
64
65    #[arg(short, long, default_value = "false")]
66    yes: bool,
67
68    #[arg(short, long)]
69    message: Option<String>,
70
71    #[arg(short, long)]
72    update_type: Option<CliUpdateType>,
73
74    /// Filter projects by language. Can be specified multiple times to include multiple languages.
75    #[arg(short, long, value_enum)]
76    language: Vec<CliLanguage>,
77}
78
79#[derive(Subcommand, Debug)]
80enum Commands {
81    Init(InitArgs),
82    Check(CheckArgs),
83    Update(UpdateArgs),
84    Config(ConfigArgs),
85    Publish(PublishArgs),
86}
87
88/// # Errors
89/// Returns error if command execution fails.
90pub async fn main(args: &[String]) -> Result<()> {
91    let cli = Cli::parse_from(args);
92    if let Some(command) = cli.command {
93        match command {
94            Commands::Init(args) => handle_init(&args).await?,
95            Commands::Check(args) => handle_check(&args).await?,
96            Commands::Update(args) => handle_update(&args).await?,
97            Commands::Config(args) => handle_config(&args).await?,
98            Commands::Publish(args) => handle_publish(&args).await?,
99        }
100    } else {
101        handle_changepack(&ChangepackArgs {
102            filter: cli.filter,
103            remote: cli.remote,
104            yes: cli.yes,
105            message: cli.message,
106            update_type: cli.update_type.map(Into::into),
107            language: cli.language,
108        })
109        .await?;
110    }
111    Ok(())
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use rstest::rstest;
118
119    #[rstest]
120    #[case(CliUpdateType::Major, UpdateType::Major)]
121    #[case(CliUpdateType::Minor, UpdateType::Minor)]
122    #[case(CliUpdateType::Patch, UpdateType::Patch)]
123    fn test_cli_update_type_to_update_type(
124        #[case] cli_type: CliUpdateType,
125        #[case] expected: UpdateType,
126    ) {
127        let result: UpdateType = cli_type.into();
128        assert_eq!(result, expected);
129    }
130
131    // Test that Cli struct parses correctly
132    #[test]
133    fn test_cli_parsing_init() {
134        use clap::Parser;
135        let cli = Cli::parse_from(["changepacks", "init"]);
136        assert!(matches!(cli.command, Some(Commands::Init(_))));
137    }
138
139    #[test]
140    fn test_cli_parsing_check() {
141        use clap::Parser;
142        let cli = Cli::parse_from(["changepacks", "check"]);
143        assert!(matches!(cli.command, Some(Commands::Check(_))));
144    }
145
146    #[test]
147    fn test_cli_parsing_update() {
148        use clap::Parser;
149        let cli = Cli::parse_from(["changepacks", "update", "--dry-run"]);
150        assert!(matches!(cli.command, Some(Commands::Update(_))));
151    }
152
153    #[test]
154    fn test_cli_parsing_config() {
155        use clap::Parser;
156        let cli = Cli::parse_from(["changepacks", "config"]);
157        assert!(matches!(cli.command, Some(Commands::Config(_))));
158    }
159
160    #[test]
161    fn test_cli_parsing_publish() {
162        use clap::Parser;
163        let cli = Cli::parse_from(["changepacks", "publish", "--dry-run"]);
164        assert!(matches!(cli.command, Some(Commands::Publish(_))));
165    }
166
167    #[test]
168    fn test_cli_parsing_default_with_options() {
169        use clap::Parser;
170        let cli = Cli::parse_from([
171            "changepacks",
172            "--yes",
173            "--message",
174            "test",
175            "--update-type",
176            "patch",
177        ]);
178        assert!(cli.command.is_none());
179        assert!(cli.yes);
180        assert_eq!(cli.message, Some("test".to_string()));
181        assert!(matches!(cli.update_type, Some(CliUpdateType::Patch)));
182    }
183
184    #[test]
185    fn test_cli_parsing_with_filter() {
186        use clap::Parser;
187        let cli = Cli::parse_from(["changepacks", "--filter", "package"]);
188        assert!(cli.command.is_none());
189        assert!(matches!(cli.filter, Some(FilterOptions::Package)));
190    }
191
192    #[test]
193    fn test_cli_parsing_with_remote() {
194        use clap::Parser;
195        let cli = Cli::parse_from(["changepacks", "--remote"]);
196        assert!(cli.remote);
197    }
198
199    #[test]
200    fn test_cli_parsing_with_language() {
201        use clap::Parser;
202        let cli = Cli::parse_from(["changepacks", "--language", "node"]);
203        assert_eq!(cli.language.len(), 1);
204    }
205
206    #[test]
207    fn test_cli_parsing_with_multiple_languages() {
208        use clap::Parser;
209        let cli = Cli::parse_from(["changepacks", "--language", "node", "--language", "rust"]);
210        assert_eq!(cli.language.len(), 2);
211    }
212}