#![allow(unstable_features)]
#![cfg_attr(test, allow(unused_imports, unused_qualifications))]
#![feature(coverage_attribute)]
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::ffi::OsString;
use std::fs;
#[cfg(feature = "mcp")]
use std::future::Future;
use std::io::IsTerminal;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command as ProcessCommand;
use std::process::ExitCode;
use std::time::Duration;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use analyze::render_analyze_report;
pub(crate) use monochange_changelog::ChangelogBuildContext;
pub(crate) use monochange_changelog::build_changelog_updates;
#[cfg(test)]
pub(crate) use monochange_changelog::render_group_filtered_update_message;
pub(crate) use monochange_changelog::render_jinja_template;
pub mod changelog {
pub use monochange_changelog::render_message_template;
}
pub use changeset_policy::affected_packages;
pub(crate) use changeset_policy::compute_changed_paths_since;
pub use changeset_policy::evaluate_changeset_policy;
pub(crate) use changeset_policy::is_changeset_markdown_path;
pub(crate) use changeset_policy::normalize_changed_path;
pub use changeset_policy::verify_changesets;
pub(crate) use changesets::*;
use clap::ValueEnum;
use clap::error::ErrorKind;
#[cfg(test)]
pub(crate) use cli::apply_runtime_change_type_choices;
#[cfg(test)]
pub(crate) use cli::apply_runtime_prepare_release_markdown_defaults;
#[cfg(test)]
pub(crate) use cli::build_cli_command_subcommand;
pub use cli::build_command;
#[cfg(test)]
pub(crate) use cli::build_command_for_root;
use cli::build_command_with_cli;
#[cfg(test)]
pub(crate) use cli::build_skill_subcommand;
#[cfg(test)]
pub(crate) use cli::build_subagents_subcommand;
#[cfg(test)]
pub(crate) use cli::cli_command_after_help;
#[cfg(test)]
pub(crate) use cli::cli_commands_for_root;
use cli::cli_commands_from_config;
#[cfg(test)]
pub(crate) use cli::configured_change_type_choices;
use cli::current_dir_or_dot;
#[cfg(test)]
pub(crate) use cli_runtime::build_cli_template_context;
#[cfg(test)]
pub(crate) use cli_runtime::build_retarget_release_report;
pub(crate) use cli_runtime::collect_cli_command_inputs;
pub(crate) use cli_runtime::execute_cli_command;
use cli_runtime::execute_matches;
#[cfg(test)]
pub(crate) use cli_runtime::inferred_retarget_source_configuration;
#[cfg(test)]
pub(crate) use cli_runtime::lookup_template_value;
pub(crate) use cli_runtime::maybe_render_markdown_for_terminal;
#[cfg(test)]
pub(crate) use cli_runtime::parse_boolean_step_input;
#[cfg(test)]
pub(crate) use cli_runtime::parse_change_bump;
#[cfg(test)]
pub(crate) use cli_runtime::parse_direct_template_reference;
pub(crate) use cli_runtime::parse_output_format;
#[cfg(test)]
pub(crate) use cli_runtime::render_cli_command_markdown_result;
#[cfg(test)]
pub(crate) use cli_runtime::render_cli_command_result;
#[cfg(test)]
pub(crate) use cli_runtime::render_markdown_if_terminal;
#[cfg(test)]
pub(crate) use cli_runtime::render_retarget_release_report;
#[cfg(test)]
pub(crate) use cli_runtime::retarget_operation_label;
#[cfg(test)]
pub(crate) use cli_runtime::template_value_to_input_values;
use command_wizard::run_command_wizard;
use git_support::git_commit_paths;
use git_support::git_head_commit;
use git_support::git_stage_paths;
#[cfg(test)]
pub(crate) use git_support::read_git_commit_message;
#[cfg(test)]
pub(crate) use git_support::run_git_capture;
#[cfg(test)]
pub(crate) use git_support::run_git_process;
#[cfg(test)]
pub(crate) use git_support::run_git_status;
use migration_audit::run_migration_command;
#[cfg(feature = "cargo")]
use monochange_cargo::RustSemverProvider;
use monochange_config::load_workspace_configuration;
use monochange_config::resolve_package_reference;
use monochange_core::BumpSeverity;
use monochange_core::ChangeSignal;
use monochange_core::ChangelogFormat;
use monochange_core::ChangelogTarget;
use monochange_core::ChangesetContext;
use monochange_core::ChangesetPolicyEvaluation;
use monochange_core::ChangesetRevision;
use monochange_core::CliCommandDefinition;
use monochange_core::CliStepDefinition;
use monochange_core::CommitMessage;
use monochange_core::DEFAULT_CHANGELOG_VERSION_TITLE_NAMESPACED;
use monochange_core::DEFAULT_CHANGELOG_VERSION_TITLE_PRIMARY;
use monochange_core::DEFAULT_RELEASE_TITLE_NAMESPACED;
use monochange_core::DEFAULT_RELEASE_TITLE_PRIMARY;
use monochange_core::DiscoveryReport;
use monochange_core::Ecosystem;
use monochange_core::HostedActorRef;
use monochange_core::HostedActorSourceKind;
use monochange_core::HostedCommitRef;
use monochange_core::HostedIssueCommentPlan;
use monochange_core::HostingCapabilities;
use monochange_core::HostingProviderKind;
use monochange_core::MonochangeError;
use monochange_core::MonochangeResult;
use monochange_core::PackagePublicationTarget;
use monochange_core::PackageRecord;
use monochange_core::PreparedChangeset;
use monochange_core::PreparedChangesetTarget;
use monochange_core::ReleaseManifest;
use monochange_core::ReleaseManifestChangelog;
use monochange_core::ReleaseManifestCompatibilityEvidence;
use monochange_core::ReleaseManifestPlan;
use monochange_core::ReleaseManifestPlanDecision;
use monochange_core::ReleaseManifestPlanGroup;
use monochange_core::ReleaseManifestTarget;
use monochange_core::ReleaseNotesDocument;
use monochange_core::ReleaseOwnerKind;
use monochange_core::ReleasePlan;
use monochange_core::ReleaseRecord;
use monochange_core::ReleaseRecordDiscovery;
use monochange_core::ReleaseRecordProvider;
use monochange_core::ReleaseRecordTarget;
use monochange_core::RetargetOperation;
use monochange_core::RetargetProviderResult;
use monochange_core::RetargetResult;
use monochange_core::RetargetTagResult;
use monochange_core::SourceChangeRequest;
use monochange_core::SourceChangeRequestOperation;
use monochange_core::SourceChangeRequestOutcome;
use monochange_core::SourceConfiguration;
use monochange_core::SourceProvider;
use monochange_core::SourceReleaseOperation;
use monochange_core::SourceReleaseOutcome;
use monochange_core::SourceReleaseRequest;
use monochange_core::VersionFormat;
use monochange_core::VersionedFileDefinition;
use monochange_core::materialize_dependency_edges;
use monochange_core::relative_to_root;
#[cfg(feature = "forgejo")]
use monochange_forgejo as forgejo_provider;
#[cfg(feature = "gitea")]
use monochange_gitea as gitea_provider;
#[cfg(feature = "github")]
use monochange_github as github_provider;
#[cfg(feature = "gitlab")]
use monochange_gitlab as gitlab_provider;
use monochange_graph::build_release_plan;
use monochange_semver::CompatibilityProvider;
use monochange_semver::collect_assessments;
#[cfg(test)]
pub(crate) use workspace_ops::build_lockfile_command_executions;
#[cfg(test)]
pub(crate) use workspace_ops::change_type_default_bump;
#[cfg(test)]
pub(crate) use workspace_ops::prepare_release_execution;
#[cfg(test)]
pub(crate) use workspace_ops::render_cli_commands_toml;
#[cfg(test)]
pub(crate) use workspace_ops::render_interactive_changeset_markdown;
#[cfg(test)]
pub(crate) static TEST_ENV_LOCK: std::sync::LazyLock<std::sync::Mutex<()>> =
std::sync::LazyLock::new(|| std::sync::Mutex::new(()));
pub(crate) use release_artifacts::*;
pub use release_record::discover_release_record;
pub use release_record::execute_release_retarget;
pub use release_record::plan_release_retarget;
use release_record::render_release_record_discovery;
use release_record::render_release_tag_report;
pub use release_record::retarget_release;
use serde::Deserialize;
use serde::Serialize;
use serde_json::json;
use skill::SkillOptions;
use skill::run_skill;
use subagents::SubagentOptions;
use subagents::run_subagents;
pub(crate) use versioned_files::*;
pub use workspace_ops::AddChangeFileRequest;
use workspace_ops::PopulateWorkspaceResult;
pub use workspace_ops::add_change_file;
pub(crate) use workspace_ops::add_interactive_change_file;
pub use workspace_ops::discover_workspace;
use workspace_ops::init_workspace;
pub use workspace_ops::plan_release;
use workspace_ops::populate_workspace;
pub use workspace_ops::prepare_release;
pub(crate) use workspace_ops::prepare_release_execution_with_file_diffs;
pub(crate) use workspace_ops::push_change_target_markdown;
#[cfg(feature = "cargo")]
pub(crate) use workspace_ops::validate_cargo_workspace_version_groups;
pub(crate) fn render_config_step_json(
root: &Path,
configuration: &monochange_core::WorkspaceConfiguration,
) -> String {
let project_root = root
.canonicalize()
.unwrap_or_else(|_| root.to_path_buf())
.display()
.to_string();
let config_path = monochange_config::config_path(root).display().to_string();
let output = serde_json::json!({
"projectRoot": project_root,
"configPath": config_path,
"config": configuration,
});
serde_json::to_string_pretty(&output)
.unwrap_or_else(|error| panic!("serializing a serde_json::Value failed: {error}"))
}
pub(crate) fn synthetic_step_command_definition(
cli_command_name: &str,
) -> MonochangeResult<CliCommandDefinition> {
let kebab = cli_command_name
.strip_prefix("step:")
.unwrap_or(cli_command_name);
let step = monochange_core::all_step_variants()
.into_iter()
.find(|step| step.step_kebab_name() == kebab)
.ok_or_else(|| {
MonochangeError::Config(format!("unknown step command: {cli_command_name}"))
})?;
Ok(CliCommandDefinition {
name: cli_command_name.to_string(),
help_text: step.name().map(ToString::to_string),
inputs: step.step_inputs_schema(),
steps: vec![step.with_inherited_step_inputs()],
dry_run: false,
})
}
mod analyze;
mod changeset_policy;
mod changesets;
mod cli;
mod cli_help;
mod cli_progress;
mod cli_runtime;
mod cli_theme;
mod command_wizard;
mod git_support;
mod hosted_sources;
mod interactive;
mod jq_filter;
mod lint;
mod lint_check_reporter;
#[cfg(feature = "mcp")]
mod mcp;
mod migration_audit;
mod package_publish;
mod prepared_release_cache;
mod publish_progress;
mod publish_rate_limits;
mod publish_readiness;
mod release_artifacts;
mod release_branch_policy;
mod release_record;
mod skill;
mod subagents;
mod sync;
pub use sync::sync_workspace_versions;
mod tracing_setup;
mod versioned_files;
mod workspace_ops;
pub(crate) use prepared_release_cache::ensure_monochange_artifact_ignored;
pub(crate) use prepared_release_cache::maybe_load_prepared_release_execution;
pub(crate) use prepared_release_cache::save_prepared_release_execution;
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum OutputFormat {
Text,
Markdown,
Json,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum ChangeBump {
None,
Patch,
Minor,
Major,
}
impl From<ChangeBump> for BumpSeverity {
fn from(value: ChangeBump) -> Self {
match value {
ChangeBump::None => Self::None,
ChangeBump::Patch => Self::Patch,
ChangeBump::Minor => Self::Minor,
ChangeBump::Major => Self::Major,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SubagentTarget {
Claude,
Vscode,
Copilot,
Pi,
Codex,
Cursor,
}
impl SubagentTarget {
fn all() -> Vec<Self> {
vec![
Self::Claude,
Self::Vscode,
Self::Copilot,
Self::Pi,
Self::Codex,
Self::Cursor,
]
}
fn from_cli_value(value: &str) -> Option<Self> {
match value {
"claude" => Some(Self::Claude),
"vscode" => Some(Self::Vscode),
"copilot" => Some(Self::Copilot),
"pi" => Some(Self::Pi),
"codex" => Some(Self::Codex),
"cursor" => Some(Self::Cursor),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SubagentOutputFormat {
Markdown,
Text,
Json,
}
fn parse_subagent_output_format_or_default(value: Option<&String>) -> SubagentOutputFormat {
match value.map_or("markdown", String::as_str) {
"json" => SubagentOutputFormat::Json,
"text" => SubagentOutputFormat::Text,
_ => SubagentOutputFormat::Markdown,
}
}
fn parse_subagent_targets<'value, I>(values: Option<I>) -> MonochangeResult<Vec<SubagentTarget>>
where
I: IntoIterator<Item = &'value String>,
{
let mut targets = Vec::new();
for value in values.into_iter().flatten() {
let Some(target) = SubagentTarget::from_cli_value(value) else {
return Err(MonochangeError::Config(format!(
"unsupported subagent target `{value}`"
)));
};
if targets.contains(&target) {
continue;
}
targets.push(target);
}
if targets.is_empty() {
return Err(MonochangeError::Config(
"expected at least one subagent target or `--all`".to_string(),
));
}
Ok(targets)
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct ReleaseTarget {
pub id: String,
pub kind: ReleaseOwnerKind,
pub version: String,
pub tag: bool,
pub release: bool,
pub version_format: VersionFormat,
pub tag_name: String,
pub members: Vec<String>,
pub rendered_title: String,
pub rendered_changelog_title: String,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct PreparedChangelog {
pub owner_id: String,
pub owner_kind: ReleaseOwnerKind,
pub path: PathBuf,
pub format: ChangelogFormat,
pub notes: ReleaseNotesDocument,
pub rendered: String,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct PreparedRelease {
pub plan: ReleasePlan,
pub changeset_paths: Vec<PathBuf>,
pub changesets: Vec<PreparedChangeset>,
pub released_packages: Vec<String>,
pub package_publications: Vec<PackagePublicationTarget>,
pub version: Option<String>,
pub group_version: Option<String>,
pub release_targets: Vec<ReleaseTarget>,
pub changed_files: Vec<PathBuf>,
pub changelogs: Vec<PreparedChangelog>,
pub updated_changelogs: Vec<PathBuf>,
pub deleted_changesets: Vec<PathBuf>,
pub dry_run: bool,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct PreparedFileDiff {
path: PathBuf,
diff: String,
#[serde(skip_serializing)]
display_diff: String,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) struct StepPhaseTiming {
label: String,
duration: Duration,
}
#[derive(Debug, Clone, Eq, PartialEq)]
struct PreparedReleaseExecution {
prepared_release: PreparedRelease,
file_diffs: Vec<PreparedFileDiff>,
phase_timings: Vec<StepPhaseTiming>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
struct FileUpdate {
path: PathBuf,
content: Vec<u8>,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ChangesetDiagnosticsReport {
pub(crate) requested_changesets: Vec<PathBuf>,
pub(crate) changesets: Vec<PreparedChangeset>,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct RetargetReleaseReport {
from: String,
target: String,
resolved_from_commit: String,
record_commit: String,
target_commit: String,
distance: usize,
is_descendant: bool,
force: bool,
dry_run: bool,
sync_provider: bool,
tags: Vec<String>,
git_tag_results: Vec<RetargetTagResult>,
provider_results: Vec<RetargetProviderResult>,
status: String,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct CommitReleaseReport {
subject: String,
body: String,
commit: Option<String>,
tracked_paths: Vec<PathBuf>,
dry_run: bool,
status: String,
}
#[derive(Debug, Clone, Eq, PartialEq)]
struct CliContext {
root: PathBuf,
dry_run: bool,
quiet: bool,
show_diff: bool,
inputs: BTreeMap<String, Vec<String>>,
last_step_inputs: BTreeMap<String, Vec<String>>,
prepared_release: Option<PreparedRelease>,
prepared_file_diffs: Vec<PreparedFileDiff>,
release_manifest_path: Option<PathBuf>,
release_requests: Vec<SourceReleaseRequest>,
release_results: Vec<String>,
release_request: Option<SourceChangeRequest>,
release_request_result: Option<String>,
release_commit_report: Option<CommitReleaseReport>,
package_publish_report: Option<package_publish::PackagePublishReport>,
rate_limit_report: Option<monochange_core::PublishRateLimitReport>,
issue_comment_plans: Vec<HostedIssueCommentPlan>,
issue_comment_results: Vec<String>,
changeset_policy_evaluation: Option<ChangesetPolicyEvaluation>,
changeset_diagnostics: Option<ChangesetDiagnosticsReport>,
retarget_report: Option<RetargetReleaseReport>,
step_outputs: BTreeMap<String, CommandStepOutput>,
command_logs: Vec<String>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
struct CommandStepOutput {
stdout: String,
stderr: String,
}
const CHANGESET_DIR: &str = ".changeset";
#[must_use = "the run result must be checked"]
#[allow(clippy::large_futures)]
pub async fn run_from_env(bin_name: &'static str) -> MonochangeResult<()> {
let log_level = extract_log_level_from_args();
tracing_setup::init_tracing(log_level.as_deref());
let quiet = extract_quiet_from_args(std::env::args_os());
let args = std::env::args_os();
let output = run_with_args(bin_name, args).await?;
if !quiet && !output.is_empty() {
let format = detect_output_format_from_env_args(std::env::args());
if format == OutputFormat::Markdown {
println!("{}", maybe_render_markdown_for_terminal(&output));
} else {
println!("{output}");
}
}
Ok(())
}
#[coverage(off)]
#[must_use = "the process exit code must be returned"]
pub async fn run_cli_binary_from_env(bin_name: &'static str) -> ExitCode {
let _ = rustls::crypto::ring::default_provider().install_default();
let quiet = extract_quiet_from_args(std::env::args_os());
let result = Box::pin(run_from_env(bin_name)).await;
let Err(error) = result else {
return ExitCode::SUCCESS;
};
if !quiet {
eprintln!("{}", error.render());
}
ExitCode::FAILURE
}
pub(crate) fn detect_output_format_from_env_args(
args: impl Iterator<Item = String>,
) -> OutputFormat {
let args: Vec<String> = args.collect();
for (i, arg) in args.iter().enumerate() {
if arg == "step:config" {
return OutputFormat::Json;
}
if arg == "--format"
&& let Some(value) = args.get(i + 1)
{
return parse_output_format(value).unwrap_or(OutputFormat::Markdown);
}
if let Some(value) = arg.strip_prefix("--format=") {
return parse_output_format(value).unwrap_or(OutputFormat::Markdown);
}
}
OutputFormat::Markdown
}
fn extract_log_level_from_args() -> Option<String> {
extract_log_level(std::env::args())
}
fn quiet_from_os_arg(arg: &OsString) -> bool {
matches!(arg.to_str(), Some("--quiet" | "-q"))
}
fn is_root_help_request(args: &[OsString]) -> bool {
let mut positional = args.iter().skip(1).filter_map(|arg| arg.to_str());
matches!(positional.next(), None | Some("-h" | "--help")) && positional.next().is_none()
}
fn extract_quiet_from_args<I>(args: I) -> bool
where
I: IntoIterator<Item = OsString>,
{
args.into_iter().any(|arg| quiet_from_os_arg(&arg))
}
fn extract_log_level<I>(args: I) -> Option<String>
where
I: IntoIterator<Item = String>,
{
let mut args = args.into_iter();
while let Some(arg) = args.next() {
if arg == "--log-level" {
return args.next();
}
if let Some(value) = arg.strip_prefix("--log-level=") {
return Some(value.to_string());
}
}
None
}
#[must_use = "the run result must be checked"]
pub async fn run_with_args<I>(bin_name: &'static str, args: I) -> MonochangeResult<String>
where
I: IntoIterator<Item = OsString>,
{
let root = current_dir_or_dot();
run_with_args_in_dir(bin_name, args, &root).await
}
#[tracing::instrument(skip_all, fields(bin_name))]
fn format_clap_error(error: &clap::Error, colored: bool) -> String {
if colored {
let _ = error.print();
String::new()
} else {
error.to_string()
}
}
fn paint(text: &str, style: anstyle::Style, colored: bool) -> String {
if colored {
format!("{style}{text}{style:#}")
} else {
text.to_string()
}
}
fn unexpected_argument_from_error(error: &clap::Error) -> Option<String> {
let message = error.to_string();
let (_, rest) = message.split_once("unexpected argument '")?;
let (argument, _) = rest.split_once('\'')?;
Some(argument.to_string())
}
fn custom_command_name_from_args(
args: &[OsString],
cli: &[CliCommandDefinition],
) -> Option<String> {
args.iter()
.skip(1)
.filter_map(|arg| arg.to_str())
.find_map(|arg| {
cli.iter()
.any(|command| command.name == arg)
.then(|| arg.to_string())
})
}
fn render_custom_command_argument_error(
error: &clap::Error,
cli_command: &CliCommandDefinition,
colored: bool,
) -> String {
let command = format!("mc {}", cli_command.name);
let argument =
unexpected_argument_from_error(error).unwrap_or_else(|| "the supplied option".to_string());
let heading = paint("✖ Unexpected command input", cli_theme::error(), colored);
let argument = paint(&argument, cli_theme::literal(), colored);
let command = paint(&command, cli_theme::usage(), colored);
let config_path = paint(
&format!("[cli.{}]", cli_command.name),
cli_theme::header(),
colored,
);
let usage = paint("Usage", cli_theme::header(), colored);
let fix = paint("How to fix", cli_theme::header(), colored);
format!(
"{heading}\n\n Argument {argument} is not declared for custom command {command}.\n\n{usage}:\n {}\n\n{fix}:\n This command comes from {config_path} in monochange.toml.\n Add a matching input there to make this option valid, for example:\n\n [cli.{}]\n inputs = [\n {{ name = \"{}\", type = \"boolean\" }},\n ]\n\n Then run `mc help {}` to confirm the option is listed.",
cli::cli_command_usage(cli_command),
cli_command.name,
argument.trim_start_matches('-').replace('-', "_"),
cli_command.name
)
}
fn format_populate_workspace_result(result: &PopulateWorkspaceResult) -> String {
if result.added_commands.is_empty() {
format!(
"{} already defines all default CLI commands",
result.path.display()
)
} else {
let mut message = format!(
"updated {} and added {} default CLI commands: ",
result.path.display(),
result.added_commands.len()
);
for (index, command) in result.added_commands.iter().enumerate() {
if index > 0 {
message.push_str(", ");
}
message.push_str(command);
}
message
}
}
#[doc(hidden)]
#[allow(clippy::redundant_closure_for_method_calls)]
pub async fn run_with_args_in_dir<I>(
bin_name: &'static str,
args: I,
root: &Path,
) -> MonochangeResult<String>
where
I: IntoIterator<Item = OsString>,
{
let args = args.into_iter().collect::<Vec<_>>();
let configuration = load_workspace_configuration(root);
let cli = cli_commands_from_config(&configuration);
let quiet = extract_quiet_from_args(args.iter().cloned());
let root_help_requested = is_root_help_request(&args);
let matches = match build_command_with_cli(bin_name, &cli).try_get_matches_from(args.clone()) {
Ok(matches) => matches,
Err(error)
if matches!(
error.kind(),
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion
) =>
{
if root_help_requested && matches!(error.kind(), ErrorKind::DisplayHelp) {
return Ok(cli_help::render_overview_help_with_cli(bin_name, &cli));
}
return Ok(format_clap_error(
&error,
!cfg!(test) && std::io::stdout().is_terminal(),
));
}
Err(error) => {
if matches!(error.kind(), ErrorKind::UnknownArgument)
&& let Some(command_name) = custom_command_name_from_args(&args, &cli)
&& let Some(cli_command) = cli.iter().find(|command| command.name == command_name)
{
return Err(MonochangeError::Diagnostic(
render_custom_command_argument_error(
&error,
cli_command,
!cfg!(test) && std::io::stderr().is_terminal(),
),
));
}
return Err(MonochangeError::Config(error.to_string()));
}
};
let jq_expression = matches.get_one::<String>("jq").cloned();
let output = match matches.subcommand() {
Some(("help", help_matches)) => {
let command_name = help_matches
.get_one::<String>("command")
.map_or("", String::as_str);
let output = if command_name.is_empty() {
cli_help::render_overview_help_with_cli(bin_name, &cli)
} else {
cli_help::render_command_help_with_cli(bin_name, command_name, &cli)
};
Ok(output)
}
Some(("init", init_matches)) => {
let provider = init_matches
.get_one::<String>("provider")
.map(String::as_str);
let result = init_workspace(root, init_matches.get_flag("force"), provider)?;
if quiet {
Ok(String::new())
} else {
Ok(result.summary())
}
}
Some(("populate", _)) => {
if quiet {
return Ok(String::new());
}
let result = populate_workspace(root)?;
Ok(format_populate_workspace_result(&result))
}
Some(("command", _)) => run_command_wizard_for_cli(root, quiet),
Some(("skill", skill_matches)) => {
let forwarded_args = skill_matches
.get_many::<String>("args")
.into_iter()
.flatten()
.cloned()
.collect();
let options = SkillOptions { forwarded_args };
run_skill(root, &options)
}
Some(("subagents", subagent_matches)) => {
let targets = if subagent_matches.get_flag("all") {
SubagentTarget::all()
} else {
parse_subagent_targets(subagent_matches.get_many::<String>("target"))?
};
let format = parse_subagent_output_format_or_default(
subagent_matches.get_one::<String>("format"),
);
let options = SubagentOptions {
targets,
force: subagent_matches.get_flag("force"),
dry_run: quiet || subagent_matches.get_flag("dry-run"),
format,
generate_mcp: !subagent_matches.get_flag("no-mcp"),
};
let output = run_subagents(root, &options)?;
if quiet { Ok(String::new()) } else { Ok(output) }
}
Some(("analyze", analyze_matches)) => {
if quiet {
return Ok(String::new());
}
let package = analyze_matches
.get_one::<String>("package")
.map(String::as_str)
.ok_or_else(|| MonochangeError::Config("missing analyze package".to_string()))?;
let release_ref = analyze_matches
.get_one::<String>("release-ref")
.map(String::as_str);
let main_ref = analyze_matches
.get_one::<String>("main-ref")
.map(String::as_str);
let head_ref = analyze_matches
.get_one::<String>("head-ref")
.map(String::as_str);
let detection_level = analyze_matches
.get_one::<String>("detection-level")
.map_or("signature", String::as_str);
let format = analyze_matches
.get_one::<String>("format")
.map_or(Ok(OutputFormat::Markdown), |value| {
parse_output_format(value)
})?;
render_analyze_report(
root,
package,
release_ref,
main_ref,
head_ref,
detection_level,
format,
)
.await
}
Some(("migrate", migrate_matches)) => run_migration_command(root, quiet, migrate_matches),
#[cfg(feature = "mcp")]
Some(("mcp", _)) => run_mcp_command_with(quiet, mcp::run_server).await,
Some(("check", check_matches)) => {
if quiet {
return Ok(String::new());
}
let fix = check_matches.get_flag("fix");
let format = check_matches
.get_one::<String>("format")
.map_or(Ok(OutputFormat::Markdown), |value| {
parse_output_format(value)
})?;
let ecosystems: Vec<String> = check_matches
.get_many::<String>("ecosystem")
.map(|values| values.map(String::as_str).map(String::from).collect())
.unwrap_or_default();
let only_rules: Vec<String> = check_matches
.get_many::<String>("only")
.map(|values| values.map(String::as_str).map(String::from).collect())
.unwrap_or_default();
lint::run_check_command(root, fix, &ecosystems, &only_rules, format)
}
Some(("lint", lint_matches)) => {
if quiet {
return Ok(String::new());
}
lint::handle_lint_subcommand(root, lint_matches)
}
Some(("versions", versions_matches)) => {
let strategy_str = versions_matches
.get_one::<String>("strategy")
.map_or("default", String::as_str);
let strategy = sync::parse_strategy(strategy_str);
let dry_run = versions_matches.get_flag("dry-run");
let format_str = versions_matches
.get_one::<String>("format")
.map_or("text", String::as_str);
let format = sync::parse_versions_output_format(format_str);
let result = sync_workspace_versions(root, strategy, dry_run)?;
Ok(sync::format_sync_result_for_cli(
&result, dry_run, quiet, format,
))
}
Some((cli_command_name, cli_command_matches)) if cli_command_name.starts_with("step:") => {
let configuration = configuration?;
let synthetic = synthetic_step_command_definition(cli_command_name)?;
let inputs = collect_cli_command_inputs(&synthetic, cli_command_matches);
let dry_run = quiet || cli_command_matches.get_flag("dry-run");
execute_cli_command(root, &configuration, &synthetic, dry_run, inputs).await
}
Some((cli_command_name, cli_command_matches)) => {
let configuration = configuration?;
execute_matches(
root,
&configuration,
cli_command_name,
cli_command_matches,
quiet,
)
.await
}
None => Err(MonochangeError::Config("Usage: mc".to_string())),
}?;
if let Some(expression) = jq_expression {
jq_filter::apply_jq_filter(&output, &expression)
} else {
Ok(output)
}
}
#[coverage(off)]
fn run_command_wizard_for_cli(root: &Path, quiet: bool) -> MonochangeResult<String> {
if quiet {
return Ok(String::new());
}
run_command_wizard(root)
}
#[cfg(feature = "mcp")]
async fn run_mcp_command_with<F, Fut>(quiet: bool, run_server: F) -> MonochangeResult<String>
where
F: FnOnce() -> Fut,
Fut: Future<Output = ()>,
{
if quiet {
return Ok(String::new());
}
run_server().await;
Ok(String::new())
}
fn format_publish_state(publish_state: monochange_core::PublishState) -> &'static str {
match publish_state {
monochange_core::PublishState::Public => "public",
monochange_core::PublishState::Private => "private",
monochange_core::PublishState::Unpublished => "unpublished",
monochange_core::PublishState::Excluded => "excluded",
_ => "unknown",
}
}
#[cfg(test)]
#[path = "__tests__/lib_tests.rs"]
pub(crate) mod tests;
#[cfg(test)]
#[path = "__tests__/sync_tests.rs"]
pub(crate) mod sync_tests;