use super::common::{ChangeOutputFormat, PlanOutputFormat, SplitOutputFormat, TextJsonOutputFormat, UnifyOutputFormat};
use crate::sync::ConflictStrategy;
use crate::test::runner::TestRunnerPreference;
use clap::{CommandFactory, Parser, Subcommand, error::ErrorKind};
use clap_complete::Shell;
use std::path::PathBuf;
const MAIN_HELP: &str = "\
Monorepo orchestration for Rust workspaces.
Quick start:
cargo rail init # Generate .config/rail.toml (default)
cargo rail plan # Build deterministic change plan
cargo rail run # Execute planner-selected surfaces
cargo rail unify --check # Preview dependency unification
Docs: https://github.com/loadingalias/cargo-rail";
#[derive(Parser)]
#[command(name = "cargo")]
#[command(bin_name = "cargo")]
#[command(styles = get_styles())]
pub enum CargoCli {
#[command(about = "Monorepo orchestration for Rust workspaces", long_about = MAIN_HELP)]
Rail(RailCli),
}
#[derive(Parser)]
#[command(name = "rail")]
#[command(version)]
#[command(about = "Monorepo orchestration for Rust workspaces")]
#[command(long_about = MAIN_HELP)]
#[command(propagate_version = true)]
#[command(styles = get_styles())]
pub struct RailCli {
#[arg(long, short, global = true)]
pub quiet: bool,
#[arg(long, global = true)]
pub json: bool,
#[arg(long, global = true, value_name = "PATH")]
pub config: Option<PathBuf>,
#[arg(long, global = true, value_name = "PATH")]
pub workspace_root: Option<PathBuf>,
#[command(subcommand)]
pub command: Commands,
}
const RUN_HELP: &str = "\
Examples:
cargo rail run # Execute planner-selected test surface
cargo rail run --merge-base # Compare from branch point (CI)
cargo rail run --surface build --surface test
cargo rail run --profile ci # Built-in profile (local|ci|nightly)
cargo rail run --workflow commit # Resolve profile from [run.workflow.commit]
cargo rail run --profile bench # User-defined profile from [run.profile.bench]
cargo rail run --all --surface test # Force full test run
cargo rail run --dry-run --print-cmd # Preview exact execution
cargo rail run --test-filter parser # Portable test-name filter
cargo rail run --cargo-test-arg=--all-features --test-runner cargo
cargo rail run --nextest-arg=-P --nextest-arg=commit
cargo rail run -- --nocapture # Pass harness args after --";
const PLAN_HELP: &str = "\
Examples:
cargo rail plan # Changes since default branch
cargo rail plan --merge-base # Changes since branch point (CI recommended)
cargo rail plan --confidence-profile strict # Conservative planner profile
cargo rail plan --since HEAD~5 # Changes in last 5 commits
cargo rail plan --from abc --to def # Changes between two SHAs
cargo rail plan --explain # Show concise proof chain
cargo rail plan --schema # Print the versioned JSON Schema
cargo rail plan -f json # Full machine-readable contract
cargo rail plan -f github # Compact GitHub Actions key=value output
cargo rail plan -f github-debug # GitHub Actions output plus plan_json";
const UNIFY_HELP: &str = "\
Examples:
cargo rail unify --check # Preview changes (CI mode)
cargo rail unify --check --explain # Show why each decision was made
cargo rail unify --check -f json -o out.json # Write JSON output to file
cargo rail unify # Apply changes
cargo rail unify --backup # Apply with backup
cargo rail unify --show-diff # Show manifest changes
cargo rail unify undo # Restore from backup
cargo rail unify undo --list # List available backups";
const SPLIT_HELP: &str = "\
This is an advanced feature for extracting crates to standalone repositories
while preserving git history. Most teams should start with 'plan', 'run',
and 'unify' before using split/sync.
Examples:
cargo rail split init my-crate # Configure split for my-crate
cargo rail split init my-crate --check # Preview generated config
cargo rail split run my-crate --check # Preview the split
cargo rail split run my-crate # Execute the split
cargo rail split run my-crate --yes # Non-interactive apply confirmation
cargo rail split run --all # Split all configured crates";
const SYNC_HELP: &str = "\
This is an advanced feature for bidirectional sync between monorepo and split
repositories. Requires 'split' to be configured first.
Examples:
cargo rail sync my-crate # Bidirectional sync
cargo rail sync my-crate --to-remote # Push monorepo -> split repo
cargo rail sync my-crate --from-remote # Pull split repo -> monorepo (PR branch)
cargo rail sync my-crate --to-remote --yes # Non-interactive apply confirmation
cargo rail sync --all # Sync all configured crates";
const RELEASE_HELP: &str = "\
Examples:
cargo rail release init my-crate # Configure release for my-crate
cargo rail release check my-crate # Validate release readiness
cargo rail release check my-crate --extended # Run extended checks (dry-run, MSRV)
cargo rail release run my-crate --check # Preview release plan
cargo rail release run my-crate # Release (patch bump)
cargo rail release run my-crate --include-dependents # Release selected crate plus dependent closure
cargo rail release run my-crate --yes # Non-interactive apply confirmation
cargo rail release run my-crate --bump auto # Infer per-crate bump from commits
cargo rail release run --all --bump auto --pr # Open a release PR with bumps/changelogs only
cargo rail release finalize --all # Tag/publish after the release PR merges
cargo rail release run my-crate --bump minor
cargo rail release run my-crate --bump prerelease # 1.0.0 -> 1.0.0-rc.1
cargo rail release run my-crate --bump release # 1.0.0-rc.2 -> 1.0.0
cargo rail release run --all --bump patch # Release all crates
cargo rail release run my-crate --skip-publish # Tag only, no crates.io";
const CHANGE_HELP: &str = "\
Examples:
cargo rail change add rail-core --bump minor --message \"Added auto bump planning\"
cargo rail change add rail-core rail-cli --bump patch --message \"Fixed release notes\"
cargo rail change add rail-core --bump patch --name fix-parser
cargo rail change status
cargo rail change status --format json
cargo rail change check --merge-base --required
cargo rail change check --since origin/main --format json
Omit --message in an interactive terminal to author in $VISUAL or $EDITOR.
Change files are consumed (deleted in the release commit) when released.
Consumption is all-or-nothing: a release plan that covers only some of a
file's crates is rejected so no pending intent is ever lost.";
const INIT_HELP: &str = "\
Examples:
cargo rail init # Generate .config/rail.toml
cargo rail init --check # Preview generated config
cargo rail init -o rail.toml # Custom output path
cargo rail init --force # Overwrite existing config";
const CLEAN_HELP: &str = "\
Examples:
cargo rail clean # Clean all artifacts
cargo rail clean --cache # Clean metadata cache only
cargo rail clean --backups # Prune old backups
cargo rail clean --reports # Clean generated reports
cargo rail clean --check # Preview what would be cleaned";
const CONFIG_HELP: &str = "\
Examples:
cargo rail config locate # Show which config file is active
cargo rail config print # Show effective config with defaults
cargo rail config validate # Validate rail.toml
cargo rail config validate -f json # JSON output for CI
cargo rail config sync --check # Preview config updates
cargo rail config sync # Run after upgrades; add fields and sync targets";
const HASH_HELP: &str = "\
Examples:
cargo rail hash # Portable identity of the current plan
cargo rail hash --merge-base # Identity for the merge-base comparison
cargo rail hash -f json # Structured identity metadata
cargo rail diff-hash plan-a.json plan-b.json
cargo rail diff-hash plan-a.json plan-b.json -f json";
const GRAPH_HELP: &str = "\
Examples:
cargo rail graph # Planner reasoning graph (json)
cargo rail graph --merge-base # Graph against merge-base comparison
cargo rail graph --dot # GraphViz DOT output
cargo rail graph --since HEAD~3 -o graph.dot # Write graph output to file";
const COMPLETIONS_HELP: &str = "\
Examples:
cargo rail completions bash # Output bash completions
cargo rail completions zsh # Output zsh completions
cargo rail completions fish # Output fish completions
cargo rail completions powershell # Output PowerShell completions
Installation:
# Bash (~/.bashrc)
eval \"$(cargo rail completions bash)\"
# Zsh (~/.zshrc)
eval \"$(cargo rail completions zsh)\"
# Fish (~/.config/fish/config.fish)
cargo rail completions fish | source
# PowerShell
cargo rail completions powershell | Out-String | Invoke-Expression";
#[derive(Subcommand)]
pub enum Commands {
#[command(after_long_help = RUN_HELP)]
Run {
#[arg(long)]
since: Option<String>,
#[arg(long, conflicts_with = "since")]
merge_base: bool,
#[arg(long, short = 'a')]
all: bool,
#[arg(long = "surface", value_name = "SURFACE")]
surfaces: Vec<String>,
#[arg(long, value_name = "PROFILE", conflicts_with_all = ["surfaces", "workflow"])]
profile: Option<String>,
#[arg(long, value_name = "WORKFLOW", conflicts_with_all = ["surfaces", "profile"])]
workflow: Option<String>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
print_cmd: bool,
#[arg(long)]
explain: bool,
#[arg(long)]
ignore_bin_crates: bool,
#[arg(long, conflicts_with = "test_runner")]
skip_nextest: bool,
#[arg(long, value_enum, default_value = "auto")]
test_runner: TestRunnerPreference,
#[arg(long = "cargo-test-arg", value_name = "ARG", allow_hyphen_values = true)]
cargo_test_args: Vec<String>,
#[arg(
long = "nextest-arg",
value_name = "ARG",
allow_hyphen_values = true,
conflicts_with = "skip_nextest"
)]
nextest_args: Vec<String>,
#[arg(long, value_name = "FILTER")]
test_filter: Option<String>,
#[arg(last = true)]
run_args: Vec<String>,
},
#[command(after_long_help = PLAN_HELP)]
Plan {
#[arg(long)]
since: Option<String>,
#[arg(long, conflicts_with = "since", requires = "to")]
from: Option<String>,
#[arg(long, requires = "from")]
to: Option<String>,
#[arg(long, conflicts_with_all = ["since", "from", "to"])]
merge_base: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: PlanOutputFormat,
#[arg(long, short = 'o', value_name = "PATH")]
output: Option<PathBuf>,
#[arg(long)]
explain: bool,
#[arg(long, value_name = "PROFILE", value_parser = ["strict", "balanced", "fast"])]
confidence_profile: Option<String>,
#[arg(long, conflicts_with_all = ["since", "from", "to", "merge_base", "output", "explain", "confidence_profile"])]
schema: bool,
},
#[command(after_long_help = UNIFY_HELP)]
Unify {
#[command(subcommand)]
command: Option<UnifyCommand>,
#[arg(long, short = 'c')]
check: bool,
#[arg(long, value_name = "PATH", conflicts_with = "check")]
plan: Option<PathBuf>,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: UnifyOutputFormat,
#[arg(long)]
backup: bool,
#[arg(long)]
skip_report: bool,
#[arg(long)]
report_path: Option<PathBuf>,
#[arg(long, short = 'o', value_name = "PATH", requires = "check")]
output: Option<PathBuf>,
#[arg(long)]
show_diff: bool,
#[arg(long)]
explain: bool,
},
#[command(after_long_help = INIT_HELP)]
Init {
#[arg(long, short, default_value = ".config/rail.toml")]
output: String,
#[arg(long)]
force: bool,
#[arg(long, short = 'c')]
check: bool,
},
#[command(after_long_help = SPLIT_HELP)]
Split {
#[command(subcommand)]
command: SplitCommand,
},
#[command(after_long_help = SYNC_HELP)]
Sync {
#[arg(conflicts_with = "all")]
crate_name: Option<String>,
#[arg(short, long)]
all: bool,
#[arg(long)]
remote: Option<String>,
#[arg(long)]
from_remote: bool,
#[arg(long)]
to_remote: bool,
#[arg(long, default_value_t, value_enum)]
strategy: ConflictStrategy,
#[arg(long, short = 'c')]
check: bool,
#[arg(long, value_name = "PATH", conflicts_with = "check")]
plan: Option<PathBuf>,
#[arg(
long,
value_name = "RECEIPT",
conflicts_with_all = ["crate_name", "all", "remote", "from_remote", "to_remote", "check", "plan"]
)]
resume: Option<PathBuf>,
#[arg(long)]
allow_dirty: bool,
#[arg(short = 'y', long)]
yes: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
#[command(after_long_help = RELEASE_HELP)]
Release {
#[command(subcommand)]
command: ReleaseCommand,
},
#[command(after_long_help = CHANGE_HELP)]
Change {
#[command(subcommand)]
command: ChangeCommand,
},
#[command(after_long_help = CLEAN_HELP)]
Clean {
#[arg(long)]
cache: bool,
#[arg(long)]
backups: bool,
#[arg(long)]
reports: bool,
#[arg(long, short = 'c')]
check: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
#[command(name = "config")]
#[command(after_long_help = CONFIG_HELP)]
Config {
#[command(subcommand)]
command: ConfigCommand,
},
#[command(after_long_help = HASH_HELP)]
Hash {
#[arg(long)]
since: Option<String>,
#[arg(long, conflicts_with = "since", requires = "to")]
from: Option<String>,
#[arg(long, requires = "from")]
to: Option<String>,
#[arg(long, conflicts_with_all = ["since", "from", "to"])]
merge_base: bool,
#[arg(long, value_name = "PROFILE", value_parser = ["strict", "balanced", "fast"])]
confidence_profile: Option<String>,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
#[command(after_long_help = HASH_HELP)]
DiffHash {
a: PathBuf,
b: PathBuf,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
#[command(after_long_help = GRAPH_HELP)]
Graph {
#[arg(long)]
since: Option<String>,
#[arg(long, conflicts_with = "since", requires = "to")]
from: Option<String>,
#[arg(long, requires = "from")]
to: Option<String>,
#[arg(long, conflicts_with_all = ["since", "from", "to"])]
merge_base: bool,
#[arg(long, value_name = "PROFILE", value_parser = ["strict", "balanced", "fast"])]
confidence_profile: Option<String>,
#[arg(long)]
dot: bool,
#[arg(long, short = 'o', value_name = "PATH")]
output: Option<PathBuf>,
},
#[command(after_long_help = COMPLETIONS_HELP)]
Completions {
#[arg(value_enum, value_name = "SHELL")]
shell: Shell,
},
}
#[derive(Subcommand)]
pub enum ConfigCommand {
Locate {
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
Print {
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
Validate {
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
#[arg(long, conflicts_with = "no_strict")]
strict: bool,
#[arg(long, conflicts_with = "strict")]
no_strict: bool,
},
Sync {
#[arg(long, short = 'c')]
check: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
}
#[derive(Subcommand)]
pub enum UnifyCommand {
Undo {
#[arg(long)]
list: bool,
#[arg(long = "backup-id")]
backup_id: Option<String>,
},
}
#[derive(Subcommand)]
pub enum SplitCommand {
Init {
#[arg(value_name = "CRATE")]
crate_names: Vec<String>,
#[arg(long, short = 'c')]
check: bool,
},
Run {
#[arg(conflicts_with = "all", value_name = "CRATE")]
crate_name: Option<String>,
#[arg(short, long)]
all: bool,
#[arg(long)]
remote: Option<String>,
#[arg(long, short = 'c')]
check: bool,
#[arg(long, value_name = "PATH", conflicts_with = "check")]
plan: Option<PathBuf>,
#[arg(long)]
allow_dirty: bool,
#[arg(short = 'y', long)]
yes: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: SplitOutputFormat,
},
}
#[derive(Subcommand)]
pub enum ReleaseCommand {
Init {
#[arg(value_name = "CRATE")]
crate_names: Vec<String>,
#[arg(long, short = 'c')]
check: bool,
},
Run {
#[arg(conflicts_with = "all", value_name = "CRATE")]
crate_names: Vec<String>,
#[arg(short, long)]
all: bool,
#[arg(long, default_value = "patch")]
bump: String,
#[arg(long, short = 'c')]
check: bool,
#[arg(long, value_name = "PATH", conflicts_with = "check")]
plan: Option<PathBuf>,
#[arg(long)]
skip_publish: bool,
#[arg(long)]
skip_tag: bool,
#[arg(long)]
pr: bool,
#[arg(long)]
include_dependents: bool,
#[arg(short = 'y', long)]
yes: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
Check {
#[arg(conflicts_with = "all", value_name = "CRATE")]
crate_names: Vec<String>,
#[arg(short, long)]
all: bool,
#[arg(long, short = 'e')]
extended: bool,
#[arg(long)]
include_dependents: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
Finalize {
#[arg(conflicts_with = "all", value_name = "CRATE")]
crate_names: Vec<String>,
#[arg(short, long)]
all: bool,
#[arg(long)]
skip_publish: bool,
#[arg(long)]
skip_tag: bool,
#[arg(long)]
include_dependents: bool,
#[arg(short = 'y', long)]
yes: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: TextJsonOutputFormat,
},
Resume {
#[arg(value_name = "STATE")]
state: PathBuf,
},
Abort {
#[arg(value_name = "STATE")]
state: PathBuf,
#[arg(short = 'y', long)]
yes: bool,
},
}
#[derive(Subcommand)]
pub enum ChangeCommand {
Add {
#[arg(value_name = "CRATE")]
crate_names: Vec<String>,
#[arg(long)]
bump: String,
#[arg(long, short = 'm')]
message: Option<String>,
#[arg(long, value_name = "SLUG")]
name: Option<String>,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: ChangeOutputFormat,
},
Status {
#[arg(long, short = 'f', default_value_t, value_enum)]
format: ChangeOutputFormat,
},
Check {
#[arg(long, conflicts_with_all = ["merge_base", "all"], value_name = "REF")]
since: Option<String>,
#[arg(long, conflicts_with_all = ["since", "all"])]
merge_base: bool,
#[arg(long, conflicts_with_all = ["since", "merge_base"])]
all: bool,
#[arg(long)]
required: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: ChangeOutputFormat,
},
}
fn get_styles() -> clap::builder::Styles {
clap::builder::Styles::styled()
}
impl Commands {
pub fn is_json_format(&self) -> bool {
match self {
Commands::Sync { format, .. } | Commands::Clean { format, .. } => format.is_json_like(),
Commands::Plan { format, schema, .. } => *schema || format.is_json_like(),
Commands::Unify { format, .. } => format.is_json_like(),
Commands::Split { command } => match command {
SplitCommand::Init { .. } => false,
SplitCommand::Run { format, .. } => format.is_json_like(),
},
Commands::Release { command } => match command {
ReleaseCommand::Init { .. } => false,
ReleaseCommand::Resume { .. } | ReleaseCommand::Abort { .. } => false,
ReleaseCommand::Run { format, .. }
| ReleaseCommand::Check { format, .. }
| ReleaseCommand::Finalize { format, .. } => format.is_json_like(),
},
Commands::Change { command } => match command {
ChangeCommand::Add { format, .. } | ChangeCommand::Status { format } | ChangeCommand::Check { format, .. } => {
format.is_json_like()
}
},
Commands::Config { command } => match command {
ConfigCommand::Locate { format }
| ConfigCommand::Print { format }
| ConfigCommand::Validate { format, .. }
| ConfigCommand::Sync { format, .. } => format.is_json_like(),
},
Commands::Hash { format, .. } | Commands::DiffHash { format, .. } => format.is_json_like(),
Commands::Graph { dot, .. } => !dot,
_ => false,
}
}
pub fn apply_json_override(&mut self) -> Result<(), clap::Error> {
let unsupported = match self {
Commands::Run { .. } => Some("run"),
Commands::Unify { command: Some(_), .. } => Some("unify undo"),
Commands::Split {
command: SplitCommand::Init { .. },
} => Some("split init"),
Commands::Release {
command: ReleaseCommand::Init { .. },
} => Some("release init"),
Commands::Release {
command: ReleaseCommand::Resume { .. },
} => Some("release resume"),
Commands::Release {
command: ReleaseCommand::Abort { .. },
} => Some("release abort"),
Commands::Graph { dot: true, .. } => Some("graph --dot"),
Commands::Completions { .. } => Some("completions"),
_ => None,
};
if let Some(command) = unsupported {
return Err(CargoCli::command().error(
ErrorKind::ArgumentConflict,
format!("--json is not supported by 'cargo rail {command}'"),
));
}
match self {
Commands::Sync { format, .. } | Commands::Clean { format, .. } => *format = TextJsonOutputFormat::Json,
Commands::Plan { format, .. } => *format = PlanOutputFormat::Json,
Commands::Unify { format, .. } => *format = UnifyOutputFormat::Json,
Commands::Split {
command: SplitCommand::Run { format, .. },
} => *format = SplitOutputFormat::Json,
Commands::Split { .. } => {}
Commands::Release {
command:
ReleaseCommand::Run { format, .. }
| ReleaseCommand::Check { format, .. }
| ReleaseCommand::Finalize { format, .. },
} => *format = TextJsonOutputFormat::Json,
Commands::Release { .. } => {}
Commands::Change {
command:
ChangeCommand::Add { format, .. } | ChangeCommand::Status { format } | ChangeCommand::Check { format, .. },
} => *format = ChangeOutputFormat::Json,
Commands::Config { command } => match command {
ConfigCommand::Locate { format }
| ConfigCommand::Print { format }
| ConfigCommand::Validate { format, .. }
| ConfigCommand::Sync { format, .. } => *format = TextJsonOutputFormat::Json,
},
Commands::Hash { format, .. } | Commands::DiffHash { format, .. } => *format = TextJsonOutputFormat::Json,
Commands::Graph { .. } => {}
_ => {}
}
Ok(())
}
}
pub fn generate_completions(shell: Shell) {
let mut cmd = CargoCli::command();
clap_complete::generate(shell, &mut cmd, "cargo-rail", &mut std::io::stdout());
}