use crate::version::VersionBump;
use clap::{Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
use std::time::Duration;
#[derive(Parser, Debug)]
#[command(
name = "cyrup_release",
version,
about = "Production-quality release management for Rust workspaces",
long_about = "Cyrup Release provides atomic release operations with proper error handling,
automatic internal dependency version synchronization, and rollback capabilities
including crate yanking for published packages."
)]
pub struct Args {
#[command(subcommand)]
pub command: Command,
#[arg(short, long, global = true)]
pub verbose: bool,
#[arg(short, long, global = true, conflicts_with = "verbose")]
pub quiet: bool,
#[arg(short, long, global = true, value_name = "PATH")]
pub workspace: Option<PathBuf>,
#[arg(long, global = true, value_name = "PATH")]
pub state_file: Option<PathBuf>,
#[arg(short, long, global = true, value_name = "PATH")]
pub config: Option<PathBuf>,
}
#[derive(Subcommand, Debug)]
pub enum Command {
Release {
#[arg(value_enum)]
bump_type: BumpType,
#[arg(short, long)]
dry_run: bool,
#[arg(long)]
skip_validation: bool,
#[arg(long)]
allow_dirty: bool,
#[arg(long)]
no_push: bool,
#[arg(long, value_name = "REGISTRY")]
registry: Option<String>,
#[arg(long, default_value = "15", value_name = "SECONDS")]
package_delay: u64,
#[arg(long, default_value = "3", value_name = "COUNT")]
max_retries: usize,
#[arg(long, default_value = "300", value_name = "SECONDS")]
timeout: u64,
#[arg(long)]
no_backup: bool,
#[arg(long, default_value = "1", value_name = "COUNT")]
max_concurrent: usize,
},
Rollback {
#[arg(short, long)]
force: bool,
#[arg(long)]
git_only: bool,
#[arg(long, conflicts_with = "git_only")]
packages_only: bool,
#[arg(short, long)]
yes: bool,
},
Resume {
#[arg(short, long)]
force: bool,
#[arg(long, value_enum)]
reset_to_phase: Option<ResumePhase>,
#[arg(long)]
skip_validation: bool,
},
Status {
#[arg(short, long)]
detailed: bool,
#[arg(long)]
history: bool,
#[arg(long)]
json: bool,
},
Cleanup {
#[arg(short, long)]
all: bool,
#[arg(long, value_name = "DAYS")]
older_than: Option<u32>,
#[arg(short, long)]
yes: bool,
},
Validate {
#[arg(long)]
fix: bool,
#[arg(short, long)]
detailed: bool,
#[arg(long)]
json: bool,
},
Preview {
#[arg(value_enum)]
bump_type: BumpType,
#[arg(short, long)]
detailed: bool,
#[arg(long)]
json: bool,
},
}
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
pub enum BumpType {
Major,
Minor,
Patch,
Exact,
}
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
pub enum ResumePhase {
Validation,
VersionUpdate,
GitOperations,
Publishing,
}
impl From<BumpType> for VersionBump {
fn from(bump_type: BumpType) -> Self {
match bump_type {
BumpType::Major => VersionBump::Major,
BumpType::Minor => VersionBump::Minor,
BumpType::Patch => VersionBump::Patch,
BumpType::Exact => {
panic!("Exact version bump requires additional version parameter")
}
}
}
}
impl Args {
pub fn parse_args() -> Self {
Self::parse()
}
pub fn workspace_path(&self) -> PathBuf {
self.workspace.clone().unwrap_or_else(|| PathBuf::from("."))
}
pub fn state_file_path(&self) -> PathBuf {
self.state_file.clone()
.unwrap_or_else(|| PathBuf::from(".cyrup_release_state.json"))
}
pub fn is_verbose(&self) -> bool {
self.verbose && !self.quiet
}
pub fn is_quiet(&self) -> bool {
self.quiet
}
pub fn validate(&self) -> Result<(), String> {
if self.verbose && self.quiet {
return Err("Cannot specify both --verbose and --quiet".to_string());
}
if let Some(ref workspace) = self.workspace {
if !workspace.exists() {
return Err(format!("Workspace path does not exist: {}", workspace.display()));
}
if !workspace.is_dir() {
return Err(format!("Workspace path is not a directory: {}", workspace.display()));
}
}
if let Some(ref state_file) = self.state_file {
if let Some(parent) = state_file.parent() {
if !parent.exists() {
return Err(format!("State file directory does not exist: {}", parent.display()));
}
}
}
match &self.command {
Command::Release {
package_delay,
max_retries,
timeout,
..
} => {
if *package_delay > 3600 {
return Err("Package delay cannot exceed 1 hour (3600 seconds)".to_string());
}
if *max_retries > 10 {
return Err("Max retries cannot exceed 10".to_string());
}
if *timeout < 30 {
return Err("Timeout cannot be less than 30 seconds".to_string());
}
if *timeout > 3600 {
return Err("Timeout cannot exceed 1 hour (3600 seconds)".to_string());
}
}
Command::Cleanup { older_than, .. } => {
if let Some(days) = older_than {
if *days > 365 {
return Err("Cleanup age cannot exceed 365 days".to_string());
}
}
}
_ => {}
}
Ok(())
}
}
impl Command {
pub fn name(&self) -> &'static str {
match self {
Command::Release { .. } => "release",
Command::Rollback { .. } => "rollback",
Command::Resume { .. } => "resume",
Command::Status { .. } => "status",
Command::Cleanup { .. } => "cleanup",
Command::Validate { .. } => "validate",
Command::Preview { .. } => "preview",
}
}
pub fn requires_state(&self) -> bool {
matches!(self, Command::Rollback { .. } | Command::Resume { .. })
}
pub fn is_modifying(&self) -> bool {
matches!(
self,
Command::Release { dry_run: false, .. } |
Command::Rollback { .. } |
Command::Resume { .. } |
Command::Validate { fix: true, .. }
)
}
pub fn requires_validation(&self) -> bool {
matches!(
self,
Command::Release { skip_validation: false, .. } |
Command::Resume { skip_validation: false, .. }
)
}
}
#[derive(Debug, Clone)]
pub struct RuntimeConfig {
pub workspace_path: PathBuf,
pub state_file_path: PathBuf,
pub verbosity: VerbosityLevel,
pub package_delay: Duration,
pub max_retries: usize,
pub timeout: Duration,
pub registry: Option<String>,
pub create_backups: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VerbosityLevel {
Quiet,
Normal,
Verbose,
}
impl From<&Args> for RuntimeConfig {
fn from(args: &Args) -> Self {
let verbosity = if args.quiet {
VerbosityLevel::Quiet
} else if args.verbose {
VerbosityLevel::Verbose
} else {
VerbosityLevel::Normal
};
let (package_delay, max_retries, timeout, registry, create_backups) = match &args.command {
Command::Release {
package_delay,
max_retries,
timeout,
registry,
no_backup,
..
} => (
Duration::from_secs(*package_delay),
*max_retries,
Duration::from_secs(*timeout),
registry.clone(),
!no_backup,
),
_ => (
Duration::from_secs(15), 3, Duration::from_secs(300), None, true, ),
};
Self {
workspace_path: args.workspace_path(),
state_file_path: args.state_file_path(),
verbosity,
package_delay,
max_retries,
timeout,
registry,
create_backups,
}
}
}
impl RuntimeConfig {
pub fn is_quiet(&self) -> bool {
self.verbosity == VerbosityLevel::Quiet
}
pub fn is_verbose(&self) -> bool {
self.verbosity == VerbosityLevel::Verbose
}
pub fn println(&self, message: &str) {
if !self.is_quiet() {
println!("{}", message);
}
}
pub fn verbose_println(&self, message: &str) {
if self.is_verbose() {
println!("🔍 {}", message);
}
}
pub fn error_println(&self, message: &str) {
eprintln!("❌ {}", message);
}
pub fn warning_println(&self, message: &str) {
if !self.is_quiet() {
println!("⚠️ {}", message);
}
}
pub fn success_println(&self, message: &str) {
if !self.is_quiet() {
println!("✅ {}", message);
}
}
}