mod changes;
mod config;
mod project;
mod publish;
mod ui;
use anyhow::Result;
use clap::{ArgAction, Parser, Subcommand};
#[derive(Parser)]
#[command(name = "cvm")]
#[command(about = "Crate Version Manager - Manages versions of Cargo packages")]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[arg(long = "crate", value_name = "NAME", action = ArgAction::Append)]
crates: Option<Vec<String>>,
#[arg(
long,
value_name = "TYPE",
value_parser = ["major", "minor", "patch"],
action = ArgAction::Append
)]
bump: Option<Vec<String>>,
#[arg(long, value_name = "TEXT")]
summary: Option<String>,
#[arg(long)]
dry_run: bool,
}
#[derive(Subcommand)]
enum Commands {
Setup,
Apply {
#[arg(long)]
dry_run: bool,
},
Status,
Info {
#[arg(long, conflicts_with = "format")]
version: bool,
#[arg(long, default_value = "json", value_parser = ["json"])]
format: String,
},
Pre {
#[command(subcommand)]
action: PreAction,
},
Publish {
#[arg(long)]
dry_run: bool,
#[arg(long)]
token: Option<String>,
#[arg(long)]
allow_dirty: bool,
},
}
#[derive(Subcommand)]
enum PreAction {
Start {
identifier: Option<String>,
},
Exit,
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Some(Commands::Setup) => {
config::create_default_config()?;
println!("✅ CVM initialized successfully!");
println!("\nConfiguration file created at: .cvm/config.toml");
println!("Change files will be stored in: .cvm/changes/");
println!("\nNext steps:");
println!(" 1. Run 'cvm' to create version changes interactively");
println!(" 2. Run 'cvm apply' to apply pending changes");
println!(" 3. Run 'cvm status' to check for pending changes");
Ok(())
}
None => {
if cli.crates.is_some() || cli.bump.is_some() || cli.summary.is_some() {
return handle_non_interactive(cli.crates, cli.bump, cli.summary, cli.dry_run);
}
if !std::path::Path::new(".cvm/config.toml").exists() {
eprintln!("❌ CVM is not initialized in this project.");
eprintln!("\nRun 'cvm setup' to initialize CVM.");
std::process::exit(1);
}
let crates = project::analyze_project()?;
if crates.is_empty() {
println!("No crates found.");
return Ok(());
}
let mut remaining = crates;
let major_selected = ui::select_crates(
"Select crates for \x1b[91mmajor\x1b[0m bump:",
&mut remaining,
);
let minor_selected = if !remaining.is_empty() {
ui::select_crates(
"Select crates for \x1b[93mminor\x1b[0m bump:",
&mut remaining,
)
} else {
vec![]
};
let patch_selected = if !remaining.is_empty() {
ui::select_crates(
"Select crates for \x1b[94mpatch\x1b[0m bump:",
&mut remaining,
)
} else {
vec![]
};
if major_selected.is_empty() && minor_selected.is_empty() && patch_selected.is_empty() {
println!("No crates selected for bumping.");
return Ok(());
}
let summary = ui::prompt_summary();
changes::save_pending(&summary, &major_selected, &minor_selected, &patch_selected)?;
Ok(())
}
Some(Commands::Apply { dry_run }) => {
if !std::path::Path::new(".cvm").exists() {
eprintln!("❌ CVM is not initialized in this project.");
eprintln!("\nRun 'cvm setup' to initialize CVM.");
std::process::exit(1);
}
changes::load_and_apply_pending(dry_run)?;
Ok(())
}
Some(Commands::Status) => {
if !std::path::Path::new(".cvm").exists() {
eprintln!("❌ CVM is not initialized in this project.");
eprintln!("\nRun 'cvm setup' to initialize CVM.");
std::process::exit(1);
}
changes::check_pending_changes()?;
Ok(())
}
Some(Commands::Info { format, version }) => {
if version {
println!("{}", project::release_version()?);
return Ok(());
}
let crates = project::analyze_project()?;
let is_workspace = project::is_workspace_project()?;
if format == "json" {
if crates.len() == 1 && !is_workspace {
use std::io::{self, Write};
let stdout = io::stdout();
let mut handle = stdout.lock();
write!(handle, "{}", crates[0].version)?;
handle.flush()?;
} else {
use std::io::{self, Write};
let json = serde_json::to_string(&crates)?;
let stdout = io::stdout();
let mut handle = stdout.lock();
write!(handle, "{}", json)?;
handle.flush()?;
}
} else {
eprintln!("❌ Unsupported format: {}", format);
std::process::exit(1);
}
Ok(())
}
Some(Commands::Pre { action }) => {
match action {
PreAction::Start { identifier } => {
config::set_prerelease(true, identifier.as_deref())?;
println!("Prerelease mode started.");
}
PreAction::Exit => {
config::set_prerelease(false, None)?;
println!("Prerelease mode exited.");
}
}
Ok(())
}
Some(Commands::Publish {
dry_run,
token,
allow_dirty,
}) => {
if !std::path::Path::new(".cvm").exists() {
eprintln!("❌ CVM is not initialized in this project.");
eprintln!("\nRun 'cvm setup' to initialize CVM.");
std::process::exit(1);
}
let opts = publish::PublishOptions {
dry_run,
token,
allow_dirty,
};
let published = publish::publish_crates(&opts)?;
let json = serde_json::to_string(&published)?;
println!("\n::cvm-output-json::{}", json);
Ok(())
}
}
}
fn handle_non_interactive(
crate_args: Option<Vec<String>>,
bump_args: Option<Vec<String>>,
summary_arg: Option<String>,
dry_run: bool,
) -> Result<()> {
if !std::path::Path::new(".cvm/config.toml").exists() {
eprintln!("❌ CVM is not initialized in this project.");
eprintln!("\nRun 'cvm setup' to initialize CVM.");
std::process::exit(1);
}
let crate_names = crate_args.unwrap_or_default();
if crate_names.is_empty() {
eprintln!("❌ --crate is required. Specify one or more crate names, or use 'all'.");
std::process::exit(1);
}
let bump_types = bump_args.unwrap_or_default();
if bump_types.is_empty() {
eprintln!("❌ --bump is required (major, minor, or patch).");
std::process::exit(1);
}
let summary = match summary_arg {
Some(s) if !s.trim().is_empty() => s,
_ => {
eprintln!("❌ --summary is required and cannot be empty.");
std::process::exit(1);
}
};
let all_crates = project::analyze_project()?;
if all_crates.is_empty() {
eprintln!("❌ No crates found in the project.");
std::process::exit(1);
}
let crate_map: std::collections::HashMap<String, project::CrateInfo> = all_crates
.iter()
.map(|c| (c.name.clone(), c.clone()))
.collect();
let resolved: Vec<String> = if crate_names.len() == 1 && crate_names[0] == "all" {
all_crates.iter().map(|c| c.name.clone()).collect()
} else {
if crate_names.iter().any(|n| n == "all") {
eprintln!("❌ 'all' cannot be combined with specific crate names.");
std::process::exit(1);
}
let mut unknown: Vec<&str> = Vec::new();
for name in &crate_names {
if !crate_map.contains_key(name) {
unknown.push(name.as_str());
}
}
if !unknown.is_empty() {
let available: Vec<&str> = all_crates.iter().map(|c| c.name.as_str()).collect();
eprintln!(
"❌ Unknown crate(s): {}. Available: {}",
unknown.join(", "),
available.join(", ")
);
std::process::exit(1);
}
crate_names.clone()
};
let pairs: Vec<(&str, &str)> = if bump_types.len() == 1 {
resolved
.iter()
.map(|n| (n.as_str(), bump_types[0].as_str()))
.collect()
} else if bump_types.len() == resolved.len() {
resolved
.iter()
.zip(bump_types.iter())
.map(|(n, b)| (n.as_str(), b.as_str()))
.collect()
} else {
eprintln!(
"❌ Number of --bump values ({}) must be 1 or match the number of --crate values ({}).",
bump_types.len(),
resolved.len()
);
std::process::exit(1);
};
let mut major: Vec<project::CrateInfo> = Vec::new();
let mut minor: Vec<project::CrateInfo> = Vec::new();
let mut patch: Vec<project::CrateInfo> = Vec::new();
for (name, bump) in &pairs {
let info = crate_map[*name].clone();
match *bump {
"major" => major.push(info),
"minor" => minor.push(info),
"patch" => patch.push(info),
_ => unreachable!(), }
}
if dry_run {
let toml_content = changes::generate_pending_toml(&summary, &major, &minor, &patch)?;
println!("DRY RUN — change file that would be written to .cvm/changes/:\n");
print!("{}", toml_content);
} else {
changes::save_pending(&summary, &major, &minor, &patch)?;
println!("✅ Change staged successfully.");
println!(" Run 'cvm status' to review pending changes.");
println!(" Run 'cvm apply' to apply them.");
}
Ok(())
}