use std::{
env,
ffi::OsString,
path::{Path, PathBuf},
process::ExitCode,
};
use anyhow::Context;
use clap::Parser;
use crate::workspace::Workspace;
mod bump;
mod changelog;
mod changeset;
mod commands;
mod config;
mod jsonc;
mod output;
mod package_json;
mod plan;
mod pre;
mod release_plan;
mod skip;
mod snapshot;
mod workspace;
#[derive(Parser)]
#[command(version, args_conflicts_with_subcommands = true)]
struct Cli {
#[command(subcommand)]
command: Option<Command>,
#[command(flatten)]
add: AddArgs,
#[arg(long, value_name = "LEVEL", global = true, default_value = "info")]
log_level: LogLevel,
#[arg(long, value_name = "DIR", global = true, env = "CHANGESETTE_ROOT")]
root: Option<OsString>,
}
#[derive(Clone, Copy, clap::ValueEnum)]
enum LogLevel {
Error,
Warn,
Info,
Debug,
}
impl LogLevel {
fn filter(self) -> tracing::level_filters::LevelFilter {
use tracing::level_filters::LevelFilter;
match self {
LogLevel::Error => LevelFilter::ERROR,
LogLevel::Warn => LevelFilter::WARN,
LogLevel::Info => LevelFilter::INFO,
LogLevel::Debug => LevelFilter::DEBUG,
}
}
}
#[derive(clap::Args)]
struct AddArgs {
#[arg(long, conflicts_with_all = ["major", "minor", "patch"])]
empty: bool,
#[arg(long)]
open: bool,
#[arg(short, long)]
message: Option<String>,
#[arg(long, value_name = "PACKAGES", value_delimiter = ',')]
major: Vec<String>,
#[arg(long, value_name = "PACKAGES", value_delimiter = ',')]
minor: Vec<String>,
#[arg(long, value_name = "PACKAGES", value_delimiter = ',')]
patch: Vec<String>,
}
#[derive(clap::Args)]
struct VersionArgs {
#[arg(long, value_name = "PACKAGES", value_delimiter = ',')]
ignore: Vec<String>,
#[arg(
long,
value_name = "TAG",
num_args = 0..=1,
value_parser = clap::builder::NonEmptyStringValueParser::new()
)]
#[expect(clippy::option_option)]
snapshot: Option<Option<String>>,
#[arg(
long,
value_name = "TEMPLATE",
requires = "snapshot",
value_parser = clap::builder::NonEmptyStringValueParser::new()
)]
snapshot_prerelease_template: Option<String>,
#[arg(short, long)]
allow_no_changesets: bool,
#[arg(short, long, value_name = "FILE")]
output: Option<PathBuf>,
}
#[derive(clap::Subcommand)]
enum Command {
Init,
Add(AddArgs),
Version(VersionArgs),
Pre {
#[command(subcommand)]
command: PreCommand,
},
Status {
#[arg(short, long)]
verbose: bool,
#[arg(short, long, value_name = "FILE")]
output: Option<PathBuf>,
},
GetPackages {
#[arg(long)]
all: bool,
},
GetChangelogEntry {
package: String,
version: semver::Version,
},
SetSummary {
id: String,
summary: String,
},
}
#[derive(clap::Subcommand)]
enum PreCommand {
Enter {
tag: String,
},
Exit,
}
fn main() -> ExitCode {
let cli = Cli::parse();
output::init_subscriber(cli.log_level.filter());
match run(cli) {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
tracing::error!("{err:#}");
ExitCode::FAILURE
}
}
}
fn run(cli: Cli) -> anyhow::Result<()> {
let (root, marker) = if let Some(dir) = cli.root.filter(|dir| !dir.is_empty()) {
let dir = Path::new(&dir);
let root = workspace::resolve_root(dir)
.with_context(|| format!("invalid --root {}", dir.display()))?;
(root, None)
} else {
let cwd = env::current_dir()?;
let cwd = workspace::resolve_root(&cwd)
.with_context(|| format!("invalid working directory {}", cwd.display()))?;
let (root, marker) = workspace::find_root(&cwd)?;
(root, Some(marker))
};
let config = config::load(&root.join(".changeset"))?;
let workspace = Workspace::load(&root, config.packages.as_deref(), marker)?;
match cli.command.unwrap_or(Command::Add(cli.add)) {
Command::Init => commands::init::run(&workspace),
Command::Add(args) => commands::add::run(&workspace, &config, args),
Command::Version(args) => commands::version::run(workspace, &config, args),
Command::Pre { command } => match command {
PreCommand::Enter { tag } => commands::pre::enter(&workspace, &tag),
PreCommand::Exit => commands::pre::exit(&workspace),
},
Command::Status { verbose, output } => {
commands::status::run(workspace, &config, verbose, output.as_deref())
}
Command::GetPackages { all } => commands::get_packages::run(&workspace, &config, all),
Command::GetChangelogEntry { package, version } => {
commands::get_changelog_entry::run(&workspace, &package, &version)
}
Command::SetSummary { id, summary } => {
commands::set_summary::run(&workspace, &id, &summary)
}
}
}