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::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
75#[derive(Subcommand, Debug)]
76enum Commands {
77    Init(InitArgs),
78    Check(CheckArgs),
79    Update(UpdateArgs),
80    Config(ConfigArgs),
81    Publish(PublishArgs),
82}
83
84/// # Errors
85/// Returns error if command execution fails.
86pub async fn main(args: &[String]) -> Result<()> {
87    let cli = Cli::parse_from(args);
88    if let Some(command) = cli.command {
89        match command {
90            Commands::Init(args) => handle_init(&args).await?,
91            Commands::Check(args) => handle_check(&args).await?,
92            Commands::Update(args) => handle_update(&args).await?,
93            Commands::Config(args) => handle_config(&args).await?,
94            Commands::Publish(args) => handle_publish(&args).await?,
95        }
96    } else {
97        handle_changepack(&ChangepackArgs {
98            filter: cli.filter,
99            remote: cli.remote,
100            yes: cli.yes,
101            message: cli.message,
102            update_type: cli.update_type.map(Into::into),
103        })
104        .await?;
105    }
106    Ok(())
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use rstest::rstest;
113
114    #[rstest]
115    #[case(CliUpdateType::Major, UpdateType::Major)]
116    #[case(CliUpdateType::Minor, UpdateType::Minor)]
117    #[case(CliUpdateType::Patch, UpdateType::Patch)]
118    fn test_cli_update_type_to_update_type(
119        #[case] cli_type: CliUpdateType,
120        #[case] expected: UpdateType,
121    ) {
122        let result: UpdateType = cli_type.into();
123        assert_eq!(result, expected);
124    }
125
126    // Test that Cli struct parses correctly
127    #[test]
128    fn test_cli_parsing_init() {
129        use clap::Parser;
130        let cli = Cli::parse_from(["changepacks", "init"]);
131        assert!(matches!(cli.command, Some(Commands::Init(_))));
132    }
133
134    #[test]
135    fn test_cli_parsing_check() {
136        use clap::Parser;
137        let cli = Cli::parse_from(["changepacks", "check"]);
138        assert!(matches!(cli.command, Some(Commands::Check(_))));
139    }
140
141    #[test]
142    fn test_cli_parsing_update() {
143        use clap::Parser;
144        let cli = Cli::parse_from(["changepacks", "update", "--dry-run"]);
145        assert!(matches!(cli.command, Some(Commands::Update(_))));
146    }
147
148    #[test]
149    fn test_cli_parsing_config() {
150        use clap::Parser;
151        let cli = Cli::parse_from(["changepacks", "config"]);
152        assert!(matches!(cli.command, Some(Commands::Config(_))));
153    }
154
155    #[test]
156    fn test_cli_parsing_publish() {
157        use clap::Parser;
158        let cli = Cli::parse_from(["changepacks", "publish", "--dry-run"]);
159        assert!(matches!(cli.command, Some(Commands::Publish(_))));
160    }
161
162    #[test]
163    fn test_cli_parsing_default_with_options() {
164        use clap::Parser;
165        let cli = Cli::parse_from([
166            "changepacks",
167            "--yes",
168            "--message",
169            "test",
170            "--update-type",
171            "patch",
172        ]);
173        assert!(cli.command.is_none());
174        assert!(cli.yes);
175        assert_eq!(cli.message, Some("test".to_string()));
176        assert!(matches!(cli.update_type, Some(CliUpdateType::Patch)));
177    }
178
179    #[test]
180    fn test_cli_parsing_with_filter() {
181        use clap::Parser;
182        let cli = Cli::parse_from(["changepacks", "--filter", "package"]);
183        assert!(cli.command.is_none());
184        assert!(matches!(cli.filter, Some(FilterOptions::Package)));
185    }
186
187    #[test]
188    fn test_cli_parsing_with_remote() {
189        use clap::Parser;
190        let cli = Cli::parse_from(["changepacks", "--remote"]);
191        assert!(cli.remote);
192    }
193}